1import * as Effect from "effect/Effect";
2import type * as HttpClient from "effect/http/HttpClient";
3import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
4import type { Credentials } from "../Credentials.ts";
5import { dispatchByMode } from "../LocalGateway.ts";
6import type { Bucket } from "./Bucket.ts";
7import type { makeHelpers } from "./BucketBinding.ts";
8import type { R2Auth } from "./BucketHttp.ts";
9import { 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 */
27export 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 });