1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { Activity } from "./Activity.ts";
6import type { StateMachine } from "./StateMachine.ts";
7
8/**
9 * Shared scaffolding for AWS Step Functions HTTP bindings.
10 *
11 * NOT exported from `index.ts` — every near-identical `{Op}Http.ts` in this
12 * service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of
13 * the builders below. Everything except the operation, the IAM action list,
14 * and the injected/derived identifier is boilerplate. Genuinely-different
15 * bindings (none currently — `StartSyncExecution` folds its bespoke
16 * `sync-states` endpoint into the `operation` effect it passes) stay
17 * bespoke.
18 */
19
20/**
21 * Execution ARNs replace the `:stateMachine:` segment with `:execution:`
22 * and append the execution name — grant on the machine's execution ARN
23 * pattern.
24 */
25export const executionArnPattern = (stateMachine: StateMachine) =>
26 Output.map(
27 stateMachine.stateMachineArn,
28 (arn) => `${arn.replace(":stateMachine:", ":execution:")}:*`,
29 );
30
31/**
32 * Map Run ARNs replace the `:stateMachine:` segment with `:mapRun:` and
33 * append `/{executionName}:{uuid}` — grant on the machine's Map Run ARN
34 * pattern.
35 */
36export const mapRunArnPattern = (stateMachine: StateMachine) =>
37 Output.map(
38 stateMachine.stateMachineArn,
39 (arn) => `${arn.replace(":stateMachine:", ":mapRun:")}/*`,
40 );
41
42const grant = (
43 tag: string,
44 scope: { LogicalId: string } | string,
45 actions: readonly string[],
46 resources: readonly (string | Output.Output<string, any>)[],
47) =>
48 Effect.gen(function* () {
49 if (!globalThis.__ALCHEMY_RUNTIME__) {
50 const host = yield* Binding.Host;
51 if (isBindingHost(host)) {
52 yield* host.bind`Allow(${host}, ${tag}(${scope}))`({
53 policyStatements: [
54 {
55 Effect: "Allow",
56 Action: [...actions],
57 Resource: [...resources],
58 },
59 ],
60 });
61 }
62 }
63 });
64
65/**
66 * Build the impl Effect for a state-machine-scoped operation whose request
67 * carries the machine identity as `stateMachineArn` (`StartExecution`,
68 * `ListExecutions`, …): the runtime callable injects the bound
69 * {@link StateMachine}'s ARN and the deploy-time half grants `actions` on
70 * it.
71 */
72export const makeStateMachineArnHttpBinding = <
73 I extends { stateMachineArn?: string },
74 A,
75 E,
76 R,
77>(options: {
78 /** Fully-qualified binding tag, e.g. `AWS.StepFunctions.StartExecution`. */
79 tag: string;
80 /** The distilled operation; the machine ARN is injected as `stateMachineArn`. */
81 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
82 /** IAM actions granted on the state machine ARN. */
83 actions: readonly string[];
84}) =>
85 Effect.gen(function* () {
86 const op = yield* options.operation;
87
88 return Effect.fn(function* (stateMachine: StateMachine) {
89 const StateMachineArn = yield* stateMachine.stateMachineArn;
90 yield* grant(options.tag, stateMachine, options.actions, [
91 stateMachine.stateMachineArn,
92 ]);
93 return Effect.fn(`${options.tag}(${stateMachine.LogicalId})`)(function* (
94 request?: Omit<I, "stateMachineArn">,
95 ) {
96 return yield* op({
97 ...request,
98 stateMachineArn: yield* StateMachineArn,
99 } as I);
100 });
101 });
102 });
103
104/**
105 * Build the impl Effect for an operation addressed by an execution or Map
106 * Run ARN the caller already holds (`DescribeExecution`, `StopExecution`,
107 * `GetExecutionHistory`, `RedriveExecution`, `DescribeMapRun`, …): the
108 * runtime callable passes the request through unchanged while the
109 * deploy-time half grants `actions` on the bound {@link StateMachine}'s
110 * execution (or Map Run) ARN pattern.
111 */
112export const makeExecutionScopedHttpBinding = <I, A, E, R>(options: {
113 /** Fully-qualified binding tag, e.g. `AWS.StepFunctions.StopExecution`. */
114 tag: string;
115 /** The distilled operation, invoked with the caller's request as-is. */
116 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
117 /** IAM actions granted on the derived ARN pattern. */
118 actions: readonly string[];
119 /**
120 * Which derived ARN pattern the grant targets.
121 * @default "execution"
122 */
123 scope?: "execution" | "mapRun";
124}) =>
125 Effect.gen(function* () {
126 const op = yield* options.operation;
127
128 return Effect.fn(function* (stateMachine: StateMachine) {
129 yield* grant(options.tag, stateMachine, options.actions, [
130 options.scope === "mapRun"
131 ? mapRunArnPattern(stateMachine)
132 : executionArnPattern(stateMachine),
133 ]);
134 return Effect.fn(`${options.tag}(${stateMachine.LogicalId})`)(function* (
135 request: I,
136 ) {
137 return yield* op(request);
138 });
139 });
140 });
141
142/**
143 * Build the impl Effect for an activity-scoped operation whose request
144 * carries the activity identity as `activityArn` (`GetActivityTask`): the
145 * runtime callable injects the bound {@link Activity}'s ARN and the
146 * deploy-time half grants `actions` on it.
147 */
148export const makeActivityArnHttpBinding = <
149 I extends { activityArn?: string },
150 A,
151 E,
152 R,
153>(options: {
154 /** Fully-qualified binding tag, e.g. `AWS.StepFunctions.GetActivityTask`. */
155 tag: string;
156 /** The distilled operation; the activity ARN is injected as `activityArn`. */
157 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
158 /** IAM actions granted on the activity ARN. */
159 actions: readonly string[];
160}) =>
161 Effect.gen(function* () {
162 const op = yield* options.operation;
163
164 return Effect.fn(function* (activity: Activity) {
165 const ActivityArn = yield* activity.activityArn;
166 yield* grant(options.tag, activity, options.actions, [
167 activity.activityArn,
168 ]);
169 return Effect.fn(`${options.tag}(${activity.LogicalId})`)(function* (
170 request?: Omit<I, "activityArn">,
171 ) {
172 return yield* op({
173 ...request,
174 activityArn: yield* ActivityArn,
175 } as I);
176 });
177 });
178 });
179
180/**
181 * Build the impl Effect for a task-token callback operation
182 * (`SendTaskSuccess`/`SendTaskFailure`/`SendTaskHeartbeat`): task tokens
183 * from `.waitForTaskToken` service integrations carry no IAM resource, so
184 * the grant is `*` unless an {@link Activity} is bound (only Activity tasks
185 * support resource-level scoping). The request passes through unchanged.
186 */
187export const makeTaskCallbackHttpBinding = <I, A, E, R>(options: {
188 /** Fully-qualified binding tag, e.g. `AWS.StepFunctions.SendTaskSuccess`. */
189 tag: string;
190 /** The distilled operation, invoked with the caller's request as-is. */
191 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
192 /** IAM actions granted on the activity ARN (or `*`). */
193 actions: readonly string[];
194}) =>
195 Effect.gen(function* () {
196 const op = yield* options.operation;
197
198 return Effect.fn(function* (activity?: Activity) {
199 yield* grant(options.tag, activity ?? "*", options.actions, [
200 activity ? Output.interpolate`${activity.activityArn}` : "*",
201 ]);
202 return Effect.fn(`${options.tag}(${activity?.LogicalId})`)(function* (
203 request: I,
204 ) {
205 return yield* op(request);
206 });
207 });
208 });
209
210/**
211 * Build the impl Effect for a zero-arity, service-scoped Step Functions
212 * binding (`TestState`, `ValidateStateMachineDefinition`): registers the
213 * service-scoped IAM policy statement on the host (deploy time) and passes
214 * the request through unchanged (runtime).
215 */
216export const makeSfnServiceHttpBinding = <I, A, E, R>(options: {
217 /** Fully-qualified binding tag, e.g. `AWS.StepFunctions.TestState`. */
218 tag: string;
219 /** The distilled operation, invoked with the caller's request as-is. */
220 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
221 /** IAM actions the operation needs (service-scoped, `Resource: ["*"]`). */
222 actions: readonly string[];
223}) =>
224 Effect.gen(function* () {
225 const op = yield* options.operation;
226
227 return Effect.fn(function* () {
228 if (!globalThis.__ALCHEMY_RUNTIME__) {
229 const host = yield* Binding.Host;
230 if (isBindingHost(host)) {
231 yield* host.bind`Allow(${host}, ${options.tag}())`({
232 policyStatements: [
233 {
234 Effect: "Allow",
235 Action: [...options.actions],
236 Resource: ["*"],
237 },
238 ],
239 });
240 }
241 }
242 return Effect.fn(options.tag)(function* (request: I) {
243 return yield* op(request);
244 });
245 });
246 });