@lunora/container 0.0.0 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ /** A `fetch` implementation — defaults to the runtime global. */
2
+ type FetchLike = (input: string, init: {
3
+ body: string;
4
+ headers: Record<string, string>;
5
+ method: string;
6
+ }) => Promise<{
7
+ json: () => Promise<unknown>;
8
+ ok: boolean;
9
+ status: number;
10
+ statusText?: string;
11
+ }>;
12
+ interface ContainerBridgeOptions {
13
+ /**
14
+ * Base URL of the deployed Lunora Worker (no trailing `/_lunora/rpc`), e.g.
15
+ * `https://my-app.workers.dev`. In a Lunora container, surface it as an
16
+ * `env` value on the definition.
17
+ */
18
+ baseUrl: string;
19
+ /** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
20
+ fetch?: FetchLike;
21
+ /**
22
+ * Bearer token sent as `Authorization: Bearer &lt;token>`. Your Worker's
23
+ * `resolveIdentity` maps it to the identity the called functions run as.
24
+ * Pass it to the container as a `secret`, never bake it into the image.
25
+ */
26
+ token?: string;
27
+ }
28
+ /** Thrown when a Lunora function returns an error envelope. Carries the wire `code`. */
29
+ declare class ContainerBridgeError extends Error {
30
+ readonly code: string;
31
+ constructor(code: string, message: string);
32
+ }
33
+ /**
34
+ * Structural mirror of `@lunora/client`'s `FunctionReference` — the typed
35
+ * handle the generated `_generated/api` object carries. Declared locally (not
36
+ * imported) so the bridge stays dependency-free and its `.d.ts` is
37
+ * self-contained; the `__lunoraPhantom` shape matches, so a real `api.x.y`
38
+ * reference is assignable and its arg/return types are inferable.
39
+ */
40
+ interface BridgeFunctionReference<Args = unknown, Result = unknown> {
41
+ readonly __lunoraPhantom?: {
42
+ args: Args;
43
+ returns: Result;
44
+ };
45
+ readonly __lunoraRef: string;
46
+ }
47
+ /** Infer the args type from a {@link BridgeFunctionReference} (or a `@lunora/client` reference). */
48
+ type ArgsOfReference<Reference> = Reference extends {
49
+ __lunoraPhantom?: {
50
+ args: infer Args;
51
+ };
52
+ } ? Args : never;
53
+ /** Infer the result type from a {@link BridgeFunctionReference} (or a `@lunora/client` reference). */
54
+ type ResultOfReference<Reference> = Reference extends {
55
+ __lunoraPhantom?: {
56
+ returns: infer Result;
57
+ };
58
+ } ? Result : never;
59
+ interface ContainerBridge {
60
+ /** Call an `action` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
61
+ action: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
62
+ /** Call any Lunora function by `namespace:fn` path; the server resolves its kind. */
63
+ call: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
64
+ /** Call a `mutation` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
65
+ mutation: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
66
+ /** Call a `query` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
67
+ query: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
68
+ /**
69
+ * Fully-typed call via a generated function reference. Pass a reference from
70
+ * the project's `_generated/api` (e.g. `api.messages.list`) and the args +
71
+ * result are inferred from it — the typed counterpart to {@link ContainerBridge.call}
72
+ * for JS/TS containers that can import the generated `api`.
73
+ */
74
+ run: <Reference extends BridgeFunctionReference>(reference: Reference, args: ArgsOfReference<Reference>, shardKey?: string) => Promise<ResultOfReference<Reference>>;
75
+ }
76
+ /**
77
+ * Build a container→Lunora bridge bound to a Worker URL + token.
78
+ *
79
+ * ```ts
80
+ * const lunora = createContainerBridge({ baseUrl: process.env.LUNORA_URL!, token: process.env.LUNORA_TOKEN });
81
+ * const messages = await lunora.query("messages:list", { limit: 20 });
82
+ * await lunora.mutation("messages:markProcessed", { id });
83
+ * ```
84
+ *
85
+ * `query`/`mutation`/`action` are intent-revealing aliases of one `call` — the
86
+ * wire is identical and the server dispatches by the function's registered
87
+ * kind, so a query path called via `.mutation(...)` still runs as a query.
88
+ */
89
+ declare const createContainerBridge: (options: ContainerBridgeOptions) => ContainerBridge;
90
+ export { type BridgeFunctionReference, type ContainerBridge, ContainerBridgeError, type ContainerBridgeOptions, type FetchLike, createContainerBridge };
@@ -0,0 +1,90 @@
1
+ /** A `fetch` implementation — defaults to the runtime global. */
2
+ type FetchLike = (input: string, init: {
3
+ body: string;
4
+ headers: Record<string, string>;
5
+ method: string;
6
+ }) => Promise<{
7
+ json: () => Promise<unknown>;
8
+ ok: boolean;
9
+ status: number;
10
+ statusText?: string;
11
+ }>;
12
+ interface ContainerBridgeOptions {
13
+ /**
14
+ * Base URL of the deployed Lunora Worker (no trailing `/_lunora/rpc`), e.g.
15
+ * `https://my-app.workers.dev`. In a Lunora container, surface it as an
16
+ * `env` value on the definition.
17
+ */
18
+ baseUrl: string;
19
+ /** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
20
+ fetch?: FetchLike;
21
+ /**
22
+ * Bearer token sent as `Authorization: Bearer &lt;token>`. Your Worker's
23
+ * `resolveIdentity` maps it to the identity the called functions run as.
24
+ * Pass it to the container as a `secret`, never bake it into the image.
25
+ */
26
+ token?: string;
27
+ }
28
+ /** Thrown when a Lunora function returns an error envelope. Carries the wire `code`. */
29
+ declare class ContainerBridgeError extends Error {
30
+ readonly code: string;
31
+ constructor(code: string, message: string);
32
+ }
33
+ /**
34
+ * Structural mirror of `@lunora/client`'s `FunctionReference` — the typed
35
+ * handle the generated `_generated/api` object carries. Declared locally (not
36
+ * imported) so the bridge stays dependency-free and its `.d.ts` is
37
+ * self-contained; the `__lunoraPhantom` shape matches, so a real `api.x.y`
38
+ * reference is assignable and its arg/return types are inferable.
39
+ */
40
+ interface BridgeFunctionReference<Args = unknown, Result = unknown> {
41
+ readonly __lunoraPhantom?: {
42
+ args: Args;
43
+ returns: Result;
44
+ };
45
+ readonly __lunoraRef: string;
46
+ }
47
+ /** Infer the args type from a {@link BridgeFunctionReference} (or a `@lunora/client` reference). */
48
+ type ArgsOfReference<Reference> = Reference extends {
49
+ __lunoraPhantom?: {
50
+ args: infer Args;
51
+ };
52
+ } ? Args : never;
53
+ /** Infer the result type from a {@link BridgeFunctionReference} (or a `@lunora/client` reference). */
54
+ type ResultOfReference<Reference> = Reference extends {
55
+ __lunoraPhantom?: {
56
+ returns: infer Result;
57
+ };
58
+ } ? Result : never;
59
+ interface ContainerBridge {
60
+ /** Call an `action` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
61
+ action: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
62
+ /** Call any Lunora function by `namespace:fn` path; the server resolves its kind. */
63
+ call: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
64
+ /** Call a `mutation` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
65
+ mutation: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
66
+ /** Call a `query` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
67
+ query: <Result = unknown>(functionPath: string, args?: Record<string, unknown>, shardKey?: string) => Promise<Result>;
68
+ /**
69
+ * Fully-typed call via a generated function reference. Pass a reference from
70
+ * the project's `_generated/api` (e.g. `api.messages.list`) and the args +
71
+ * result are inferred from it — the typed counterpart to {@link ContainerBridge.call}
72
+ * for JS/TS containers that can import the generated `api`.
73
+ */
74
+ run: <Reference extends BridgeFunctionReference>(reference: Reference, args: ArgsOfReference<Reference>, shardKey?: string) => Promise<ResultOfReference<Reference>>;
75
+ }
76
+ /**
77
+ * Build a container→Lunora bridge bound to a Worker URL + token.
78
+ *
79
+ * ```ts
80
+ * const lunora = createContainerBridge({ baseUrl: process.env.LUNORA_URL!, token: process.env.LUNORA_TOKEN });
81
+ * const messages = await lunora.query("messages:list", { limit: 20 });
82
+ * await lunora.mutation("messages:markProcessed", { id });
83
+ * ```
84
+ *
85
+ * `query`/`mutation`/`action` are intent-revealing aliases of one `call` — the
86
+ * wire is identical and the server dispatches by the function's registered
87
+ * kind, so a query path called via `.mutation(...)` still runs as a query.
88
+ */
89
+ declare const createContainerBridge: (options: ContainerBridgeOptions) => ContainerBridge;
90
+ export { type BridgeFunctionReference, type ContainerBridge, ContainerBridgeError, type ContainerBridgeOptions, type FetchLike, createContainerBridge };
@@ -0,0 +1,76 @@
1
+ const RPC_PATH = "/_lunora/rpc";
2
+ class ContainerBridgeError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.name = "ContainerBridgeError";
7
+ this.code = code;
8
+ }
9
+ }
10
+ const joinUrl = (baseUrl, path) => {
11
+ let base = baseUrl;
12
+ while (base.endsWith("/")) {
13
+ base = base.slice(0, -1);
14
+ }
15
+ return `${base}${path}`;
16
+ };
17
+ const statusError = (functionPath, response) => new Error(
18
+ `createContainerBridge: request to "${functionPath}" failed (status ${String(response.status)}${response.statusText ? ` ${response.statusText}` : ""})`
19
+ );
20
+ const parseResponseBody = async (response, functionPath) => {
21
+ try {
22
+ return await response.json();
23
+ } catch {
24
+ if (!response.ok) {
25
+ throw statusError(functionPath, response);
26
+ }
27
+ throw new Error(`createContainerBridge: request to "${functionPath}" returned a non-JSON response (status ${String(response.status)})`);
28
+ }
29
+ };
30
+ const createContainerBridge = (options) => {
31
+ if (typeof options.baseUrl !== "string" || options.baseUrl.length === 0) {
32
+ throw new TypeError("createContainerBridge: `baseUrl` must be a non-empty Worker URL (e.g. https://my-app.workers.dev) — is the URL env var set?");
33
+ }
34
+ const fetchImpl = options.fetch ?? globalThis.fetch;
35
+ const call = async (functionPath, args = {}, shardKey) => {
36
+ if (typeof fetchImpl !== "function") {
37
+ throw new TypeError("createContainerBridge: no `fetch` available — pass `fetch` in options for this runtime.");
38
+ }
39
+ const headers = { "content-type": "application/json" };
40
+ if (options.token !== void 0) {
41
+ headers.authorization = `Bearer ${options.token}`;
42
+ }
43
+ const response = await fetchImpl(joinUrl(options.baseUrl, RPC_PATH), {
44
+ body: JSON.stringify({ args, functionPath, shardKey }),
45
+ headers,
46
+ method: "POST"
47
+ });
48
+ const body = await parseResponseBody(response, functionPath);
49
+ if (typeof body === "object" && body !== null && "error" in body) {
50
+ const { error } = body;
51
+ if (typeof error === "object" && error !== null) {
52
+ const { code, message } = error;
53
+ if (typeof code === "string" && typeof message === "string") {
54
+ throw new ContainerBridgeError(code, message);
55
+ }
56
+ }
57
+ let detail;
58
+ try {
59
+ detail = JSON.stringify(error);
60
+ } catch {
61
+ detail = String(error);
62
+ }
63
+ throw new Error(
64
+ `createContainerBridge: request to "${functionPath}" returned a malformed error envelope (status ${String(response.status)}): ${detail}`
65
+ );
66
+ }
67
+ if (!response.ok) {
68
+ throw statusError(functionPath, response);
69
+ }
70
+ return body.result;
71
+ };
72
+ const run = async (reference, args, shardKey) => call(reference.__lunoraRef, args, shardKey);
73
+ return { action: call, call, mutation: call, query: call, run };
74
+ };
75
+
76
+ export { ContainerBridgeError, createContainerBridge };
@@ -0,0 +1,43 @@
1
+ import { Container, StopParams } from '@cloudflare/containers';
2
+ import { C as ContainerDefinition } from "../packem_shared/types.d-D2l2SYol.mjs";
3
+ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
4
+ /**
5
+ * Base class for the generated Container DO classes. Applies a
6
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
7
+ * port, sleep timeout, internet access, and the container environment (static
8
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
9
+ * fails fast here rather than starting a container without its credential).
10
+ *
11
+ * Generated subclasses stay one line of behavior:
12
+ *
13
+ * ```ts
14
+ * export class TranscoderContainer extends LunoraContainer {
15
+ * constructor(ctx: DurableObjectState, env: Env) {
16
+ * super(ctx, env, transcoder, "transcoder");
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+ declare class LunoraContainer<Env = unknown> extends Container<Env> {
22
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
23
+ private readonly lunoraName;
24
+ constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string);
25
+ override onError(error: unknown): unknown;
26
+ override onStart(): Promise<void>;
27
+ override onStop(parameters: StopParams): Promise<void>;
28
+ /**
29
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
30
+ * also appears in the Studio Logs panel (the terminal already has it via
31
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
32
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
33
+ * out of a lifecycle hook — the `console` path stays the source of truth.
34
+ */
35
+ private surfaceInStudioLogs;
36
+ /**
37
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
38
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
39
+ * defensively — the id shape varies and isn't worth crashing a hook over.
40
+ */
41
+ private instanceId;
42
+ }
43
+ export { LunoraContainer as default };
@@ -0,0 +1,43 @@
1
+ import { Container, StopParams } from '@cloudflare/containers';
2
+ import { C as ContainerDefinition } from "../packem_shared/types.d-D2l2SYol.js";
3
+ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
4
+ /**
5
+ * Base class for the generated Container DO classes. Applies a
6
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
7
+ * port, sleep timeout, internet access, and the container environment (static
8
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
9
+ * fails fast here rather than starting a container without its credential).
10
+ *
11
+ * Generated subclasses stay one line of behavior:
12
+ *
13
+ * ```ts
14
+ * export class TranscoderContainer extends LunoraContainer {
15
+ * constructor(ctx: DurableObjectState, env: Env) {
16
+ * super(ctx, env, transcoder, "transcoder");
17
+ * }
18
+ * }
19
+ * ```
20
+ */
21
+ declare class LunoraContainer<Env = unknown> extends Container<Env> {
22
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
23
+ private readonly lunoraName;
24
+ constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string);
25
+ override onError(error: unknown): unknown;
26
+ override onStart(): Promise<void>;
27
+ override onStop(parameters: StopParams): Promise<void>;
28
+ /**
29
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
30
+ * also appears in the Studio Logs panel (the terminal already has it via
31
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
32
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
33
+ * out of a lifecycle hook — the `console` path stays the source of truth.
34
+ */
35
+ private surfaceInStudioLogs;
36
+ /**
37
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
38
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
39
+ * defensively — the id shape varies and isn't worth crashing a hook over.
40
+ */
41
+ private instanceId;
42
+ }
43
+ export { LunoraContainer as default };
@@ -0,0 +1,119 @@
1
+ import { Container } from '@cloudflare/containers';
2
+ import { resolveContainerEnvVars as resolveContainerEnvVariables } from '../packem_shared/containerBindingName-BGdSdFNA.mjs';
3
+
4
+ const LUNORA_EVENT_SOURCE = "lunora";
5
+ const buildContainerLifecycleEvent = (container, instance, event, message) => {
6
+ return {
7
+ container,
8
+ event,
9
+ instance,
10
+ level: event === "error" ? "error" : "info",
11
+ message,
12
+ source: LUNORA_EVENT_SOURCE,
13
+ ts: Date.now(),
14
+ type: "container"
15
+ };
16
+ };
17
+ const emitContainerLifecycle = (container, instance, event, message) => {
18
+ const envelope = buildContainerLifecycleEvent(container, instance, event, message);
19
+ const line = JSON.stringify(envelope);
20
+ if (event === "error") {
21
+ console.error(line);
22
+ } else {
23
+ console.log(line);
24
+ }
25
+ return envelope;
26
+ };
27
+
28
+ const RECORD_CONTAINER_EVENT_OP = "__lunora_admin__:recordContainerEvent";
29
+ const ROOT_SHARD_NAME = "__root__";
30
+ const isShardNamespace = (value) => {
31
+ if (value === null || typeof value !== "object") {
32
+ return false;
33
+ }
34
+ const candidate = value;
35
+ return typeof candidate.get === "function" && typeof candidate.idFromName === "function";
36
+ };
37
+ const resolveRootShard = (namespace) => {
38
+ if (typeof namespace.getByName === "function") {
39
+ return namespace.getByName(ROOT_SHARD_NAME);
40
+ }
41
+ return namespace.get(namespace.idFromName(ROOT_SHARD_NAME));
42
+ };
43
+ const reportContainerLifecycle = async (env, envelope) => {
44
+ try {
45
+ const envRecord = env ?? {};
46
+ const namespace = envRecord["SHARD"];
47
+ if (!isShardNamespace(namespace)) {
48
+ return;
49
+ }
50
+ const adminBearer = typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0;
51
+ if (!adminBearer || adminBearer.length === 0) {
52
+ return;
53
+ }
54
+ const request = new Request("https://shard.internal/rpc", {
55
+ body: JSON.stringify({ args: { event: envelope }, functionPath: RECORD_CONTAINER_EVENT_OP }),
56
+ headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
57
+ method: "POST"
58
+ });
59
+ await resolveRootShard(namespace).fetch(request);
60
+ } catch {
61
+ }
62
+ };
63
+
64
+ class LunoraContainer extends Container {
65
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
66
+ lunoraName;
67
+ constructor(context, env, definition, exportName) {
68
+ super(context, env, {
69
+ defaultPort: definition.defaultPort,
70
+ envVars: resolveContainerEnvVariables(definition, env, exportName),
71
+ sleepAfter: definition.sleepAfter
72
+ });
73
+ if (definition.enableInternet !== void 0) {
74
+ this.enableInternet = definition.enableInternet;
75
+ }
76
+ this.lunoraName = exportName ?? "container";
77
+ }
78
+ onError(error) {
79
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "error", error instanceof Error ? error.message : String(error));
80
+ this.surfaceInStudioLogs(envelope);
81
+ return super.onError(error);
82
+ }
83
+ async onStart() {
84
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "start");
85
+ this.surfaceInStudioLogs(envelope);
86
+ await super.onStart();
87
+ }
88
+ async onStop(parameters) {
89
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", `${parameters.reason} (exit ${String(parameters.exitCode)})`);
90
+ this.surfaceInStudioLogs(envelope);
91
+ await super.onStop(parameters);
92
+ }
93
+ /**
94
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
95
+ * also appears in the Studio Logs panel (the terminal already has it via
96
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
97
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
98
+ * out of a lifecycle hook — the `console` path stays the source of truth.
99
+ */
100
+ surfaceInStudioLogs(envelope) {
101
+ reportContainerLifecycle(this.env, envelope).catch(() => {
102
+ });
103
+ }
104
+ /**
105
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
106
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
107
+ * defensively — the id shape varies and isn't worth crashing a hook over.
108
+ */
109
+ instanceId() {
110
+ try {
111
+ const { id } = this.ctx;
112
+ return typeof id?.toString === "function" ? id.toString() : "unknown";
113
+ } catch {
114
+ return "unknown";
115
+ }
116
+ }
117
+ }
118
+
119
+ export { LunoraContainer as default };
@@ -0,0 +1,189 @@
1
+ import { a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/types.d-D2l2SYol.mjs";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerRollout, e as CustomContainerInstanceType, f as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/types.d-D2l2SYol.mjs";
3
+ /**
4
+ * The `ctx.containers` action surface: typed handles over the `CONTAINER_*`
5
+ * Durable Object namespace bindings the config layer reconciles.
6
+ *
7
+ * Deliberately structural (no `@cloudflare/containers` import): a Durable
8
+ * Object namespace stub is all that is needed to route a request to a
9
+ * container-enabled DO, so this module stays Node-safe and the test double
10
+ * below can satisfy the exact same shape without a workerd runtime.
11
+ */
12
+ /** Options for explicitly starting an instance (mirrors `@cloudflare/containers`). */
13
+ interface ContainerStartOptions {
14
+ /** Override outbound internet access for this start. */
15
+ enableInternet?: boolean;
16
+ /** Override the container entrypoint. */
17
+ entrypoint?: string[];
18
+ /** Per-instance environment, merged over the definition's `env`/secrets. */
19
+ envVars?: Record<string, string>;
20
+ /** Metadata labels attached for metrics/observability. */
21
+ labels?: Record<string, string>;
22
+ }
23
+ /** A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time. */
24
+ interface ContainerInstanceState {
25
+ [key: string]: unknown;
26
+ lastChange?: number;
27
+ }
28
+ /** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle RPCs the container DO exposes. */
29
+ interface ContainerStubLike {
30
+ destroy?: () => Promise<void>;
31
+ fetch: (input: Request) => Promise<Response>;
32
+ getState?: () => Promise<ContainerInstanceState>;
33
+ start?: (options?: ContainerStartOptions) => Promise<void>;
34
+ stop?: (signal?: number | string) => Promise<void>;
35
+ }
36
+ /** What the client needs from a Durable Object namespace binding. */
37
+ interface ContainerNamespaceLike {
38
+ get: (id: unknown) => ContainerStubLike;
39
+ idFromName: (name: string) => unknown;
40
+ }
41
+ /** A handle on one container instance (one Durable Object). */
42
+ interface ContainerHandle {
43
+ /**
44
+ * Send an HTTP (or WebSocket-upgrade) request to the container. A path
45
+ * string (`"/transcode"`) is resolved against a synthetic origin; a full
46
+ * `Request`/URL passes through unchanged.
47
+ */
48
+ fetch: (input: Request | string, init?: RequestInit) => Promise<Response>;
49
+ }
50
+ /**
51
+ * A handle on a *named* instance (from `.get(name)`) — `fetch` plus explicit
52
+ * lifecycle control. The per-entity pattern (a sandbox per user, a room per
53
+ * game, a job runner per id) often needs to tear down or inspect the instance
54
+ * rather than wait for `sleepAfter`, so these wrap the container DO's
55
+ * `start`/`stop`/`destroy`/`getState`.
56
+ */
57
+ interface ContainerInstanceHandle extends ContainerHandle {
58
+ /** Stop and discard the instance (its ephemeral disk is lost). */
59
+ destroy: () => Promise<void>;
60
+ /** Read the instance's current runtime state. */
61
+ getState: () => Promise<ContainerInstanceState>;
62
+ /** Explicitly start the instance, optionally with per-instance env/entrypoint. */
63
+ start: (options?: ContainerStartOptions) => Promise<void>;
64
+ /** Stop the instance (optionally with a signal); it can start again on the next request. */
65
+ stop: (signal?: number | string) => Promise<void>;
66
+ }
67
+ /** The per-definition accessor exposed as `ctx.containers.&lt;exportName>`. */
68
+ interface ContainerAccessor {
69
+ /**
70
+ * A random instance from a fixed pool of `count` (defaults to the
71
+ * definition's `maxInstances`, else 3 — mirroring `getRandom` from
72
+ * `@cloudflare/containers`). For stateless, interchangeable workloads.
73
+ */
74
+ any: (count?: number) => ContainerHandle;
75
+ /** The instance for `name` — one container per entity (user, room, job…), with lifecycle control. */
76
+ get: (name: string) => ContainerInstanceHandle;
77
+ /**
78
+ * A resilient handle over the pool: each `fetch` picks a random instance and,
79
+ * on a thrown error or a retryable response (5xx by default), retries on a
80
+ * freshly-picked instance with exponential backoff. Until Cloudflare ships
81
+ * native autoscaling + health-aware routing this is the recommended way to
82
+ * call a stateless container pool — it rides over a single cold/unhealthy
83
+ * instance instead of failing the whole request.
84
+ *
85
+ * Because a retry re-issues the request, pass a **replayable** body — a path
86
+ * string plus an `init.body` string/`ArrayBuffer` (re-created each attempt).
87
+ * A pre-built `Request` carrying a stream body can only be sent once, so it
88
+ * is not retry-safe here; use `.get()`/`.any()` for those.
89
+ */
90
+ pool: (options?: PoolOptions) => ContainerHandle;
91
+ }
92
+ /** Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}. */
93
+ interface PoolOptions {
94
+ /** Total attempts before giving up (each on a freshly-picked instance). Default 3. */
95
+ attempts?: number;
96
+ /** Base backoff in ms between attempts; doubles each retry (0 disables the wait). Default 100. */
97
+ backoffMs?: number;
98
+ /**
99
+ * Upper bound on a single backoff sleep, in ms. The doubling delay is clamped
100
+ * to this ceiling so a large `attempts` count can't produce an unboundedly
101
+ * long wait. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s).
102
+ */
103
+ maxBackoffMs?: number;
104
+ /**
105
+ * Whether a *returned* response should be retried on another instance.
106
+ * Defaults to retrying any `5xx`. A thrown error (network/start failure) is
107
+ * always retried regardless of this predicate.
108
+ */
109
+ retryOn?: (response: Response) => boolean;
110
+ /** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
111
+ size?: number;
112
+ }
113
+ /** Wiring info for one definition, emitted by codegen into the generated DO. */
114
+ interface ContainerBindingSpec {
115
+ /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
116
+ binding: string;
117
+ /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
118
+ exportName: string;
119
+ /** Pool size default for `.any()`. */
120
+ maxInstances?: number;
121
+ }
122
+ /**
123
+ * Build the `ctx.containers` record from the Worker `env`. Called by the
124
+ * generated ShardDO with the specs codegen derived from
125
+ * `lunora/containers.ts`. A missing binding doesn't throw here — only when the
126
+ * handle is actually used — so one unprovisioned container never breaks
127
+ * unrelated functions.
128
+ */
129
+ declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>) => Record<string, ContainerAccessor>;
130
+ /** A test handler: receives the request plus the targeted instance name. */
131
+ type ContainerTestHandler = (request: Request, instance: {
132
+ name: string;
133
+ }) => Promise<Response> | Response;
134
+ /**
135
+ * Docker-free test double for `ctx.containers`: each export name maps to a
136
+ * fetch handler that plays the container. Mirrors the real shape exactly, so
137
+ * action handlers under test can't tell the difference.
138
+ *
139
+ * ```ts
140
+ * const containers = createContainerTestContext({
141
+ * transcoder: (request) => new Response("ok"),
142
+ * });
143
+ * ```
144
+ */
145
+ declare const createContainerTestContext: (handlers: Record<string, ContainerTestHandler>) => Record<string, ContainerAccessor>;
146
+ /**
147
+ * Normalize a `ContainerImageSource` into the shape wrangler wants: a
148
+ * Dockerfile path + build context for local builds, or a fully-qualified
149
+ * reference for pre-built images.
150
+ *
151
+ * A local-path string whose basename starts with `Dockerfile` (so
152
+ * `Dockerfile.dev` also counts) is used as-is with its directory as the build
153
+ * context; any other path is treated as the build-context directory and the
154
+ * Dockerfile is expected at `&lt;dir>/Dockerfile`.
155
+ */
156
+ declare const normalizeContainerImage: (image: ContainerImageSource) => NormalizedContainerImage;
157
+ /**
158
+ * The generated Container DO class name for a `lunora/containers.ts` export:
159
+ * `transcoder` → `TranscoderContainer`. wrangler's `containers[].class_name`
160
+ * and the Durable Object binding's `class_name` both reference it, so codegen
161
+ * and the config layer MUST derive it identically — always via this helper.
162
+ */
163
+ declare const containerClassName: (exportName: string) => string;
164
+ /**
165
+ * The Durable Object binding name for a container export: `transcoder` →
166
+ * `CONTAINER_TRANSCODER`, `imageResizer` → `CONTAINER_IMAGE_RESIZER`. The
167
+ * `CONTAINER_` prefix namespaces these away from `SHARD`/`SESSION`/`SCHEDULER`
168
+ * so a container export can never collide with the built-in bindings.
169
+ */
170
+ declare const containerBindingName: (exportName: string) => string;
171
+ /**
172
+ * The local image tag a Railpack `{ build }` container is built and pushed
173
+ * under: `transcoder` → `lunora-transcoder:build`. The config reconciler writes
174
+ * it as the wrangler `containers[].image`, and `lunora deploy` builds that tag
175
+ * with Railpack and `wrangler containers push`es it before deploying — so all
176
+ * three derive the tag from this one helper and can never disagree.
177
+ */
178
+ declare const containerBuildTag: (exportName: string) => string;
179
+ declare const defineContainer: (config: ContainerConfig) => ContainerDefinition;
180
+ /** True when a value is a `defineContainer` result (the runtime brand check). */
181
+ declare const isContainerDefinition: (value: unknown) => value is ContainerDefinition;
182
+ /**
183
+ * The container's full environment at instance start: the static `env` block
184
+ * plus every declared secret resolved from the Worker `env`. A declared secret
185
+ * missing from the Worker env fails fast — starting the container without a
186
+ * credential it was promised yields far worse errors downstream.
187
+ */
188
+ declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
189
+ export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };