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 { Discoverer } from "./Discoverer.ts";
6import type { Registry } from "./Registry.ts";
7import type { Schema } from "./Schema.ts";
8
9/**
10 * Shared scaffolding for EventBridge Schemas HTTP bindings.
11 *
12 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
13 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
14 * below. Everything except the operation, the identifier resolver, and the
15 * IAM action list is boilerplate.
16 */
17
18/**
19 * Build the impl Effect for a Schemas operation scoped to a {@link Schema}:
20 * the deploy-time half grants `actions` on the bound schema's ARN, and the
21 * runtime half injects the schema's `RegistryName` + `SchemaName` into every
22 * request.
23 */
24export const makeSchemasSchemaHttpBinding = <
25 I extends { RegistryName: string; SchemaName: string },
26 A,
27 E,
28 R,
29>(options: {
30 /** Fully-qualified binding tag, e.g. `AWS.Schemas.DescribeSchema`. */
31 tag: string;
32 /** The distilled operation. */
33 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
34 /** IAM actions granted on the schema ARN. */
35 actions: readonly string[];
36}) =>
37 Effect.gen(function* () {
38 const op = yield* options.operation;
39
40 return Effect.fn(function* (schema: Schema) {
41 const RegistryName = yield* schema.registryName;
42 const SchemaName = yield* schema.schemaName;
43 if (!globalThis.__ALCHEMY_RUNTIME__) {
44 const host = yield* Binding.Host;
45 if (isBindingHost(host)) {
46 yield* host.bind`Allow(${host}, ${options.tag}(${schema}))`({
47 policyStatements: [
48 {
49 Effect: "Allow",
50 Action: [...options.actions],
51 Resource: [schema.schemaArn],
52 },
53 ],
54 });
55 }
56 }
57 return Effect.fn(`${options.tag}(${schema.LogicalId})`)(function* (
58 request?: Omit<I, "RegistryName" | "SchemaName">,
59 ) {
60 return yield* op({
61 ...request,
62 RegistryName: yield* RegistryName,
63 SchemaName: yield* SchemaName,
64 } as I);
65 });
66 });
67 });
68
69/**
70 * Build the impl Effect for a Schemas operation scoped to a {@link Registry}:
71 * the deploy-time half grants `actions` on the registry ARN and on the
72 * registry's schema-type ARN (`…:schema/{registryName}*` — the resource
73 * `schemas:SearchSchemas` authorizes against), and the runtime half injects
74 * the registry's `RegistryName` into every request.
75 */
76export const makeSchemasRegistryHttpBinding = <
77 I extends { RegistryName: string },
78 A,
79 E,
80 R,
81>(options: {
82 /** Fully-qualified binding tag, e.g. `AWS.Schemas.SearchSchemas`. */
83 tag: string;
84 /** The distilled operation. */
85 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
86 /** IAM actions granted on the registry ARN. */
87 actions: readonly string[];
88}) =>
89 Effect.gen(function* () {
90 const op = yield* options.operation;
91
92 return Effect.fn(function* (registry: Registry) {
93 const RegistryName = yield* registry.registryName;
94 if (!globalThis.__ALCHEMY_RUNTIME__) {
95 const host = yield* Binding.Host;
96 if (isBindingHost(host)) {
97 yield* host.bind`Allow(${host}, ${options.tag}(${registry}))`({
98 policyStatements: [
99 {
100 Effect: "Allow",
101 Action: [...options.actions],
102 Resource: [
103 registry.registryArn,
104 // Registry-scoped reads (e.g. SearchSchemas) authorize
105 // against the schema-type ARN keyed by the registry name.
106 Output.map(
107 registry.registryArn,
108 (arn) => `${arn.replace(":registry/", ":schema/")}*`,
109 ),
110 ],
111 },
112 ],
113 });
114 }
115 }
116 return Effect.fn(`${options.tag}(${registry.LogicalId})`)(function* (
117 request?: Omit<I, "RegistryName">,
118 ) {
119 return yield* op({
120 ...request,
121 RegistryName: yield* RegistryName,
122 } as I);
123 });
124 });
125 });
126
127/**
128 * Build the impl Effect for a Schemas operation scoped to a
129 * {@link Discoverer}: the deploy-time half grants `actions` on the bound
130 * discoverer's ARN (plus `ruleActions` on the discoverer's managed
131 * EventBridge rule, which Start/StopDiscoverer flip behind the scenes), and
132 * the runtime half injects the `DiscovererId` into every request.
133 */
134export const makeSchemasDiscovererHttpBinding = <
135 I extends { DiscovererId: string },
136 A,
137 E,
138 R,
139>(options: {
140 /** Fully-qualified binding tag, e.g. `AWS.Schemas.StartDiscoverer`. */
141 tag: string;
142 /** The distilled operation. */
143 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
144 /** IAM actions granted on the discoverer ARN. */
145 actions: readonly string[];
146 /**
147 * EventBridge actions granted on the discoverer's managed rule
148 * (`rule/{busName}/Schemas-{discovererId}`). StartDiscoverer enables the
149 * rule and StopDiscoverer disables it, so they additionally require
150 * `events:EnableRule` / `events:DisableRule`.
151 */
152 ruleActions?: readonly string[];
153}) =>
154 Effect.gen(function* () {
155 const op = yield* options.operation;
156
157 return Effect.fn(function* (discoverer: Discoverer) {
158 const DiscovererId = yield* discoverer.discovererId;
159 if (!globalThis.__ALCHEMY_RUNTIME__) {
160 const host = yield* Binding.Host;
161 if (isBindingHost(host)) {
162 // The managed rule is named `Schemas-{discovererId}` but TRUNCATED
163 // to EventBridge's 64-char rule-name limit, so the grant wildcards
164 // the name under the `Schemas-` prefix instead of interpolating
165 // the full discoverer id.
166 const toRuleArn = (busSegment: string) =>
167 Output.map(
168 discoverer.discovererArn,
169 (arn) =>
170 `${arn.replace(":schemas:", ":events:").split(":discoverer/")[0]}:rule/${busSegment}Schemas-*`,
171 );
172 yield* host.bind`Allow(${host}, ${options.tag}(${discoverer}))`({
173 policyStatements: [
174 {
175 Effect: "Allow",
176 Action: [...options.actions],
177 Resource: [discoverer.discovererArn],
178 },
179 ...(options.ruleActions !== undefined
180 ? [
181 {
182 Effect: "Allow" as const,
183 Action: [...options.ruleActions],
184 // The managed rule lives on the discovered bus
185 // (`rule/{busName}/Schemas-{id}`) or, for the default
186 // bus, directly under `rule/Schemas-{id}`.
187 Resource: [toRuleArn("*/"), toRuleArn("")],
188 },
189 ]
190 : []),
191 ],
192 });
193 }
194 }
195 return Effect.fn(`${options.tag}(${discoverer.LogicalId})`)(function* (
196 request?: Omit<I, "DiscovererId">,
197 ) {
198 return yield* op({
199 ...request,
200 DiscovererId: yield* DiscovererId,
201 } as I);
202 });
203 });
204 });
205
206/**
207 * Build the impl Effect for an account-level Schemas operation (e.g.
208 * `GetDiscoveredSchema`, which infers a schema from sample events and is not
209 * scoped to any registry or schema resource). The deploy-time half grants
210 * `actions` on `*`.
211 */
212export const makeSchemasAccountHttpBinding = <I, A, E, R>(options: {
213 /** Fully-qualified binding tag, e.g. `AWS.Schemas.GetDiscoveredSchema`. */
214 tag: string;
215 /** The distilled operation, invoked with the caller's request as-is. */
216 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
217 /** IAM actions granted on `*`. */
218 actions: readonly string[];
219}) =>
220 Effect.gen(function* () {
221 const op = yield* options.operation;
222
223 return Effect.fn(function* () {
224 if (!globalThis.__ALCHEMY_RUNTIME__) {
225 const host = yield* Binding.Host;
226 if (isBindingHost(host)) {
227 yield* host.bind`Allow(${host}, ${options.tag}())`({
228 policyStatements: [
229 {
230 Effect: "Allow",
231 Action: [...options.actions],
232 Resource: ["*"],
233 },
234 ],
235 });
236 }
237 }
238 return Effect.fn(options.tag)(function* (request?: I) {
239 return yield* op((request ?? {}) as I);
240 });
241 });
242 });