1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import type * as Scope from "effect/Scope";
4import type { Simplify } from "effect/Types";
5import { PluginContext, type ConfigHook } from "./PluginContext.ts";
6import type { RuntimeError } from "./RuntimeError.shared.ts";
7import type * as WorkerdConfig from "./workerd/Config.ts";
8import type * as Workerd from "./workerd/Workerd.ts";
9
10export type Plugin<Api = never> = WithApi<
11 PluginConfig & { readonly defer?: Effect.Effect<PluginConfig, RuntimeError> },
12 Api
13>;
14
15type WithApi<T, Api> = [Api] extends [never]
16 ? Simplify<T>
17 : Simplify<T & { readonly api: Api }>;
18
19export interface PluginConfig {
20 readonly services?: Array<WorkerdConfig.Service>;
21 readonly sockets?: Array<WorkerdConfig.Socket>;
22 readonly extensions?: Array<WorkerdConfig.Extension>;
23 readonly middlewares?: Array<Middleware>;
24 /**
25 * Worker-level fields merged into the user worker's configuration (e.g.
26 * `cacheApiOutbound`). Merged in plugin order; `RuntimeWorker.unsafe` still
27 * takes precedence.
28 */
29 readonly userWorker?: Partial<WorkerdConfig.Worker>;
30 /**
31 * Initializes each workerd process, including replacements after a crash.
32 * Resources and tasks acquired here are released before the next process
33 * starts. Replay any state held only inside workerd from this hook.
34 */
35 readonly start?: (
36 ports: Workerd.WorkerdPorts,
37 ) => Effect.Effect<void, RuntimeError, Scope.Scope>;
38}
39
40export interface Middleware {
41 name: string;
42 worker: WorkerdConfig.Worker;
43 upstreamBindingName: string;
44 order?: number;
45}
46
47export type PluginBuilder<Api = never> =
48 | Plugin<Api>
49 | Effect.Effect<Plugin<Api>, RuntimeError, PluginContext>;
50
51export type PluginIdentifier<T extends string = string> =
52 `cloudflare-runtime/plugin/${T}`;
53
54/**
55 * When true, simulator services backed by Durable Objects (KV, R2, ...)
56 * handle control operations (fake timers, storage inspection) sent via their
57 * reserved control-op header.
58 *
59 * **Only intended for tests** (provide with
60 * `Layer.succeed(UnsafeEnableControlEndpoints, true)`): anyone able to reach
61 * such a service can then run arbitrary SQL queries and read arbitrary blobs
62 * across every namespace/bucket in this runtime.
63 */
64export const UnsafeEnableControlEndpoints = Context.Reference(
65 "cloudflare-runtime/plugin/UnsafeEnableControlEndpoints",
66 { defaultValue: () => false },
67);
68
69export type PluginService<
70 Self,
71 Identifier extends PluginIdentifier,
72 Api = never,
73> = Context.ServiceClass<Self, Identifier, PluginBuilder<Api>>;
74
75export const Service =
76 <Self, Api = never>() =>
77 <Identifier extends PluginIdentifier>(
78 identifier: Identifier,
79 ): Context.ServiceClass<Self, Identifier, PluginBuilder<Api>> =>
80 Context.Service<Self, PluginBuilder<Api>>()(identifier);
81
82export const use = <Self, Identifier extends PluginIdentifier, Api, A, E, R>(
83 plugin: PluginService<Self, Identifier, Api>,
84 f: (plugin: Plugin<Api>) => Effect.Effect<A, E, R>,
85): ConfigHook<A, E, R | Self> =>
86 PluginContext.use((context) => context.get(plugin).pipe(Effect.flatMap(f)));
87
88export const useSync = <Self, Identifier extends PluginIdentifier, Api, A>(
89 plugin: PluginService<Self, Identifier, Api>,
90 f: (plugin: Plugin<Api>) => A,
91): ConfigHook<A, never, Self> =>
92 use(plugin, (plugin) => Effect.succeed(f(plugin)));