1import * as Effect from "effect/Effect";
2import type { Bucket } from "./Bucket.ts";
3import { BucketAccessKey } from "./BucketAccessKey.ts";
4import type { BucketCredentials } from "./BucketTypes.ts";
5import 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 */
12export 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 */
25export 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 */
54export 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 });