@lunora/container 1.0.0-alpha.31 → 1.0.0-alpha.32

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.
package/dist/bridge.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  /**
3
3
  * A `fetch` implementation — defaults to the runtime global.
4
- * @experimental
5
4
  */
6
5
  type FetchLike = (input: string, init: {
7
6
  body: string;
@@ -15,7 +14,6 @@ type FetchLike = (input: string, init: {
15
14
  }>;
16
15
  /**
17
16
  * `ContainerBridgeOptions` is part of the experimental `@lunora/container` API and may change without a major version bump.
18
- * @experimental
19
17
  */
20
18
  interface ContainerBridgeOptions {
21
19
  /**
@@ -35,7 +33,6 @@ interface ContainerBridgeOptions {
35
33
  }
36
34
  /**
37
35
  * Thrown when a Lunora function returns an error envelope. A `LunoraError` subclass carrying the wire `code`.
38
- * @experimental
39
36
  */
40
37
  declare class ContainerBridgeError extends LunoraError {
41
38
  constructor(code: string, message: string);
@@ -46,7 +43,6 @@ declare class ContainerBridgeError extends LunoraError {
46
43
  * imported) so the bridge stays dependency-free and its `.d.ts` is
47
44
  * self-contained; the `__lunoraPhantom` shape matches, so a real `api.x.y`
48
45
  * reference is assignable and its arg/return types are inferable.
49
- * @experimental
50
46
  */
51
47
  interface BridgeFunctionReference<Args = unknown, Result = unknown> {
52
48
  readonly __lunoraPhantom?: {
@@ -69,7 +65,6 @@ type ResultOfReference<Reference> = Reference extends {
69
65
  } ? Result : never;
70
66
  /**
71
67
  * `ContainerBridge` is part of the experimental `@lunora/container` API and may change without a major version bump.
72
- * @experimental
73
68
  */
74
69
  interface ContainerBridge {
75
70
  /** Call an `action` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
@@ -100,7 +95,6 @@ interface ContainerBridge {
100
95
  * `query`/`mutation`/`action` are intent-revealing aliases of one `call` — the
101
96
  * wire is identical and the server dispatches by the function's registered
102
97
  * kind, so a query path called via `.mutation(...)` still runs as a query.
103
- * @experimental
104
98
  */
105
99
  declare const createContainerBridge: (options: ContainerBridgeOptions) => ContainerBridge;
106
100
  export { type BridgeFunctionReference, type ContainerBridge, ContainerBridgeError, type ContainerBridgeOptions, type FetchLike, createContainerBridge };
package/dist/bridge.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  /**
3
3
  * A `fetch` implementation — defaults to the runtime global.
4
- * @experimental
5
4
  */
6
5
  type FetchLike = (input: string, init: {
7
6
  body: string;
@@ -15,7 +14,6 @@ type FetchLike = (input: string, init: {
15
14
  }>;
16
15
  /**
17
16
  * `ContainerBridgeOptions` is part of the experimental `@lunora/container` API and may change without a major version bump.
18
- * @experimental
19
17
  */
20
18
  interface ContainerBridgeOptions {
21
19
  /**
@@ -35,7 +33,6 @@ interface ContainerBridgeOptions {
35
33
  }
36
34
  /**
37
35
  * Thrown when a Lunora function returns an error envelope. A `LunoraError` subclass carrying the wire `code`.
38
- * @experimental
39
36
  */
40
37
  declare class ContainerBridgeError extends LunoraError {
41
38
  constructor(code: string, message: string);
@@ -46,7 +43,6 @@ declare class ContainerBridgeError extends LunoraError {
46
43
  * imported) so the bridge stays dependency-free and its `.d.ts` is
47
44
  * self-contained; the `__lunoraPhantom` shape matches, so a real `api.x.y`
48
45
  * reference is assignable and its arg/return types are inferable.
49
- * @experimental
50
46
  */
51
47
  interface BridgeFunctionReference<Args = unknown, Result = unknown> {
52
48
  readonly __lunoraPhantom?: {
@@ -69,7 +65,6 @@ type ResultOfReference<Reference> = Reference extends {
69
65
  } ? Result : never;
70
66
  /**
71
67
  * `ContainerBridge` is part of the experimental `@lunora/container` API and may change without a major version bump.
72
- * @experimental
73
68
  */
74
69
  interface ContainerBridge {
75
70
  /** Call an `action` by `namespace:fn` path. Alias of {@link ContainerBridge.call} for intent. */
@@ -100,7 +95,6 @@ interface ContainerBridge {
100
95
  * `query`/`mutation`/`action` are intent-revealing aliases of one `call` — the
101
96
  * wire is identical and the server dispatches by the function's registered
102
97
  * kind, so a query path called via `.mutation(...)` still runs as a query.
103
- * @experimental
104
98
  */
105
99
  declare const createContainerBridge: (options: ContainerBridgeOptions) => ContainerBridge;
106
100
  export { type BridgeFunctionReference, type ContainerBridge, ContainerBridgeError, type ContainerBridgeOptions, type FetchLike, createContainerBridge };
@@ -1,4 +1,4 @@
1
- import { a as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DeQ3rtYK.mjs";
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-KcQu2ZT_.mjs";
2
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
@@ -500,7 +500,6 @@ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
500
500
  * }
501
501
  * }
502
502
  * ```
503
- * @experimental
504
503
  */
505
504
  declare class LunoraContainer<Env = unknown> extends Container<Env> {
506
505
  /**
@@ -1,4 +1,4 @@
1
- import { a as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DeQ3rtYK.js";
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-KcQu2ZT_.js";
2
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
@@ -500,7 +500,6 @@ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
500
500
  * }
501
501
  * }
502
502
  * ```
503
- * @experimental
504
503
  */
505
504
  declare class LunoraContainer<Env = unknown> extends Container<Env> {
506
505
  /**
package/dist/index.d.mts CHANGED
@@ -1,8 +1,74 @@
1
- import { D as DurableObjectJurisdiction, C as ContainerConfig, a as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-DeQ3rtYK.mjs";
2
- export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-DeQ3rtYK.mjs";
1
+ import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-KcQu2ZT_.mjs";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-KcQu2ZT_.mjs";
3
+ /**
4
+ * Per-call options for `ContainerHandle.exec`.
5
+ */
6
+ interface ContainerExecOptions {
7
+ /** Arguments passed to `command`, unshelled — the container must not concatenate them into a shell string. */
8
+ args?: ReadonlyArray<string>;
9
+ /** Working directory for the command, relative to the container's own root. */
10
+ cwd?: string;
11
+ /** Extra environment for this command only, merged over the container's env. */
12
+ env?: Readonly<Record<string, string>>;
13
+ /**
14
+ * Cap on the response body, in bytes. The whole `{code,stdout,stderr}`
15
+ * document has to be held in memory to be parsed, and a build box or job
16
+ * runner routinely writes tens of megabytes — a single unbounded `exec`
17
+ * that returns more than the isolate's memory limit kills the isolate and
18
+ * every other in-flight request sharing it, not just this call. So a
19
+ * runner that overruns the cap fails the call loudly rather than taking
20
+ * the shard down with it; raise this (or have the runner cap its own
21
+ * output) when a command legitimately produces more.
22
+ *
23
+ * Default {@link DEFAULT_EXEC_MAX_OUTPUT_BYTES} (1MB).
24
+ */
25
+ maxOutputBytes?: number;
26
+ /**
27
+ * Abort the call. Composed with {@link ContainerExecOptions.timeoutMs} when
28
+ * both are given — whichever fires first wins.
29
+ */
30
+ signal?: AbortSignal;
31
+ /**
32
+ * Give up after this many ms. Covers the whole call — the request *and*
33
+ * reading the response body, since `fetch` resolves on headers and a
34
+ * runner that answers `200` and then stalls mid-body would otherwise sit
35
+ * past the deadline. Sent to the container as well, so a well-behaved
36
+ * runner can kill the process rather than leak it when the caller walks
37
+ * away.
38
+ */
39
+ timeoutMs?: number;
40
+ }
41
+ /**
42
+ * The outcome of a `ContainerHandle.exec` call.
43
+ *
44
+ * A non-zero `code` is **not** an error: a command that ran and failed is a
45
+ * result, and the caller decides what to do with it. Only a failure to *run*
46
+ * the command — transport, a non-2xx from the runner, an unparseable body —
47
+ * throws.
48
+ */
49
+ interface ContainerExecResult {
50
+ /** Process exit code. Non-zero means the command ran and failed. */
51
+ code: number;
52
+ /** Everything the command wrote to stderr. */
53
+ stderr: string;
54
+ /** Everything the command wrote to stdout. */
55
+ stdout: string;
56
+ }
57
+ /**
58
+ * The route `ContainerHandle.exec` POSTs to. Namespaced under
59
+ * `/__lunora/` so it cannot collide with an application route the container
60
+ * already serves — the previous ad-hoc convention was a bare `/exec`, which an
61
+ * app could plausibly own for its own purposes.
62
+ *
63
+ * Exported because it is a *contract*, not an implementation detail: a
64
+ * container image has to serve exactly this route, and `@lunora/agent`'s
65
+ * human-in-the-loop gate has to recognise it. Both had it hard-coded as a
66
+ * second literal, which drifts silently — and on the gate's side, drift
67
+ * un-gates model-chosen command execution.
68
+ */
69
+ declare const CONTAINER_EXEC_PATH = "/__lunora/exec";
3
70
  /**
4
71
  * Options for explicitly starting an instance (mirrors `@cloudflare/containers`).
5
- * @experimental
6
72
  */
7
73
  interface ContainerStartOptions {
8
74
  /** Override outbound internet access for this start. */
@@ -16,7 +82,6 @@ interface ContainerStartOptions {
16
82
  }
17
83
  /**
18
84
  * A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time.
19
- * @experimental
20
85
  */
21
86
  interface ContainerInstanceState {
22
87
  [key: string]: unknown;
@@ -44,7 +109,6 @@ interface ContainerStubLike {
44
109
  }
45
110
  /**
46
111
  * What the client needs from a Durable Object namespace binding.
47
- * @experimental
48
112
  */
49
113
  interface ContainerNamespaceLike {
50
114
  get: (id: unknown) => ContainerStubLike;
@@ -57,9 +121,24 @@ interface ContainerNamespaceLike {
57
121
  }
58
122
  /**
59
123
  * A handle on one container instance (one Durable Object).
60
- * @experimental
61
124
  */
62
125
  interface ContainerHandle {
126
+ /**
127
+ * Run a command inside the container and return its exit code and output.
128
+ *
129
+ * A container is an HTTP server, so there is no platform-level exec to call:
130
+ * the command is POSTed to `/__lunora/exec` and the container app
131
+ * serves that route. What this method adds over hand-rolling that fetch is
132
+ * the **contract** — a pinned path, a typed request and response, and the
133
+ * distinction between "the command failed" (a `code`) and "the command could
134
+ * not be run" (a throw). Before it existed every caller invented its own
135
+ * `/exec` convention and read the raw body back as if it were output, which
136
+ * silently turned a runner's 500 into a successful-looking result.
137
+ *
138
+ * The container side must accept `{ args, command, cwd, env, timeoutMs }`
139
+ * and answer `{ code, stdout, stderr }` as JSON.
140
+ */
141
+ exec: (command: string, options?: ContainerExecOptions) => Promise<ContainerExecResult>;
63
142
  /**
64
143
  * Send an HTTP (or WebSocket-upgrade) request to the container. A path
65
144
  * string (`"/transcode"`) is resolved against a synthetic origin; a full
@@ -82,7 +161,6 @@ interface ContainerHandle {
82
161
  * game, a job runner per id) often needs to tear down or inspect the instance
83
162
  * rather than wait for `sleepAfter`, so these wrap the container DO's
84
163
  * `start`/`stop`/`destroy`/`getState`.
85
- * @experimental
86
164
  */
87
165
  interface ContainerInstanceHandle extends ContainerHandle {
88
166
  /** Stop and discard the instance (its ephemeral disk is lost). */
@@ -117,7 +195,6 @@ interface ContainerInstanceHandle extends ContainerHandle {
117
195
  * Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
118
196
  * an app can tighten or relax a single instance's allowed/denied hosts after
119
197
  * start without redeploying.
120
- * @experimental
121
198
  */
122
199
  interface ContainerEgressControls {
123
200
  /** Add one hostname (or glob) to the allow-list. */
@@ -135,7 +212,6 @@ interface ContainerEgressControls {
135
212
  }
136
213
  /**
137
214
  * The per-definition accessor exposed as `ctx.containers.<exportName>`.
138
- * @experimental
139
215
  */
140
216
  interface ContainerAccessor {
141
217
  /**
@@ -178,7 +254,6 @@ interface ContainerAccessor {
178
254
  }
179
255
  /**
180
256
  * Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}.
181
- * @experimental
182
257
  */
183
258
  interface PoolOptions {
184
259
  /** Total attempts before giving up (each on a freshly-picked instance). Default 3. */
@@ -195,6 +270,10 @@ interface PoolOptions {
195
270
  * Whether a *returned* response should be retried on another instance.
196
271
  * Defaults to retrying any `5xx`. A thrown error (network/start failure) is
197
272
  * always retried regardless of this predicate.
273
+ *
274
+ * Applies to `fetch` only. `exec` retries on the cold-start transients
275
+ * alone, because its caller never chose the request and a command that
276
+ * already ran must not be re-run just because the runner failed afterwards.
198
277
  */
199
278
  retryOn?: (response: Response) => boolean;
200
279
  /** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
@@ -205,7 +284,6 @@ interface PoolOptions {
205
284
  * only on the platform's provisioning transients (no-instance / not-listening /
206
285
  * rate-limited — see {@link isColdStartTransient}), which is why it's safe by
207
286
  * default: those responses mean the request never reached the container.
208
- * @experimental
209
287
  */
210
288
  interface InstanceRetryOptions {
211
289
  /**
@@ -221,7 +299,6 @@ interface InstanceRetryOptions {
221
299
  }
222
300
  /**
223
301
  * Wiring info for one definition, emitted by codegen into the generated DO.
224
- * @experimental
225
302
  */
226
303
  interface ContainerBindingSpec {
227
304
  /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
@@ -240,12 +317,10 @@ interface ContainerBindingSpec {
240
317
  * `traceparent` (the inbound RPC's W3C trace context, forwarded by the runtime
241
318
  * and read off the request by the DO) is stamped onto every outbound container
242
319
  * `fetch`, so the container's own spans stitch under the Worker's trace.
243
- * @experimental
244
320
  */
245
321
  declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string) => Record<string, ContainerAccessor>;
246
322
  /**
247
323
  * A test handler: receives the request plus the targeted instance name.
248
- * @experimental
249
324
  */
250
325
  type ContainerTestHandler = (request: Request, instance: {
251
326
  name: string;
@@ -260,7 +335,6 @@ type ContainerTestHandler = (request: Request, instance: {
260
335
  * transcoder: (request) => new Response("ok"),
261
336
  * });
262
337
  * ```
263
- * @experimental
264
338
  */
265
339
  declare const createContainerTestContext: (handlers: Record<string, ContainerTestHandler>) => Record<string, ContainerAccessor>;
266
340
  /**
@@ -272,7 +346,6 @@ declare const createContainerTestContext: (handlers: Record<string, ContainerTes
272
346
  * `Dockerfile.dev` also counts) is used as-is with its directory as the build
273
347
  * context; any other path is treated as the build-context directory and the
274
348
  * Dockerfile is expected at `<dir>/Dockerfile`.
275
- * @experimental
276
349
  */
277
350
  declare const normalizeContainerImage: (image: ContainerImageSource) => NormalizedContainerImage;
278
351
  /**
@@ -280,7 +353,6 @@ declare const normalizeContainerImage: (image: ContainerImageSource) => Normaliz
280
353
  * `transcoder` → `TranscoderContainer`. wrangler's `containers[].class_name`
281
354
  * and the Durable Object binding's `class_name` both reference it, so codegen
282
355
  * and the config layer MUST derive it identically — always via this helper.
283
- * @experimental
284
356
  */
285
357
  declare const containerClassName: (exportName: string) => string;
286
358
  /**
@@ -288,7 +360,6 @@ declare const containerClassName: (exportName: string) => string;
288
360
  * `CONTAINER_TRANSCODER`, `imageResizer` → `CONTAINER_IMAGE_RESIZER`. The
289
361
  * `CONTAINER_` prefix namespaces these away from `SHARD`/`SESSION`/`SCHEDULER`
290
362
  * so a container export can never collide with the built-in bindings.
291
- * @experimental
292
363
  */
293
364
  declare const containerBindingName: (exportName: string) => string;
294
365
  /**
@@ -297,17 +368,14 @@ declare const containerBindingName: (exportName: string) => string;
297
368
  * it as the wrangler `containers[].image`, and `lunora deploy` builds that tag
298
369
  * with Railpack and `wrangler containers push`es it before deploying — so all
299
370
  * three derive the tag from this one helper and can never disagree.
300
- * @experimental
301
371
  */
302
372
  declare const containerBuildTag: (exportName: string) => string;
303
373
  /**
304
374
  * `defineContainer` is part of the experimental `@lunora/container` API and may change without a major version bump.
305
- * @experimental
306
375
  */
307
376
  declare const defineContainer: (config: ContainerConfig) => ContainerDefinition;
308
377
  /**
309
378
  * True when a value is a `defineContainer` result (the runtime brand check).
310
- * @experimental
311
379
  */
312
380
  declare const isContainerDefinition: (value: unknown) => value is ContainerDefinition;
313
381
  /**
@@ -315,10 +383,9 @@ declare const isContainerDefinition: (value: unknown) => value is ContainerDefin
315
383
  * plus every declared secret resolved from the Worker `env`. A declared secret
316
384
  * missing from the Worker env fails fast — starting the container without a
317
385
  * credential it was promised yields far worse errors downstream.
318
- * @experimental
319
386
  */
320
387
  declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
321
- export {
388
+ export { CONTAINER_EXEC_PATH,
322
389
  /**
323
390
  * `@lunora/container` — Cloudflare Containers for Lunora.
324
391
  *
@@ -351,7 +418,7 @@ type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition,
351
418
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
352
419
  * behind the `@lunora/container/do` subpath.
353
420
  */
354
- type ContainerEgressControls,
421
+ type ContainerEgressControls, type ContainerExecOptions, type ContainerExecResult,
355
422
  /**
356
423
  * `@lunora/container` — Cloudflare Containers for Lunora.
357
424
  *
package/dist/index.d.ts CHANGED
@@ -1,8 +1,74 @@
1
- import { D as DurableObjectJurisdiction, C as ContainerConfig, a as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-DeQ3rtYK.js";
2
- export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-DeQ3rtYK.js";
1
+ import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-KcQu2ZT_.js";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-KcQu2ZT_.js";
3
+ /**
4
+ * Per-call options for `ContainerHandle.exec`.
5
+ */
6
+ interface ContainerExecOptions {
7
+ /** Arguments passed to `command`, unshelled — the container must not concatenate them into a shell string. */
8
+ args?: ReadonlyArray<string>;
9
+ /** Working directory for the command, relative to the container's own root. */
10
+ cwd?: string;
11
+ /** Extra environment for this command only, merged over the container's env. */
12
+ env?: Readonly<Record<string, string>>;
13
+ /**
14
+ * Cap on the response body, in bytes. The whole `{code,stdout,stderr}`
15
+ * document has to be held in memory to be parsed, and a build box or job
16
+ * runner routinely writes tens of megabytes — a single unbounded `exec`
17
+ * that returns more than the isolate's memory limit kills the isolate and
18
+ * every other in-flight request sharing it, not just this call. So a
19
+ * runner that overruns the cap fails the call loudly rather than taking
20
+ * the shard down with it; raise this (or have the runner cap its own
21
+ * output) when a command legitimately produces more.
22
+ *
23
+ * Default {@link DEFAULT_EXEC_MAX_OUTPUT_BYTES} (1MB).
24
+ */
25
+ maxOutputBytes?: number;
26
+ /**
27
+ * Abort the call. Composed with {@link ContainerExecOptions.timeoutMs} when
28
+ * both are given — whichever fires first wins.
29
+ */
30
+ signal?: AbortSignal;
31
+ /**
32
+ * Give up after this many ms. Covers the whole call — the request *and*
33
+ * reading the response body, since `fetch` resolves on headers and a
34
+ * runner that answers `200` and then stalls mid-body would otherwise sit
35
+ * past the deadline. Sent to the container as well, so a well-behaved
36
+ * runner can kill the process rather than leak it when the caller walks
37
+ * away.
38
+ */
39
+ timeoutMs?: number;
40
+ }
41
+ /**
42
+ * The outcome of a `ContainerHandle.exec` call.
43
+ *
44
+ * A non-zero `code` is **not** an error: a command that ran and failed is a
45
+ * result, and the caller decides what to do with it. Only a failure to *run*
46
+ * the command — transport, a non-2xx from the runner, an unparseable body —
47
+ * throws.
48
+ */
49
+ interface ContainerExecResult {
50
+ /** Process exit code. Non-zero means the command ran and failed. */
51
+ code: number;
52
+ /** Everything the command wrote to stderr. */
53
+ stderr: string;
54
+ /** Everything the command wrote to stdout. */
55
+ stdout: string;
56
+ }
57
+ /**
58
+ * The route `ContainerHandle.exec` POSTs to. Namespaced under
59
+ * `/__lunora/` so it cannot collide with an application route the container
60
+ * already serves — the previous ad-hoc convention was a bare `/exec`, which an
61
+ * app could plausibly own for its own purposes.
62
+ *
63
+ * Exported because it is a *contract*, not an implementation detail: a
64
+ * container image has to serve exactly this route, and `@lunora/agent`'s
65
+ * human-in-the-loop gate has to recognise it. Both had it hard-coded as a
66
+ * second literal, which drifts silently — and on the gate's side, drift
67
+ * un-gates model-chosen command execution.
68
+ */
69
+ declare const CONTAINER_EXEC_PATH = "/__lunora/exec";
3
70
  /**
4
71
  * Options for explicitly starting an instance (mirrors `@cloudflare/containers`).
5
- * @experimental
6
72
  */
7
73
  interface ContainerStartOptions {
8
74
  /** Override outbound internet access for this start. */
@@ -16,7 +82,6 @@ interface ContainerStartOptions {
16
82
  }
17
83
  /**
18
84
  * A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time.
19
- * @experimental
20
85
  */
21
86
  interface ContainerInstanceState {
22
87
  [key: string]: unknown;
@@ -44,7 +109,6 @@ interface ContainerStubLike {
44
109
  }
45
110
  /**
46
111
  * What the client needs from a Durable Object namespace binding.
47
- * @experimental
48
112
  */
49
113
  interface ContainerNamespaceLike {
50
114
  get: (id: unknown) => ContainerStubLike;
@@ -57,9 +121,24 @@ interface ContainerNamespaceLike {
57
121
  }
58
122
  /**
59
123
  * A handle on one container instance (one Durable Object).
60
- * @experimental
61
124
  */
62
125
  interface ContainerHandle {
126
+ /**
127
+ * Run a command inside the container and return its exit code and output.
128
+ *
129
+ * A container is an HTTP server, so there is no platform-level exec to call:
130
+ * the command is POSTed to `/__lunora/exec` and the container app
131
+ * serves that route. What this method adds over hand-rolling that fetch is
132
+ * the **contract** — a pinned path, a typed request and response, and the
133
+ * distinction between "the command failed" (a `code`) and "the command could
134
+ * not be run" (a throw). Before it existed every caller invented its own
135
+ * `/exec` convention and read the raw body back as if it were output, which
136
+ * silently turned a runner's 500 into a successful-looking result.
137
+ *
138
+ * The container side must accept `{ args, command, cwd, env, timeoutMs }`
139
+ * and answer `{ code, stdout, stderr }` as JSON.
140
+ */
141
+ exec: (command: string, options?: ContainerExecOptions) => Promise<ContainerExecResult>;
63
142
  /**
64
143
  * Send an HTTP (or WebSocket-upgrade) request to the container. A path
65
144
  * string (`"/transcode"`) is resolved against a synthetic origin; a full
@@ -82,7 +161,6 @@ interface ContainerHandle {
82
161
  * game, a job runner per id) often needs to tear down or inspect the instance
83
162
  * rather than wait for `sleepAfter`, so these wrap the container DO's
84
163
  * `start`/`stop`/`destroy`/`getState`.
85
- * @experimental
86
164
  */
87
165
  interface ContainerInstanceHandle extends ContainerHandle {
88
166
  /** Stop and discard the instance (its ephemeral disk is lost). */
@@ -117,7 +195,6 @@ interface ContainerInstanceHandle extends ContainerHandle {
117
195
  * Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
118
196
  * an app can tighten or relax a single instance's allowed/denied hosts after
119
197
  * start without redeploying.
120
- * @experimental
121
198
  */
122
199
  interface ContainerEgressControls {
123
200
  /** Add one hostname (or glob) to the allow-list. */
@@ -135,7 +212,6 @@ interface ContainerEgressControls {
135
212
  }
136
213
  /**
137
214
  * The per-definition accessor exposed as `ctx.containers.<exportName>`.
138
- * @experimental
139
215
  */
140
216
  interface ContainerAccessor {
141
217
  /**
@@ -178,7 +254,6 @@ interface ContainerAccessor {
178
254
  }
179
255
  /**
180
256
  * Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}.
181
- * @experimental
182
257
  */
183
258
  interface PoolOptions {
184
259
  /** Total attempts before giving up (each on a freshly-picked instance). Default 3. */
@@ -195,6 +270,10 @@ interface PoolOptions {
195
270
  * Whether a *returned* response should be retried on another instance.
196
271
  * Defaults to retrying any `5xx`. A thrown error (network/start failure) is
197
272
  * always retried regardless of this predicate.
273
+ *
274
+ * Applies to `fetch` only. `exec` retries on the cold-start transients
275
+ * alone, because its caller never chose the request and a command that
276
+ * already ran must not be re-run just because the runner failed afterwards.
198
277
  */
199
278
  retryOn?: (response: Response) => boolean;
200
279
  /** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
@@ -205,7 +284,6 @@ interface PoolOptions {
205
284
  * only on the platform's provisioning transients (no-instance / not-listening /
206
285
  * rate-limited — see {@link isColdStartTransient}), which is why it's safe by
207
286
  * default: those responses mean the request never reached the container.
208
- * @experimental
209
287
  */
210
288
  interface InstanceRetryOptions {
211
289
  /**
@@ -221,7 +299,6 @@ interface InstanceRetryOptions {
221
299
  }
222
300
  /**
223
301
  * Wiring info for one definition, emitted by codegen into the generated DO.
224
- * @experimental
225
302
  */
226
303
  interface ContainerBindingSpec {
227
304
  /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
@@ -240,12 +317,10 @@ interface ContainerBindingSpec {
240
317
  * `traceparent` (the inbound RPC's W3C trace context, forwarded by the runtime
241
318
  * and read off the request by the DO) is stamped onto every outbound container
242
319
  * `fetch`, so the container's own spans stitch under the Worker's trace.
243
- * @experimental
244
320
  */
245
321
  declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string) => Record<string, ContainerAccessor>;
246
322
  /**
247
323
  * A test handler: receives the request plus the targeted instance name.
248
- * @experimental
249
324
  */
250
325
  type ContainerTestHandler = (request: Request, instance: {
251
326
  name: string;
@@ -260,7 +335,6 @@ type ContainerTestHandler = (request: Request, instance: {
260
335
  * transcoder: (request) => new Response("ok"),
261
336
  * });
262
337
  * ```
263
- * @experimental
264
338
  */
265
339
  declare const createContainerTestContext: (handlers: Record<string, ContainerTestHandler>) => Record<string, ContainerAccessor>;
266
340
  /**
@@ -272,7 +346,6 @@ declare const createContainerTestContext: (handlers: Record<string, ContainerTes
272
346
  * `Dockerfile.dev` also counts) is used as-is with its directory as the build
273
347
  * context; any other path is treated as the build-context directory and the
274
348
  * Dockerfile is expected at `<dir>/Dockerfile`.
275
- * @experimental
276
349
  */
277
350
  declare const normalizeContainerImage: (image: ContainerImageSource) => NormalizedContainerImage;
278
351
  /**
@@ -280,7 +353,6 @@ declare const normalizeContainerImage: (image: ContainerImageSource) => Normaliz
280
353
  * `transcoder` → `TranscoderContainer`. wrangler's `containers[].class_name`
281
354
  * and the Durable Object binding's `class_name` both reference it, so codegen
282
355
  * and the config layer MUST derive it identically — always via this helper.
283
- * @experimental
284
356
  */
285
357
  declare const containerClassName: (exportName: string) => string;
286
358
  /**
@@ -288,7 +360,6 @@ declare const containerClassName: (exportName: string) => string;
288
360
  * `CONTAINER_TRANSCODER`, `imageResizer` → `CONTAINER_IMAGE_RESIZER`. The
289
361
  * `CONTAINER_` prefix namespaces these away from `SHARD`/`SESSION`/`SCHEDULER`
290
362
  * so a container export can never collide with the built-in bindings.
291
- * @experimental
292
363
  */
293
364
  declare const containerBindingName: (exportName: string) => string;
294
365
  /**
@@ -297,17 +368,14 @@ declare const containerBindingName: (exportName: string) => string;
297
368
  * it as the wrangler `containers[].image`, and `lunora deploy` builds that tag
298
369
  * with Railpack and `wrangler containers push`es it before deploying — so all
299
370
  * three derive the tag from this one helper and can never disagree.
300
- * @experimental
301
371
  */
302
372
  declare const containerBuildTag: (exportName: string) => string;
303
373
  /**
304
374
  * `defineContainer` is part of the experimental `@lunora/container` API and may change without a major version bump.
305
- * @experimental
306
375
  */
307
376
  declare const defineContainer: (config: ContainerConfig) => ContainerDefinition;
308
377
  /**
309
378
  * True when a value is a `defineContainer` result (the runtime brand check).
310
- * @experimental
311
379
  */
312
380
  declare const isContainerDefinition: (value: unknown) => value is ContainerDefinition;
313
381
  /**
@@ -315,10 +383,9 @@ declare const isContainerDefinition: (value: unknown) => value is ContainerDefin
315
383
  * plus every declared secret resolved from the Worker `env`. A declared secret
316
384
  * missing from the Worker env fails fast — starting the container without a
317
385
  * credential it was promised yields far worse errors downstream.
318
- * @experimental
319
386
  */
320
387
  declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
321
- export {
388
+ export { CONTAINER_EXEC_PATH,
322
389
  /**
323
390
  * `@lunora/container` — Cloudflare Containers for Lunora.
324
391
  *
@@ -351,7 +418,7 @@ type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition,
351
418
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
352
419
  * behind the `@lunora/container/do` subpath.
353
420
  */
354
- type ContainerEgressControls,
421
+ type ContainerEgressControls, type ContainerExecOptions, type ContainerExecResult,
355
422
  /**
356
423
  * `@lunora/container` — Cloudflare Containers for Lunora.
357
424
  *
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createContainerContext as t,createContainerTestContext as a}from"./packem_shared/createContainerContext-C8pmlFAX.mjs";import{containerBindingName as o,containerBuildTag as r,containerClassName as C,defineContainer as m,isContainerDefinition as s,normalizeContainerImage as c,resolveContainerEnvVars as f}from"./packem_shared/containerBindingName-D01FopQt.mjs";export{o as containerBindingName,r as containerBuildTag,C as containerClassName,t as createContainerContext,a as createContainerTestContext,m as defineContainer,s as isContainerDefinition,c as normalizeContainerImage,f as resolveContainerEnvVars};
1
+ import{createContainerContext as o,createContainerTestContext as r}from"./packem_shared/createContainerContext-LXvczcU0.mjs";import{containerBindingName as a,containerBuildTag as i,containerClassName as C,defineContainer as m,isContainerDefinition as s,normalizeContainerImage as c,resolveContainerEnvVars as f}from"./packem_shared/containerBindingName-D01FopQt.mjs";import{C as l}from"./packem_shared/exec-BjOpQOTW.mjs";export{l as CONTAINER_EXEC_PATH,a as containerBindingName,i as containerBuildTag,C as containerClassName,o as createContainerContext,r as createContainerTestContext,m as defineContainer,s as isContainerDefinition,c as normalizeContainerImage,f as resolveContainerEnvVars};
package/dist/otel.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * An attribute value carried on a span or log.
3
- * @experimental
4
3
  */
5
4
  type ContainerAttributeValue = boolean | number | string;
6
5
  /**
@@ -9,7 +8,6 @@ type ContainerAttributeValue = boolean | number | string;
9
8
  * cancels the response `body` so Node/undici can release the socket for
10
9
  * keep-alive reuse instead of leaving it occupied by an unread stream. It reads
11
10
  * `ok`/`status` to detect a rejected export and nothing else from the response.
12
- * @experimental
13
11
  */
14
12
  type OtelFetchLike = (input: string, init: {
15
13
  body: string;
@@ -25,7 +23,6 @@ type OtelFetchLike = (input: string, init: {
25
23
  }>;
26
24
  /**
27
25
  * A single span the container process asks the exporter to record.
28
- * @experimental
29
26
  */
30
27
  interface ContainerSpanInput {
31
28
  /** Attributes attached to the span (rendered under the OTLP `attributes` list). */
@@ -44,7 +41,6 @@ interface ContainerSpanInput {
44
41
  }
45
42
  /**
46
43
  * A single log line the container process asks the exporter to record.
47
- * @experimental
48
44
  */
49
45
  interface ContainerLogInput {
50
46
  /** Attributes attached to the log record. */
@@ -58,7 +54,6 @@ interface ContainerLogInput {
58
54
  }
59
55
  /**
60
56
  * Options for {@link createContainerTelemetry}.
61
- * @experimental
62
57
  */
63
58
  interface ContainerTelemetryOptions {
64
59
  /**
@@ -124,7 +119,6 @@ interface ContainerTelemetryOptions {
124
119
  }
125
120
  /**
126
121
  * The exporter handle {@link createContainerTelemetry} returns.
127
- * @experimental
128
122
  */
129
123
  interface ContainerTelemetry {
130
124
  /** Record one log line (no-op when disabled). */
@@ -155,7 +149,6 @@ interface ContainerTelemetry {
155
149
  * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
156
150
  * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
157
151
  * `detectResources: true`.
158
- * @experimental
159
152
  */
160
153
  declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
161
154
  export { type ContainerAttributeValue, type ContainerLogInput, type ContainerSpanInput, type ContainerTelemetry, type ContainerTelemetryOptions, type OtelFetchLike, createContainerTelemetry };
package/dist/otel.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * An attribute value carried on a span or log.
3
- * @experimental
4
3
  */
5
4
  type ContainerAttributeValue = boolean | number | string;
6
5
  /**
@@ -9,7 +8,6 @@ type ContainerAttributeValue = boolean | number | string;
9
8
  * cancels the response `body` so Node/undici can release the socket for
10
9
  * keep-alive reuse instead of leaving it occupied by an unread stream. It reads
11
10
  * `ok`/`status` to detect a rejected export and nothing else from the response.
12
- * @experimental
13
11
  */
14
12
  type OtelFetchLike = (input: string, init: {
15
13
  body: string;
@@ -25,7 +23,6 @@ type OtelFetchLike = (input: string, init: {
25
23
  }>;
26
24
  /**
27
25
  * A single span the container process asks the exporter to record.
28
- * @experimental
29
26
  */
30
27
  interface ContainerSpanInput {
31
28
  /** Attributes attached to the span (rendered under the OTLP `attributes` list). */
@@ -44,7 +41,6 @@ interface ContainerSpanInput {
44
41
  }
45
42
  /**
46
43
  * A single log line the container process asks the exporter to record.
47
- * @experimental
48
44
  */
49
45
  interface ContainerLogInput {
50
46
  /** Attributes attached to the log record. */
@@ -58,7 +54,6 @@ interface ContainerLogInput {
58
54
  }
59
55
  /**
60
56
  * Options for {@link createContainerTelemetry}.
61
- * @experimental
62
57
  */
63
58
  interface ContainerTelemetryOptions {
64
59
  /**
@@ -124,7 +119,6 @@ interface ContainerTelemetryOptions {
124
119
  }
125
120
  /**
126
121
  * The exporter handle {@link createContainerTelemetry} returns.
127
- * @experimental
128
122
  */
129
123
  interface ContainerTelemetry {
130
124
  /** Record one log line (no-op when disabled). */
@@ -155,7 +149,6 @@ interface ContainerTelemetry {
155
149
  * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
156
150
  * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
157
151
  * `detectResources: true`.
158
- * @experimental
159
152
  */
160
153
  declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
161
154
  export { type ContainerAttributeValue, type ContainerLogInput, type ContainerSpanInput, type ContainerTelemetry, type ContainerTelemetryOptions, type OtelFetchLike, createContainerTelemetry };
@@ -0,0 +1 @@
1
+ import{C,e as E}from"./exec-BjOpQOTW.mjs";export{C as CONTAINER_EXEC_PATH,E as execViaFetch};
@@ -0,0 +1 @@
1
+ import{LunoraError as R}from"@lunora/errors";import{containerBindingName as k}from"./containerBindingName-D01FopQt.mjs";import{e as F,r as U}from"./exec-BjOpQOTW.mjs";import{a as I}from"./jurisdiction-DtE9s70w.mjs";const N=3,D=3e4,q=3,W=500,j="cf-container-target-port",v="__lunora",p=t=>t.split("/").find(e=>e!=="")??"",C=(t,e)=>{const o=typeof t=="string"?t:t.url,n=URL.parse(o.startsWith("/")?`http://container${o}`:o);if(n===null)return;const{pathname:r}=n;let s=r;try{s=decodeURIComponent(r)}catch{}if(p(r)===v||p(s)===v)throw new R("BAD_REQUEST",`${e}: \`/${v}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},P=(t,e,o,n)=>{const r=typeof t=="string"&&t.startsWith("/")?new Request(`http://container${t}`,e):new Request(t,e);return o!==void 0&&r.headers.set(j,String(o)),n!==void 0&&r.headers.set("traceparent",n),r},L=async t=>{t<=0||await new Promise(e=>{setTimeout(e,t)})},Y=/no container instance|not listening|try again later|rate.?limit|provision/i,z="no Container instance available",G="Failed to start container:",K=1024,V=async t=>(await U(t.clone().body,K)).text,O=t=>t instanceof Error&&Y.test(t.message),H=async t=>{if(t.status===429)return!0;if(t.status!==500&&t.status!==503)return!1;try{const e=await V(t);return e.includes(z)||e.startsWith(G)}catch{return!1}},w=t=>`ctx.containers.${t.exportName}`,S=(t,e,o={},n,r)=>{const s=Math.max(1,o.attempts??q),a=o.backoffMs??W,A=o.maxBackoffMs??D,g=async(i,f)=>{const T=typeof i=="string"?s:1;let l;for(let d=0;d<T;d+=1){const u=d===T-1;d>0&&await L(Math.min(a*2**(d-1),A));try{const m=await t(P(i,f,n,r));if(u||!await H(m))return m}catch(m){if(l=m,u||!O(m))throw m}}throw l instanceof Error?l:new Error("ctx.containers: cold-start retry exhausted")};return{exec:F(g,e),fetch:async(i,f)=>(C(i,e),g(i,f)),port:i=>S(t,e,o,i,r)}},x=(t,e,o,n,r)=>S(async s=>t.get(t.idFromName(e)).fetch(s),o,n,void 0,r),c=async(t,e,o,n)=>{const r=t[e];if(typeof r!="function")throw new TypeError(`ctx.containers: the "${o}" container DO does not expose ${e}() — is @lunora/container/do up to date?`);return r(n)},J=(t,e)=>({allow:async o=>c(t(),"allowHost",e,o),deny:async o=>c(t(),"denyHost",e,o),removeAllowed:async o=>c(t(),"removeAllowedHost",e,o),removeDenied:async o=>c(t(),"removeDeniedHost",e,o),setAllowed:async o=>c(t(),"setAllowedHosts",e,[...o]),setDenied:async o=>c(t(),"setDeniedHosts",e,[...o])}),b=(t,e,o,n,r)=>{const s=()=>t.get(t.idFromName(o));return{...S(async a=>s().fetch(a),w(e),n,void 0,r),destroy:async()=>c(s(),"destroy",e.binding),egress:J(s,e.binding),getState:async()=>c(s(),"getState",e.binding),renewActivityTimeout:async()=>c(s(),"renewActivityTimeout",e.binding),start:async a=>c(s(),"start",e.binding,a),stop:async a=>c(s(),"stop",e.binding,a)}},B=t=>`pool-${String(Math.floor(Math.random()*t))}`,Q=t=>t.status>=500,M=(t,e,o={},n,r)=>{const s=o.size??e.maxInstances??N,a=Math.max(1,o.attempts??3),A=o.backoffMs??100,g=o.maxBackoffMs??D,i=`${w(e)}.pool()`,f=(l,d)=>async(u,m)=>{const E=typeof u=="string"?a:1;let _;for(let h=0;h<E;h+=1){h>0&&await L(Math.min(A*2**(h-1),g));const $=P(u,m,n,r);try{const y=await t.get(t.idFromName(B(s))).fetch($);if(h===E-1||!await l(y))return y}catch(y){if(_=y,!d(y))throw y}}throw _ instanceof Error?_:new Error(`ctx.containers.${e.exportName}.pool(): all ${String(E)} attempts failed`)},T=f(o.retryOn??Q,()=>!0);return{exec:F(f(H,O),i),fetch:async(l,d)=>(C(l,i),T(l,d)),port:l=>M(t,e,o,l,r)}},X=(t,e,o)=>({any:(n,r)=>x(t,B(n??e.maxInstances??N),w(e),r,o),get:(n,r)=>b(t,e,n,r,o),pool:n=>M(t,e,n,void 0,o)}),Z=t=>{const e=()=>{throw new R("INTERNAL",`ctx.containers.${t.exportName}: no "${t.binding}" Durable Object binding found. Run \`lunora dev\` (or \`lunora deploy\`) to reconcile wrangler.jsonc, and make sure the worker entry re-exports the generated container classes.`)};return{any:e,get:e,pool:e}},st=(t,e,o,n)=>{const r={};for(const s of e){const a=t[s.binding];r[s.exportName]=a&&typeof a.idFromName=="function"&&typeof a.get=="function"?X(I(a,o),s,n):Z(s)}return r},tt=t=>{const e=o=>({allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:n=>Promise.resolve(t(n,{name:o})),getState:()=>Promise.resolve({lastChange:0}),removeAllowedHost:()=>Promise.resolve(),removeDeniedHost:()=>Promise.resolve(),renewActivityTimeout:()=>Promise.resolve(),setAllowedHosts:()=>Promise.resolve(),setDeniedHosts:()=>Promise.resolve(),start:()=>Promise.resolve(),stop:()=>Promise.resolve()});return{get:o=>e(String(o)),idFromName:o=>o}},at=t=>{const e={};for(const[o,n]of Object.entries(t)){const r=tt(n),s={binding:k(o),exportName:o};e[o]={any:()=>x(r,"pool-0",w(s),{attempts:1}),get:a=>b(r,s,a,{attempts:1}),pool:()=>x(r,"pool-0",`${w(s)}.pool()`,{attempts:1})}}return e};export{st as createContainerContext,at as createContainerTestContext};
@@ -0,0 +1 @@
1
+ import{LunoraError as d}from"@lunora/errors";const w=async(o,r,t)=>{if(o===null)return{overflowed:!1,text:""};const e=o.getReader(),n=new TextDecoder,a=()=>{e.cancel().catch(()=>{})};t?.addEventListener("abort",a);let s="",c=0,u=!1;try{for(;t?.aborted!==!0;){const{done:f,value:i}=await e.read();if(f)break;if(c+i.byteLength>r){u=!0,s+=n.decode(i.subarray(0,r-c),{stream:!0});break}c+=i.byteLength,s+=n.decode(i,{stream:!0})}s+=n.decode()}finally{t?.removeEventListener("abort",a),e.cancel().catch(()=>{})}if(t?.aborted===!0)throw t.reason;return{overflowed:u,text:s}},l="/__lunora/exec",E=512,b=1e6,v="<redacted>",h=(o,r)=>{if(r===void 0)return o;let t=o;for(const e of Object.values(r))e.length>0&&(t=t.split(e).join(v));return t},x=o=>{if(o===null||typeof o!="object")return;const{code:r,stderr:t,stdout:e}=o;if(typeof r=="number")return{code:r,stderr:typeof t=="string"?t:"",stdout:typeof e=="string"?e:""}},m=(o,r)=>{if(r===void 0)return{dispose:()=>{},signal:o};const t=new AbortController,e=setTimeout(()=>{t.abort(new d("INTERNAL",`ctx.containers: exec timed out after ${String(r)}ms`))},r);return{dispose:()=>{clearTimeout(e)},signal:o===void 0?t.signal:AbortSignal.any([o,t.signal])}},T=(o,r)=>async(t,e={})=>{if(typeof t!="string"||t.length===0)throw new d("BAD_REQUEST",`${r}: exec requires a non-empty \`command\``);const n=e.maxOutputBytes??b;if(!Number.isSafeInteger(n)||n<=0)throw new d("BAD_REQUEST",`${r}: exec \`maxOutputBytes\` must be a positive whole number of bytes, got ${String(n)}`);const a=m(e.signal,e.timeoutMs);try{const s=await o(l,{body:JSON.stringify({args:e.args??[],command:t,...e.cwd===void 0?{}:{cwd:e.cwd},...e.env===void 0?{}:{env:e.env},...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}}),headers:{"content-type":"application/json"},method:"POST",...a.signal===void 0?{}:{signal:a.signal}});if(!s.ok){const i=await w(s.body,E,a.signal).then(y=>h(y.text,e.env),()=>"<unreadable body>");throw new d("INTERNAL",`${r}: exec failed — the container answered ${String(s.status)} for POST ${l}. Does it serve that route? Body: ${i}`)}const c=await w(s.body,n,a.signal);if(c.overflowed)throw new d("INTERNAL",`${r}: exec response exceeded ${String(n)} bytes and was abandoned — it is buffered whole to be parsed, so an unbounded one would exhaust the isolate. Cap the command's output, or raise \`maxOutputBytes\`.`);let u;try{u=JSON.parse(c.text)}catch{throw new d("INTERNAL",`${r}: exec response was not JSON — expected {"code","stdout","stderr"} from POST ${l}`)}const f=x(u);if(f===void 0)throw new d("INTERNAL",`${r}: exec response is missing a numeric \`code\` — expected {"code","stdout","stderr"} from POST ${l}`);return f}finally{a.dispose()}};export{l as C,T as e,w as r};
@@ -7,14 +7,12 @@
7
7
  */
8
8
  /**
9
9
  * Named instance types Cloudflare Containers provides.
10
- * @experimental
11
10
  */
12
11
  type NamedContainerInstanceType = "basic" | "lite" | "standard-1" | "standard-2" | "standard-3" | "standard-4";
13
12
  /**
14
13
  * A custom instance type. Cloudflare's bounds at the time of writing: up to
15
14
  * 4 vCPU, 12 GiB memory, 20 GB disk, ≥ 3 GiB memory per vCPU and ≤ 2 GB disk
16
15
  * per GiB memory. The config-layer validator enforces the documented ranges.
17
- * @experimental
18
16
  */
19
17
  interface CustomContainerInstanceType {
20
18
  /** Disk in MB. Cloudflare's default is 2000 (2 GB). */
@@ -26,12 +24,10 @@ interface CustomContainerInstanceType {
26
24
  }
27
25
  /**
28
26
  * `ContainerInstanceType` is part of the experimental `@lunora/container` API and may change without a major version bump.
29
- * @experimental
30
27
  */
31
28
  type ContainerInstanceType = CustomContainerInstanceType | NamedContainerInstanceType;
32
29
  /**
33
30
  * Rolling-deploy tuning for a container.
34
- * @experimental
35
31
  */
36
32
  interface ContainerRollout {
37
33
  /** Seconds an active instance runs before it's eligible for update (wrangler `rollout_active_grace_period`). */
@@ -43,7 +39,6 @@ interface ContainerRollout {
43
39
  * A pre-built image pulled from a registry — the Cloudflare Registry, Docker
44
40
  * Hub, or Amazon ECR (the registries `wrangler deploy` supports). The
45
41
  * reference must be fully qualified, e.g. `docker.io/acme/transcoder:1.4`.
46
- * @experimental
47
42
  */
48
43
  interface RegistryImageSource {
49
44
  registry: string;
@@ -53,7 +48,6 @@ interface RegistryImageSource {
53
48
  * source directory and `lunora deploy` builds an OCI image with Railpack
54
49
  * (needs a BuildKit instance) and pushes it to the Cloudflare Registry before
55
50
  * wrangler runs. Opt-in — the Dockerfile path is the zero-extra-deps default.
56
- * @experimental
57
51
  */
58
52
  interface BuildImageSource {
59
53
  build: string;
@@ -63,7 +57,6 @@ interface BuildImageSource {
63
57
  * either a directory containing a `Dockerfile` (normalized to
64
58
  * `<dir>/Dockerfile` with the directory as the build context) or a path to
65
59
  * the Dockerfile itself — while `{ registry }` is a pre-built image reference.
66
- * @experimental
67
60
  */
68
61
  type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
69
62
  /**
@@ -76,7 +69,6 @@ type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
76
69
  * functions), so codegen and the config layer can read it without evaluating
77
70
  * code. (Upstream cloudflare/containers#188 expresses the same idea as handler
78
71
  * functions; the Lunora config is data-only, so it's modelled as descriptors.)
79
- * @experimental
80
72
  */
81
73
  interface ContainerReadinessCheck {
82
74
  /** HTTP path probed on the container, e.g. `"/ready"` (a leading slash is optional). */
@@ -88,7 +80,6 @@ interface ContainerReadinessCheck {
88
80
  }
89
81
  /**
90
82
  * `ContainerConfig` is part of the experimental `@lunora/container` API and may change without a major version bump.
91
- * @experimental
92
83
  */
93
84
  interface ContainerConfig {
94
85
  /**
@@ -246,7 +237,6 @@ interface ContainerConfig {
246
237
  /**
247
238
  * The value `defineContainer` returns: the validated config plus a brand the
248
239
  * codegen discovery and the generated Container DO class key on.
249
- * @experimental
250
240
  */
251
241
  interface ContainerDefinition extends ContainerConfig {
252
242
  /** Brand marking a value as a Lunora container definition. */
@@ -254,7 +244,6 @@ interface ContainerDefinition extends ContainerConfig {
254
244
  }
255
245
  /**
256
246
  * A normalized image source, as written into `wrangler.jsonc`.
257
- * @experimental
258
247
  */
259
248
  type NormalizedContainerImage = {
260
249
  /** Build context directory (wrangler `image_build_context`). */
@@ -275,7 +264,6 @@ type NormalizedContainerImage = {
275
264
  * Cloudflare Durable Object data-residency jurisdiction. Widening union —
276
265
  * Cloudflare adds values over time.
277
266
  * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
278
- * @experimental
279
267
  */
280
268
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
281
- export { BuildImageSource as B, ContainerConfig as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerDefinition as a, ContainerImageSource as b, ContainerInstanceType as c, ContainerReadinessCheck as d, ContainerRollout as e, CustomContainerInstanceType as f, NamedContainerInstanceType as g };
269
+ export { BuildImageSource as B, ContainerDefinition as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerConfig as a, ContainerImageSource as b, ContainerInstanceType as c, ContainerReadinessCheck as d, ContainerRollout as e, CustomContainerInstanceType as f, NamedContainerInstanceType as g };
@@ -7,14 +7,12 @@
7
7
  */
8
8
  /**
9
9
  * Named instance types Cloudflare Containers provides.
10
- * @experimental
11
10
  */
12
11
  type NamedContainerInstanceType = "basic" | "lite" | "standard-1" | "standard-2" | "standard-3" | "standard-4";
13
12
  /**
14
13
  * A custom instance type. Cloudflare's bounds at the time of writing: up to
15
14
  * 4 vCPU, 12 GiB memory, 20 GB disk, ≥ 3 GiB memory per vCPU and ≤ 2 GB disk
16
15
  * per GiB memory. The config-layer validator enforces the documented ranges.
17
- * @experimental
18
16
  */
19
17
  interface CustomContainerInstanceType {
20
18
  /** Disk in MB. Cloudflare's default is 2000 (2 GB). */
@@ -26,12 +24,10 @@ interface CustomContainerInstanceType {
26
24
  }
27
25
  /**
28
26
  * `ContainerInstanceType` is part of the experimental `@lunora/container` API and may change without a major version bump.
29
- * @experimental
30
27
  */
31
28
  type ContainerInstanceType = CustomContainerInstanceType | NamedContainerInstanceType;
32
29
  /**
33
30
  * Rolling-deploy tuning for a container.
34
- * @experimental
35
31
  */
36
32
  interface ContainerRollout {
37
33
  /** Seconds an active instance runs before it's eligible for update (wrangler `rollout_active_grace_period`). */
@@ -43,7 +39,6 @@ interface ContainerRollout {
43
39
  * A pre-built image pulled from a registry — the Cloudflare Registry, Docker
44
40
  * Hub, or Amazon ECR (the registries `wrangler deploy` supports). The
45
41
  * reference must be fully qualified, e.g. `docker.io/acme/transcoder:1.4`.
46
- * @experimental
47
42
  */
48
43
  interface RegistryImageSource {
49
44
  registry: string;
@@ -53,7 +48,6 @@ interface RegistryImageSource {
53
48
  * source directory and `lunora deploy` builds an OCI image with Railpack
54
49
  * (needs a BuildKit instance) and pushes it to the Cloudflare Registry before
55
50
  * wrangler runs. Opt-in — the Dockerfile path is the zero-extra-deps default.
56
- * @experimental
57
51
  */
58
52
  interface BuildImageSource {
59
53
  build: string;
@@ -63,7 +57,6 @@ interface BuildImageSource {
63
57
  * either a directory containing a `Dockerfile` (normalized to
64
58
  * `<dir>/Dockerfile` with the directory as the build context) or a path to
65
59
  * the Dockerfile itself — while `{ registry }` is a pre-built image reference.
66
- * @experimental
67
60
  */
68
61
  type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
69
62
  /**
@@ -76,7 +69,6 @@ type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
76
69
  * functions), so codegen and the config layer can read it without evaluating
77
70
  * code. (Upstream cloudflare/containers#188 expresses the same idea as handler
78
71
  * functions; the Lunora config is data-only, so it's modelled as descriptors.)
79
- * @experimental
80
72
  */
81
73
  interface ContainerReadinessCheck {
82
74
  /** HTTP path probed on the container, e.g. `"/ready"` (a leading slash is optional). */
@@ -88,7 +80,6 @@ interface ContainerReadinessCheck {
88
80
  }
89
81
  /**
90
82
  * `ContainerConfig` is part of the experimental `@lunora/container` API and may change without a major version bump.
91
- * @experimental
92
83
  */
93
84
  interface ContainerConfig {
94
85
  /**
@@ -246,7 +237,6 @@ interface ContainerConfig {
246
237
  /**
247
238
  * The value `defineContainer` returns: the validated config plus a brand the
248
239
  * codegen discovery and the generated Container DO class key on.
249
- * @experimental
250
240
  */
251
241
  interface ContainerDefinition extends ContainerConfig {
252
242
  /** Brand marking a value as a Lunora container definition. */
@@ -254,7 +244,6 @@ interface ContainerDefinition extends ContainerConfig {
254
244
  }
255
245
  /**
256
246
  * A normalized image source, as written into `wrangler.jsonc`.
257
- * @experimental
258
247
  */
259
248
  type NormalizedContainerImage = {
260
249
  /** Build context directory (wrangler `image_build_context`). */
@@ -275,7 +264,6 @@ type NormalizedContainerImage = {
275
264
  * Cloudflare Durable Object data-residency jurisdiction. Widening union —
276
265
  * Cloudflare adds values over time.
277
266
  * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
278
- * @experimental
279
267
  */
280
268
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
281
- export { BuildImageSource as B, ContainerConfig as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerDefinition as a, ContainerImageSource as b, ContainerInstanceType as c, ContainerReadinessCheck as d, ContainerRollout as e, CustomContainerInstanceType as f, NamedContainerInstanceType as g };
269
+ export { BuildImageSource as B, ContainerDefinition as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerConfig as a, ContainerImageSource as b, ContainerInstanceType as c, ContainerReadinessCheck as d, ContainerRollout as e, CustomContainerInstanceType as f, NamedContainerInstanceType as g };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/container",
3
- "version": "1.0.0-alpha.31",
3
+ "version": "1.0.0-alpha.32",
4
4
  "description": "Cloudflare Containers for Lunora: defineContainer, generated Container DO classes, and the ctx.containers action surface",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- import{LunoraError as F}from"@lunora/errors";import{containerBindingName as p}from"./containerBindingName-D01FopQt.mjs";import{a as N}from"./jurisdiction-DtE9s70w.mjs";const h=3,v=3e4,C=3,O=500,P="cf-container-target-port",_=(t,e,o,n)=>{const r=typeof t=="string"&&t.startsWith("/")?new Request(`http://container${t}`,e):new Request(t,e);return o!==void 0&&r.headers.set(P,String(o)),n!==void 0&&r.headers.set("traceparent",n),r},E=async t=>{t<=0||await new Promise(e=>{setTimeout(e,t)})},H=/no container instance|not listening|try again later|rate.?limit|provision/i,L="no Container instance available",b="Failed to start container:",B=1024,M=async t=>{const e=t.clone().body;if(e===null)return"";const o=e.getReader(),n=new TextDecoder;let r="";try{for(;r.length<B;){const{done:s,value:a}=await o.read();if(s)break;r+=n.decode(a,{stream:!0})}}finally{await o.cancel()}return r},k=t=>t instanceof Error&&H.test(t.message),$=async t=>{if(t.status===429)return!0;if(t.status!==500&&t.status!==503)return!1;try{const e=await M(t);return e.includes(L)||e.startsWith(b)}catch{return!1}},A=(t,e={},o,n)=>{const r=Math.max(1,e.attempts??C),s=e.backoffMs??O,a=e.maxBackoffMs??v;return{fetch:async(m,T)=>{const u=typeof m=="string"?r:1;let l;for(let d=0;d<u;d+=1){const f=d===u-1;d>0&&await E(Math.min(s*2**(d-1),a));try{const i=await t(_(m,T,o,n));if(f||!await $(i))return i}catch(i){if(l=i,f||!k(i))throw i}}throw l instanceof Error?l:new Error("ctx.containers: cold-start retry exhausted")},port:m=>A(t,e,m,n)}},w=(t,e,o,n)=>A(async r=>t.get(t.idFromName(e)).fetch(r),o,void 0,n),c=async(t,e,o,n)=>{const r=t[e];if(typeof r!="function")throw new TypeError(`ctx.containers: the "${o}" container DO does not expose ${e}() — is @lunora/container/do up to date?`);return r(n)},I=(t,e)=>({allow:async o=>c(t(),"allowHost",e,o),deny:async o=>c(t(),"denyHost",e,o),removeAllowed:async o=>c(t(),"removeAllowedHost",e,o),removeDenied:async o=>c(t(),"removeDeniedHost",e,o),setAllowed:async o=>c(t(),"setAllowedHosts",e,[...o]),setDenied:async o=>c(t(),"setDeniedHosts",e,[...o])}),S=(t,e,o,n,r)=>{const s=()=>t.get(t.idFromName(o));return{...A(async a=>s().fetch(a),n,void 0,r),destroy:async()=>c(s(),"destroy",e.binding),egress:I(s,e.binding),getState:async()=>c(s(),"getState",e.binding),renewActivityTimeout:async()=>c(s(),"renewActivityTimeout",e.binding),start:async a=>c(s(),"start",e.binding,a),stop:async a=>c(s(),"stop",e.binding,a)}},x=t=>`pool-${String(Math.floor(Math.random()*t))}`,q=t=>t.status>=500,R=(t,e,o={},n,r)=>{const s=o.size??e.maxInstances??h,a=Math.max(1,o.attempts??3),m=o.backoffMs??100,T=o.maxBackoffMs??v,u=o.retryOn??q;return{fetch:async(l,d)=>{const f=typeof l=="string"?a:1;let i;for(let y=0;y<f;y+=1){y>0&&await E(Math.min(m*2**(y-1),T));const D=_(l,d,n,r);try{const g=await t.get(t.idFromName(x(s))).fetch(D);if(y===f-1||!u(g))return g}catch(g){i=g}}throw i instanceof Error?i:new Error(`ctx.containers.${e.exportName}.pool(): all ${String(f)} attempts failed`)},port:l=>R(t,e,o,l,r)}},U=(t,e,o)=>({any:(n,r)=>w(t,x(n??e.maxInstances??h),r,o),get:(n,r)=>S(t,e,n,r,o),pool:n=>R(t,e,n,void 0,o)}),j=t=>{const e=()=>{throw new F("INTERNAL",`ctx.containers.${t.exportName}: no "${t.binding}" Durable Object binding found. Run \`lunora dev\` (or \`lunora deploy\`) to reconcile wrangler.jsonc, and make sure the worker entry re-exports the generated container classes.`)};return{any:e,get:e,pool:e}},G=(t,e,o,n)=>{const r={};for(const s of e){const a=t[s.binding];r[s.exportName]=a&&typeof a.idFromName=="function"&&typeof a.get=="function"?U(N(a,o),s,n):j(s)}return r},Y=t=>{const e=o=>({allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:n=>Promise.resolve(t(n,{name:o})),getState:()=>Promise.resolve({lastChange:0}),removeAllowedHost:()=>Promise.resolve(),removeDeniedHost:()=>Promise.resolve(),renewActivityTimeout:()=>Promise.resolve(),setAllowedHosts:()=>Promise.resolve(),setDeniedHosts:()=>Promise.resolve(),start:()=>Promise.resolve(),stop:()=>Promise.resolve()});return{get:o=>e(String(o)),idFromName:o=>o}},J=t=>{const e={};for(const[o,n]of Object.entries(t)){const r=Y(n),s={binding:p(o)};e[o]={any:()=>w(r,"pool-0",{attempts:1}),get:a=>S(r,s,a,{attempts:1}),pool:()=>w(r,"pool-0",{attempts:1})}}return e};export{G as createContainerContext,J as createContainerTestContext};