1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import type { Output as OutputType } from "../../Output.ts";
5import type { PolicyStatement } from "../IAM/Policy.ts";
6import { isBindingHost } from "../Lambda/Function.ts";
7import type { Parameter } from "./Parameter.ts";
8
9/**
10 * Shared scaffolding for AWS SSM Parameter Store HTTP bindings.
11 *
12 * NOT exported from `index.ts` — every single-parameter `{Op}Http.ts` in this
13 * service is a thin `Layer.effect(Cap, makeParameterHttpBinding({ … }))` over
14 * this builder. Everything except the operation, the IAM action list, and the
15 * granted ARNs is boilerplate:
16 *
17 * - the deploy-time half registers `Allow(host, tag(parameter))` with the
18 * requested `actions` on the bound parameter's ARN (or the ARNs produced by
19 * `resources` — e.g. the `${arn}/*` subtree wildcard for
20 * `GetParametersByPath`), plus an optional second statement granting
21 * `kmsActions` on the parameter's encryption key so `SecureString`
22 * decryption (`kms:Decrypt`) or encryption (`kms:Encrypt`,
23 * `kms:GenerateDataKey`) works out of the box;
24 * - the runtime callable injects the resolved parameter name into the request
25 * under `requestKey` (`Name` by default, `Path` for the by-path read).
26 *
27 * Genuinely-different bindings stay bespoke: `GetParameters` (variadic
28 * multi-parameter request with order-preserving name resolution).
29 */
30export const makeParameterHttpBinding = <I, A, E, R>(options: {
31 /** Fully-qualified binding tag, e.g. `AWS.SSM.GetParameterHistory`. */
32 tag: string;
33 /** The distilled operation the runtime callable invokes. */
34 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
35 /** IAM actions granted on the bound parameter. */
36 actions: readonly string[];
37 /**
38 * The request field the bound parameter's name is injected under.
39 * @default "Name"
40 */
41 requestKey?: "Name" | "Path";
42 /**
43 * KMS actions granted on the parameter's encryption key. For
44 * `String`/`StringList` parameters `keyArn` is undefined and the statement
45 * falls back to the parameter's own ARN, which matches no KMS key — the
46 * statement then grants nothing.
47 */
48 kmsActions?: readonly string[];
49 /** ARNs the SSM actions are granted on. @default the parameter ARN */
50 resources?: (parameter: Parameter) => (string | OutputType<string>)[];
51}) =>
52 Effect.gen(function* () {
53 const op = yield* options.operation;
54 const requestKey = options.requestKey ?? "Name";
55
56 return Effect.fn(function* <P extends Parameter>(parameter: P) {
57 const Name = yield* parameter.parameterName;
58 if (!globalThis.__ALCHEMY_RUNTIME__) {
59 const host = yield* Binding.Host;
60 if (isBindingHost(host)) {
61 const statements: {
62 Effect: PolicyStatement["Effect"];
63 Action: string[];
64 Resource: (string | OutputType<string>)[];
65 }[] = [
66 {
67 Effect: "Allow",
68 Action: [...options.actions],
69 Resource: options.resources?.(parameter) ?? [
70 parameter.parameterArn,
71 ],
72 },
73 ];
74 if (options.kmsActions !== undefined) {
75 statements.push({
76 Effect: "Allow",
77 Action: [...options.kmsActions],
78 Resource: [
79 Output.all(parameter.parameterArn, parameter.keyArn).pipe(
80 Output.map(
81 ([parameterArn, keyArn]) => keyArn ?? parameterArn,
82 ),
83 ),
84 ],
85 });
86 }
87 yield* host.bind`Allow(${host}, ${options.tag}(${parameter}))`({
88 policyStatements: statements,
89 });
90 }
91 }
92 return Effect.fn(`${options.tag}(${parameter.LogicalId})`)(function* (
93 request?: Omit<I, "Name" | "Path">,
94 ) {
95 return yield* op({
96 ...request,
97 [requestKey]: yield* Name,
98 } as I);
99 });
100 });
101 });