1/**
2 * Shared HTTP-binding scaffolding for the AppIntegrations service.
3 * NOT exported from the service barrel — each `{Op}Http.ts` is a thin
4 * one-call `Layer.effect(Cap, makeAppIntegrations*HttpBinding({ ... }))`.
5 */
6import * as Effect from "effect/Effect";
7import * as Binding from "../../Binding.ts";
8import type { Input } from "../../Input.ts";
9import type { Output } from "../../Output.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import { isBindingHost } from "../Lambda/Function.ts";
12
13/** Deploy-time environment handed to the `resources` IAM builder. */
14export interface AppIntegrationsIamEnv {
15 region: string;
16 accountId: string;
17}
18
19/**
20 * Build the implementation effect for a resource-scoped AppIntegrations HTTP
21 * binding: resolve the resource identifier at init, grant the IAM actions on
22 * the host at deploy time, and inject the identifier into every request at
23 * runtime under `requestKey`.
24 */
25export const makeAppIntegrationsHttpBinding = <
26 Res extends { LogicalId: string },
27 IdKey extends string,
28 WireReq extends { [K in IdKey]?: unknown },
29 Out,
30 Err,
31 OpR,
32>(options: {
33 /** Capability name used in the bind sid and runtime span. */
34 name: string;
35 /** The distilled AppIntegrations operation backing the binding. */
36 operation: Effect.Effect<
37 (input: WireReq) => Effect.Effect<Out, Err>,
38 never,
39 OpR
40 >;
41 /** Wire key the resolved resource identifier is injected under. */
42 requestKey: IdKey;
43 /** The resource attribute used as the wire identifier. */
44 identifier: (resource: Res) => Output<string, never>;
45 /** IAM actions granted on the host at deploy time. */
46 iamActions: string[];
47 /** IAM resources the actions are granted on. */
48 resources: (resource: Res, env: AppIntegrationsIamEnv) => Input<string>[];
49 /**
50 * Dependent actions the AppIntegrations service invokes on the caller's
51 * behalf (e.g. `appflow:CreateFlow` for data-integration associations).
52 * Granted on `*` in a second statement — these AWS-managed side resources
53 * have no stable ARN at deploy time.
54 */
55 dependentActions?: string[];
56}) =>
57 Effect.gen(function* () {
58 const op = yield* options.operation;
59
60 return Effect.fn(function* (resource: Res) {
61 const Identifier = yield* options.identifier(resource);
62 if (!globalThis.__ALCHEMY_RUNTIME__) {
63 const host = yield* Binding.Host;
64 if (isBindingHost(host)) {
65 const { accountId, region } =
66 yield* AWSEnvironment.current as unknown as Effect.Effect<{
67 accountId: string;
68 region: string;
69 }>;
70 yield* host.bind`Allow(${host}, AWS.AppIntegrations.${options.name}(${resource}))`(
71 {
72 policyStatements: [
73 {
74 Effect: "Allow",
75 Action: options.iamActions,
76 Resource: options.resources(resource, { region, accountId }),
77 },
78 ...(options.dependentActions?.length
79 ? [
80 {
81 Effect: "Allow" as const,
82 Action: options.dependentActions,
83 Resource: ["*"],
84 },
85 ]
86 : []),
87 ],
88 },
89 );
90 }
91 }
92 return Effect.fn(
93 `AWS.AppIntegrations.${options.name}(${resource.LogicalId})`,
94 )(function* (request?: Omit<WireReq, IdKey>) {
95 // The identifier key is re-added on top of the caller's request, so
96 // the widened spread is exactly a WireReq.
97 return yield* op({
98 ...request,
99 [options.requestKey]: yield* Identifier,
100 } as unknown as WireReq);
101 });
102 });
103 });
104
105/**
106 * Build the implementation effect for an account-scoped AppIntegrations HTTP
107 * binding (no resource argument): grant the IAM actions on `*` at deploy time
108 * and forward requests to the operation at runtime.
109 */
110export const makeAppIntegrationsAccountHttpBinding = <
111 WireReq,
112 Out,
113 Err,
114 OpR,
115>(options: {
116 /** Capability name used in the bind sid and runtime span. */
117 name: string;
118 /** The distilled AppIntegrations operation backing the binding. */
119 operation: Effect.Effect<
120 (input: WireReq) => Effect.Effect<Out, Err>,
121 never,
122 OpR
123 >;
124 /** IAM actions granted on the host at deploy time. */
125 iamActions: string[];
126}) =>
127 Effect.gen(function* () {
128 const op = yield* options.operation;
129
130 return Effect.fn(function* () {
131 if (!globalThis.__ALCHEMY_RUNTIME__) {
132 const host = yield* Binding.Host;
133 if (isBindingHost(host)) {
134 yield* host.bind`Allow(${host}, AWS.AppIntegrations.${options.name}())`(
135 {
136 policyStatements: [
137 {
138 Effect: "Allow",
139 Action: options.iamActions,
140 Resource: ["*"],
141 },
142 ],
143 },
144 );
145 }
146 }
147 return Effect.fn(`AWS.AppIntegrations.${options.name}`)(function* (
148 request?: WireReq,
149 ) {
150 // List requests are all-optional structs; an absent request is the
151 // empty request.
152 return yield* op((request ?? {}) as WireReq);
153 });
154 });
155 });