1/**
2 * Shared scaffolding for the Amazon S3 Control HTTP bindings.
3 *
4 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
5 * a `Layer.effect(Cap, makeS3Control…HttpBinding({ … }))` over one of the
6 * builders below. Every S3 Control request carries the owning `AccountId`
7 * (an endpoint host label), so each builder's only real job is deciding
8 * where that account id comes from:
9 *
10 * - Access-point-scoped operations inject `AccountId` + `Name` from the
11 * bound {@link AccessPoint} and are granted on the access point ARN.
12 * - Multi-Region Access Point operations inject `AccountId` + `Mrap` from
13 * the bound {@link MultiRegionAccessPoint}, are granted on the MRAP ARN,
14 * and are pinned to `us-west-2` (the region the MRAP control plane and
15 * route APIs are served from).
16 * - Account-level operations (access point listing, S3 Batch Operations
17 * jobs) resolve the caller's account once via `sts:GetCallerIdentity`
18 * (cached per binding) and are granted on `*` — job ARNs are
19 * server-assigned at runtime and unknowable at deploy time.
20 */
21import { Region } from "@distilled.cloud/aws/Region";
22import * as sts from "@distilled.cloud/aws/sts";
23import * as Effect from "effect/Effect";
24import * as Binding from "../../Binding.ts";
25import type { PolicyStatement } from "../IAM/Policy.ts";
26import { isBindingHost } from "../Lambda/Function.ts";
27import type { AccessPoint } from "./AccessPoint.ts";
28import type { MultiRegionAccessPoint } from "./MultiRegionAccessPoint.ts";
29
30/**
31 * All Multi-Region Access Point control-plane requests are routed to the
32 * US West (Oregon) region regardless of the ambient region — mirrors the
33 * {@link MultiRegionAccessPoint} resource provider.
34 */
35const inMrapRegion = <A, E, R>(
36 self: Effect.Effect<A, E, R>,
37): Effect.Effect<A, E, Exclude<R, Region>> =>
38 self.pipe(Effect.provideService(Region, Effect.succeed("us-west-2")));
39
40/**
41 * Build the impl Effect for an S3 Control operation scoped to an
42 * {@link AccessPoint}: the deploy-time half grants `actions` on the bound
43 * access point's ARN, and the runtime half injects the access point's
44 * `AccountId` and `Name` into every request.
45 */
46export const makeS3ControlAccessPointHttpBinding = <
47 I extends { AccountId: string; Name: string },
48 A,
49 E,
50 R,
51>(options: {
52 /** Fully-qualified binding tag, e.g. `AWS.S3Control.GetAccessPoint`. */
53 tag: string;
54 /** The distilled operation; `AccountId`/`Name` are injected. */
55 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
56 /** IAM actions granted on the access point ARN. */
57 actions: readonly string[];
58}) =>
59 Effect.gen(function* () {
60 const op = yield* options.operation;
61
62 return Effect.fn(function* (accessPoint: AccessPoint) {
63 const AccountId = yield* accessPoint.accountId;
64 const Name = yield* accessPoint.accessPointName;
65 if (!globalThis.__ALCHEMY_RUNTIME__) {
66 const host = yield* Binding.Host;
67 if (isBindingHost(host)) {
68 yield* host.bind`Allow(${host}, ${options.tag}(${accessPoint}))`({
69 policyStatements: [
70 {
71 Effect: "Allow",
72 Action: [...options.actions],
73 Resource: [accessPoint.accessPointArn],
74 },
75 ],
76 });
77 }
78 }
79 return Effect.fn(`${options.tag}(${accessPoint.LogicalId})`)(function* (
80 request?: Omit<I, "AccountId" | "Name">,
81 ) {
82 return yield* op({
83 ...request,
84 AccountId: yield* AccountId,
85 Name: yield* Name,
86 } as I);
87 });
88 });
89 });
90
91/**
92 * Build the impl Effect for an S3 Control operation scoped to a
93 * {@link MultiRegionAccessPoint}: the deploy-time half grants `actions` on
94 * the bound MRAP's ARN, and the runtime half injects the MRAP's `AccountId`
95 * and alias-addressed ARN (`Mrap`) into every request, pinned to the
96 * `us-west-2` MRAP control-plane region.
97 */
98export const makeS3ControlMrapHttpBinding = <
99 I extends { AccountId: string; Mrap: string },
100 A,
101 E,
102 R,
103>(options: {
104 /** Fully-qualified binding tag, e.g. `AWS.S3Control.GetMultiRegionAccessPointRoutes`. */
105 tag: string;
106 /** The distilled operation; `AccountId`/`Mrap` are injected. */
107 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
108 /** IAM actions granted on the Multi-Region Access Point ARN. */
109 actions: readonly string[];
110}) =>
111 Effect.gen(function* () {
112 const op = yield* inMrapRegion(options.operation);
113
114 return Effect.fn(function* (mrap: MultiRegionAccessPoint) {
115 const AccountId = yield* mrap.accountId;
116 const Mrap = yield* mrap.multiRegionAccessPointArn;
117 if (!globalThis.__ALCHEMY_RUNTIME__) {
118 const host = yield* Binding.Host;
119 if (isBindingHost(host)) {
120 yield* host.bind`Allow(${host}, ${options.tag}(${mrap}))`({
121 policyStatements: [
122 {
123 Effect: "Allow",
124 Action: [...options.actions],
125 Resource: [mrap.multiRegionAccessPointArn],
126 },
127 ],
128 });
129 }
130 }
131 return Effect.fn(`${options.tag}(${mrap.LogicalId})`)(function* (
132 request?: Omit<I, "AccountId" | "Mrap">,
133 ) {
134 return yield* op({
135 ...request,
136 AccountId: yield* AccountId,
137 Mrap: yield* Mrap,
138 } as I);
139 });
140 });
141 });
142
143/**
144 * Build the impl Effect for an account-level S3 Control operation (access
145 * point listing, S3 Batch Operations jobs). The deploy-time half grants
146 * `actions` on `*` — job ARNs are server-assigned at runtime and unknowable
147 * at deploy time. The runtime half resolves the caller's account id once via
148 * `sts:GetCallerIdentity` (needs no extra IAM permission) and injects it as
149 * `AccountId`.
150 */
151export const makeS3ControlAccountHttpBinding = <
152 I extends { AccountId: string },
153 A,
154 E,
155 R,
156>(options: {
157 /** Fully-qualified binding tag, e.g. `AWS.S3Control.ListJobs`. */
158 tag: string;
159 /** The distilled operation; `AccountId` is injected. */
160 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
161 /** IAM actions granted on `*`. */
162 actions: readonly string[];
163 /**
164 * Grant `iam:PassRole` so the function can hand S3 Batch Operations the
165 * execution role named in the job. Set on `CreateJob`. Matches the AWS
166 * Batch Operations permission model (an `iam:PassedToService` condition is
167 * NOT populated for `s3:CreateJob` — IAM denies a conditioned grant).
168 */
169 passRole?: boolean;
170}) =>
171 Effect.gen(function* () {
172 const op = yield* options.operation;
173 const getCallerIdentity = yield* sts.getCallerIdentity;
174
175 return Effect.fn(function* () {
176 if (!globalThis.__ALCHEMY_RUNTIME__) {
177 const host = yield* Binding.Host;
178 if (isBindingHost(host)) {
179 const policyStatements: PolicyStatement[] = [
180 {
181 Effect: "Allow",
182 Action: [...options.actions],
183 Resource: ["*"],
184 },
185 ];
186 if (options.passRole) {
187 // No `iam:PassedToService` condition — IAM does not populate the
188 // key for `s3:CreateJob`, so a conditioned grant is always
189 // denied. This matches the policy AWS documents for Batch
190 // Operations job creators.
191 policyStatements.push({
192 Effect: "Allow",
193 Action: ["iam:PassRole"],
194 Resource: ["*"],
195 });
196 }
197 yield* host.bind`Allow(${host}, ${options.tag}())`({
198 policyStatements,
199 });
200 }
201 }
202 // Resolve the caller's account id lazily (first call inside the
203 // Lambda) and cache it for the life of the binding.
204 const accountId = yield* Effect.cached(
205 getCallerIdentity({}).pipe(Effect.map((r) => r.Account!)),
206 );
207 return Effect.fn(options.tag)(function* (request?: Omit<I, "AccountId">) {
208 return yield* op({
209 ...request,
210 AccountId: yield* accountId,
211 } as I);
212 });
213 });
214 });