1import type * as workers from "@distilled.cloud/cloudflare/workers";
2import * as Config from "effect/Config";
3import * as Effect from "effect/Effect";
4import * as Redacted from "effect/Redacted";
5import type { Json } from "effect/Schema";
6import type * as Output from "../../Output.ts";
7import type { Rpc } from "../../Rpc.ts";
8import { isYieldableEffectLike } from "../../Util/effect.ts";
9import type { Gateway as AiGateway } from "../AI/Gateway.ts";
10import type { SearchInstance } from "../AI/SearchInstance.ts";
11import type { SearchNamespace } from "../AI/SearchNamespace.ts";
12import { Dataset } from "../AnalyticsEngine/Dataset.ts";
13import type { Namespace as ArtifactsNamespace } from "../Artifacts/Namespace.ts";
14import type { Container } from "../Containers/Container.ts";
15import type { Database as D1Database } from "../D1/Database.ts";
16import { SendEmail } from "../Email/SendEmail.ts";
17import type { App as FlagshipApp } from "../Flagship/App.ts";
18import type { Connection as Hyperdrive } from "../Hyperdrive/Connection.ts";
19import type { ImagesBinding } from "../Images/ImagesBinding.ts";
20import type { Namespace } from "../KV/Namespace.ts";
21import type { LegacyPipeline } from "../Pipelines/LegacyPipeline.ts";
22import type { Stream as PipelinesStream } from "../Pipelines/Stream.ts";
23import type { Queue } from "../Queues/Queue.ts";
24import type { Bucket } from "../R2/Bucket.ts";
25import type { S3Credentials } from "../R2/S3Credentials.ts";
26import type { Secret } from "../SecretsStore/Secret.ts";
27import type { StreamBinding } from "../Stream/StreamBinding.ts";
28import type { Index as VectorizeIndex } from "../Vectorize/VectorizeIndex.ts";
29import type { VpcService } from "../VpcService/VpcService.ts";
30import type { VpcServiceLookup } from "../VpcService/VpcServiceLookup.ts";
31import type { DispatchNamespace } from "../WorkersForPlatforms/DispatchNamespace.ts";
32import type { WorkflowLike } from "../Workflows/Workflow.ts";
33import type { AIBinding } from "./AIBinding.ts";
34import type { AnyBindingEffect } from "./Binding.ts";
35import type { Assets } from "./Assets.ts";
36import type { URLEffect } from "./Worker.ts";
37import type { BrowserBinding } from "./BrowserBinding.ts";
38import type { DurableObjectLike } from "./DurableObject.ts";
39import type { RateLimitBinding } from "./RateLimitBinding.ts";
40import { makeRpcStub } from "./Rpc.ts";
41import type { SecretKeyBinding } from "./SecretKeyBinding.ts";
42import type { VersionMetadataBinding } from "./VersionMetadataBinding.ts";
43import { Worker, WorkerEnvironment } from "./Worker.ts";
44import type { WorkerEntrypointBinding } from "./WorkerEntrypoint.ts";
45import type { WorkerLoader } from "./WorkerLoader.ts";
46
47type DistilledWorkerBinding = Exclude<
48 workers.PutScriptRequest["metadata"]["bindings"],
49 undefined
50>[number];
51
52/**
53 * The `durable_object_namespace` metadata binding extended with alchemy-only
54 * transfer metadata. `transferredFrom` names the Worker(s) — by logical id in
55 * this stack + stage, or by physical script name — that previously hosted the
56 * class, so the provider can drive Cloudflare's data-preserving
57 * `transferred_classes` migration. It is stripped from the binding before the
58 * script upload — Cloudflare never sees it.
59 */
60export type DurableObjectNamespaceWorkerBinding = Extract<
61 DistilledWorkerBinding,
62 { type: "durable_object_namespace" }
63> & {
64 transferredFrom?: string | string[];
65};
66
67/**
68 * Alchemy-only binding: the host Worker's own public URL (`Worker.URL`). The
69 * provider resolves the URL the Worker will be served at (first custom domain,
70 * else its `workers.dev` URL) and lowers this into a `plain_text` binding
71 * before the script upload — Cloudflare never sees this type.
72 */
73export interface SelfUrlWorkerBinding {
74 type: "self_url";
75 name: string;
76}
77
78/**
79 * Alchemy-only binding: a service binding that points at the host Worker
80 * ITSELF (`Worker.Self`). The provider lowers this into a
81 * `service` binding targeting the Worker's own physical name just before
82 * the script upload — Cloudflare never sees this type. In local dev it
83 * lowers to the runtime's in-process self service (bypassing the assets
84 * middleware), matching production semantics.
85 *
86 * The canonical consumer is OpenNext's `WORKER_SELF_REFERENCE` (the ISR
87 * revalidation queue re-fetches the worker through it).
88 */
89export interface SelfServiceWorkerBinding {
90 type: "self_service";
91 name: string;
92}
93
94/**
95 * Alchemy-only, dev-only binding: R2 S3 credentials for a locally-emulated
96 * (`dev:`) bucket (`Cloudflare.R2.S3Credentials`). The local runtime lowers
97 * it into a text binding holding the credentials JSON for the Worker's local
98 * S3 endpoint (`{worker url}/cdn-cgi/local/r2/s3`) and serves the bucket on
99 * that endpoint. Deployed Workers receive a `secret_text` binding instead —
100 * Cloudflare never sees this type.
101 */
102export interface R2S3CredentialsWorkerBinding {
103 type: "r2_s3_credentials";
104 name: string;
105 bucketName: string;
106}
107
108/**
109 * The `queue` metadata binding extended with the alchemy-only `queueId`.
110 * The local worker provider uses it to discriminate a locally-emulated
111 * queue (`dev:` id → local broker) from an `Alchemy.remote()` queue in dev
112 * (real id → remote-proxied producer). Stripped from the binding before
113 * the script upload — Cloudflare never sees it.
114 */
115export type QueueWorkerBinding = Extract<
116 DistilledWorkerBinding,
117 { type: "queue" }
118> & {
119 queueId?: string;
120 /**
121 * Alchemy-only (stripped before upload): dev-mode remote-producer shim
122 * for an `Alchemy.remote()` queue. Cloudflare preview sessions reject
123 * queue bindings, so a local worker produces to the live queue through
124 * this deployed shim worker instead (see `Queues/QueueShim.ts`).
125 */
126 shim?: {
127 /** The shim worker's workers.dev URL. */
128 url: string | undefined;
129 /** Bearer token the shim requires. */
130 token: Redacted.Redacted<string> | string;
131 };
132};
133
134/**
135 * The `service` metadata binding extended with workerd's `ctx.props`.
136 * `props` is what a `Cloudflare.WorkerEntrypoint(worker, { props })` env
137 * entry lowers to; the local runtime delivers it to the target entrypoint.
138 * The Cloudflare API's binding schema does not carry the field yet, so on
139 * live uploads it is dropped at encode until the distilled `workers`
140 * service adds it.
141 */
142export type ServiceWorkerBinding = Extract<
143 DistilledWorkerBinding,
144 { type: "service" }
145> & {
146 props?: Record<string, unknown>;
147};
148
149/**
150 * The wire-shape binding union the Cloudflare API accepts — {@link WorkerBinding}
151 * minus the alchemy-only members that must be lowered before upload.
152 */
153export type WireWorkerBinding = Exclude<
154 WorkerBinding,
155 SelfUrlWorkerBinding | SelfServiceWorkerBinding | R2S3CredentialsWorkerBinding
156>;
157
158export type WorkerBinding =
159 | Exclude<
160 DistilledWorkerBinding,
161 | { type: "durable_object_namespace" }
162 | { type: "queue" }
163 | { type: "service" }
164 >
165 | DurableObjectNamespaceWorkerBinding
166 | QueueWorkerBinding
167 | ServiceWorkerBinding
168 | SelfUrlWorkerBinding
169 | SelfServiceWorkerBinding
170 | R2S3CredentialsWorkerBinding;
171
172/**
173 * Drop dev-only binding sentinels ({@link R2S3CredentialsWorkerBinding})
174 * before a live upload. They are only emitted for Workers running locally,
175 * so a live Worker never carries one; this narrows the type for the wire.
176 */
177export const withoutDevOnlyBindings = <B extends WorkerBinding>(
178 bindings: ReadonlyArray<B>,
179): Array<Exclude<B, R2S3CredentialsWorkerBinding>> =>
180 bindings.filter(
181 (binding): binding is Exclude<B, R2S3CredentialsWorkerBinding> =>
182 binding.type !== "r2_s3_credentials",
183 );
184
185export type WorkerSettingsBinding = Exclude<
186 workers.GetScriptScriptAndVersionSettingResponse["bindings"],
187 null | undefined
188>[number];
189
190export type WorkerBindingResource =
191 // Config values
192 | Json
193 | Redacted.Redacted<Json>
194 | Config.Config<Json>
195 // Outputs that resolve to a plain env value (e.g. `Alchemy.makeRandom`,
196 // `Output.literal`), classified by their resolved value at deploy time.
197 // Whole-resource Outputs (`Output.of(bucket)`) cannot be excluded here:
198 // `Input<T>` wraps this whole union in `Output<T>`, so any Output whose
199 // A is structurally Json (most resource attribute shapes) is admitted
200 // upstream regardless of this arm. `bindWorkerAsyncBindings` rejects
201 // them at deploy time instead.
202 | Output.Output<Json | Redacted.Redacted<Json>, unknown>
203 // CF resources
204 | Assets
205 | Bucket
206 | S3Credentials
207 | D1Database
208 | Namespace
209 | Queue
210 | AiGateway
211 | AIBinding
212 | SearchInstance
213 | SearchNamespace
214 | Dataset
215 | SendEmail
216 | ArtifactsNamespace
217 | RateLimitBinding
218 | SecretKeyBinding
219 | BrowserBinding
220 | FlagshipApp
221 | ImagesBinding
222 | StreamBinding
223 | PipelinesStream
224 | LegacyPipeline
225 | Hyperdrive
226 | VectorizeIndex
227 | Secret
228 | Worker
229 | WorkerEntrypointBinding<any>
230 | WorkerLoader
231 | VersionMetadataBinding
232 // The Worker's own URL (`Worker.URL`).
233 | URLEffect
234 // A Worker-only binding lifted by `.pipe(Alchemy.remote())`.
235 | AnyBindingEffect
236 | DispatchNamespace
237 | DurableObjectLike<any>
238 | WorkflowLike<any>
239 | VpcService
240 | VpcServiceLookup
241 // A Container bound directly in `env` declares a container-backed Durable
242 // Object class (DO namespace binding + ContainerApplication in one).
243 | Container.Decl.Any;
244
245export type WorkerBindings = {
246 [bindingName in string]: WorkerBindingResource;
247};
248
249export const bindWorker = Effect.fn(function* <Shape, Req = never>(
250 workerEff:
251 | (Worker & Rpc<Shape>)
252 | Effect.Effect<Worker & Rpc<Shape>, never, Req>,
253) {
254 // Worker classes and regular Effects are both yieldable here.
255 const worker = isYieldableEffectLike(workerEff)
256 ? yield* workerEff as Effect.Effect<Worker & Rpc<Shape>, never, Req>
257 : workerEff;
258 const self = yield* Worker;
259 yield* self.bind`${worker}`({
260 bindings: [
261 {
262 type: "service",
263 name: worker.LogicalId,
264 service: worker.workerName,
265 },
266 ],
267 });
268
269 // `bindWorker` runs at *init* phase (both at plantime and at runtime
270 // cold-start). `WorkerEnvironment` only exists at exec phase on the
271 // deployed worker, so we hand `makeRpcStub` an `Effect<stub>` that
272 // resolves the binding lazily on each method call.
273 const stubEff = WorkerEnvironment.pipe(
274 Effect.map((env) => (env as Record<string, unknown>)[worker.LogicalId]),
275 );
276 return makeRpcStub<Shape>(stubEff);
277});