| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import * as Output from "../../Output.ts"; |
| 4 | import { isBindingHost } from "../Lambda/Function.ts"; |
| 5 | import type { QApp } from "./QApp.ts"; |
| 6 | |
| 7 | /** |
| 8 | * Shared HTTP scaffolding for the Amazon Q Apps runtime bindings. |
| 9 | * |
| 10 | * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a |
| 11 | * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders |
| 12 | * below. Everything except the operation and the IAM action list is |
| 13 | * boilerplate: every Q Apps operation runs against the Q Business |
| 14 | * application environment instance of the bound {@link QApp} (its |
| 15 | * `instanceId` is injected as the `instance-id` header), and app-keyed |
| 16 | * operations additionally inject the bound app's `appId`. |
| 17 | * |
| 18 | * Two IAM grant shapes: |
| 19 | * |
| 20 | * - {@link makeQAppHttpBinding} — operations keyed by the bound Q App or one |
| 21 | * of its sessions. Granted on the app's ARN and its sub-resources |
| 22 | * (`{appArn}`, `{appArn}/*` — Q App session ARNs are children of the app |
| 23 | * ARN). |
| 24 | * - {@link makeQAppsInstanceHttpBinding} — instance-level operations |
| 25 | * (library items, categories, listing, generation). Library-item and |
| 26 | * category authorization ARNs are service-assigned and not derivable from |
| 27 | * a bound Q App, so these grant on `Resource: ["*"]`. |
| 28 | */ |
| 29 | |
| 30 | /** |
| 31 | * Build the impl Effect for a Q Apps operation keyed by the bound |
| 32 | * {@link QApp} (or one of its sessions): the runtime callable injects the |
| 33 | * app's `instanceId` (and, with `injectAppId`, its `appId`); the deploy-time |
| 34 | * half grants `iamActions` on the app ARN and its session sub-resources. |
| 35 | */ |
| 36 | export const makeQAppHttpBinding = < |
| 37 | I extends { instanceId: string }, |
| 38 | A, |
| 39 | E, |
| 40 | R, |
| 41 | >(options: { |
| 42 | /** |
| 43 | * Short capability name used in the binding sid and runtime span, e.g. |
| 44 | * `"StartQAppSession"`. |
| 45 | */ |
| 46 | capability: string; |
| 47 | /** IAM actions granted on the app ARN + `{appArn}/*`. */ |
| 48 | iamActions: readonly string[]; |
| 49 | /** The distilled operation; `instanceId` (and `appId`) are injected. */ |
| 50 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 51 | /** Inject the bound app's `appId` into the request. */ |
| 52 | injectAppId?: boolean; |
| 53 | }) => |
| 54 | Effect.gen(function* () { |
| 55 | const op = yield* options.operation; |
| 56 | |
| 57 | return Effect.fn(function* (app: QApp) { |
| 58 | const instanceId = yield* app.instanceId; |
| 59 | const appId = yield* app.appId; |
| 60 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 61 | const host = yield* Binding.Host; |
| 62 | if (isBindingHost(host)) { |
| 63 | yield* host.bind`Allow(${host}, AWS.QApps.${options.capability}(${app}))`( |
| 64 | { |
| 65 | policyStatements: [ |
| 66 | { |
| 67 | Effect: "Allow", |
| 68 | Action: [...options.iamActions], |
| 69 | Resource: [ |
| 70 | Output.interpolate`${app.appArn}`, |
| 71 | Output.interpolate`${app.appArn}/*`, |
| 72 | ], |
| 73 | }, |
| 74 | ], |
| 75 | }, |
| 76 | ); |
| 77 | } |
| 78 | } |
| 79 | return Effect.fn(`AWS.QApps.${options.capability}(${app.LogicalId})`)( |
| 80 | function* (request?: Omit<I, "instanceId" | "appId">) { |
| 81 | const input = options.injectAppId |
| 82 | ? { |
| 83 | ...request, |
| 84 | instanceId: yield* instanceId, |
| 85 | appId: yield* appId, |
| 86 | } |
| 87 | : { ...request, instanceId: yield* instanceId }; |
| 88 | return yield* op(input as I); |
| 89 | }, |
| 90 | ); |
| 91 | }); |
| 92 | }); |
| 93 | |
| 94 | /** |
| 95 | * Build the impl Effect for an instance-level Q Apps operation (library |
| 96 | * items, categories, listing, generation): the runtime callable injects the |
| 97 | * bound {@link QApp}'s `instanceId`. Library-item and category authorization |
| 98 | * ARNs are service-assigned and not derivable from a bound Q App, so the |
| 99 | * deploy-time half grants `iamActions` on `Resource: ["*"]`. |
| 100 | */ |
| 101 | export const makeQAppsInstanceHttpBinding = < |
| 102 | I extends { instanceId: string }, |
| 103 | A, |
| 104 | E, |
| 105 | R, |
| 106 | >(options: { |
| 107 | /** |
| 108 | * Short capability name used in the binding sid and runtime span, e.g. |
| 109 | * `"ListLibraryItems"`. |
| 110 | */ |
| 111 | capability: string; |
| 112 | /** IAM actions granted on `Resource: ["*"]`. */ |
| 113 | iamActions: readonly string[]; |
| 114 | /** The distilled operation; `instanceId` is injected. */ |
| 115 | operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>; |
| 116 | }) => |
| 117 | Effect.gen(function* () { |
| 118 | const op = yield* options.operation; |
| 119 | |
| 120 | return Effect.fn(function* (app: QApp) { |
| 121 | const instanceId = yield* app.instanceId; |
| 122 | if (!globalThis.__ALCHEMY_RUNTIME__) { |
| 123 | const host = yield* Binding.Host; |
| 124 | if (isBindingHost(host)) { |
| 125 | yield* host.bind`Allow(${host}, AWS.QApps.${options.capability}(${app}))`( |
| 126 | { |
| 127 | policyStatements: [ |
| 128 | { |
| 129 | Effect: "Allow", |
| 130 | Action: [...options.iamActions], |
| 131 | Resource: ["*"], |
| 132 | }, |
| 133 | ], |
| 134 | }, |
| 135 | ); |
| 136 | } |
| 137 | } |
| 138 | return Effect.fn(`AWS.QApps.${options.capability}(${app.LogicalId})`)( |
| 139 | function* (request?: Omit<I, "instanceId">) { |
| 140 | return yield* op({ ...request, instanceId: yield* instanceId } as I); |
| 141 | }, |
| 142 | ); |
| 143 | }); |
| 144 | }); |