1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import { isBindingHost } from "../Lambda/Function.ts";
4import type { GraphqlApi } from "./GraphqlApi.ts";
5
6/**
7 * Shared scaffolding for AppSync HTTP bindings.
8 *
9 * NOT exported from `index.ts` — every single-operation `{Op}Http.ts` in
10 * this service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over
11 * one of the two builders below. Everything except the operation and the
12 * IAM action is boilerplate:
13 *
14 * - {@link makeAppSyncApiHttpBinding} — control-plane operations scoped to a
15 * bound {@link GraphqlApi} (`FlushApiCache`, `GetIntrospectionSchema`).
16 * The runtime callable injects the bound API's `apiId`.
17 * - {@link makeAppSyncAccountHttpBinding} — account-level pass-through
18 * operations that bind to no resource (`EvaluateCode`).
19 *
20 * Both grant on `Resource: "*"` — these AppSync control-plane actions define
21 * no IAM resource types (per the Service Authorization Reference), so a
22 * scoped API ARN is AccessDenied. `GraphQLHttp` stays bespoke: the data
23 * plane has no SDK operation, each request is a SigV4-signed POST to the
24 * API's `graphqlUrl`.
25 */
26
27/**
28 * Build the impl Effect for a control-plane operation scoped to a bound
29 * {@link GraphqlApi}. The runtime callable injects the API's `apiId` into
30 * the request; the deploy-time half grants `actions` on `*` (these actions
31 * define no IAM resource types).
32 */
33export const makeAppSyncApiHttpBinding = <
34 I extends { apiId: string },
35 A,
36 E,
37 R,
38>(options: {
39 /** Fully-qualified binding tag, e.g. `AWS.AppSync.FlushApiCache`. */
40 tag: string;
41 /** The distilled operation; `apiId` is injected from the bound API. */
42 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
43 /** IAM actions granted on `*`. */
44 actions: readonly string[];
45}) =>
46 Effect.gen(function* () {
47 const op = yield* options.operation;
48
49 return Effect.fn(function* (api: GraphqlApi) {
50 // Outputs yield DEFERRED effects — resolving them here registers the
51 // attribute on the host environment; re-yield per invocation below.
52 const ApiId = yield* api.apiId;
53
54 if (!globalThis.__ALCHEMY_RUNTIME__) {
55 const host = yield* Binding.Host;
56 if (isBindingHost(host)) {
57 yield* host.bind`Allow(${host}, ${options.tag}(${api}))`({
58 policyStatements: [
59 {
60 Effect: "Allow",
61 Action: [...options.actions],
62 // These AppSync control-plane actions define no IAM resource
63 // types — IAM only authorizes them against "*" (a scoped API
64 // ARN is AccessDenied).
65 Resource: ["*"],
66 },
67 ],
68 });
69 }
70 }
71
72 return Effect.fn(`${options.tag}(${api.LogicalId})`)(function* (
73 request?: Omit<I, "apiId">,
74 ) {
75 return yield* op({ ...request, apiId: yield* ApiId } as I);
76 });
77 });
78 });
79
80/**
81 * Build the impl Effect for an account-level AppSync operation that binds
82 * to no resource. The runtime callable passes the caller's request through
83 * unchanged; the deploy-time half grants `actions` on `*` (these actions
84 * define no IAM resource types).
85 */
86export const makeAppSyncAccountHttpBinding = <I, A, E, R>(options: {
87 /** Fully-qualified binding tag, e.g. `AWS.AppSync.EvaluateCode`. */
88 tag: string;
89 /** The distilled operation, invoked with the caller's request as-is. */
90 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
91 /** IAM actions granted on `*`. */
92 actions: readonly string[];
93}) =>
94 Effect.gen(function* () {
95 const op = yield* options.operation;
96
97 return Effect.fn(function* () {
98 if (!globalThis.__ALCHEMY_RUNTIME__) {
99 const host = yield* Binding.Host;
100 if (isBindingHost(host)) {
101 yield* host.bind`Allow(${host}, ${options.tag}())`({
102 policyStatements: [
103 {
104 Effect: "Allow",
105 Action: [...options.actions],
106 // No IAM resource types defined for these actions.
107 Resource: ["*"],
108 },
109 ],
110 });
111 }
112 }
113
114 return Effect.fn(options.tag)(function* (request: I) {
115 return yield* op(request);
116 });
117 });
118 });