| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type { Bucket } from "./Bucket.ts"; |
| 3 | import { BucketAccessKey } from "./BucketAccessKey.ts"; |
| 4 | import type { BucketCredentials } from "./BucketTypes.ts"; |
| 5 | import type { BucketKeyRole } from "./Types.ts"; |
| 6 | |
| 7 | /** |
| 8 | * Access level a bucket binding grants. The three levels are separate binding |
| 9 | * services so a Function can ask for the least privilege it needs; see the |
| 10 | * role caveat on {@link makeBucketBinding}. |
| 11 | */ |
| 12 | export type BucketCapability = "Read" | "Write" | "ReadWrite"; |
| 13 | |
| 14 | /** |
| 15 | * Logical ID of the {@link BucketAccessKey} a bucket binding creates on the |
| 16 | * caller's behalf. |
| 17 | * |
| 18 | * Derived from the bucket and the access level only — deliberately not from |
| 19 | * the host. The deployed bundle re-runs the same composition code with no host |
| 20 | * resolved, and it has to arrive at the same identity, or the runtime half |
| 21 | * would look for credentials under a different name than the deploy-time half |
| 22 | * wrote them. Two hosts binding the same bucket at the same level therefore |
| 23 | * share one key, which is the intended outcome: the credentials are identical. |
| 24 | */ |
| 25 | export const bucketAccessKeyLogicalId = ( |
| 26 | bucket: Pick<Bucket, "LogicalId">, |
| 27 | capability: BucketCapability, |
| 28 | ) => `${bucket.LogicalId}${capability}BucketAccessKey`; |
| 29 | |
| 30 | /** |
| 31 | * Shared scaffolding for the Prisma bucket bindings. |
| 32 | * |
| 33 | * NOT exported from `index.ts` — the capability modules (`ReadBucket.ts`, |
| 34 | * `WriteBucket.ts`, `ReadWriteBucket.ts`) are each a thin |
| 35 | * `Layer.effect(Cap, makeBucketBinding({ … }))` over this builder; |
| 36 | * `makeClient` receives the resolved credential accessors and knows nothing |
| 37 | * about where they came from. |
| 38 | * |
| 39 | * Binding a bucket creates a {@link BucketAccessKey} for it — the logical ID |
| 40 | * is derived from the bucket and the access level (see |
| 41 | * {@link bucketAccessKeyLogicalId}), so repeated binds reconcile onto the same |
| 42 | * key instead of minting a new credential every deploy. The key's outputs are |
| 43 | * bound to the host by yielding them, so any host with a runtime context |
| 44 | * carries them automatically; `Redacted` values survive the round-trip. |
| 45 | * |
| 46 | * **Role caveat.** Prisma bucket access keys carry one of two coarse roles, |
| 47 | * `read` and `read_write`; there is no write-only role. A `Read` binding mints |
| 48 | * a `read` key and is genuinely scoped, but a `Write` binding mints a |
| 49 | * `read_write` key, so its credential can also read. The split between `Write` |
| 50 | * and `ReadWrite` is therefore a forward contract enforced client-side — the |
| 51 | * `Write` client exposes no read operations — and it becomes an enforced |
| 52 | * server-side boundary if Prisma grows a write-only role. |
| 53 | */ |
| 54 | export const makeBucketBinding = <Client>(options: { |
| 55 | /** Access level this binding grants; part of the key identity. */ |
| 56 | capability: BucketCapability; |
| 57 | /** Role to mint the bucket access key with. */ |
| 58 | role: BucketKeyRole; |
| 59 | /** Build the runtime client from the resolved credential accessors. */ |
| 60 | makeClient: (credentials: BucketCredentials) => Client; |
| 61 | }) => |
| 62 | Effect.gen(function* () { |
| 63 | const Key = yield* BucketAccessKey; |
| 64 | |
| 65 | return Effect.fn(function* (bucket: Bucket) { |
| 66 | const key = yield* Key( |
| 67 | bucketAccessKeyLogicalId(bucket, options.capability), |
| 68 | { bucket, role: options.role }, |
| 69 | ); |
| 70 | return options.makeClient({ |
| 71 | endpoint: yield* key.endpoint, |
| 72 | bucketName: yield* key.bucketName, |
| 73 | accessKeyId: yield* key.accessKeyId, |
| 74 | secretAccessKey: yield* key.secretAccessKey, |
| 75 | }); |
| 76 | }); |
| 77 | }); |