1import * as Effect from "effect/Effect";
2import * as Redacted from "effect/Redacted";
3import type { Json } from "effect/Schema";
4import type { InputProps } from "../../Input.ts";
5import * as Output from "../../Output.ts";
6import type { ResourceBinding } from "../../Resource.ts";
7import * as Namespace from "../../Namespace.ts";
8import { defaultProviderMode } from "../../ProviderMode.ts";
9import { isYieldableEffectLike } from "../../Util/effect.ts";
10import {
11 Application,
12 isApplication,
13 type Application as AccessApplication,
14} from "../Access/Application.ts";
15import { isAiGateway } from "../AI/Gateway.ts";
16import { isSearchInstance } from "../AI/SearchInstance.ts";
17import { isSearchNamespace } from "../AI/SearchNamespace.ts";
18import { isDataset } from "../AnalyticsEngine/Dataset.ts";
19import { isNamespace } from "../Artifacts/Namespace.ts";
20import type { Container } from "../Containers/Container.ts";
21import type { ContainerApplication } from "../Containers/ContainerApplication.ts";
22import { isDatabase } from "../D1/Database.ts";
23import { isSendEmail } from "../Email/SendEmail.ts";
24import { isApp } from "../Flagship/App.ts";
25import { getHyperdriveDevOrigin } from "../Hyperdrive/ConnectBinding.ts";
26import { isHyperdriveConnection } from "../Hyperdrive/Connection.ts";
27import { isImages } from "../Images/Images.ts";
28import { isNamespace as isKVNamespace } from "../KV/Namespace.ts";
29import { isLegacyPipeline } from "../Pipelines/LegacyPipeline.ts";
30import { isStream as isPipelinesStream } from "../Pipelines/Stream.ts";
31import { isQueue } from "../Queues/Queue.ts";
32import { maybeQueueShim } from "../Queues/QueueShim.ts";
33import { isBucket } from "../R2/Bucket.ts";
34import {
35 bindS3Credentials,
36 isS3Credentials,
37 resolveBucket,
38} from "../R2/S3CredentialsBinding.ts";
39import { isSecret } from "../SecretsStore/Secret.ts";
40import { isStream } from "../Stream/Stream.ts";
41import { isIndex } from "../Vectorize/VectorizeIndex.ts";
42import { isVpcService } from "../VpcService/VpcService.ts";
43import type { VpcServiceLookup } from "../VpcService/VpcServiceLookup.ts";
44import { isDispatchNamespace } from "../WorkersForPlatforms/DispatchNamespace.ts";
45import {
46 isWorkflowLike,
47 WorkflowResource,
48 type WorkflowBinding,
49} from "../Workflows/Workflow.ts";
50import {
51 asScriptNameOutput,
52 makeWorkflowName,
53} from "../Workflows/WorkflowName.ts";
54import { isAI } from "./AI.ts";
55import { isAssets } from "./Assets.ts";
56import { isBinding as isWorkerOnlyBinding } from "./Binding.ts";
57import { isBrowser } from "./Browser.ts";
58import {
59 isDurableObjectLike,
60 normalizeTransferredFrom,
61} from "./DurableObject.ts";
62import { isRateLimit } from "./RateLimit.ts";
63import { isSecretKey } from "./SecretKey.ts";
64import { isVersionMetadata } from "./VersionMetadata.ts";
65import type { WorkerBindingProps } from "./Worker.ts";
66import {
67 isSelf,
68 isSelfUrl,
69 isWorker,
70 type Worker,
71 type WorkerObservability,
72 type WorkerProps,
73} from "./Worker.ts";
74import type { WorkerAccessApplication } from "./WorkerAccess.ts";
75import type { WorkerBinding, WorkerBindingResource } from "./WorkerBinding.ts";
76import { isWorkerEntrypoint } from "./WorkerEntrypoint.ts";
77import { isWorkerLoader } from "./WorkerLoader.ts";
78
79export const bindWorkerAsyncBindings = Effect.fn(function* (
80 resource: Worker,
81 props: InputProps<WorkerProps<WorkerBindingProps>>,
82) {
83 if (globalThis.__ALCHEMY_RUNTIME__) return;
84
85 // Access enrollment (`access` prop): push this Worker's
86 // `worker`/`preview_worker` destinations onto the application's binding
87 // contract. The application deploys with — and converges on — every
88 // enrolled Worker's destinations, including at create time, where
89 // Cloudflare requires a self-hosted app to be born with a domain or
90 // destinations.
91 //
92 // Skipped when this Worker runs locally (`alchemy dev`): a local worker
93 // has no cloud script (no immutable ID to enroll), and Access cannot
94 // front a localhost URL — the `dev.access` stub covers the runtime
95 // instead. A Worker opted out via `Alchemy.remote()` enrolls normally.
96 const accessHostMode = resource.Mode ?? (yield* defaultProviderMode);
97 if (props.access && accessHostMode !== "local") {
98 const accessInput = props.access;
99 // The shared form is the application itself — as the module-scope
100 // declaration Effect (`const App = Cloudflare.Access.Application(...)`)
101 // or an already-yielded resource. Resolve the yieldable spelling first,
102 // then discriminate.
103 const access = (
104 isYieldableEffectLike(accessInput) && !Output.isOutput(accessInput)
105 ? yield* accessInput as Effect.Effect<unknown>
106 : accessInput
107 ) as AccessApplication | InputProps<WorkerAccessApplication>;
108 const application = isApplication(access)
109 ? access
110 : // Dedicated form: declare an application owned by this Worker —
111 // per-Worker Access configuration means a per-Worker application
112 // (Cloudflare attaches policies to applications, not Workers). It
113 // lives in the Worker's namespace: `<Worker>/Access`.
114 yield* Application("Access", {
115 type: "self_hosted",
116 name: access.name,
117 policies: access.policies,
118 sessionDuration: access.sessionDuration,
119 allowedIdps: access.allowedIdps,
120 autoRedirectToIdentity: access.autoRedirectToIdentity,
121 appLauncherVisible: access.appLauncherVisible,
122 }).pipe(Namespace.push(resource.LogicalId));
123 const previews = isApplication(access) || access.previews !== false;
124 yield* application.bind(`enroll:${resource.FQN}`, {
125 destinations: [
126 { type: "worker", workerId: resource.workerId },
127 ...(previews
128 ? [
129 {
130 type: "preview_worker" as const,
131 workerId: resource.workerId,
132 },
133 ]
134 : []),
135 ],
136 });
137 }
138 const env: Record<string, unknown> = props.assets
139 ? { ASSETS: { kind: "Cloudflare.Workers.Assets" } }
140 : {};
141 if (props.env) {
142 for (const bindingName in props.env) {
143 // @ts-expect-error
144 const bindingEff = props.env?.[bindingName] as
145 | WorkerBindingResource
146 | Effect.Effect<WorkerBindingResource>;
147 // A Container bound directly in `env` declares a container-backed
148 // Durable Object class: the Container IS the DO namespace binding plus
149 // its ContainerApplication. Handled before the generic Effect
150 // resolution below — yielding the Container class would resolve its
151 // *started instance* tag, which only exists inside a Durable Object.
152 if (isContainerDecl(bindingEff)) {
153 env[bindingName] = bindingEff;
154 yield* bindContainerClass(resource, bindingName, bindingEff);
155 continue;
156 }
157 // `Cloudflare.R2.S3Credentials` is also an Effect (its Effect-native
158 // form resolves to a runtime accessor): bind its deploy-time half under
159 // the env key instead of yielding it.
160 if (isS3Credentials(bindingEff)) {
161 env[bindingName] = bindingEff;
162 yield* bindS3Credentials(
163 resource,
164 bindingName,
165 yield* resolveBucket(bindingEff.bucket),
166 bindingEff.access,
167 );
168 continue;
169 }
170 // Bindings can be passed as a plain resource value, an Effect that
171 // yields a resource, or an effect-class (e.g. a `Cloudflare.Worker`
172 // class). Resolve the yieldable forms before deriving binding metadata.
173 // Avoid yielding outputs as this requires `RuntimeContext`;
174 // allow the engine to resolve them instead.
175 const binding = (
176 isYieldableEffectLike(bindingEff) && !Output.isOutput(bindingEff)
177 ? yield* bindingEff as Effect.Effect<unknown>
178 : bindingEff
179 ) as WorkerBindingResource;
180 env[bindingName] = binding;
181
182 // Queue producer bindings may need the dev-mode remote-producer shim
183 // (a LOCAL worker binding a LIVE queue): `maybeQueueShim` registers
184 // the shim worker + token resources at eval time — the same idiom as
185 // the WorkflowResource registration below — and contributes their
186 // outputs to the binding data.
187 if (isQueue(binding)) {
188 const shim = yield* maybeQueueShim(binding, resource);
189 yield* resource.bind`${bindingName}`({
190 bindings: [
191 {
192 type: "queue",
193 name: bindingName,
194 queueName: binding.queueName,
195 // Alchemy-only mode discriminator for dev (stripped before
196 // upload).
197 queueId: binding.queueId,
198 ...(shim ? { shim } : {}),
199 },
200 ],
201 });
202 continue;
203 }
204
205 const bindingMeta:
206 | BindingSpec
207 | Output.Output<WorkerBinding>
208 | undefined = toBinding(bindingName, binding);
209
210 if (Output.isOutput(bindingMeta)) {
211 // A whole-resource Output resolves to the resource's raw attributes;
212 // uploading those as a plaintext json env var is never intended.
213 // This cannot be rejected at compile time (`Input<T>` admits any
214 // Output whose A is structurally Json), so this guard is the
215 // enforcement point.
216 if (Output.isResourceExpr(binding) || Output.isRefExpr(binding)) {
217 return yield* Effect.die(
218 `Cannot bind whole-resource Output "${bindingName}": pass the resource (or a typed ref) directly, or bind one of its attribute Outputs`,
219 );
220 }
221 // Deferred classification (see `toBinding`'s Output arm): the engine
222 // resolves the Output inside the binding data before the Worker
223 // provider reads it.
224 yield* resource.bind`${bindingName}`({ bindings: [bindingMeta] });
225 continue;
226 }
227
228 if (bindingMeta) {
229 let resolvedBindingMeta: InputProps<WorkerBinding> = bindingMeta;
230
231 if (isWorkflowLike(binding)) {
232 const className = binding.className ?? binding.name;
233 const scriptName = binding.scriptName ?? resource.workerName;
234 const workflowName =
235 binding.workflowName ?? makeWorkflowName(scriptName, className);
236 resolvedBindingMeta = {
237 ...resolvedBindingMeta,
238 workflowName,
239 };
240
241 // A locally-hosted Workflow (no `scriptName`) must be registered
242 // with Cloudflare via `putWorkflow` once the host Worker exists.
243 // Cross-script references are binding-only; both sides use the
244 // explicit physical name when supplied, or derive the same default
245 // from the host script and class.
246 const workflow = binding.scriptName
247 ? undefined
248 : yield* WorkflowResource(binding.name, {
249 workflowName: binding.workflowName,
250 className,
251 scriptName:
252 binding.workflowName === undefined
253 ? resource.workerName
254 : undefined,
255 limits: binding.limits,
256 schedules: binding.schedules,
257 });
258 if (workflow) {
259 // Host linkage must not block the named identity's adoption probe.
260 if (binding.workflowName !== undefined) {
261 yield* workflow.bind`${resource}`({
262 scriptName: resource.workerName,
263 });
264 }
265 resolvedBindingMeta = {
266 ...resolvedBindingMeta,
267 workflowName: binding.workflowName ?? workflow.workflowName,
268 };
269 }
270
271 // Local outputs depend on registration and preserve deployed identity.
272 env[bindingName] = {
273 kind: binding.kind,
274 name: binding.name,
275 className,
276 workflowName: workflow
277 ? workflow.workflowName
278 : Output.asOutput(workflowName),
279 scriptName: workflow
280 ? workflow.scriptName
281 : asScriptNameOutput(scriptName),
282 } satisfies WorkflowBinding;
283 }
284
285 yield* resource.bind`${bindingName}`({
286 bindings: [resolvedBindingMeta],
287 hyperdrives: isHyperdriveConnection(binding)
288 ? getHyperdriveDevOrigin(binding)
289 : undefined,
290 // Dev-only local-emulation opt-out channel (like `hyperdrives`):
291 // worker-only bindings and `SendEmail` descriptors piped through
292 // `Alchemy.remote()` carry the internal `devRemote` flag on their
293 // binding value; contribute it as binding data so the wire binding
294 // stays pure.
295 devRemote:
296 (isWorkerOnlyBinding(binding) || isSendEmail(binding)) &&
297 binding.devRemote
298 ? { [bindingName]: true }
299 : undefined,
300 });
301 } else {
302 // Defensive catch-all: `toBinding` currently always classifies
303 // (its final arm is the `json` fallback), but keep the branch so a
304 // future gap fails loudly instead of silently skipping the binding.
305 return yield* Effect.die(`Unknown binding type: ${bindingName}`);
306 }
307 }
308 }
309 if (resource.Props?.isExternal === true) {
310 Object.assign(resource, { env });
311 }
312});
313
314/**
315 * Structural check for a `Cloudflare.Container` class declaration. Keyed on
316 * the `~alchemy/Container/ClassName` marker (rather than importing from
317 * `Containers/Container.ts`) to avoid a value-level import cycle through
318 * `ContainerPlatform` → `Worker.ts` → this module.
319 *
320 * A Container declaration is Effect-shaped (yielding it resolves the
321 * *started instance* tag, which only exists inside a Durable Object), so
322 * every env-resolution site must check this before `Effect.isEffect`.
323 */
324export const isContainerDecl = (value: unknown): value is Container.Decl.Any =>
325 (typeof value === "function" || typeof value === "object") &&
326 value !== null &&
327 "~alchemy/Container/ClassName" in value;
328
329/**
330 * Structural check for a yielded {@link ContainerApplication} resource
331 * instance (same import-cycle note as above).
332 */
333const isContainerApplicationResource = (
334 value: unknown,
335): value is ContainerApplication =>
336 typeof value === "object" &&
337 value !== null &&
338 (value as { Type?: unknown }).Type === "Cloudflare.Container";
339
340/**
341 * Wire a Container bound directly on an async Worker's `env`
342 * (`env: { Sandbox: Cloudflare.Container("Sandbox", { image }) }`). Mirrors
343 * v1 alchemy, where a Container binding is a Durable Object namespace plus
344 * its ContainerApplication in one declaration:
345 *
346 * 1. emit the `durable_object_namespace` binding for the class,
347 * 2. attach the class's namespace id to the ContainerApplication, and
348 * 3. contribute `containers: [{ className }]` binding data so the Worker's
349 * script metadata marks the class as container-backed.
350 *
351 * The namespace id only exists once the Worker uploads, while the Worker's
352 * metadata needs the container class — the same circularity as the
353 * Effect-native path (`ContainerPlatform.bind`), resolved through bindings +
354 * precreate.
355 */
356const bindContainerClass = Effect.fn(function* (
357 resource: Worker,
358 bindingName: string,
359 decl: Container.Decl.Any,
360) {
361 // Effect-valued container props (the shape that threads a sibling
362 // resource's outputs into `env`) can only surface `className` here, once
363 // the props Effect runs.
364 const declaredClassName = decl["~alchemy/Container/ClassName"];
365 const className =
366 (Effect.isEffect(declaredClassName)
367 ? yield* declaredClassName
368 : declaredClassName) ?? bindingName;
369 // Resolve the ContainerApplication resource declaration carried on the
370 // class. An effectful (`main`) container has no application declaration of
371 // its own here (it is created by its `.make()` Layer inside a Durable
372 // Object host), so it cannot back an async class.
373 const declaration = (decl as { Application?: unknown }).Application;
374 const application =
375 isYieldableEffectLike(declaration) && !Output.isOutput(declaration)
376 ? yield* declaration as Effect.Effect<unknown>
377 : undefined;
378 if (!isContainerApplicationResource(application)) {
379 return yield* Effect.die(
380 `Worker binding '${bindingName}' is a Container without a deployable image. Declare the container with props (image, or context/dockerfile) to bind it on an async Worker — effectful (main) containers require an Effect-native Durable Object host.`,
381 );
382 }
383 // Both halves of the Worker's side describe the same env entry, so they
384 // share one `sid`. Binding rows are collapsed by sid (last write wins), so
385 // splitting them across two `bind` calls silently drops the namespace
386 // binding whenever the two sids coincide — e.g. the env key and the
387 // Container's logical id match (`Sandbox: Container("Sandbox", …)`). The
388 // Worker then uploads the Container declaration itself as a `json` binding
389 // and `env.NAME` is not a DO namespace at runtime.
390 yield* resource.bind`${bindingName}`({
391 bindings: [
392 {
393 type: "durable_object_namespace",
394 name: bindingName,
395 className,
396 },
397 ],
398 containers: [
399 {
400 className,
401 dev: application.dev,
402 hash: application.hash.pipe(Output.map((h) => h?.image)),
403 },
404 ],
405 });
406 yield* application.bind`${bindingName}`({
407 durableObjects: {
408 namespaceId: resource.durableObjectNamespaces.pipe(
409 Output.map((namespaces) => namespaces?.[className]),
410 ),
411 },
412 });
413});
414
415type BindingSpec = InputProps<WorkerBinding>;
416
417/**
418 * Classify a plain env value (string / Redacted / Json) into its wire
419 * binding. Called by {@link toBinding} both eagerly (literal values) and
420 * deferred (the resolved value of an Output env entry).
421 */
422const toValueBinding = (
423 bindingName: string,
424 value: Json | Redacted.Redacted<Json>,
425): WorkerBinding => {
426 if (typeof value === "string") {
427 return {
428 type: "plain_text",
429 name: bindingName,
430 text: value,
431 };
432 }
433 if (Redacted.isRedacted(value)) {
434 const val = Redacted.value(value);
435 return {
436 type: "secret_text",
437 name: bindingName,
438 text: typeof val === "string" ? val : JSON.stringify(val),
439 };
440 }
441 return {
442 type: "json",
443 name: bindingName,
444 json: value,
445 };
446};
447
448/**
449 * Classify a binding into its wire shape.
450 *
451 * An Output that no native classifier recognized resolves to a plain env
452 * value, unknown until the engine resolves it, so its classification
453 * (plain_text vs secret_text vs json) is deferred to resolution time via
454 * {@link toValueBinding} — classifying eagerly would fall through to the
455 * `json` fallback and deploy e.g. a resolved `Redacted` secret as an
456 * unencrypted json binding. Resource refs never land in the Output arm:
457 * they surface their target's `Type` statically, so a native classifier
458 * (kv_namespace, r2_bucket, ...) already matched above. Whole-resource
459 * Outputs also land here (lazily, nothing is resolved); the caller rejects
460 * them before the returned Output is ever bound.
461 */
462const toBinding = (
463 bindingName: string,
464 binding: WorkerBindingResource,
465): BindingSpec | Output.Output<WorkerBinding, unknown> => {
466 if (typeof binding === "string" || Redacted.isRedacted(binding)) {
467 return toValueBinding(bindingName, binding);
468 } else if (isAssets(binding)) {
469 return {
470 type: "assets",
471 name: bindingName,
472 };
473 } else if (isNamespace(binding)) {
474 return {
475 type: "artifacts",
476 name: bindingName,
477 namespace: binding.namespace,
478 };
479 } else if (isImages(binding)) {
480 return {
481 type: "images",
482 name: bindingName,
483 };
484 } else if (isBrowser(binding)) {
485 return {
486 type: "browser",
487 name: bindingName,
488 };
489 } else if (isStream(binding)) {
490 return {
491 type: "stream",
492 name: bindingName,
493 };
494 } else if (isApp(binding)) {
495 return {
496 type: "flagship",
497 name: bindingName,
498 appId: binding.appId,
499 };
500 } else if (isDataset(binding)) {
501 return {
502 type: "analytics_engine",
503 name: bindingName,
504 dataset: binding.dataset,
505 };
506 } else if (isRateLimit(binding)) {
507 return {
508 type: "ratelimit",
509 name: bindingName,
510 namespaceId: binding.namespaceId,
511 simple: binding.simple,
512 };
513 } else if (isSecretKey(binding)) {
514 return {
515 type: "secret_key",
516 name: bindingName,
517 format: binding.format,
518 algorithm: binding.algorithm,
519 usages: binding.usages,
520 keyBase64: binding.keyBase64,
521 keyJwk: binding.keyJwk,
522 };
523 } else if (isSendEmail(binding)) {
524 return {
525 type: "send_email",
526 name: bindingName,
527 destinationAddress: binding.destinationAddress,
528 allowedDestinationAddresses: binding.allowedDestinationAddresses,
529 allowedSenderAddresses: binding.allowedSenderAddresses,
530 };
531 } else if (isDurableObjectLike(binding)) {
532 return {
533 type: "durable_object_namespace",
534 name: bindingName,
535 className: binding.className ?? binding.name,
536 scriptName: binding.scriptName,
537 transferredFrom: normalizeTransferredFrom(binding.transferredFrom),
538 };
539 } else if (isWorkflowLike(binding)) {
540 return {
541 type: "workflow",
542 name: bindingName,
543 workflowName: binding.workflowName ?? binding.name,
544 className: binding.className ?? binding.name,
545 scriptName: binding.scriptName,
546 };
547 } else if (isVpcService(binding)) {
548 return {
549 type: "vpc_service",
550 name: bindingName,
551 serviceId: binding.serviceId,
552 };
553 } else if (isDatabase(binding)) {
554 return {
555 type: "d1",
556 databaseId: binding.databaseId,
557 name: bindingName,
558 };
559 } else if (isBucket(binding)) {
560 return {
561 type: "r2_bucket",
562 name: bindingName,
563 bucketName: binding.bucketName,
564 jurisdiction: binding.jurisdiction.pipe(
565 Output.map((jurisdiction) =>
566 jurisdiction === "default" ? undefined : jurisdiction,
567 ),
568 ),
569 };
570 } else if (isKVNamespace(binding)) {
571 return {
572 type: "kv_namespace",
573 name: bindingName,
574 namespaceId: binding.namespaceId,
575 };
576 } else if (isQueue(binding)) {
577 return {
578 type: "queue",
579 name: bindingName,
580 queueName: binding.queueName,
581 // Alchemy-only mode discriminator for dev (stripped before upload).
582 queueId: binding.queueId,
583 };
584 } else if (isDispatchNamespace(binding)) {
585 return {
586 type: "dispatch_namespace",
587 name: bindingName,
588 namespace: binding.name,
589 };
590 } else if (isAiGateway(binding)) {
591 return {
592 type: "ai",
593 name: bindingName,
594 };
595 } else if (isAI(binding)) {
596 return {
597 type: "ai",
598 name: bindingName,
599 };
600 } else if (isSearchInstance(binding)) {
601 // Single-instance binding: `env.NAME` is the instance itself. The
602 // `namespace` qualifies which namespace the instance lives in (the
603 // account-provided `default` when unspecified).
604 return {
605 type: "ai_search",
606 name: bindingName,
607 instanceName: binding.instanceId,
608 namespace: binding.namespace,
609 };
610 } else if (isSearchNamespace(binding)) {
611 // Namespace binding: `env.NAME.get(instanceName)` selects an instance
612 // within the namespace at runtime.
613 return {
614 type: "ai_search_namespace",
615 name: bindingName,
616 namespace: binding.name,
617 };
618 } else if (isHyperdriveConnection(binding)) {
619 return {
620 type: "hyperdrive",
621 name: bindingName,
622 id: binding.hyperdriveId,
623 };
624 } else if (isWorkerEntrypoint(binding)) {
625 // A named-entrypoint service binding (`Cloudflare.WorkerEntrypoint`).
626 // Tested BEFORE `isWorker` — the marker carries the Worker rather than
627 // being one, but keep the specific classifier ahead of the general one.
628 return {
629 type: "service",
630 name: bindingName,
631 service: binding.worker.workerName,
632 entrypoint: binding.entrypoint,
633 props: binding.props,
634 };
635 } else if (isWorker(binding)) {
636 return {
637 type: "service",
638 name: bindingName,
639 service: binding.workerName,
640 };
641 } else if (isIndex(binding)) {
642 return {
643 type: "vectorize",
644 name: bindingName,
645 indexName: binding.indexName,
646 };
647 } else if (isSecret(binding)) {
648 return {
649 type: "secrets_store_secret",
650 name: bindingName,
651 secretName: binding.secretName,
652 storeId: binding.storeId,
653 };
654 } else if (isVersionMetadata(binding)) {
655 return {
656 type: "version_metadata",
657 name: bindingName,
658 };
659 } else if (isSelfUrl(binding)) {
660 // The Worker's own URL. The provider lowers this sentinel into a
661 // `plain_text` binding holding the resolved URL just before upload.
662 return {
663 type: "self_url",
664 name: bindingName,
665 };
666 } else if (isSelf(binding)) {
667 // A service binding to the Worker itself. The provider lowers this
668 // sentinel into a `service` binding targeting the Worker's own
669 // physical name just before upload.
670 return {
671 type: "self_service",
672 name: bindingName,
673 };
674 } else if (isWorkerLoader(binding)) {
675 return {
676 type: "worker_loader",
677 name: bindingName,
678 };
679 } else if (isPipelinesStream(binding)) {
680 return {
681 type: "pipelines",
682 name: bindingName,
683 pipeline: binding.streamId,
684 };
685 } else if (isLegacyPipeline(binding)) {
686 return {
687 type: "pipelines",
688 name: bindingName,
689 pipeline: binding.name,
690 };
691 } else if (Output.isOutput(binding)) {
692 return Output.map(
693 binding,
694 (value: Json | Redacted.Redacted<Json> | VpcServiceLookup) =>
695 // A `VpcService.lookup(...)` data source resolves to the service's
696 // attributes branded with the resource `Type`; classify it like the
697 // managed resource instead of a plain json env value.
698 isVpcService(value)
699 ? {
700 type: "vpc_service" as const,
701 name: bindingName,
702 serviceId: (value as VpcServiceLookup).serviceId,
703 }
704 : toValueBinding(
705 bindingName,
706 value as Json | Redacted.Redacted<Json>,
707 ),
708 );
709 } else {
710 return {
711 type: "json",
712 name: bindingName,
713 json: binding,
714 };
715 }
716};
717
718export const getCronBindings = (
719 bindings: ReadonlyArray<ResourceBinding<Worker["Binding"]>>,
720) => Array.from(new Set(bindings.flatMap((b) => b.data.crons ?? [])));
721
722/**
723 * Merge the Workers Cache settings contributed by `yield* Cloudflare.cache()`
724 * bindings. Commutative: the cache is enabled (and cross-version) if any
725 * contributor asked for it.
726 */
727export const getCacheBinding = (
728 bindings: ReadonlyArray<ResourceBinding<Worker["Binding"]>>,
729) => {
730 const configs = bindings.flatMap((b) => (b.data.cache ? [b.data.cache] : []));
731 if (configs.length === 0) {
732 return undefined;
733 }
734 return {
735 enabled: configs.some((c) => c.enabled),
736 crossVersionCache: configs.some((c) => c.crossVersionCache) || undefined,
737 };
738};
739
740const DEFAULT_OBSERVABILITY: WorkerObservability = {
741 enabled: true,
742 logs: {
743 enabled: true,
744 invocationLogs: true,
745 },
746};
747
748/**
749 * Resolve the observability metadata to upload. An explicit
750 * `news.observability.traces` wins; otherwise the traces contributed by
751 * `Cloudflare.Telemetry()` (a single binding row — rows are collapsed by
752 * sid) fill in without clobbering the logs config. A cache-style `??` on
753 * the whole object would drop the default `logs.invocationLogs`.
754 */
755export const resolveObservability = (
756 news: Pick<WorkerProps, "observability">,
757 bindings: ReadonlyArray<ResourceBinding<Worker["Binding"]>>,
758): WorkerObservability => {
759 const observability = news.observability ?? DEFAULT_OBSERVABILITY;
760 const bound = bindings.find((b) => b.data.observability?.traces != null)?.data
761 .observability?.traces;
762 return observability.traces != null || bound == null
763 ? observability
764 : { ...observability, traces: bound };
765};