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 { Namespace } from "./Namespace.ts";
6
7/**
8 * Shared scaffolding for Amazon Redshift Serverless HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every snapshot/recovery-point/restore
11 * `{Op}Http.ts` in this service is a thin
12 * `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
13 * below. Everything except the operation, the identifier resolver, and the
14 * IAM action list is boilerplate. (`ConnectHttp` stays bespoke — it also
15 * publishes endpoint environment variables and formats connection URLs.)
16 */
17
18/**
19 * Build the impl Effect for an account-level operation (snapshot
20 * administration, recovery-point reads, table-restore status). The
21 * deploy-time half grants `actions` on `*` — these operations span every
22 * namespace in the account and the identifiers they filter on are runtime
23 * data.
24 */
25export const makeServerlessAccountHttpBinding = <I, A, E, R>(options: {
26 /** Fully-qualified binding tag, e.g. `AWS.RedshiftServerless.GetSnapshot`. */
27 tag: string;
28 /** The distilled operation, invoked with the caller's request as-is. */
29 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
30 /** IAM actions granted on `*`. */
31 actions: readonly string[];
32}) =>
33 Effect.gen(function* () {
34 const op = yield* options.operation;
35
36 return Effect.fn(function* () {
37 if (!globalThis.__ALCHEMY_RUNTIME__) {
38 const host = yield* Binding.Host;
39 if (isBindingHost(host)) {
40 yield* host.bind`Allow(${host}, ${options.tag}())`({
41 policyStatements: [
42 {
43 Effect: "Allow",
44 Action: [...options.actions],
45 Resource: ["*"],
46 },
47 ],
48 });
49 }
50 }
51 return Effect.fn(options.tag)(function* (request?: I) {
52 return yield* op((request ?? {}) as I);
53 });
54 });
55 });
56
57/**
58 * Build the impl Effect for a namespace-scoped operation: the runtime
59 * callable injects the bound {@link Namespace}'s name as `namespaceName`
60 * and the deploy-time half grants `actions` on the namespace ARN (plus any
61 * `extraResources`, e.g. the `snapshot/*` or `recoverypoint/*` ARN patterns
62 * that snapshot creation and restores also authorize against).
63 */
64export const makeServerlessNamespaceHttpBinding = <
65 I extends { namespaceName?: string },
66 A,
67 E,
68 R,
69>(options: {
70 /** Fully-qualified binding tag, e.g. `AWS.RedshiftServerless.CreateSnapshot`. */
71 tag: string;
72 /** The distilled operation; `namespaceName` is injected. */
73 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
74 /** IAM actions granted on the namespace ARN. */
75 actions: readonly string[];
76 /** Additional IAM resource ARNs derived from the namespace ARN. */
77 extraResources?: (namespaceArn: string) => string[];
78}) =>
79 Effect.gen(function* () {
80 const op = yield* options.operation;
81
82 return Effect.fn(function* (namespace: Namespace) {
83 const Identifier = yield* namespace.namespaceName;
84 if (!globalThis.__ALCHEMY_RUNTIME__) {
85 const host = yield* Binding.Host;
86 if (isBindingHost(host)) {
87 const resources = options.extraResources;
88 yield* host.bind`Allow(${host}, ${options.tag}(${namespace}))`({
89 policyStatements: [
90 {
91 Effect: "Allow",
92 Action: [...options.actions],
93 Resource: resources
94 ? Output.map(namespace.namespaceArn, (arn) => [
95 arn,
96 ...resources(arn),
97 ])
98 : [Output.interpolate`${namespace.namespaceArn}`],
99 },
100 ],
101 });
102 }
103 }
104 return Effect.fn(`${options.tag}(${namespace.LogicalId})`)(function* (
105 request: Omit<I, "namespaceName">,
106 ) {
107 return yield* op({
108 ...request,
109 namespaceName: yield* Identifier,
110 } as I);
111 });
112 });
113 });
114
115/**
116 * The `arn:…:{account}` prefix of a Redshift Serverless resource ARN —
117 * used to widen a namespace grant to sibling resource types (`snapshot/*`,
118 * `recoverypoint/*`, `workgroup/*`) that snapshot and restore operations
119 * also authorize against.
120 */
121export const serverlessArnPrefix = (arn: string): string =>
122 arn.split(":").slice(0, 5).join(":");