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 { Application } from "./Application.ts";
6import type { DataSource } from "./DataSource.ts";
7import type { Index } from "./SearchIndex.ts";
8import type { WebExperience } from "./WebExperience.ts";
9
10/**
11 * Shared scaffolding for AWS QBusiness HTTP bindings.
12 *
13 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
14 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the four
15 * builders below. Everything except the operation and the IAM action list
16 * is boilerplate: every Amazon Q Business operation is scoped to one
17 * application (whose id is injected as `applicationId`), and the
18 * index/data-source/web-experience operations additionally inject their
19 * sub-resource ids and receive grants on the sub-resource ARN plus its
20 * parents (Q Business authorizes most actions against both the application
21 * and the sub-resource).
22 */
23
24/**
25 * Build the impl Effect for an application-scoped Q Business operation
26 * (chat, conversations, users, subscriptions, chat controls, policy): the
27 * runtime callable injects the bound {@link Application}'s id as
28 * `applicationId` and the deploy-time half grants `actions` on the
29 * application ARN (plus any `subResources` suffix patterns, e.g.
30 * `retriever/*` for `SearchRelevantContent`, which Q Business additionally
31 * authorizes against the retriever it searches).
32 */
33export const makeQBusinessApplicationHttpBinding = <
34 I extends { applicationId: string },
35 A,
36 E,
37 R,
38>(options: {
39 /** Fully-qualified binding tag, e.g. `AWS.QBusiness.ChatSync`. */
40 tag: string;
41 /** The distilled operation; `applicationId` is injected from the application. */
42 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
43 /** IAM actions granted on the application ARN. */
44 actions: readonly string[];
45 /**
46 * Extra ARN suffix patterns (relative to the application ARN) the
47 * actions are also granted on, e.g. `retriever/*`.
48 */
49 subResources?: readonly string[];
50}) =>
51 Effect.gen(function* () {
52 const op = yield* options.operation;
53
54 return Effect.fn(function* (application: Application) {
55 const applicationId = yield* application.applicationId;
56 if (!globalThis.__ALCHEMY_RUNTIME__) {
57 const host = yield* Binding.Host;
58 if (isBindingHost(host)) {
59 yield* host.bind`Allow(${host}, ${options.tag}(${application}))`({
60 policyStatements: [
61 {
62 Effect: "Allow",
63 Action: [...options.actions],
64 Resource: [
65 application.applicationArn,
66 ...(options.subResources ?? []).map(
67 (suffix) =>
68 Output.interpolate`${application.applicationArn}/${suffix}`,
69 ),
70 ],
71 },
72 ],
73 });
74 }
75 }
76 return Effect.fn(`${options.tag}(${application.LogicalId})`)(function* (
77 request?: Omit<I, "applicationId">,
78 ) {
79 return yield* op({
80 ...request,
81 applicationId: yield* applicationId,
82 } as I);
83 });
84 });
85 });
86
87/**
88 * Build the impl Effect for an index-scoped Q Business operation (document
89 * batches, groups, document reads): the runtime callable injects the bound
90 * {@link Index}'s `applicationId` + `indexId` and the deploy-time half
91 * grants `actions` on the index ARN **and** its parent application ARN —
92 * Q Business authorizes index actions against both.
93 */
94export const makeQBusinessIndexHttpBinding = <
95 I extends { applicationId: string; indexId: string },
96 A,
97 E,
98 R,
99>(options: {
100 /** Fully-qualified binding tag, e.g. `AWS.QBusiness.BatchPutDocument`. */
101 tag: string;
102 /**
103 * The distilled operation; `applicationId` and `indexId` are injected
104 * from the index.
105 */
106 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
107 /** IAM actions granted on the index ARN + its parent application ARN. */
108 actions: readonly string[];
109 /**
110 * Extra ARN suffix patterns (relative to the index ARN) the actions are
111 * also granted on, e.g. `data-source/*` for the principal-group
112 * operations that also act on data-source-scoped groups.
113 */
114 subResources?: readonly string[];
115}) =>
116 Effect.gen(function* () {
117 const op = yield* options.operation;
118
119 return Effect.fn(function* (index: Index) {
120 const applicationId = yield* index.applicationId;
121 const indexId = yield* index.indexId;
122 if (!globalThis.__ALCHEMY_RUNTIME__) {
123 const host = yield* Binding.Host;
124 if (isBindingHost(host)) {
125 yield* host.bind`Allow(${host}, ${options.tag}(${index}))`({
126 policyStatements: [
127 {
128 Effect: "Allow",
129 Action: [...options.actions],
130 Resource: [
131 index.indexArn,
132 // The index ARN is `…:application/{appId}/index/{indexId}`
133 // — the parent application ARN is its prefix.
134 index.indexArn.pipe(
135 Output.map((arn) => arn.split("/index/")[0]!),
136 ),
137 ...(options.subResources ?? []).map(
138 (suffix) => Output.interpolate`${index.indexArn}/${suffix}`,
139 ),
140 ],
141 },
142 ],
143 });
144 }
145 }
146 return Effect.fn(`${options.tag}(${index.LogicalId})`)(function* (
147 request?: Omit<I, "applicationId" | "indexId">,
148 ) {
149 return yield* op({
150 ...request,
151 applicationId: yield* applicationId,
152 indexId: yield* indexId,
153 } as I);
154 });
155 });
156 });
157
158/**
159 * Build the impl Effect for a data-source-scoped Q Business operation (the
160 * sync job start/stop/list trio): the runtime callable injects the bound
161 * {@link DataSource}'s `applicationId` + `indexId` + `dataSourceId`; the
162 * deploy-time half grants `actions` on the data source ARN **and** its
163 * parent index + application ARNs.
164 */
165export const makeQBusinessDataSourceHttpBinding = <
166 I extends { applicationId: string; indexId: string; dataSourceId: string },
167 A,
168 E,
169 R,
170>(options: {
171 /** Fully-qualified binding tag, e.g. `AWS.QBusiness.StartDataSourceSyncJob`. */
172 tag: string;
173 /**
174 * The distilled operation; `applicationId`, `indexId`, and
175 * `dataSourceId` are injected from the data source.
176 */
177 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
178 /**
179 * IAM actions granted on the data source ARN + its parent index and
180 * application ARNs.
181 */
182 actions: readonly string[];
183}) =>
184 Effect.gen(function* () {
185 const op = yield* options.operation;
186
187 return Effect.fn(function* (dataSource: DataSource) {
188 const applicationId = yield* dataSource.applicationId;
189 const indexId = yield* dataSource.indexId;
190 const dataSourceId = yield* dataSource.dataSourceId;
191 if (!globalThis.__ALCHEMY_RUNTIME__) {
192 const host = yield* Binding.Host;
193 if (isBindingHost(host)) {
194 yield* host.bind`Allow(${host}, ${options.tag}(${dataSource}))`({
195 policyStatements: [
196 {
197 Effect: "Allow",
198 Action: [...options.actions],
199 Resource: [
200 dataSource.dataSourceArn,
201 // The data source ARN is
202 // `…:application/{appId}/index/{indexId}/data-source/{dsId}`
203 // — the parent index and application ARNs are prefixes.
204 dataSource.dataSourceArn.pipe(
205 Output.map((arn) => arn.split("/data-source/")[0]!),
206 ),
207 dataSource.dataSourceArn.pipe(
208 Output.map((arn) => arn.split("/index/")[0]!),
209 ),
210 ],
211 },
212 ],
213 });
214 }
215 }
216 return Effect.fn(`${options.tag}(${dataSource.LogicalId})`)(function* (
217 request?: Omit<I, "applicationId" | "indexId" | "dataSourceId">,
218 ) {
219 return yield* op({
220 ...request,
221 applicationId: yield* applicationId,
222 indexId: yield* indexId,
223 dataSourceId: yield* dataSourceId,
224 } as I);
225 });
226 });
227 });
228
229/**
230 * Build the impl Effect for a web-experience-scoped Q Business operation
231 * (anonymous URL minting): the runtime callable injects the bound
232 * {@link WebExperience}'s `applicationId` + `webExperienceId`; the
233 * deploy-time half grants `actions` on the web experience ARN **and** its
234 * parent application ARN.
235 */
236export const makeQBusinessWebExperienceHttpBinding = <
237 I extends { applicationId: string; webExperienceId: string },
238 A,
239 E,
240 R,
241 Req = Omit<I, "applicationId" | "webExperienceId">,
242>(options: {
243 /** Fully-qualified binding tag, e.g. `AWS.QBusiness.CreateAnonymousWebExperienceUrl`. */
244 tag: string;
245 /**
246 * The distilled operation; `applicationId` and `webExperienceId` are
247 * injected from the web experience.
248 */
249 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
250 /**
251 * IAM actions granted on the web experience ARN + its parent application
252 * ARN.
253 */
254 actions: readonly string[];
255 /**
256 * Map the public request shape to the wire request (defaults to
257 * identity) — e.g. `CreateAnonymousWebExperienceUrl` converts a
258 * `Duration.Input` into the wire `sessionDurationInMinutes`.
259 */
260 prepare?: (
261 request: Req | undefined,
262 ) => Omit<I, "applicationId" | "webExperienceId">;
263}) =>
264 Effect.gen(function* () {
265 const op = yield* options.operation;
266
267 return Effect.fn(function* (webExperience: WebExperience) {
268 const applicationId = yield* webExperience.applicationId;
269 const webExperienceId = yield* webExperience.webExperienceId;
270 if (!globalThis.__ALCHEMY_RUNTIME__) {
271 const host = yield* Binding.Host;
272 if (isBindingHost(host)) {
273 yield* host.bind`Allow(${host}, ${options.tag}(${webExperience}))`({
274 policyStatements: [
275 {
276 Effect: "Allow",
277 Action: [...options.actions],
278 Resource: [
279 webExperience.webExperienceArn,
280 // The web experience ARN is
281 // `…:application/{appId}/web-experience/{weId}` — the
282 // parent application ARN is its prefix.
283 webExperience.webExperienceArn.pipe(
284 Output.map((arn) => arn.split("/web-experience/")[0]!),
285 ),
286 ],
287 },
288 ],
289 });
290 }
291 }
292 return Effect.fn(`${options.tag}(${webExperience.LogicalId})`)(function* (
293 request?: Req,
294 ) {
295 const wire = options.prepare
296 ? options.prepare(request)
297 : (request as unknown as
298 | Omit<I, "applicationId" | "webExperienceId">
299 | undefined);
300 return yield* op({
301 ...wire,
302 applicationId: yield* applicationId,
303 webExperienceId: yield* webExperienceId,
304 } as I);
305 });
306 });
307 });