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 { makeProxyKVNamespaceHelpers } from "./LocalKVGateway.ts";
7import type { Namespace } from "./Namespace.ts";
8import type { makeKVNamespaceHelpers } from "./NamespaceBinding.ts";
9import type { KVAuth } from "./NamespaceHttp.ts";
10
11/**
12 * Shared scaffolding for the `*Local` KV services.
13 *
14 * Instead of minting a scoped {@link AccountApiToken} (the `*Http` path) or
15 * resolving a native Worker binding (the `*Binding` path), it captures the
16 * ambient current-credentials context available during stack-eval and
17 * builds the client per resolved namespace id:
18 *
19 * - a REAL id gets the HTTP client (same builders as the `*Http` variant,
20 * authorized with the current credentials — no `host.bind`, no minted
21 * token);
22 * - a `dev:` id (local emulation under `alchemy dev`) gets the native
23 * binding client (same builders as the `*Binding` variant) over a scoped
24 * platform-proxy gateway (see `LocalKVGateway.ts`).
25 *
26 * The id resolves lazily at apply time, so the returned client dispatches
27 * per call.
28 *
29 * NOT exported from `index.ts` — this is internal scaffolding shared by the
30 * three access-level Local layers.
31 */
32export const makeLocalKVNamespaceBinding = <Client extends object>(options: {
33 makeHttpClient: (auth: KVAuth, namespaceId: Effect.Effect<string>) => Client;
34 makeNativeClient: (
35 helpers: ReturnType<typeof makeKVNamespaceHelpers>,
36 ) => Client;
37}) =>
38 Effect.gen(function* () {
39 // Capture the account resolver without authenticating. Only the HTTP
40 // branch evaluates it; native local clients need no cloud credentials.
41 // Credentials are ambient during stack-eval (the stack's
42 // providers layer). Capture the full context so KV HTTP ops can run with
43 // the current credentials — no `host.bind`, no minted token.
44 const environment = yield* CloudflareEnvironment;
45 const context = yield* Effect.context<
46 Credentials | HttpClient.HttpClient
47 >();
48 // The FULL ambient context, for the dev-mode gateway: booting an
49 // ephemeral workerd needs the platform services and the Cloudflare
50 // environment, all present during stack-eval but not statically
51 // enumerable here.
52 const ambient = yield* Effect.context<never>();
53
54 return Effect.fn(function* (namespace: Namespace) {
55 // Deferred accessor — resolves the namespaceId against the tracker at
56 // apply time (in an Action, that's the engine's resolve context).
57 const namespaceId = yield* namespace.namespaceId;
58 const auth: KVAuth = {
59 authorize: (eff) => eff.pipe(Effect.provideContext(context)),
60 accountId: Effect.map(environment, (env) => env.accountId),
61 };
62 const httpClient = options.makeHttpClient(auth, namespaceId);
63 return dispatchByMode(namespaceId, httpClient, (id) =>
64 options.makeNativeClient(makeProxyKVNamespaceHelpers(id, ambient)),
65 );
66 });
67 });