1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { AWSEnvironment } from "../Environment.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { EventBus } from "./EventBus.ts";
7import type { Rule } from "./Rule.ts";
8
9/**
10 * Shared scaffolding for the EventBridge runtime bindings.
11 *
12 * NOT exported from `index.ts` — every single-operation `{Op}Http.ts` in
13 * this service is a thin `Layer.effect(Cap, makeEventBridge…HttpBinding({ … }))`
14 * over one of the three builders below. Everything except the operation, the
15 * IAM action list, and the injected identifier(s) is boilerplate:
16 *
17 * - {@link makeEventBridgeAccountHttpBinding} — account-level operations
18 * (`ListEventBuses`, `TestEventPattern`, `ListRuleNamesByTarget`, replay
19 * reads). The runtime callable passes the caller's request through
20 * unchanged; the deploy-time half grants `actions` on `resources`
21 * (default `*` — most EventBridge list/test actions do not support
22 * resource-level permissions).
23 * - {@link makeEventBridgeBusHttpBinding} — operations scoped to an optional
24 * bound {@link EventBus} (`DescribeEventBus`, `ListRules`). The runtime
25 * callable injects the bus name under `busNameKey` (omitted for the
26 * default bus); the deploy-time half grants `actions` on the bus ARN, or
27 * the account default bus ARN when no bus is bound.
28 * - {@link makeEventBridgeRuleHttpBinding} — operations scoped to a bound
29 * {@link Rule} (`DescribeRule`, `ListTargetsByRule`, `EnableRule`,
30 * `DisableRule`). The runtime callable injects the rule name under
31 * `ruleNameKey` and the bus name under `EventBusName` (omitted for the
32 * default bus); the deploy-time half grants `actions` on the rule ARN.
33 *
34 * Genuinely-different bindings stay bespoke: `PutEvents` (per-entry bus-name
35 * injection), `BusSink` (a batching sink over `PutEvents`), and
36 * `StartReplay` (archive-scoped with a replay-wildcard grant).
37 */
38
39/**
40 * Build the impl Effect for an account-level EventBridge operation. The
41 * runtime callable passes the caller's request through unchanged; the
42 * deploy-time half grants `actions` on `resources` (default `*`).
43 */
44export const makeEventBridgeAccountHttpBinding = <I, A, E, R>(options: {
45 /** Fully-qualified binding tag, e.g. `AWS.EventBridge.ListEventBuses`. */
46 tag: string;
47 /** The distilled operation, invoked with the caller's request as-is. */
48 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
49 /** IAM actions granted on `resources`. */
50 actions: readonly string[];
51 /**
52 * Resolve the IAM resources the actions are granted on, given the ambient
53 * account and region (e.g. a `replay/*` ARN). Defaults to `["*"]`.
54 */
55 resources?: (env: { accountId: string; region: string }) => readonly string[];
56}) =>
57 Effect.gen(function* () {
58 const op = yield* options.operation;
59
60 return Effect.fn(function* () {
61 if (!globalThis.__ALCHEMY_RUNTIME__) {
62 const host = yield* Binding.Host;
63 if (isBindingHost(host)) {
64 const { accountId, region } =
65 yield* AWSEnvironment.current as unknown as Effect.Effect<{
66 accountId: string;
67 region: string;
68 }>;
69 yield* host.bind`Allow(${host}, ${options.tag}())`({
70 policyStatements: [
71 {
72 Effect: "Allow",
73 Action: [...options.actions],
74 Resource: options.resources
75 ? [...options.resources({ accountId, region })]
76 : ["*"],
77 },
78 ],
79 });
80 }
81 }
82 return Effect.fn(options.tag)(function* (request?: I) {
83 return yield* op((request ?? {}) as I);
84 });
85 });
86 });
87
88/**
89 * Build the impl Effect for an EventBridge operation scoped to an optional
90 * bound {@link EventBus}. The runtime callable injects the bus name under
91 * `busNameKey` (omitted for the default bus); the deploy-time half grants
92 * `actions` on the bus ARN, or the account default bus ARN when no bus is
93 * bound.
94 */
95export const makeEventBridgeBusHttpBinding = <I, A, E, R>(options: {
96 /** Fully-qualified binding tag, e.g. `AWS.EventBridge.DescribeEventBus`. */
97 tag: string;
98 /** The distilled operation; the bus name is injected from the bound bus. */
99 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
100 /** IAM actions granted on the bus ARN. */
101 actions: readonly string[];
102 /** Request field the bound bus's name is injected under. */
103 busNameKey: string;
104 /**
105 * Override the IAM resources the actions are granted on. Some bus-scoped
106 * operations (`events:ListRules`) do not support resource-level
107 * permissions and must be granted on `*`; the default is the bound bus's
108 * ARN (or the account default bus ARN when no bus is bound).
109 */
110 resources?: (env: { accountId: string; region: string }) => readonly string[];
111}) =>
112 Effect.gen(function* () {
113 const op = yield* options.operation;
114
115 return Effect.fn(function* (bus?: EventBus) {
116 const EventBusName = bus ? yield* bus.eventBusName : undefined;
117 if (!globalThis.__ALCHEMY_RUNTIME__) {
118 const host = yield* Binding.Host;
119 if (isBindingHost(host)) {
120 const { accountId, region } =
121 yield* AWSEnvironment.current as unknown as Effect.Effect<{
122 accountId: string;
123 region: string;
124 }>;
125 // Pass the ARN as an unresolved Output — binding data is resolved
126 // by the engine before the host reconciles. Eagerly yielding here
127 // (during plan) produces a deferred object that serializes into an
128 // invalid IAM policy (MalformedPolicyDocumentException).
129 const resource = bus
130 ? Output.interpolate`${bus.eventBusArn}`
131 : (`arn:aws:events:${region}:${accountId}:event-bus/default` as const);
132
133 yield* host.bind`Allow(${host}, ${options.tag}(${bus ?? "default"}))`(
134 {
135 policyStatements: [
136 {
137 Effect: "Allow",
138 Action: [...options.actions],
139 Resource: options.resources
140 ? [...options.resources({ accountId, region })]
141 : [resource],
142 },
143 ],
144 },
145 );
146 }
147 }
148 return Effect.fn(`${options.tag}(${bus?.LogicalId})`)(function* (
149 request?: Partial<I>,
150 ) {
151 const eventBusName = EventBusName ? yield* EventBusName : undefined;
152 const input: Record<string, unknown> = { ...request };
153 if (eventBusName !== undefined && eventBusName !== "default") {
154 input[options.busNameKey] = eventBusName;
155 }
156 return yield* op(input as I);
157 });
158 });
159 });
160
161/**
162 * Build the impl Effect for an EventBridge operation scoped to a bound
163 * {@link Rule}. The runtime callable injects the rule name under
164 * `ruleNameKey` and the rule's bus name under `EventBusName` (omitted for
165 * the default bus); the deploy-time half grants `actions` on the rule ARN.
166 */
167export const makeEventBridgeRuleHttpBinding = <I, A, E, R>(options: {
168 /** Fully-qualified binding tag, e.g. `AWS.EventBridge.DescribeRule`. */
169 tag: string;
170 /** The distilled operation; the rule and bus names are injected. */
171 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
172 /** IAM actions granted on the rule ARN. */
173 actions: readonly string[];
174 /** Request field the bound rule's name is injected under. */
175 ruleNameKey: string;
176}) =>
177 Effect.gen(function* () {
178 const op = yield* options.operation;
179
180 return Effect.fn(function* (rule: Rule) {
181 const RuleName = yield* rule.ruleName;
182 const EventBusName = yield* rule.eventBusName;
183 if (!globalThis.__ALCHEMY_RUNTIME__) {
184 const host = yield* Binding.Host;
185 if (isBindingHost(host)) {
186 yield* host.bind`Allow(${host}, ${options.tag}(${rule}))`({
187 policyStatements: [
188 {
189 Effect: "Allow",
190 Action: [...options.actions],
191 Resource: [rule.ruleArn],
192 },
193 ],
194 });
195 }
196 }
197 return Effect.fn(`${options.tag}(${rule.LogicalId})`)(function* (
198 request?: Partial<I>,
199 ) {
200 const ruleName = yield* RuleName;
201 const eventBusName = yield* EventBusName;
202 const input: Record<string, unknown> = { ...request };
203 input[options.ruleNameKey] = ruleName;
204 if (eventBusName !== "default") {
205 input.EventBusName = eventBusName;
206 }
207 return yield* op(input as I);
208 });
209 });
210 });