1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import type { PolicyStatement } from "../IAM/Policy.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { Application } from "./Application.ts";
7
8/**
9 * Shared scaffolding for the Amazon EMR Serverless HTTP bindings.
10 *
11 * Every EMR Serverless data-plane operation is addressed by an
12 * `applicationId`, so every binding is scoped to a bound {@link Application}:
13 * the deploy-time half grants `actions` on the application's ARN (plus any
14 * `subresources` wildcards — job runs and sessions are authorized against
15 * `{applicationArn}/jobruns/*` / `{applicationArn}/sessions/*` sub-resource
16 * ARNs), and the runtime half injects the application's id into every
17 * request. Operations that hand the service an execution role to assume
18 * (`StartJobRun`, `StartSession`) additionally grant `iam:PassRole`
19 * conditioned to `emr-serverless.amazonaws.com`.
20 *
21 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
22 * a `Layer.effect(Cap, makeEmrServerlessHttpBinding({ … }))`.
23 */
24export const makeEmrServerlessHttpBinding = <I, A, E, R>(options: {
25 /** Fully-qualified binding tag, e.g. `AWS.EMRServerless.StartJobRun`. */
26 tag: string;
27 /** The distilled operation. */
28 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
29 /** IAM actions granted on the application ARN (+ `subresources`). */
30 actions: readonly string[];
31 /**
32 * Sub-resource wildcards additionally granted below the application ARN,
33 * e.g. `"/jobruns/*"` for job-run-addressed actions.
34 */
35 subresources?: readonly string[];
36 /**
37 * Grant `iam:PassRole` (conditioned to `emr-serverless.amazonaws.com`) so
38 * the function can hand the service the execution role a job run or
39 * session assumes. Set on `StartJobRun` and `StartSession`.
40 */
41 passRole?: boolean;
42}) =>
43 Effect.gen(function* () {
44 const op = yield* options.operation;
45
46 return Effect.fn(function* (application: Application) {
47 // Outputs yield a DEFERRED effect — resolve again per invocation below.
48 const ApplicationId = yield* application.applicationId;
49 if (!globalThis.__ALCHEMY_RUNTIME__) {
50 const host = yield* Binding.Host;
51 if (isBindingHost(host)) {
52 const passRoleStatements: PolicyStatement[] = options.passRole
53 ? [
54 {
55 Effect: "Allow",
56 Action: ["iam:PassRole"],
57 Resource: ["*"],
58 Condition: {
59 StringEquals: {
60 "iam:PassedToService": "emr-serverless.amazonaws.com",
61 },
62 },
63 },
64 ]
65 : [];
66 yield* host.bind`Allow(${host}, ${options.tag}(${application}))`({
67 policyStatements: [
68 {
69 Effect: "Allow",
70 Action: [...options.actions],
71 Resource: [
72 Output.interpolate`${application.applicationArn}`,
73 ...(options.subresources ?? []).map(
74 (suffix) =>
75 Output.interpolate`${application.applicationArn}${suffix}`,
76 ),
77 ],
78 },
79 ...passRoleStatements,
80 ],
81 });
82 }
83 }
84 return Effect.fn(`${options.tag}(${application.LogicalId})`)(function* (
85 request?: Omit<I, "applicationId" | "clientToken">,
86 ) {
87 return yield* op({
88 ...request,
89 applicationId: yield* ApplicationId,
90 } as I);
91 });
92 });
93 });