@lunora/container 0.0.0 → 1.0.0-alpha.2

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,63 @@
1
+ import { Container, StopParams } from '@cloudflare/containers';
2
+ import { C as ContainerDefinition } from "../packem_shared/types.d-D2l2SYol.mjs";
3
+ /**
4
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
5
+ * Cloudflare adds values over time.
6
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
7
+ */
8
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
9
+ /**
10
+ * Forward one lifecycle envelope to the root ShardDO's log buffer, best-effort.
11
+ *
12
+ * `env` is the Container DO's worker `env`: we read the `SHARD` namespace and
13
+ * the `LUNORA_ADMIN_TOKEN` from it. Returns a promise that NEVER rejects — every
14
+ * failure path (missing binding, missing token, fetch error) resolves to
15
+ * `undefined` — so the caller can `void` it from a lifecycle hook safely.
16
+ */
17
+ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
18
+ /**
19
+ * Base class for the generated Container DO classes. Applies a
20
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
21
+ * port, sleep timeout, internet access, and the container environment (static
22
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
23
+ * fails fast here rather than starting a container without its credential).
24
+ *
25
+ * Generated subclasses stay one line of behavior:
26
+ *
27
+ * ```ts
28
+ * export class TranscoderContainer extends LunoraContainer {
29
+ * constructor(ctx: DurableObjectState, env: Env) {
30
+ * super(ctx, env, transcoder, "transcoder");
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ declare class LunoraContainer<Env = unknown> extends Container<Env> {
36
+ /**
37
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
38
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
39
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
40
+ */
41
+ private readonly lunoraJurisdiction?;
42
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
43
+ private readonly lunoraName;
44
+ constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
45
+ override onError(error: unknown): unknown;
46
+ override onStart(): Promise<void>;
47
+ override onStop(parameters: StopParams): Promise<void>;
48
+ /**
49
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
50
+ * also appears in the Studio Logs panel (the terminal already has it via
51
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
52
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
53
+ * out of a lifecycle hook — the `console` path stays the source of truth.
54
+ */
55
+ private surfaceInStudioLogs;
56
+ /**
57
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
58
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
59
+ * defensively — the id shape varies and isn't worth crashing a hook over.
60
+ */
61
+ private instanceId;
62
+ }
63
+ export { LunoraContainer as default };
@@ -0,0 +1,63 @@
1
+ import { Container, StopParams } from '@cloudflare/containers';
2
+ import { C as ContainerDefinition } from "../packem_shared/types.d-D2l2SYol.js";
3
+ /**
4
+ * Cloudflare Durable Object data-residency jurisdiction. Widening union —
5
+ * Cloudflare adds values over time.
6
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
7
+ */
8
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
9
+ /**
10
+ * Forward one lifecycle envelope to the root ShardDO's log buffer, best-effort.
11
+ *
12
+ * `env` is the Container DO's worker `env`: we read the `SHARD` namespace and
13
+ * the `LUNORA_ADMIN_TOKEN` from it. Returns a promise that NEVER rejects — every
14
+ * failure path (missing binding, missing token, fetch error) resolves to
15
+ * `undefined` — so the caller can `void` it from a lifecycle hook safely.
16
+ */
17
+ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
18
+ /**
19
+ * Base class for the generated Container DO classes. Applies a
20
+ * `defineContainer` definition onto `@cloudflare/containers`' `Container`:
21
+ * port, sleep timeout, internet access, and the container environment (static
22
+ * `env` merged with the declared Worker secrets — a declared-but-unset secret
23
+ * fails fast here rather than starting a container without its credential).
24
+ *
25
+ * Generated subclasses stay one line of behavior:
26
+ *
27
+ * ```ts
28
+ * export class TranscoderContainer extends LunoraContainer {
29
+ * constructor(ctx: DurableObjectState, env: Env) {
30
+ * super(ctx, env, transcoder, "transcoder");
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ declare class LunoraContainer<Env = unknown> extends Container<Env> {
36
+ /**
37
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
38
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
39
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
40
+ */
41
+ private readonly lunoraJurisdiction?;
42
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
43
+ private readonly lunoraName;
44
+ constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
45
+ override onError(error: unknown): unknown;
46
+ override onStart(): Promise<void>;
47
+ override onStop(parameters: StopParams): Promise<void>;
48
+ /**
49
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
50
+ * also appears in the Studio Logs panel (the terminal already has it via
51
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
52
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
53
+ * out of a lifecycle hook — the `console` path stays the source of truth.
54
+ */
55
+ private surfaceInStudioLogs;
56
+ /**
57
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
58
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
59
+ * defensively — the id shape varies and isn't worth crashing a hook over.
60
+ */
61
+ private instanceId;
62
+ }
63
+ export { LunoraContainer as default };
@@ -0,0 +1,138 @@
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 applyJurisdiction = (namespace, jurisdiction) => {
31
+ if (jurisdiction === void 0) {
32
+ return namespace;
33
+ }
34
+ if (typeof namespace.jurisdiction !== "function") {
35
+ throw new TypeError(
36
+ `@lunora/container: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
37
+ );
38
+ }
39
+ return namespace.jurisdiction(jurisdiction);
40
+ };
41
+ const isShardNamespace = (value) => {
42
+ if (value === null || typeof value !== "object") {
43
+ return false;
44
+ }
45
+ const candidate = value;
46
+ return typeof candidate.get === "function" && typeof candidate.idFromName === "function";
47
+ };
48
+ const resolveRootShard = (namespace, jurisdiction) => {
49
+ const pinned = applyJurisdiction(namespace, jurisdiction);
50
+ if (typeof pinned.getByName === "function") {
51
+ return pinned.getByName(ROOT_SHARD_NAME);
52
+ }
53
+ return pinned.get(pinned.idFromName(ROOT_SHARD_NAME));
54
+ };
55
+ const reportContainerLifecycle = async (env, envelope, jurisdiction) => {
56
+ try {
57
+ const envRecord = env ?? {};
58
+ const namespace = envRecord["SHARD"];
59
+ if (!isShardNamespace(namespace)) {
60
+ return;
61
+ }
62
+ const adminBearer = typeof envRecord["LUNORA_ADMIN_TOKEN"] === "string" ? envRecord["LUNORA_ADMIN_TOKEN"] : void 0;
63
+ if (!adminBearer || adminBearer.length === 0) {
64
+ return;
65
+ }
66
+ const request = new Request("https://shard.internal/rpc", {
67
+ body: JSON.stringify({ args: { event: envelope }, functionPath: RECORD_CONTAINER_EVENT_OP }),
68
+ headers: { authorization: `Bearer ${adminBearer}`, "content-type": "application/json" },
69
+ method: "POST"
70
+ });
71
+ await resolveRootShard(namespace, jurisdiction).fetch(request);
72
+ } catch {
73
+ }
74
+ };
75
+
76
+ class LunoraContainer extends Container {
77
+ /**
78
+ * Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
79
+ * schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
80
+ * to the same region as the root shard. `undefined` ⇒ un-pinned.
81
+ */
82
+ lunoraJurisdiction;
83
+ /** The `lunora/containers.ts` export name, for lifecycle log correlation. */
84
+ lunoraName;
85
+ constructor(context, env, definition, exportName, jurisdiction) {
86
+ super(context, env, {
87
+ defaultPort: definition.defaultPort,
88
+ envVars: resolveContainerEnvVariables(definition, env, exportName),
89
+ sleepAfter: definition.sleepAfter
90
+ });
91
+ if (definition.enableInternet !== void 0) {
92
+ this.enableInternet = definition.enableInternet;
93
+ }
94
+ this.lunoraName = exportName ?? "container";
95
+ this.lunoraJurisdiction = jurisdiction;
96
+ }
97
+ onError(error) {
98
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "error", error instanceof Error ? error.message : String(error));
99
+ this.surfaceInStudioLogs(envelope);
100
+ return super.onError(error);
101
+ }
102
+ async onStart() {
103
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "start");
104
+ this.surfaceInStudioLogs(envelope);
105
+ await super.onStart();
106
+ }
107
+ async onStop(parameters) {
108
+ const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", `${parameters.reason} (exit ${String(parameters.exitCode)})`);
109
+ this.surfaceInStudioLogs(envelope);
110
+ await super.onStop(parameters);
111
+ }
112
+ /**
113
+ * Best-effort push of `envelope` into the root ShardDO's log buffer so it
114
+ * also appears in the Studio Logs panel (the terminal already has it via
115
+ * `emitContainerLifecycle`). Fire-and-forget and fully swallowed: a missing
116
+ * `SHARD` binding, a missing admin token, or a fetch failure NEVER throws
117
+ * out of a lifecycle hook — the `console` path stays the source of truth.
118
+ */
119
+ surfaceInStudioLogs(envelope) {
120
+ reportContainerLifecycle(this.env, envelope, this.lunoraJurisdiction).catch(() => {
121
+ });
122
+ }
123
+ /**
124
+ * Per-instance correlation id: the Durable Object id, which Cloudflare also
125
+ * injects into the container as `CLOUDFLARE_DURABLE_OBJECT_ID`. Read
126
+ * defensively — the id shape varies and isn't worth crashing a hook over.
127
+ */
128
+ instanceId() {
129
+ try {
130
+ const { id } = this.ctx;
131
+ return typeof id?.toString === "function" ? id.toString() : "unknown";
132
+ } catch {
133
+ return "unknown";
134
+ }
135
+ }
136
+ }
137
+
138
+ export { LunoraContainer as default };