1/**
2 * Shared HTTP-binding scaffolding for App Runner capabilities.
3 *
4 * INTERNAL — deliberately NOT exported from `index.ts`. Each `{Op}Http.ts`
5 * is a thin call to {@link makeAppRunnerHttpBinding}; everything except the
6 * operation, the identifier resolvers, and the IAM grant is boilerplate:
7 * resolve the bound resources' identifiers (which also registers them on the
8 * host's environment), register the IAM policy statement on the binding host
9 * (skipped inside the deployed function), and return the traced runtime
10 * client that merges the identifiers into each wire request.
11 */
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as Binding from "../../Binding.ts";
15import type { Input } from "../../Input.ts";
16import type * as Output from "../../Output.ts";
17import type { ResourceLike } from "../../Resource.ts";
18import { AWSEnvironment } from "../Environment.ts";
19import { isBindingHost } from "../Lambda/Function.ts";
20
21/** Region/account available to an IAM grant factory. */
22export interface AppRunnerIamContext {
23 region: string;
24 accountId: string;
25}
26
27export interface AppRunnerHttpBindingSpec {
28 /**
29 * Wire-request fields resolved from the bound resources' attributes,
30 * keyed by the operation's request field name (e.g. `ServiceArn`).
31 */
32 identifiers: Record<string, Output.Output<string, never>>;
33 /** The IAM actions + resource ARNs the host needs for this capability. */
34 iam: (ctx: AppRunnerIamContext) => {
35 actions: string[];
36 resources: Input<string>[];
37 };
38}
39
40/**
41 * Build the `Layer.effect` for an App Runner HTTP binding from its three
42 * distinguishing parts: the distilled operation, the identifier resolvers,
43 * and the IAM grant.
44 */
45export const makeAppRunnerHttpBinding = <
46 Self,
47 Identifier extends string,
48 Shape extends (...args: any[]) => Effect.Effect<any, any, any>,
49 WireIn extends object,
50 Out,
51 Err,
52 OpReq,
53>(
54 service: Binding.Service<Self, Identifier, Shape>,
55 options: {
56 /** The distilled App Runner operation backing the capability. */
57 operation: Effect.Effect<
58 (input: WireIn) => Effect.Effect<Out, Err>,
59 never,
60 OpReq
61 >;
62 /** Derive identifiers + IAM grant from the bound resources. */
63 spec: (...args: Parameters<Shape>) => AppRunnerHttpBindingSpec;
64 },
65): Layer.Layer<Self, never, OpReq> =>
66 Layer.effect(
67 service,
68 Effect.gen(function* () {
69 const operation = yield* options.operation;
70
71 return Effect.fn(function* (...args: Parameters<Shape>) {
72 const { identifiers, iam } = options.spec(...args);
73 const label = (args as ResourceLike[])
74 .map((arg) => arg.LogicalId)
75 .join(", ");
76
77 // Outputs yield DEFERRED effects — resolving them here registers the
78 // attributes on the host environment; re-yield per invocation below.
79 const resolved: [string, Effect.Effect<string>][] = [];
80 for (const [key, output] of Object.entries(identifiers)) {
81 resolved.push([key, yield* output]);
82 }
83
84 if (!globalThis.__ALCHEMY_RUNTIME__) {
85 const host = yield* Binding.Host;
86 if (isBindingHost(host)) {
87 const { accountId, region } =
88 yield* AWSEnvironment.current as unknown as Effect.Effect<{
89 accountId: string;
90 region: string;
91 }>;
92 const { actions, resources } = iam({ accountId, region });
93 yield* host.bind(
94 `Allow(${host.LogicalId}, ${service.key}(${label}))`,
95 {
96 policyStatements: [
97 {
98 Effect: "Allow",
99 Action: actions,
100 Resource: resources,
101 },
102 ],
103 },
104 );
105 }
106 }
107
108 return Effect.fn(`${service.key}(${label})`)(function* (
109 request?: object,
110 ) {
111 const ids: Record<string, string> = {};
112 for (const [key, effect] of resolved) {
113 ids[key] = yield* effect;
114 }
115 return yield* operation({ ...request, ...ids } as WireIn);
116 });
117 }) as unknown as Shape;
118 }),
119 );