| 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 { makeProxyKVNamespaceHelpers } from "./LocalKVGateway.ts"; |
| 7 | import type { Namespace } from "./Namespace.ts"; |
| 8 | import type { makeKVNamespaceHelpers } from "./NamespaceBinding.ts"; |
| 9 | import 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 | */ |
| 32 | export 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 | }); |