| 1 | import * as Effect from "effect/Effect"; |
| 2 | import type * as HttpClient from "effect/http/HttpClient"; |
| 3 | import { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; |
| 4 | import type { Credentials } from "../Credentials.ts"; |
| 5 | import { dispatchByMode } from "../LocalGateway.ts"; |
| 6 | import type { Bucket } from "./Bucket.ts"; |
| 7 | import type { makeHelpers } from "./BucketBinding.ts"; |
| 8 | import type { R2Auth } from "./BucketHttp.ts"; |
| 9 | import { makeProxyBucketHelpers } from "./LocalR2Gateway.ts"; |
| 10 | |
| 11 | /** |
| 12 | * Shared scaffolding for the R2 `*Local` binding layers. |
| 13 | * |
| 14 | * Captures the lazy account resolver and ambient credentials context at |
| 15 | * layer construction, then returns the deferred binding callable. The |
| 16 | * callable reads the bucket name/jurisdiction as deferred accessors |
| 17 | * (resolved at apply time) and builds the client per resolved name: |
| 18 | * |
| 19 | * - a REAL name gets the HTTP client (same builders as the `*Http` variant, |
| 20 | * authorized with the current CLI credentials — no minted token); |
| 21 | * - a `dev:` name (local emulation under `alchemy dev`) gets the native |
| 22 | * binding client (same builders as the `*Binding` variant) over a scoped |
| 23 | * platform-proxy gateway (see `LocalR2Gateway.ts`). |
| 24 | * |
| 25 | * NOT exported from `index.ts`. |
| 26 | */ |
| 27 | export const makeLocalBucketBinding = <Client extends object>(options: { |
| 28 | makeHttpClient: ( |
| 29 | auth: R2Auth, |
| 30 | bucketName: Effect.Effect<string>, |
| 31 | jurisdiction: Effect.Effect<string>, |
| 32 | ) => Client; |
| 33 | makeNativeClient: (helpers: ReturnType<typeof makeHelpers>) => Client; |
| 34 | }) => |
| 35 | Effect.gen(function* () { |
| 36 | // Capture the account resolver without authenticating. Only the HTTP |
| 37 | // branch evaluates it; native local clients need no cloud credentials. |
| 38 | // Credentials are ambient during stack-eval (the stack's |
| 39 | // providers layer). Capture the full context so each op can be run with the |
| 40 | // current credentials. |
| 41 | const environment = yield* CloudflareEnvironment; |
| 42 | const context = yield* Effect.context< |
| 43 | Credentials | HttpClient.HttpClient |
| 44 | >(); |
| 45 | // The FULL ambient context, for the dev-mode gateway: booting an |
| 46 | // ephemeral workerd needs the platform services and the Cloudflare |
| 47 | // environment, all present during stack-eval but not statically |
| 48 | // enumerable here. |
| 49 | const ambient = yield* Effect.context<never>(); |
| 50 | |
| 51 | const auth: R2Auth = { |
| 52 | authorize: (eff) => eff.pipe(Effect.provideContext(context)), |
| 53 | accountId: Effect.map(environment, (env) => env.accountId), |
| 54 | }; |
| 55 | |
| 56 | return Effect.fn(function* (bucket: Bucket) { |
| 57 | // Deferred accessors — resolved against the tracker at apply time. No |
| 58 | // `host.bind`: the local variant registers no binding. |
| 59 | const bucketName = yield* bucket.bucketName; |
| 60 | const jurisdiction = yield* bucket.jurisdiction; |
| 61 | const httpClient = options.makeHttpClient(auth, bucketName, jurisdiction); |
| 62 | return dispatchByMode(bucketName, httpClient, (name) => |
| 63 | options.makeNativeClient(makeProxyBucketHelpers(name, ambient)), |
| 64 | ); |
| 65 | }); |
| 66 | }); |