1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { Output } from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { TaggableResource } from "./binding-common.ts";
6import { sortByLogicalId } from "./common.ts";
7
8/**
9 * Shared scaffolding for CloudWatch HTTP bindings.
10 *
11 * NOT exported from `index.ts` — every single-operation `{Op}Http.ts` in
12 * this service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over
13 * one of the three builders below. Everything except the operation, the IAM
14 * action, and the injected identifier(s) is boilerplate:
15 *
16 * - {@link makeCloudWatchAccountHttpBinding} — account-level operations
17 * (`PutMetricData`, `GetMetricData`, `List*`, account-wide `Describe*`).
18 * CloudWatch metric-data and account-wide describe/list actions do not
19 * support resource-level permissions, so the deploy-time half grants
20 * `actions` on `*`.
21 * - {@link makeCloudWatchResourceHttpBinding} — operations scoped to one
22 * bound resource (`GetDashboard`, `SetAlarmState`, `GetMetricStream`, …).
23 * The runtime callable injects the resource's identifier under
24 * `requestKey`; the deploy-time half grants `actions` on the resource ARN.
25 * - {@link makeCloudWatchResourceSetHttpBinding} — batch toggles over a
26 * variadic set of bound resources (`EnableAlarmActions`,
27 * `DisableInsightRules`, `StartMetricStreams`, …). The runtime callable
28 * takes no request and injects the sorted resource names under `namesKey`;
29 * the deploy-time half grants `action` on every resource ARN.
30 *
31 * Genuinely-different bindings stay bespoke: `DescribeAlarms` (computes
32 * `AlarmTypes` from the bound alarm set and accepts a filter request) and
33 * `MetricSink` (a batching sink over the `PutMetricData` capability).
34 */
35
36/**
37 * Build the impl Effect for an account-level CloudWatch operation. The
38 * runtime callable passes the caller's request through unchanged; the
39 * deploy-time half grants `actions` on `*` (these CloudWatch actions do not
40 * support resource-level permissions).
41 */
42export const makeCloudWatchAccountHttpBinding = <I, A, E, R>(options: {
43 /** Fully-qualified binding tag, e.g. `AWS.CloudWatch.GetMetricData`. */
44 tag: string;
45 /** The distilled operation, invoked with the caller's request as-is. */
46 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
47 /** IAM actions granted on `*`. */
48 actions: readonly string[];
49}) =>
50 Effect.gen(function* () {
51 const op = yield* options.operation;
52
53 return Effect.fn(function* () {
54 if (!globalThis.__ALCHEMY_RUNTIME__) {
55 const host = yield* Binding.Host;
56 if (isBindingHost(host)) {
57 yield* host.bind`Allow(${host}, ${options.tag}())`({
58 policyStatements: [
59 {
60 Effect: "Allow",
61 Action: [...options.actions],
62 Resource: ["*"],
63 },
64 ],
65 });
66 }
67 }
68 return Effect.fn(options.tag)(function* (request?: I) {
69 return yield* op((request ?? {}) as I);
70 });
71 });
72 });
73
74/**
75 * Build the impl Effect for an operation scoped to a single bound CloudWatch
76 * resource. The runtime callable injects the resolved `identifier` under
77 * `requestKey`; the deploy-time half grants `actions` on `resourceArn`.
78 */
79export const makeCloudWatchResourceHttpBinding = <
80 Res extends TaggableResource,
81 K extends string,
82 I,
83 A,
84 E,
85 R,
86>(options: {
87 /** Fully-qualified binding tag, e.g. `AWS.CloudWatch.GetDashboard`. */
88 tag: string;
89 /** The distilled operation; the identifier is injected from the resource. */
90 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
91 /** IAM actions granted on the resource ARN. */
92 actions: readonly string[];
93 /** Request field the resolved identifier is injected under. */
94 requestKey: K;
95 /** Resolve the injected identifier from the bound resource. */
96 identifier: (resource: Res) => Output<string, never>;
97 /** Resolve the IAM resource the actions are granted on. */
98 resourceArn: (resource: Res) => Output<string, never>;
99}) =>
100 Effect.gen(function* () {
101 const op = yield* options.operation;
102
103 return Effect.fn(function* (resource: Res) {
104 const identifier = yield* options.identifier(resource);
105 if (!globalThis.__ALCHEMY_RUNTIME__) {
106 const host = yield* Binding.Host;
107 if (isBindingHost(host)) {
108 yield* host.bind`Allow(${host}, ${options.tag}(${resource}))`({
109 policyStatements: [
110 {
111 Effect: "Allow",
112 Action: [...options.actions],
113 Resource: [options.resourceArn(resource)],
114 },
115 ],
116 });
117 }
118 }
119 return Effect.fn(`${options.tag}(${resource.LogicalId})`)(function* (
120 request?: Omit<I, K>,
121 ) {
122 const input: Record<string, unknown> = { ...request };
123 input[options.requestKey] = yield* identifier;
124 return yield* op(input as unknown as I);
125 });
126 });
127 });
128
129/**
130 * Build the impl Effect for a batch toggle over a variadic set of bound
131 * CloudWatch resources. The runtime callable takes no request and injects
132 * the resources' names (sorted by Logical ID for a deterministic binding
133 * identity) under `namesKey`; the deploy-time half grants `action` on every
134 * resource ARN.
135 */
136export const makeCloudWatchResourceSetHttpBinding = <
137 Res extends TaggableResource,
138 K extends string,
139 I,
140 A,
141 E,
142 R,
143>(options: {
144 /** Fully-qualified binding tag, e.g. `AWS.CloudWatch.EnableAlarmActions`. */
145 tag: string;
146 /** The distilled operation; the sorted names array is injected. */
147 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
148 /** IAM action granted on every bound resource ARN. */
149 action: string;
150 /** Request field the resolved names array is injected under. */
151 namesKey: K;
152 /** Resolve a resource's name (the injected identifier). */
153 name: (resource: Res) => Output<string, never>;
154 /** Resolve a resource's ARN (the IAM resource). */
155 arn: (resource: Res) => Output<string, never>;
156}) =>
157 Effect.gen(function* () {
158 const op = yield* options.operation;
159
160 return Effect.fn(function* (...resources: [Res, ...Res[]]) {
161 const sorted = sortByLogicalId(resources) as [Res, ...Res[]];
162 const names = yield* Effect.forEach(sorted, (resource) =>
163 options.name(resource).asEffect(),
164 );
165 if (!globalThis.__ALCHEMY_RUNTIME__) {
166 const host = yield* Binding.Host;
167 if (isBindingHost(host)) {
168 yield* host.bind`Allow(${host}, ${options.tag}(${sorted}))`({
169 policyStatements: [
170 {
171 Effect: "Allow",
172 Action: [options.action],
173 Resource: sorted.map((resource) => options.arn(resource)),
174 },
175 ],
176 });
177 }
178 }
179 return Effect.fn(`${options.tag}(${sorted})`)(function* () {
180 const input: Record<string, unknown> = {};
181 input[options.namesKey] = yield* Effect.forEach(names, (name) => name);
182 return yield* op(input as unknown as I);
183 });
184 });
185 });