1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import { isBindingHost } from "../Lambda/Function.ts";
4import type { PlatformApplication } from "./PlatformApplication.ts";
5import type { Subscription } from "./Subscription.ts";
6import type { Topic } from "./Topic.ts";
7
8/**
9 * Shared scaffolding for AWS SNS HTTP bindings.
10 *
11 * NOT exported from `index.ts` — every near-identical `{Op}Http.ts` in this
12 * service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of
13 * the builders below. Everything except the operation, the IAM action list,
14 * and the injected identifier is boilerplate. Genuinely-different bindings
15 * (the batched `TopicSink`) stay bespoke.
16 */
17
18/**
19 * Build the impl Effect for an account-level operation (`ListTopics`,
20 * `ListSubscriptions`, the SMS/sandbox APIs): the runtime callable passes
21 * the caller's request through unchanged and the deploy-time half grants
22 * `actions` on `*` (these SNS actions do not support resource-level
23 * permissions — only topic-scoped actions do).
24 */
25export const makeSnsAccountHttpBinding = <I, A, E, R>(options: {
26 /** Fully-qualified binding tag, e.g. `AWS.SNS.ListTopics`. */
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 topic-scoped operation: the runtime callable
59 * injects the bound {@link Topic}'s ARN under `key` (`TopicArn` for the
60 * pub/sub and attribute APIs, `ResourceArn` for the tagging and
61 * data-protection-policy APIs) and the deploy-time half grants `actions` on
62 * the topic's ARN.
63 */
64export const makeSnsTopicHttpBinding = <
65 K extends "TopicArn" | "ResourceArn",
66 I extends { [P in K]?: string },
67 A,
68 E,
69 R,
70>(options: {
71 /** Fully-qualified binding tag, e.g. `AWS.SNS.Publish`. */
72 tag: string;
73 /** The distilled operation; the topic ARN is injected under `key`. */
74 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
75 /** IAM actions granted on the topic ARN. */
76 actions: readonly string[];
77 /** The request field the topic ARN is injected under. */
78 key: K;
79}) =>
80 Effect.gen(function* () {
81 const op = yield* options.operation;
82
83 return Effect.fn(function* (topic: Topic) {
84 const TopicArn = yield* topic.topicArn;
85 if (!globalThis.__ALCHEMY_RUNTIME__) {
86 const host = yield* Binding.Host;
87 if (isBindingHost(host)) {
88 yield* host.bind`Allow(${host}, ${options.tag}(${topic}))`({
89 policyStatements: [
90 {
91 Effect: "Allow",
92 Action: [...options.actions],
93 Resource: [topic.topicArn],
94 },
95 ],
96 });
97 }
98 }
99 return Effect.fn(`${options.tag}(${topic.LogicalId})`)(function* (
100 request?: Omit<I, K>,
101 ) {
102 return yield* op({
103 ...request,
104 [options.key]: yield* TopicArn,
105 } as I);
106 });
107 });
108 });
109
110/**
111 * Build the impl Effect for a subscription-scoped operation: the runtime
112 * callable injects the bound {@link Subscription}'s identity under `key`
113 * (`SubscriptionArn` for the attribute APIs, `TopicArn` for
114 * `ConfirmSubscription`) and the deploy-time half grants `actions` on the
115 * subscription's parent topic ARN (SNS checks subscription actions against
116 * the topic resource).
117 */
118export const makeSnsSubscriptionHttpBinding = <
119 K extends "SubscriptionArn" | "TopicArn",
120 I extends { [P in K]?: string },
121 A,
122 E,
123 R,
124>(options: {
125 /** Fully-qualified binding tag, e.g. `AWS.SNS.GetSubscriptionAttributes`. */
126 tag: string;
127 /** The distilled operation; the subscription identity is injected under `key`. */
128 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
129 /** IAM actions granted on the subscription's topic ARN. */
130 actions: readonly string[];
131 /** The request field the subscription identity is injected under. */
132 key: K;
133}) =>
134 Effect.gen(function* () {
135 const op = yield* options.operation;
136
137 return Effect.fn(function* (subscription: Subscription) {
138 const identity = yield* options.key === "TopicArn"
139 ? subscription.topicArn
140 : subscription.subscriptionArn;
141 if (!globalThis.__ALCHEMY_RUNTIME__) {
142 const host = yield* Binding.Host;
143 if (isBindingHost(host)) {
144 yield* host.bind`Allow(${host}, ${options.tag}(${subscription}))`({
145 policyStatements: [
146 {
147 Effect: "Allow",
148 Action: [...options.actions],
149 Resource: [subscription.topicArn],
150 },
151 ],
152 });
153 }
154 }
155 return Effect.fn(`${options.tag}(${subscription.LogicalId})`)(function* (
156 request?: Omit<I, K>,
157 ) {
158 return yield* op({
159 ...request,
160 [options.key]: yield* identity,
161 } as I);
162 });
163 });
164 });
165
166/**
167 * Build the impl Effect for a platform-application-scoped mobile-push
168 * operation whose request accepts `PlatformApplicationArn`
169 * (`CreatePlatformEndpoint`, `ListEndpointsByPlatformApplication`): the
170 * runtime callable injects the bound {@link PlatformApplication}'s ARN and
171 * the deploy-time half grants `actions` on `*` (SNS mobile-push actions do
172 * not support resource-level permissions).
173 */
174export const makeSnsPlatformHttpBinding = <
175 I extends { PlatformApplicationArn?: string },
176 A,
177 E,
178 R,
179>(options: {
180 /** Fully-qualified binding tag, e.g. `AWS.SNS.CreatePlatformEndpoint`. */
181 tag: string;
182 /** The distilled operation; the application ARN is injected. */
183 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
184 /** IAM actions granted on `*` (no resource-level support). */
185 actions: readonly string[];
186}) =>
187 Effect.gen(function* () {
188 const op = yield* options.operation;
189
190 return Effect.fn(function* (application: PlatformApplication) {
191 const PlatformApplicationArn = yield* application.platformApplicationArn;
192 if (!globalThis.__ALCHEMY_RUNTIME__) {
193 const host = yield* Binding.Host;
194 if (isBindingHost(host)) {
195 yield* host.bind`Allow(${host}, ${options.tag}(${application}))`({
196 policyStatements: [
197 {
198 Effect: "Allow",
199 Action: [...options.actions],
200 Resource: ["*"],
201 },
202 ],
203 });
204 }
205 }
206 return Effect.fn(`${options.tag}(${application.LogicalId})`)(function* (
207 request?: Omit<I, "PlatformApplicationArn">,
208 ) {
209 return yield* op({
210 ...request,
211 PlatformApplicationArn: yield* PlatformApplicationArn,
212 } as I);
213 });
214 });
215 });
216
217/**
218 * Build the impl Effect for an endpoint-scoped mobile-push operation
219 * (`GetEndpointAttributes`, `SetEndpointAttributes`, `DeleteEndpoint`,
220 * publish-to-endpoint): the caller's request already carries the endpoint
221 * identity (`EndpointArn`/`TargetArn`) and is passed through unchanged; the
222 * binding is still scoped to a {@link PlatformApplication} for IAM naming
223 * and grants `actions` on `*` (SNS mobile-push actions do not support
224 * resource-level permissions).
225 */
226export const makeSnsEndpointHttpBinding = <I, A, E, R>(options: {
227 /** Fully-qualified binding tag, e.g. `AWS.SNS.DeleteEndpoint`. */
228 tag: string;
229 /** The distilled operation, invoked with the caller's request as-is. */
230 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
231 /** IAM actions granted on `*` (no resource-level support). */
232 actions: readonly string[];
233}) =>
234 Effect.gen(function* () {
235 const op = yield* options.operation;
236
237 return Effect.fn(function* (application: PlatformApplication) {
238 if (!globalThis.__ALCHEMY_RUNTIME__) {
239 const host = yield* Binding.Host;
240 if (isBindingHost(host)) {
241 yield* host.bind`Allow(${host}, ${options.tag}(${application}))`({
242 policyStatements: [
243 {
244 Effect: "Allow",
245 Action: [...options.actions],
246 Resource: ["*"],
247 },
248 ],
249 });
250 }
251 }
252 return Effect.fn(`${options.tag}(${application.LogicalId})`)(function* (
253 request: I,
254 ) {
255 return yield* op(request);
256 });
257 });
258 });