@lunora/container 1.0.0-alpha.64 → 1.0.0-alpha.66

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/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { C as ContainerSnapshot, a as ContainerRuntimeInstanceType, D as DurableObjectJurisdiction, b as DefaultScheduledContainerConfig, c as DurableObjectScheduledContainerConfig, d as ContainerDefinition, e as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-CoLtDVr_.js";
2
- export type { B as BuildImageSource, f as ContainerConfig, g as ContainerConfigBase, h as ContainerInstanceType, i as ContainerNamedImageSource, j as ContainerReadinessCheck, k as ContainerRollout, l as CustomContainerInstanceType, m as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-CoLtDVr_.js";
1
+ import { a as ContainerSpawnOptions, b as ContainerSnapshot, c as ContainerProcess, d as ContainerRuntimeInstanceType, e as ContainerSpawnRequest, S as SpawnResult, D as DurableObjectJurisdiction, f as DefaultScheduledContainerConfig, g as DurableObjectScheduledContainerConfig, C as ContainerDefinition, h as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-t4mtuqYW.js";
2
+ export type { B as BuildImageSource, i as ContainerBackupStorage, j as ContainerConfig, k as ContainerConfigBase, l as ContainerInstanceType, m as ContainerNamedImageSource, n as ContainerReadinessCheck, o as ContainerRollout, p as CustomContainerInstanceType, q as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-t4mtuqYW.js";
3
+ import { b as ContainerBackupOptions, C as ContainerFileOptions, c as ContainerMountRequest, a as ContainerFileContent, d as ContainerSandboxControls } from "./packem_shared/sandbox-types.d-DXlHnsEg.js";
4
+ export type { e as ContainerFiles, f as ContainerMountCredentials } from "./packem_shared/sandbox-types.d-DXlHnsEg.js";
5
+ import { DirectoryBackupRecord, S3MountInspection, SandboxDirectoryEntry, SandboxFileStat } from '@cloudflare/sandbox';
6
+ export type { DirectoryBackupRecord, S3MountInspection, SandboxDirectoryEntry, SandboxFileStat } from '@cloudflare/sandbox';
3
7
  /**
4
8
  * Per-call options for `ContainerHandle.exec`.
5
9
  */
@@ -67,6 +71,15 @@ interface ContainerExecResult {
67
71
  * un-gates model-chosen command execution.
68
72
  */
69
73
  declare const CONTAINER_EXEC_PATH = "/__lunora/exec";
74
+ /** Options for `handle.terminal()`. */
75
+ interface ContainerTerminalOptions extends Omit<ContainerSpawnOptions, "pty" | "signal" | "stdin"> {
76
+ /** Initial size. Defaults to the request's `?cols=&rows=` search params, else `80×24`. */
77
+ cols?: number;
78
+ /** The program to run. Defaults to `"sh"`. */
79
+ command?: string;
80
+ /** Initial size. Defaults to the request's `?cols=&rows=` search params, else `80×24`. */
81
+ rows?: number;
82
+ }
70
83
  /**
71
84
  * Options for explicitly starting an instance (mirrors `@cloudflare/containers`).
72
85
  */
@@ -129,12 +142,34 @@ interface ContainerStubLike {
129
142
  destroy?: () => Promise<void>;
130
143
  fetch: (input: Request) => Promise<Response>;
131
144
  getState?: () => Promise<ContainerInstanceState>;
145
+ lunoraBackup?: (directory: string, options?: ContainerBackupOptions) => Promise<DirectoryBackupRecord>;
146
+ lunoraDeleteBackup?: (backup: DirectoryBackupRecord) => Promise<void>;
132
147
  /** The container DO's exec entry (`LunoraContainer.lunoraExec`), the only way a request reaches `/__lunora/exec`. */
133
148
  lunoraExec?: (request: Request) => Promise<Response>;
149
+ lunoraInspectMount?: (path: string) => Promise<S3MountInspection>;
150
+ lunoraMkdir?: (path: string, options?: ContainerFileOptions & {
151
+ recursive?: boolean;
152
+ }) => Promise<void>;
153
+ lunoraMount?: (request: ContainerMountRequest) => Promise<void>;
154
+ lunoraReadDirectory?: (path: string, options?: ContainerFileOptions) => Promise<SandboxDirectoryEntry[]>;
155
+ lunoraReadFile?: (path: string, options?: ContainerFileOptions) => Promise<Response>;
156
+ lunoraRemove?: (path: string, options?: ContainerFileOptions & {
157
+ force?: boolean;
158
+ recursive?: boolean;
159
+ }) => Promise<void>;
160
+ lunoraRename?: (source: string, destination: string, options?: ContainerFileOptions) => Promise<void>;
161
+ lunoraRestore?: (backup: DirectoryBackupRecord, options?: {
162
+ directory?: string;
163
+ }) => Promise<void>;
134
164
  /** The container DO's snapshot entry (`LunoraContainer.lunoraSnapshot`). */
135
165
  lunoraSnapshot?: (options?: {
136
166
  name?: string;
137
167
  }) => Promise<ContainerSnapshot>;
168
+ /** The container DO's streaming-exec entry (`LunoraContainer.lunoraSpawn`). */
169
+ lunoraSpawn?: (request: ContainerSpawnRequest) => Promise<SpawnResult>;
170
+ lunoraStat?: (path: string, options?: ContainerFileOptions) => Promise<SandboxFileStat>;
171
+ lunoraUnmount?: (path: string) => Promise<void>;
172
+ lunoraWriteFile?: (path: string, content: ContainerFileContent, options?: ContainerFileOptions) => Promise<void>;
138
173
  removeAllowedHost?: (hostname: string) => Promise<void>;
139
174
  removeDeniedHost?: (hostname: string) => Promise<void>;
140
175
  renewActivityTimeout?: () => Promise<void>;
@@ -230,11 +265,34 @@ interface ContainerInstanceHandle extends ContainerHandle {
230
265
  snapshot: (options?: {
231
266
  name?: string;
232
267
  }) => Promise<ContainerSnapshot>;
268
+ /**
269
+ * Start a process and stream it, rather than buffer it the way `exec`
270
+ * does: its stdout/stderr arrive as streams, `stdin` can be piped, and the
271
+ * process can be killed or (under `pty`) resized while it runs. Needs the
272
+ * runtime's native exec; the container is started first when it is not
273
+ * running, and kept awake until the process exits.
274
+ */
275
+ spawn: (command: string, options?: ContainerSpawnOptions) => Promise<ContainerProcess>;
233
276
  /** Explicitly start the instance, optionally with per-instance env/entrypoint. */
234
277
  start: (options?: ContainerStartOptions) => Promise<void>;
235
278
  /** Stop the instance (optionally with a signal); it can start again on the next request. */
236
279
  stop: (signal?: number | string) => Promise<void>;
280
+ /**
281
+ * Answer a WebSocket upgrade `request` with a browser terminal: a shell
282
+ * (default `sh`) on a PTY, bridged to the socket. Binary frames are
283
+ * keystrokes, a text frame `{"cols":n,"rows":n}` resizes, and the socket
284
+ * closes when the shell exits. Authenticate the request first — whoever
285
+ * holds the socket has a shell in the container. Call it from a Worker
286
+ * route (see {@link getContainer}) and return the response as-is.
287
+ */
288
+ terminal: (request: Request, options?: ContainerTerminalOptions) => Promise<Response>;
237
289
  }
290
+ /**
291
+ * A named instance of a `defineContainer({ sandbox: true })` container: the
292
+ * {@link ContainerInstanceHandle} plus the Sandbox SDK helpers — structured
293
+ * file operations, directory backups to R2 and S3-compatible bucket mounts.
294
+ */
295
+ interface SandboxContainerInstanceHandle extends ContainerInstanceHandle, ContainerSandboxControls {}
238
296
  /**
239
297
  * Runtime egress-firewall controls for a named instance (`handle.egress.*`).
240
298
  * Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
@@ -297,6 +355,10 @@ interface ContainerAccessor {
297
355
  */
298
356
  pool: (options?: PoolOptions) => ContainerHandle;
299
357
  }
358
+ /** `ctx.containers.<name>` for a `defineContainer({ sandbox: true })` container: `.get()` adds the sandbox helpers. */
359
+ interface SandboxContainerAccessor extends ContainerAccessor {
360
+ get: (name: string, options?: InstanceRetryOptions) => SandboxContainerInstanceHandle;
361
+ }
300
362
  /**
301
363
  * Tuning for a pooled, retrying container handle. See {@link ContainerAccessor.pool}.
302
364
  */
@@ -371,6 +433,22 @@ interface ContainerBindingSpec {
371
433
  * header and leaves the container on its own configuration.
372
434
  */
373
435
  declare const createContainerContext: (env: Record<string, unknown>, specs: ReadonlyArray<ContainerBindingSpec>, jurisdiction?: DurableObjectJurisdiction, traceparent?: string, sampleErrors?: boolean) => Record<string, ContainerAccessor>;
436
+ /**
437
+ * A named container instance straight off the Worker `env`, for code that has
438
+ * no `ctx.containers` — typically an `httpRouter` route that opens a
439
+ * {@link ContainerInstanceHandle.terminal} or proxies a preview port, both of
440
+ * which must hand a WebSocket upgrade response back to the browser. The
441
+ * binding is derived from `exportName` exactly as codegen derives it. Pass
442
+ * {@link SandboxContainerInstanceHandle} as the type argument for a
443
+ * `sandbox: true` container.
444
+ *
445
+ * ```ts
446
+ * app.get("/terminal", async (c) => getContainer(c.env, "box", userId).terminal(c.req.raw));
447
+ * ```
448
+ */
449
+ declare const getContainer: <Handle extends ContainerInstanceHandle = ContainerInstanceHandle>(env: Record<string, unknown>, exportName: string, name: string, options?: InstanceRetryOptions & {
450
+ jurisdiction?: DurableObjectJurisdiction;
451
+ }) => Handle;
374
452
  /**
375
453
  * A test handler: receives the request plus the targeted instance name.
376
454
  */
@@ -465,7 +543,7 @@ export { CONTAINER_EXEC_PATH,
465
543
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
466
544
  * behind the `@lunora/container/do` subpath.
467
545
  */
468
- type ContainerAccessor,
546
+ type ContainerAccessor, type ContainerBackupOptions,
469
547
  /**
470
548
  * `@lunora/container` — Cloudflare Containers for Lunora.
471
549
  *
@@ -487,7 +565,7 @@ type ContainerBindingSpec, type ContainerDefinition,
487
565
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
488
566
  * behind the `@lunora/container/do` subpath.
489
567
  */
490
- type ContainerEgressControls, type ContainerExecOptions, type ContainerExecResult,
568
+ type ContainerEgressControls, type ContainerExecOptions, type ContainerExecResult, type ContainerFileContent, type ContainerFileOptions,
491
569
  /**
492
570
  * `@lunora/container` — Cloudflare Containers for Lunora.
493
571
  *
@@ -520,7 +598,7 @@ type ContainerInstanceHandle,
520
598
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
521
599
  * behind the `@lunora/container/do` subpath.
522
600
  */
523
- type ContainerInstanceState,
601
+ type ContainerInstanceState, type ContainerMountRequest,
524
602
  /**
525
603
  * `@lunora/container` — Cloudflare Containers for Lunora.
526
604
  *
@@ -531,7 +609,7 @@ type ContainerInstanceState,
531
609
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
532
610
  * behind the `@lunora/container/do` subpath.
533
611
  */
534
- type ContainerNamespaceLike, type ContainerRuntimeInstanceType, type ContainerSnapshot,
612
+ type ContainerNamespaceLike, type ContainerProcess, type ContainerRuntimeInstanceType, type ContainerSandboxControls, type ContainerSnapshot, type ContainerSpawnOptions,
535
613
  /**
536
614
  * `@lunora/container` — Cloudflare Containers for Lunora.
537
615
  *
@@ -542,7 +620,7 @@ type ContainerNamespaceLike, type ContainerRuntimeInstanceType, type ContainerSn
542
620
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
543
621
  * behind the `@lunora/container/do` subpath.
544
622
  */
545
- type ContainerStartOptions,
623
+ type ContainerStartOptions, type ContainerTerminalOptions,
546
624
  /**
547
625
  * `@lunora/container` — Cloudflare Containers for Lunora.
548
626
  *
@@ -586,4 +664,26 @@ type InstanceRetryOptions, type NormalizedContainerImage,
586
664
  * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
587
665
  * behind the `@lunora/container/do` subpath.
588
666
  */
589
- type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isCloudflareRegistryDigest, isContainerDefinition, isManagedImage, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
667
+ type PoolOptions,
668
+ /**
669
+ * `@lunora/container` — Cloudflare Containers for Lunora.
670
+ *
671
+ * This root export is Node-safe (no Cloudflare runtime imports): the
672
+ * `defineContainer` authoring surface, the naming/normalization helpers
673
+ * codegen and `@lunora/config` share, the `ctx.containers` client wiring, and
674
+ * a Docker-free test double. The workerd-only `LunoraContainer` base class
675
+ * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
676
+ * behind the `@lunora/container/do` subpath.
677
+ */
678
+ type SandboxContainerAccessor,
679
+ /**
680
+ * `@lunora/container` — Cloudflare Containers for Lunora.
681
+ *
682
+ * This root export is Node-safe (no Cloudflare runtime imports): the
683
+ * `defineContainer` authoring surface, the naming/normalization helpers
684
+ * codegen and `@lunora/config` share, the `ctx.containers` client wiring, and
685
+ * a Docker-free test double. The workerd-only `LunoraContainer` base class
686
+ * (which pulls in `@cloudflare/containers` → `cloudflare:workers`) lives
687
+ * behind the `@lunora/container/do` subpath.
688
+ */
689
+ type SandboxContainerInstanceHandle, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, getContainer, isCloudflareRegistryDigest, isContainerDefinition, isManagedImage, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createContainerContext as a,createContainerTestContext as i}from"./packem_shared/createContainerContext-DP-kZaQ1.mjs";import{DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS as t,containerBindingName as o,containerBuildTag as C,containerClassName as s,defineContainer as m,isCloudflareRegistryDigest as E,isContainerDefinition as g,isManagedImage as f,normalizeContainerImage as l,resolveContainerEnvVars as _}from"./packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs";import{C as d}from"./packem_shared/exec-BYLW70sR.mjs";export{d as CONTAINER_EXEC_PATH,t as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,o as containerBindingName,C as containerBuildTag,s as containerClassName,a as createContainerContext,i as createContainerTestContext,m as defineContainer,E as isCloudflareRegistryDigest,g as isContainerDefinition,f as isManagedImage,l as normalizeContainerImage,_ as resolveContainerEnvVars};
1
+ import{createContainerContext as a,createContainerTestContext as t,getContainer as i}from"./packem_shared/createContainerContext-CcW1olml.mjs";import{DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS as o,containerBindingName as C,containerBuildTag as s,containerClassName as g,defineContainer as m,isCloudflareRegistryDigest as E,isContainerDefinition as f,isManagedImage as l,normalizeContainerImage as _,resolveContainerEnvVars as c}from"./packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-B9DkqZkf.mjs";import{C as x}from"./packem_shared/exec-BYLW70sR.mjs";export{x as CONTAINER_EXEC_PATH,o as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,C as containerBindingName,s as containerBuildTag,g as containerClassName,a as createContainerContext,t as createContainerTestContext,m as defineContainer,i as getContainer,E as isCloudflareRegistryDigest,f as isContainerDefinition,l as isManagedImage,_ as normalizeContainerImage,c as resolveContainerEnvVars};
@@ -0,0 +1 @@
1
+ import{LunoraError as h}from"@lunora/errors";const d=new Set(["basic","lite","standard-1","standard-2","standard-3","standard-4"]),l=new Set(["lite","standard-1","standard-2","standard-3","standard-4"]),f="cloudflare/",w=e=>e.startsWith(f),g=["maxInstances","rollout"],c=100,u=128,s=/^[A-Z_]\w*$/i,y=/^\d+[smh]$/,E={h:3600,m:60,s:1},D=e=>{if(typeof e=="number")return Math.floor(e);if(y.exec(e)===null)throw new TypeError(`Invalid duration "${e}" — expected a number of seconds or "<n>[smh]"`);return Number(e.slice(0,-1))*(E[e.slice(-1)]??1)},T=e=>{const t=e.endsWith("/")?e.slice(0,-1):e,n=t.lastIndexOf("/");return n===-1?t:t.slice(n+1)},v=e=>{const t=e.lastIndexOf("/");return t===-1?".":e.slice(0,t)||"/"},M=e=>{if(typeof e!="string")return"build"in e?{buildDir:e.build.endsWith("/")?e.build.slice(0,-1):e.build,kind:"build"}:{kind:"registry",reference:e.registry};if(T(e).startsWith("Dockerfile"))return{buildContext:v(e),dockerfilePath:e,kind:"dockerfile"};const t=e.endsWith("/")?e.slice(0,-1):e;return{buildContext:t,dockerfilePath:`${t}/Dockerfile`,kind:"dockerfile"}},j=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}Container`,R=e=>`CONTAINER_${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"_").toUpperCase()}`,V=e=>`lunora-${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"-").toLowerCase()}:build`,m=e=>{if(typeof e=="string"){if(e.length===0)throw new TypeError("defineContainer: `image` must be a non-empty path or a { registry } reference");if(e.includes(":"))throw new TypeError(`defineContainer: \`image\` string "${e}" looks like a registry reference — pass it as { registry: "${e}" } instead. Plain strings are local Dockerfile paths.`);return}if("build"in e){if(typeof e.build!="string"||e.build.length===0)throw new TypeError("defineContainer: `image.build` must be a non-empty source directory for Railpack to build");return}if(typeof e.registry!="string"||e.registry.length===0)throw new TypeError("defineContainer: `image.registry` must be a non-empty fully-qualified image reference")},C=(e,t,n)=>{for(const[r,o]of Object.entries(e.secretsStore??{})){if(!s.test(r))throw new TypeError(`defineContainer: secretsStore env name "${r}" is not a valid environment variable name`);if(typeof o!="string"||o.trim().length===0)throw new TypeError(`defineContainer: \`secretsStore["${r}"]\` must be a non-empty Secrets Store binding name`);if(t.has(r)||n.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`secretsStore\` and \`env\`/\`secrets\` — pick one source for the value`)}},$=e=>{for(const r of Object.keys(e.env??{}))if(!s.test(r))throw new TypeError(`defineContainer: env variable name "${r}" is not a valid environment variable name`);for(const r of Object.keys(e.buildArgs??{}))if(!s.test(r))throw new TypeError(`defineContainer: buildArg name "${r}" is not a valid environment variable name`);const t=new Set(Object.keys(e.env??{})),n=new Set(e.secrets);for(const r of e.secrets??[]){if(!s.test(r))throw new TypeError(`defineContainer: secret name "${r}" is not a valid environment variable name`);if(t.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`env\` and \`secrets\` — a secret would silently overwrite the static env value; pick one`)}C(e,t,n)},i=(e,t)=>{if(!Number.isInteger(e)||e<1||e>65535)throw new TypeError(`defineContainer: \`${t}\` must be an integer in 1–65535 (got ${String(e)})`)},k=e=>{for(const t of e.readyOn??[]){if(typeof t.path!="string"||t.path.trim().length===0)throw new TypeError("defineContainer: `readyOn[].path` must be a non-empty HTTP path string");if(t.path!==t.path.trim())throw new TypeError("defineContainer: `readyOn[].path` must not have leading or trailing whitespace");if(t.port!==void 0&&i(t.port,"readyOn[].port"),t.status!==void 0&&(!Number.isInteger(t.status)||t.status<100||t.status>599))throw new TypeError(`defineContainer: \`readyOn[].status\` must be an HTTP status code in 100–599 (got ${String(t.status)})`)}},p=(e,t)=>{if(e!==void 0){if(typeof e=="string"){if(!y.test(e))throw new TypeError(`defineContainer: \`${t}\` string "${e}" must be a number of seconds followed by a unit, e.g. "30s", "5m", or "1h"`)}else if(!Number.isInteger(e)||e<1)throw new TypeError(`defineContainer: \`${t}\` must be a positive integer number of seconds or a duration string like "5m" (got ${String(e)})`)}},S=e=>{const{backups:t}=e;if(t!==void 0){if(e.sandbox!==!0)throw new TypeError("defineContainer: `backups` needs `sandbox: true` — directory backups are a Sandbox SDK helper");if(typeof t.bucket!="string"||t.bucket.trim().length===0)throw new TypeError("defineContainer: `backups.bucket` must name an R2 bucket binding");if(t.prefix!==void 0&&(typeof t.prefix!="string"||t.prefix.length>0&&!t.prefix.endsWith("/")))throw new TypeError("defineContainer: `backups.prefix` must be a string ending in `/`, or omitted")}},I=e=>{for(const t of["allowedHosts","deniedHosts"])if(e[t]?.some(r=>typeof r!="string"||r.trim().length===0))throw new TypeError(`defineContainer: \`${t}\` must be an array of non-empty hostname patterns`);if(e.interceptHttps!==void 0&&typeof e.interceptHttps!="boolean")throw new TypeError("defineContainer: `interceptHttps` must be a boolean, or omitted")},N=e=>{if(e.requiredPorts!==void 0){if(e.requiredPorts.length===0)throw new TypeError("defineContainer: `requiredPorts` must be a non-empty array of ports, or omitted");for(const t of e.requiredPorts)i(t,"requiredPorts[]")}if(e.entrypoint!==void 0&&(e.entrypoint.length===0||e.entrypoint.some(t=>typeof t!="string"||t.trim().length===0)))throw new TypeError("defineContainer: `entrypoint` must be a non-empty array of non-empty strings, or omitted");if(I(e),e.pingEndpoint!==void 0&&(typeof e.pingEndpoint!="string"||e.pingEndpoint.trim().length===0))throw new TypeError("defineContainer: `pingEndpoint` must be a non-empty path string");for(const[t,n]of Object.entries(e.labels??{}))if(t.trim().length===0||typeof n!="string")throw new TypeError("defineContainer: `labels` must be a record of non-empty keys to string values");if(k(e),e.sandbox!==void 0&&typeof e.sandbox!="boolean")throw new TypeError("defineContainer: `sandbox` must be a boolean, or omitted");S(e)},P=e=>typeof e=="string"&&e.startsWith("registry.cloudflare.com/")&&e.includes("@sha256:"),A=(e,t)=>{if(e.length===0||e.length>u)throw new TypeError(`defineContainer: image name "${e}" must be 1–${String(u)} characters`);if(typeof t=="string"){m(t);return}if(!P(t.registry))throw new TypeError(`defineContainer: \`images["${e}"].registry\` must be a digest-pinned (@sha256) reference under registry.cloudflare.com — push other registries' images there first`)},_=e=>{for(const r of g)if(e[r]!==void 0)throw new TypeError(`defineContainer: \`${r}\` is not supported with schedulingPolicy "durable_object"`);const t=Object.entries(e.images??{});if(t.length>c)throw new TypeError(`defineContainer: \`images\` holds at most ${String(c)} entries (got ${String(t.length)})`);for(const[r,o]of t)A(r,o);if(e.image!==void 0&&!Object.hasOwn(e.images??{},e.image)&&!w(e.image))throw new TypeError(`defineContainer: \`image\` "${e.image}" must name an entry of \`images\` or a Cloudflare-managed "${f}…" image`);const{instanceType:n}=e;if(typeof n=="string"&&!l.has(n))throw new TypeError(`defineContainer: \`instanceType\` "${n}" is not a runtime size — use one of ${[...l].join(", ")}, or a full { vcpu, memoryMib, diskMb } object`);if(typeof n=="object"&&[n.vcpu,n.memoryMib,n.diskMb].some(r=>typeof r!="number"||r<=0))throw new TypeError('defineContainer: a custom `instanceType` under schedulingPolicy "durable_object" needs positive vcpu, memoryMib and diskMb')},x=e=>{if(e.images!==void 0)throw new TypeError('defineContainer: `images` needs schedulingPolicy "durable_object"');m(e.image);const t=e.rollout?.stepPercentage;if(t!==void 0&&(!Number.isInteger(t)||t<1||t>100))throw new TypeError(`defineContainer: \`rollout.stepPercentage\` must be an integer in 1–100 (got ${String(t)})`);const n=e.rollout?.gracePeriodSeconds;if(n!==void 0&&(!Number.isInteger(n)||n<0))throw new TypeError(`defineContainer: \`rollout.gracePeriodSeconds\` must be a non-negative integer (got ${String(n)})`);if(e.maxInstances!==void 0&&(!Number.isInteger(e.maxInstances)||e.maxInstances<1))throw new TypeError(`defineContainer: \`maxInstances\` must be a positive integer (got ${String(e.maxInstances)})`);if(typeof e.instanceType=="string"&&!d.has(e.instanceType))throw new TypeError(`defineContainer: unknown \`instanceType\` "${e.instanceType}" — use one of ${[...d].join(", ")}, or a custom { vcpu, memoryMib, diskMb } object`)},L=e=>{const t=e.schedulingPolicy;if(e.schedulingPolicy==="durable_object")_(e);else if(t===void 0||t==="default")x(e);else throw new TypeError(`defineContainer: unknown \`schedulingPolicy\` ${JSON.stringify(t)} — use "default" or "durable_object"`);return e.defaultPort!==void 0&&i(e.defaultPort,"defaultPort"),p(e.sleepAfter,"sleepAfter"),p(e.hardTimeout,"hardTimeout"),$(e),N(e),{...e,isLunoraContainer:!0}},H=e=>typeof e=="object"&&e!==null&&e.isLunoraContainer===!0,W=(e,t,n)=>{const r={...e.env};for(const o of e.secrets??[]){const a=t[o];if(typeof a!="string"){const b=n===void 0?"container":`container "${n}"`;throw new h("INTERNAL",`${b}: declared secret "${o}" is not set on the Worker environment. Add it to .dev.vars for local dev and run \`wrangler secret put ${o}\` for production.`)}r[o]=a}return r};export{g as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,R as containerBindingName,V as containerBuildTag,j as containerClassName,L as defineContainer,P as isCloudflareRegistryDigest,H as isContainerDefinition,w as isManagedImage,M as normalizeContainerImage,D as parseDurationSeconds,W as resolveContainerEnvVars};
@@ -0,0 +1 @@
1
+ import{SandboxFileError as u,SandboxBackupError as d,SandboxS3MountError as l,SandboxProtocolError as p,Files as w,DirectoryBackup as f,S3Mount as b}from"@cloudflare/sandbox";import{LunoraError as s}from"@lunora/errors";import{LunoraContainer as y}from"../do/index.mjs";const m={EACCES:"FORBIDDEN",EBUSY:"CONFLICT",EEXIST:"CONFLICT",ENOENT:"NOT_FOUND",ENOTEMPTY:"CONFLICT",EPERM:"FORBIDDEN"},N={BACKUP_INTEGRITY:"CONFLICT",BACKUP_NOT_FOUND:"NOT_FOUND",BACKUP_TRANSFER:"INTERNAL"},E={S3_MOUNT_BUSY:"CONFLICT",S3_MOUNT_CONFLICT:"CONFLICT",S3_MOUNT_FAILED:"INTERNAL",S3_MOUNT_INCOMPATIBLE:"BAD_REQUEST"},h=(n,t)=>{if(u.is(n)){const e=n.destination===void 0?n.path:`${n.path} → ${n.destination}`;return new s(m[n.code]??"BAD_REQUEST",`${t}: ${n.operation} ${e} failed with ${n.code}: ${n.detail}`,{cause:n,data:{errno:n.code,operation:n.operation,path:n.path,...n.destination===void 0?{}:{destination:n.destination}}})}return d.is(n)?new s(N[n.code]??"INTERNAL",`${t}: ${n.operation} of ${n.path} failed (${n.code}): ${n.detail}`,{cause:n,data:{operation:n.operation,path:n.path,reason:n.code}}):l.is(n)?new s(E[n.code]??"INTERNAL",`${t}: ${n.operation} at ${n.path} failed (${n.code}): ${n.detail}`,{cause:n,data:{operation:n.operation,path:n.path,reason:n.code}}):p.is(n)?new s("INTERNAL",`${t}: the container did not answer the sandbox helper protocol: ${n.detail}`,{cause:n,hint:"The image needs `sandbox-shim` at `/usr/local/bin/sandbox-shim` in a version matching `@cloudflare/sandbox`. Copy it from Cloudflare's `cloudflare/sandbox` image (`COPY --from=docker.io/cloudflare/sandbox:<version> /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim`)."}):n},k=(n,t)=>{if(n.body===null)return t(),n;const{readable:e,writable:i}=new TransformStream;return n.body.pipeTo(i).finally(t).catch(()=>{}),new Response(e,n)};class O extends y{#t;#e;#n;#i;constructor(t,e,i,a,o){super(t,e,i,a,o),this.#t=i.backups}async lunoraReadFile(t,e={}){const{container:i,release:a}=await this.lunoraAcquire("files.readFile");try{return k(await this.files(i).readFile(t,e),a)}catch(o){throw a(),h(o,this.label("files.readFile"))}}async lunoraWriteFile(t,e,i={}){await this.withContainer("files.writeFile",async a=>this.files(a).writeFile(t,e,i))}async lunoraStat(t,e={}){return this.withContainer("files.stat",async i=>this.files(i).stat(t,e))}async lunoraReadDirectory(t,e={}){return this.withContainer("files.readDirectory",async i=>this.files(i).readDirectory(t,e))}async lunoraMkdir(t,e={}){await this.withContainer("files.mkdir",async i=>this.files(i).mkdir(t,e))}async lunoraRename(t,e,i={}){await this.withContainer("files.rename",async a=>this.files(a).rename(t,e,i))}async lunoraRemove(t,e={}){await this.withContainer("files.remove",async i=>this.files(i).remove(t,e))}async lunoraBackup(t,e={}){const i={dir:t,...e.exclude===void 0?{}:{exclude:[...e.exclude]},...e.gitignore===void 0?{}:{gitignore:e.gitignore},...e.name===void 0?{}:{name:e.name}};return this.withContainer("backup",async a=>this.backups(a).backup(i))}async lunoraRestore(t,e={}){const i=e.directory===void 0?{}:{dir:e.directory};await this.withContainer("restore",async a=>this.backups(a).restore(t,i))}async lunoraDeleteBackup(t){const e=this.ctx.container;if(e===void 0)throw new s("INTERNAL",`${this.label("deleteBackup")}: this Durable Object has no container binding`);try{await this.backups(e).delete(t)}catch(i){throw h(i,this.label("deleteBackup"))}}async lunoraMount(t){if(this.usingInterception)throw new s("BAD_REQUEST",`${this.label("mount")}: bucket mounts cannot be combined with an egress policy (allowedHosts, deniedHosts, interceptHttps, outbound handlers or runtime egress controls) on the same container`);const e={bucket:t.bucket,credentials:{type:"static",...this.mountCredentials(t)},endpoint:t.endpoint,region:t.region,type:"s3"},i={access:t.access,mountPath:t.path,source:e,...t.keyPrefix===void 0?{}:{keyPrefix:t.keyPrefix},...t.s3fsOptions===void 0?{}:{s3fsOptions:t.s3fsOptions}};await this.withContainer("mount",async a=>this.mounts(a).mount(i))}async lunoraInspectMount(t){return this.withContainer("inspectMount",async e=>this.mounts(e).inspect(t))}async lunoraUnmount(t){await this.withContainer("unmount",async e=>this.mounts(e).unmount(t))}async beforeContainerStart(){await super.beforeContainerStart();const t=this.ctx.container;this.#t!==void 0&&this.usingInterception&&t!==void 0&&await this.backups(t).intercept()}async withContainer(t,e){const{container:i,release:a}=await this.lunoraAcquire(t);try{return await e(i)}catch(o){throw h(o,this.label(t))}finally{a()}}files(t){return this.#e??=new w(t),this.#e}backups(t){if(this.#t===void 0)throw new s("BAD_REQUEST",`container "${this.lunoraName}": backups need \`backups: { bucket }\` in lunora/containers.ts`);const{bucket:e}=this.#t;if(typeof this.env[e]?.get!="function")throw new s("INTERNAL",`container "${this.lunoraName}": backups.bucket "${e}" is not an R2 bucket binding on the Worker env — add an r2_buckets entry with binding "${e}" to wrangler.jsonc`);return this.#n??=new f(t,this.workerExport("DirectoryBackupGateway"),{binding:this.#t.bucket,...this.#t.prefix===void 0?{}:{prefix:this.#t.prefix}}),this.#n}mounts(t){return this.#i??=new b(t,this.workerExport("S3Gateway")),this.#i}workerExport(t){const e=this.ctx.exports?.[t];if(e===void 0)throw new s("INTERNAL",`container "${this.lunoraName}": the worker does not export ${t}. Re-export the generated containers file from the worker entry (\`export * from "./lunora/_generated/containers.js"\`).`);return e}mountCredentials(t){const e=c=>{const r=this.env[c];if(typeof r!="string"||r.length===0)throw new s("INTERNAL",`${this.label("mount")}: credential secret "${c}" is not set on the Worker env — add it with \`wrangler secret put ${c}\` (or .dev.vars)`);return r},{accessKeyIdSecret:i,secretAccessKeySecret:a,sessionTokenSecret:o}=t.credentials;return{accessKeyId:e(i),secretAccessKey:e(a),...o===void 0?{}:{sessionToken:e(o)}}}label(t){return`container "${this.lunoraName}": ${t}()`}}export{O as default};
@@ -0,0 +1 @@
1
+ import{LunoraError as N}from"@lunora/errors";import{e as I,p as J,a as x,r as G}from"./exec-BYLW70sR.mjs";import{S as K}from"./sampling-D21rpu_s.mjs";import{containerBindingName as $}from"./DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-B9DkqZkf.mjs";import{a as Z}from"./jurisdiction-DtE9s70w.mjs";const ee=(e,t)=>{if(e.length===0)throw new TypeError("spawn: `command` must be a non-empty string");return{command:e,...t.args===void 0?{}:{args:[...t.args]},...t.cwd===void 0?{}:{cwd:t.cwd},...t.env===void 0?{}:{env:{...t.env}},...t.pty===void 0?{}:{pty:{...t.pty}},...t.stdin===void 0?{}:{stdin:t.stdin},...t.timeoutMs===void 0?{}:{timeoutMs:t.timeoutMs}}},te=(e,t)=>{const{control:r}=e,o=r.exitCode();if(t!==void 0){const n=()=>{Promise.resolve(r.kill()).catch(()=>{})};t.aborted?n():(t.addEventListener("abort",n,{once:!0}),o.finally(()=>{t.removeEventListener("abort",n)}).catch(()=>{}))}return{exitCode:o,isPty:e.isPty,kill:async n=>{await r.kill(n)},pid:e.pid,resize:async(n,s)=>{await r.resize(n,s)},stderr:e.stderr,stdin:e.stdin,stdout:e.stdout}},re=1048576,oe=80,ne=24,B=(e,t)=>{const r=Number(e.searchParams.get(t));return Number.isInteger(r)&&r>0?r:void 0},se={pair:()=>{const{WebSocketPair:e}=globalThis;if(e===void 0)throw new TypeError("terminal(): WebSocketPair is not available — open a terminal from a Worker or Durable Object");const t=new e;return[t[0],t[1]]},upgrade:e=>new Response(void 0,{status:101,webSocket:e})},ae=e=>{try{const t=JSON.parse(e);return Number.isInteger(t.cols)&&Number.isInteger(t.rows)?{cols:t.cols,rows:t.rows}:void 0}catch{return}},ie=async e=>{if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer())},ce=e=>e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e.byteLength:e instanceof Blob?e.size:0,le=async(e,t)=>{const r=e.stdout?.getReader();try{for(;r!==void 0;){const{done:n,value:s}=await r.read();if(n)break;t.send(s)}const o=await e.exitCode;t.close(1e3,`exited with code ${String(o)}`)}catch{t.close(1011,"terminal stream failed")}},de=async(e,t,r={},o=se)=>{if(t.headers.get("upgrade")?.toLowerCase()!=="websocket")return new Response("terminal(): expected a WebSocket upgrade request",{status:426});const n=new URL(t.url),{command:s="sh",cols:c,rows:a,...i}=r,u=await e(s,{...i,env:{TERM:"xterm-256color",...i.env},pty:{cols:c??B(n,"cols")??oe,rows:a??B(n,"rows")??ne},stdin:!0}),[v,m]=o.pair(),d=u.stdin?.getWriter(),f=()=>{u.kill().catch(()=>{}),d?.close().catch(()=>{})};let p=Promise.resolve();const h=y=>{p=p.then(y).catch(f)};let R=0;const S=(y,g)=>{if(R+=y,R>re){m.close(1009,"terminal input backlog too large"),f();return}h(async()=>{try{const E=await g;E!==void 0&&await d?.write(E)}finally{R-=y}})};return m.accept(),m.addEventListener("message",({data:y})=>{if(typeof y!="string"){S(ce(y),ie(y));return}const g=ae(y);if(g===void 0){const E=new TextEncoder().encode(y);S(E.byteLength,Promise.resolve(E))}else h(async()=>u.resize(g.cols,g.rows).catch(()=>{}))}),m.addEventListener("close",f),m.addEventListener("error",f),le(u,m).catch(()=>{}),o.upgrade(v)},ue={EEXIST:"CONFLICT",ENOENT:"NOT_FOUND",ENOTEMPTY:"CONFLICT"},w=(e,t,r)=>new N(ue[e]??"BAD_REQUEST",`test container: ${t} ${r} failed with ${e}`,{data:{errno:e,operation:t,path:r}}),T=(e,t={})=>{const r=e.startsWith("/")?e:`${t.cwd??""}/${e}`;if(!r.startsWith("/"))throw new TypeError(`test container: "${e}" is relative and no absolute \`cwd\` was given`);const o=[];for(const n of r.split("/"))n===".."?o.pop():n!==""&&n!=="."&&o.push(n);return`/${o.join("/")}`},C=e=>e.slice(0,e.lastIndexOf("/"))||"/",fe=async e=>typeof e=="string"?new TextEncoder().encode(e):e instanceof ArrayBuffer?Uint8Array.from(new Uint8Array(e)):ArrayBuffer.isView(e)?new Uint8Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)):new Uint8Array(await new Response(e).arrayBuffer()),L=(e,t)=>{const r=new Date;return{accessedAt:r,changedAt:r,gid:0,modifiedAt:r,mode:e==="directory"?16877:33188,size:BigInt(t),type:e,uid:0}},ye=()=>{const e=new Map,t=new Set(["/"]),r=s=>e.has(s)||t.has(s),o=s=>[...e.keys(),...t].filter(c=>c!==s&&C(c)===s).toSorted((c,a)=>c.localeCompare(a)),n=(s,c)=>{const a=C(s);if(e.has(a))throw w("ENOTDIR",c,s);if(!t.has(a))throw w("ENOENT",c,s)};return{mkdir:async(s,c={})=>{const a=T(s,c);if(c.recursive===!0){let i="";for(const u of a.split("/").filter(Boolean)){if(i=`${i}/${u}`,e.has(i))throw w("ENOTDIR","mkdir",i);t.add(i)}return}if(r(a))throw w("EEXIST","mkdir",a);n(a,"mkdir"),t.add(a)},readDirectory:async(s,c)=>{const a=T(s,c);if(e.has(a))throw w("ENOTDIR","readDirectory",a);if(!t.has(a))throw w("ENOENT","readDirectory",a);return o(a).map(i=>({name:i.slice(i.lastIndexOf("/")+1),type:t.has(i)?"directory":"file"}))},readFile:async(s,c)=>{const a=T(s,c),i=e.get(a);if(i===void 0)throw w(t.has(a)?"EISDIR":"ENOENT","readFile",a);return new Response(new Uint8Array(i))},remove:async(s,c={})=>{const a=T(s,c);if(!e.delete(a)){if(!t.has(a)){if(c.force!==!0)throw w("ENOENT","remove",a);return}if(c.recursive!==!0)throw w(o(a).length>0?"ENOTEMPTY":"EISDIR","remove",a);for(const i of[...e.keys(),...t])(i===a||i.startsWith(`${a}/`))&&(e.delete(i),t.delete(i));t.add("/")}},rename:async(s,c,a)=>{const i=T(s,a),u=T(c,a);if(!r(i))throw w("ENOENT","rename",i);if(n(u,"rename"),u.startsWith(`${i}/`))throw w("EINVAL","rename",i);const v=[...e],m=[...t];for(const[d,f]of v)(d===i||d.startsWith(`${i}/`))&&(e.delete(d),e.set(u+d.slice(i.length),f));for(const d of m)(d===i||d.startsWith(`${i}/`))&&(t.delete(d),t.add(u+d.slice(i.length)))},stat:async(s,c)=>{const a=T(s,c),i=e.get(a);if(i!==void 0)return L("file",i.byteLength);if(t.has(a))return L("directory",4096);throw w("ENOENT","stat",a)},writeFile:async(s,c,a)=>{const i=T(s,a);if(t.has(i))throw w("EISDIR","writeFile",i);n(i,"writeFile"),e.set(i,await fe(c))}}},k=3,U=3e4,me=3,we=500,he="cf-container-target-port",M="__lunora",Ee=e=>{const t=e.indexOf(";");return t===-1?e:e.slice(0,t)},ve=e=>{const t=e.split("/").find(r=>r!=="")??"";return Ee(t).toLowerCase()},H=(e,t)=>{const r=typeof e=="string"?e:e.url,o=URL.parse(r.startsWith("/")?`http://container${r}`:r);if(o!==null&&J(o.pathname,n=>ve(n)===M))throw new N("BAD_REQUEST",`${t}: \`/${M}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},W=(e,t)=>{if(typeof e!="string"){const o=new Request(e,t);return o.headers.delete(x),[o,void 0]}if(t?.headers===void 0)return[e,t];const r=new Headers(t.headers);return r.delete(x),[e,{...t,headers:r}]},z=(e,t,r,o)=>{const n=typeof e=="string"&&e.startsWith("/")?new Request(`http://container${e}`,t):new Request(e,t);return r!==void 0&&n.headers.set(he,String(r)),o?.traceparent!==void 0&&n.headers.set("traceparent",o.traceparent),o?.sampleErrors!==void 0&&n.headers.set(K,o.sampleErrors?"1":"0"),n},j=async e=>{e<=0||await new Promise(t=>{setTimeout(t,e)})},pe=/no container instance|not listening|try again later|rate.?limit|provision/i,Te="no Container instance available",Re="Failed to start container:",ge=1024,Ae=async e=>(await G(e.clone().body,ge)).text,X=e=>e instanceof Error&&pe.test(e.message),V=async e=>{if(e.status===429)return!0;if(e.status!==500&&e.status!==503)return!1;try{const t=await Ae(e);return t.includes(Te)||t.startsWith(Re)}catch{return!1}},O=e=>`ctx.containers.${e.exportName}`,_=(e,t,r={},o,n)=>{const s=Math.max(1,r.attempts??me),c=r.backoffMs??we,a=r.maxBackoffMs??U,i=async(u,v)=>{const m=typeof u=="string"?s:1;let d;for(let f=0;f<m;f+=1){const p=f===m-1;f>0&&await j(Math.min(c*2**(f-1),a));try{const h=await e(z(u,v,o,n),v?.signal);if(p||!await V(h))return h}catch(h){if(d=h,p||!X(h))throw h}}throw d instanceof Error?d:new Error("ctx.containers: cold-start retry exhausted")};return{exec:I(i,t),fetch:async(u,v)=>(H(u,t),i(...W(u,v))),port:u=>_(e,t,r,u,n)}},Se=async(e,t)=>{if(t.aborted)throw e.catch(()=>{}),t.reason;const r=new AbortController,o=new Promise((n,s)=>{t.addEventListener("abort",()=>{s(t.reason)},{once:!0,signal:r.signal})});try{return await Promise.race([e,o])}finally{r.abort(),e.catch(()=>{})}},F=async(e,t,r)=>{if(!t.headers.has(x))return e.fetch(t);if(typeof e.lunoraExec!="function")throw new TypeError("ctx.containers: this container DO does not expose lunoraExec() — is @lunora/container/do up to date?");t.headers.delete(x);const o=e.lunoraExec(new Request(t.url,{body:await t.text(),headers:t.headers,method:t.method}));return r?Se(o,r):o},Y=(e,t,r,o,n)=>_(async(s,c)=>F(e.get(e.idFromName(t)),s,c),r,o,void 0,n),Ne=new Set(["lunoraBackup","lunoraDeleteBackup","lunoraInspectMount","lunoraMkdir","lunoraMount","lunoraReadDirectory","lunoraReadFile","lunoraRemove","lunoraRename","lunoraRestore","lunoraStat","lunoraUnmount","lunoraWriteFile"]),l=async(e,t,r,...o)=>{const n=e[t];if(typeof n!="function")throw Ne.has(t)?new N("BAD_REQUEST",`ctx.containers: the "${r}" container DO does not expose ${t}() — sandbox helpers need \`sandbox: true\` in its defineContainer() definition`):new TypeError(`ctx.containers: the "${r}" container DO does not expose ${t}() — is @lunora/container/do up to date?`);return n(...o)},Oe=(e,t)=>({backup:async(r,o)=>l(e(),"lunoraBackup",t,r,o),deleteBackup:async r=>l(e(),"lunoraDeleteBackup",t,r),files:{mkdir:async(r,o)=>l(e(),"lunoraMkdir",t,r,o),readDirectory:async(r,o)=>l(e(),"lunoraReadDirectory",t,r,o),readFile:async(r,o)=>l(e(),"lunoraReadFile",t,r,o),remove:async(r,o)=>l(e(),"lunoraRemove",t,r,o),rename:async(r,o,n)=>l(e(),"lunoraRename",t,r,o,n),stat:async(r,o)=>l(e(),"lunoraStat",t,r,o),writeFile:async(r,o,n)=>l(e(),"lunoraWriteFile",t,r,o,n)},inspectMount:async r=>l(e(),"lunoraInspectMount",t,r),mount:async r=>l(e(),"lunoraMount",t,r),restore:async(r,o)=>l(e(),"lunoraRestore",t,r,o),unmount:async r=>l(e(),"lunoraUnmount",t,r)}),De=(e,t)=>({allow:async r=>l(e(),"allowHost",t,r),deny:async r=>l(e(),"denyHost",t,r),removeAllowed:async r=>l(e(),"removeAllowedHost",t,r),removeDenied:async r=>l(e(),"removeDeniedHost",t,r),setAllowed:async r=>l(e(),"setAllowedHosts",t,[...r]),setDenied:async r=>l(e(),"setDeniedHosts",t,[...r])}),Q=(e,t,r,o,n)=>{const s=()=>e.get(e.idFromName(r)),c=async(a,i={})=>te(await l(s(),"lunoraSpawn",t.binding,ee(a,i)),i.signal);return{..._(async(a,i)=>F(s(),a,i),O(t),o,void 0,n),...Oe(s,t.binding),destroy:async()=>l(s(),"destroy",t.binding),egress:De(s,t.binding),getState:async()=>l(s(),"getState",t.binding),renewActivityTimeout:async()=>l(s(),"renewActivityTimeout",t.binding),snapshot:async a=>l(s(),"lunoraSnapshot",t.binding,a),spawn:c,start:async a=>l(s(),"start",t.binding,a),stop:async a=>l(s(),"stop",t.binding,a),terminal:async(a,i)=>de(c,a,i)}},D="pool-",P=e=>`${D}${String(Math.floor(Math.random()*e))}`,q=(e,t)=>{if(e.startsWith(D))throw new N("BAD_REQUEST",`${O(t)}.get(${JSON.stringify(e)}): the "${D}" prefix is reserved for the instances .any()/.pool() pick, so this name would share a container (disk, lifecycle) with a pool member. Pick a name that does not start with "${D}".`)},xe=e=>e.status>=500,b=(e,t,r={},o,n)=>{const s=r.size??t.maxInstances??k,c=Math.max(1,r.attempts??3),a=r.backoffMs??100,i=r.maxBackoffMs??U,u=`${O(t)}.pool()`,v=(d,f)=>async(p,h)=>{const R=typeof p=="string"?c:1;let S;for(let y=0;y<R;y+=1){y>0&&await j(Math.min(a*2**(y-1),i));const g=z(p,h,o,n);try{const E=await F(e.get(e.idFromName(P(s))),g,h?.signal);if(y===R-1||!await d(E))return E}catch(E){if(S=E,!f(E))throw E}}throw S instanceof Error?S:new Error(`ctx.containers.${t.exportName}.pool(): all ${String(R)} attempts failed`)},m=v(r.retryOn??xe,()=>!0);return{exec:I(v(V,X),u),fetch:async(d,f)=>(H(d,u),m(...W(d,f))),port:d=>b(e,t,r,d,n)}},ke=(e,t,r)=>({any:(o,n)=>Y(e,P(o??t.maxInstances??k),O(t),n,r),get:(o,n)=>(q(o,t),Q(e,t,o,n,r)),pool:o=>b(e,t,o,void 0,r)}),_e=e=>{const t=()=>{throw new N("INTERNAL",`ctx.containers.${e.exportName}: no "${e.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:t,get:t,pool:t}},Fe=(e,t,r,o,n)=>{const s={};for(const c of t){const a=e[c.binding];s[c.exportName]=a&&typeof a.idFromName=="function"&&typeof a.get=="function"?ke(Z(a,r),c,{...n===void 0?{}:{sampleErrors:n},...o===void 0?{}:{traceparent:o}}):_e(c)}return s},Ie=(e,t,r,o={})=>{const{jurisdiction:n,...s}=o,c={binding:$(t),exportName:t};return Fe(e,[c],n)[t].get(r,s)},A=e=>()=>Promise.reject(new N("NOT_IMPLEMENTED",`createContainerTestContext: ${e}() needs a real container — the test double has no processes or buckets. Stub the handle method in your test instead.`)),Pe=e=>{const t=new Map,r=o=>{const n=t.get(o)??ye();return t.set(o,n),{allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:s=>Promise.resolve(e(s,{name:o})),lunoraBackup:A("backup"),lunoraDeleteBackup:A("deleteBackup"),lunoraExec:s=>Promise.resolve(e(s,{name:o})),lunoraInspectMount:A("inspectMount"),lunoraMkdir:n.mkdir,lunoraMount:A("mount"),lunoraReadDirectory:n.readDirectory,lunoraReadFile:n.readFile,lunoraRemove:n.remove,lunoraRename:n.rename,lunoraRestore:A("restore"),lunoraSnapshot:s=>Promise.resolve({id:`test-snapshot-${o}`,size:0,...s?.name===void 0?{}:{name:s.name}}),lunoraSpawn:A("spawn"),lunoraStat:n.stat,lunoraUnmount:A("unmount"),lunoraWriteFile:n.writeFile,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=>r(String(o)),idFromName:o=>o}},$e=e=>{const t={};for(const[r,o]of Object.entries(e)){const n=Pe(o),s={binding:$(r),exportName:r};t[r]={any:(c,a)=>Y(n,P(c??s.maxInstances??k),O(s),{attempts:1,...a}),get:(c,a)=>(q(c,s),Q(n,s,c,{attempts:1,...a})),pool:c=>b(n,s,{...c,attempts:1})}}return t};export{Fe as createContainerContext,$e as createContainerTestContext,Ie as getContainer};
@@ -1,3 +1,92 @@
1
+ /**
2
+ * The `handle.spawn()` contract: start a process in a named container
3
+ * instance and stream it, rather than buffer it the way `exec` does.
4
+ *
5
+ * Node-safe (no runtime imports): the Durable Object half lives in
6
+ * `do/spawn.ts`. This module owns the request shape both halves agree on and
7
+ * turns the RPC result into a {@link ContainerProcess}.
8
+ */
9
+ /** Per-call options for `handle.spawn()`. */
10
+ interface ContainerSpawnOptions {
11
+ /** Arguments passed to `command`, unshelled. */
12
+ args?: ReadonlyArray<string>;
13
+ /** Working directory for the process. */
14
+ cwd?: string;
15
+ /** Extra environment for this process, merged over the container's start env. */
16
+ env?: Readonly<Record<string, string>>;
17
+ /**
18
+ * Run the process on a pseudo-terminal of this size (defaults `80×24`).
19
+ * Under a PTY, stderr is merged into `stdout` and `resize()` works — what an
20
+ * interactive shell or a browser terminal needs.
21
+ */
22
+ pty?: {
23
+ cols?: number;
24
+ rows?: number;
25
+ };
26
+ /** Kill the process when this signal aborts. */
27
+ signal?: AbortSignal;
28
+ /**
29
+ * Give the process a stdin: `true` returns a writable `stdin` stream, a
30
+ * `ReadableStream` is piped in as the process's whole input. Omitted or
31
+ * `false` leaves stdin closed.
32
+ */
33
+ stdin?: ReadableStream<Uint8Array> | boolean;
34
+ /**
35
+ * Kill the process after this many ms. Unlike `exec`, a spawned process has
36
+ * no default deadline: it runs until it exits, is killed, or the container
37
+ * stops.
38
+ */
39
+ timeoutMs?: number;
40
+ }
41
+ /** The request `lunoraSpawn` takes over RPC. An `AbortSignal` cannot cross RPC, so it stays on the caller's side. */
42
+ interface ContainerSpawnRequest extends Omit<ContainerSpawnOptions, "args" | "env" | "signal"> {
43
+ args?: string[];
44
+ command: string;
45
+ env?: Record<string, string>;
46
+ }
47
+ /** The control stub `lunoraSpawn` returns, as the caller sees it over RPC. */
48
+ interface ContainerProcessControlStub {
49
+ exitCode: () => Promise<number>;
50
+ kill: (signal?: number) => Promise<void> | void;
51
+ resize: (cols: number, rows: number) => Promise<void> | void;
52
+ }
53
+ /** `lunoraSpawn`'s RPC result. */
54
+ interface SpawnResult {
55
+ control: ContainerProcessControlStub;
56
+ isPty: boolean;
57
+ pid: number;
58
+ stderr: ReadableStream<Uint8Array> | null;
59
+ stdin: WritableStream<Uint8Array> | null;
60
+ stdout: ReadableStream<Uint8Array> | null;
61
+ }
62
+ /**
63
+ * A running process in a container instance, returned by `handle.spawn()`.
64
+ *
65
+ * The container finishes a process's output, and settles `exitCode`, only
66
+ * while its output is being drained. Lunora drains `stdout` and `stderr` into
67
+ * a 1 MiB buffer each, so they can be read in either order, or after awaiting
68
+ * `exitCode`, while the stream you are not reading stays under 1 MiB. Past
69
+ * that the process stalls until you read it: for a process that may write more
70
+ * to one stream, read both concurrently, or cancel the one you do not need.
71
+ */
72
+ interface ContainerProcess {
73
+ /** Resolves with the exit code once the process ends. */
74
+ exitCode: Promise<number>;
75
+ /** Whether the process runs on a PTY (`spawn({ pty })`). */
76
+ isPty: boolean;
77
+ /** Send `signal` (default `SIGTERM`). Resolves once the signal is sent, not when the process exits. */
78
+ kill: (signal?: number) => Promise<void>;
79
+ /** The process id inside the container, or `-1` when the runtime does not report one. */
80
+ pid: number;
81
+ /** Resize the PTY. Rejects for a process spawned without `pty`. */
82
+ resize: (cols: number, rows: number) => Promise<void>;
83
+ /** The process's stderr, or `null` under `pty` (merged into `stdout`). */
84
+ stderr: ReadableStream<Uint8Array> | null;
85
+ /** The process's stdin when spawned with `stdin: true`, else `null`. */
86
+ stdin: WritableStream<Uint8Array> | null;
87
+ /** The process's stdout (and, under `pty`, its stderr too). */
88
+ stdout: ReadableStream<Uint8Array> | null;
89
+ }
1
90
  /**
2
91
  * Public configuration types for `@lunora/container`.
3
92
  *
@@ -97,6 +186,16 @@ interface ContainerReadinessCheck {
97
186
  /**
98
187
  * The fields every container accepts, whichever scheduling policy it uses.
99
188
  */
189
+ /**
190
+ * Where `handle.backup()` stores directory backups for a `sandbox: true`
191
+ * container: an R2 bucket binding and an optional key prefix.
192
+ */
193
+ interface ContainerBackupStorage {
194
+ /** Name of an R2 bucket binding on the Worker env (`r2_buckets[].binding`). */
195
+ bucket: string;
196
+ /** Key prefix for the backup objects. A non-empty prefix ends in `/`. */
197
+ prefix?: string;
198
+ }
100
199
  interface ContainerConfigBase {
101
200
  /**
102
201
  * Hostnames the container may reach **even when {@link ContainerConfigBase.enableInternet}
@@ -109,6 +208,13 @@ interface ContainerConfigBase {
109
208
  * handle's `egress` controls adjust the lists at runtime.
110
209
  */
111
210
  allowedHosts?: ReadonlyArray<string>;
211
+ /**
212
+ * Where `handle.backup()` saves directory backups (requires `sandbox: true`).
213
+ * The backup and restore traffic goes through the `DirectoryBackupGateway`
214
+ * Worker entrypoint, which hands the container a grant for one object per
215
+ * operation, so the container never holds bucket credentials.
216
+ */
217
+ backups?: ContainerBackupStorage;
112
218
  /**
113
219
  * Build-time variables for a Dockerfile/Railpack image — wrangler's
114
220
  * `image_vars` (equivalent to `docker build --build-arg`). For *runtime*
@@ -204,6 +310,17 @@ interface ContainerConfigBase {
204
310
  * request doesn't pick one.
205
311
  */
206
312
  requiredPorts?: ReadonlyArray<number>;
313
+ /**
314
+ * Opt in to the Sandbox SDK helpers (`@cloudflare/sandbox`): structured
315
+ * file operations, directory backups to R2 and S3-compatible bucket mounts
316
+ * on a named instance. The image must ship the `sandbox-shim` helper at
317
+ * `/usr/local/bin/sandbox-shim`, copied from Cloudflare's shim-only
318
+ * `cloudflare/sandbox` image (`COPY --from=…`). Turning it on also makes codegen export the `S3Gateway` and
319
+ * `DirectoryBackupGateway` entrypoints from the worker, which the helpers
320
+ * route the container's storage traffic through. Must be a `true`/`false`
321
+ * literal, because codegen reads it. Defaults to `false`.
322
+ */
323
+ sandbox?: boolean;
207
324
  /**
208
325
  * Names of Worker secrets (from `wrangler secret` / `.dev.vars`) forwarded
209
326
  * into the container's environment at instance start. Each declared name
@@ -347,4 +464,4 @@ type NormalizedContainerImage = {
347
464
  * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
348
465
  */
349
466
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
350
- export { BuildImageSource as B, ContainerSnapshot as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerRuntimeInstanceType as a, DefaultScheduledContainerConfig as b, DurableObjectScheduledContainerConfig as c, ContainerDefinition as d, ContainerImageSource as e, ContainerConfig as f, ContainerConfigBase as g, ContainerInstanceType as h, ContainerNamedImageSource as i, ContainerReadinessCheck as j, ContainerRollout as k, CustomContainerInstanceType as l, NamedContainerInstanceType as m };
467
+ export { BuildImageSource as B, ContainerDefinition as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, SpawnResult as S, ContainerSpawnOptions as a, ContainerSnapshot as b, ContainerProcess as c, ContainerRuntimeInstanceType as d, ContainerSpawnRequest as e, DefaultScheduledContainerConfig as f, DurableObjectScheduledContainerConfig as g, ContainerImageSource as h, ContainerBackupStorage as i, ContainerConfig as j, ContainerConfigBase as k, ContainerInstanceType as l, ContainerNamedImageSource as m, ContainerReadinessCheck as n, ContainerRollout as o, CustomContainerInstanceType as p, NamedContainerInstanceType as q };
@@ -1,3 +1,92 @@
1
+ /**
2
+ * The `handle.spawn()` contract: start a process in a named container
3
+ * instance and stream it, rather than buffer it the way `exec` does.
4
+ *
5
+ * Node-safe (no runtime imports): the Durable Object half lives in
6
+ * `do/spawn.ts`. This module owns the request shape both halves agree on and
7
+ * turns the RPC result into a {@link ContainerProcess}.
8
+ */
9
+ /** Per-call options for `handle.spawn()`. */
10
+ interface ContainerSpawnOptions {
11
+ /** Arguments passed to `command`, unshelled. */
12
+ args?: ReadonlyArray<string>;
13
+ /** Working directory for the process. */
14
+ cwd?: string;
15
+ /** Extra environment for this process, merged over the container's start env. */
16
+ env?: Readonly<Record<string, string>>;
17
+ /**
18
+ * Run the process on a pseudo-terminal of this size (defaults `80×24`).
19
+ * Under a PTY, stderr is merged into `stdout` and `resize()` works — what an
20
+ * interactive shell or a browser terminal needs.
21
+ */
22
+ pty?: {
23
+ cols?: number;
24
+ rows?: number;
25
+ };
26
+ /** Kill the process when this signal aborts. */
27
+ signal?: AbortSignal;
28
+ /**
29
+ * Give the process a stdin: `true` returns a writable `stdin` stream, a
30
+ * `ReadableStream` is piped in as the process's whole input. Omitted or
31
+ * `false` leaves stdin closed.
32
+ */
33
+ stdin?: ReadableStream<Uint8Array> | boolean;
34
+ /**
35
+ * Kill the process after this many ms. Unlike `exec`, a spawned process has
36
+ * no default deadline: it runs until it exits, is killed, or the container
37
+ * stops.
38
+ */
39
+ timeoutMs?: number;
40
+ }
41
+ /** The request `lunoraSpawn` takes over RPC. An `AbortSignal` cannot cross RPC, so it stays on the caller's side. */
42
+ interface ContainerSpawnRequest extends Omit<ContainerSpawnOptions, "args" | "env" | "signal"> {
43
+ args?: string[];
44
+ command: string;
45
+ env?: Record<string, string>;
46
+ }
47
+ /** The control stub `lunoraSpawn` returns, as the caller sees it over RPC. */
48
+ interface ContainerProcessControlStub {
49
+ exitCode: () => Promise<number>;
50
+ kill: (signal?: number) => Promise<void> | void;
51
+ resize: (cols: number, rows: number) => Promise<void> | void;
52
+ }
53
+ /** `lunoraSpawn`'s RPC result. */
54
+ interface SpawnResult {
55
+ control: ContainerProcessControlStub;
56
+ isPty: boolean;
57
+ pid: number;
58
+ stderr: ReadableStream<Uint8Array> | null;
59
+ stdin: WritableStream<Uint8Array> | null;
60
+ stdout: ReadableStream<Uint8Array> | null;
61
+ }
62
+ /**
63
+ * A running process in a container instance, returned by `handle.spawn()`.
64
+ *
65
+ * The container finishes a process's output, and settles `exitCode`, only
66
+ * while its output is being drained. Lunora drains `stdout` and `stderr` into
67
+ * a 1 MiB buffer each, so they can be read in either order, or after awaiting
68
+ * `exitCode`, while the stream you are not reading stays under 1 MiB. Past
69
+ * that the process stalls until you read it: for a process that may write more
70
+ * to one stream, read both concurrently, or cancel the one you do not need.
71
+ */
72
+ interface ContainerProcess {
73
+ /** Resolves with the exit code once the process ends. */
74
+ exitCode: Promise<number>;
75
+ /** Whether the process runs on a PTY (`spawn({ pty })`). */
76
+ isPty: boolean;
77
+ /** Send `signal` (default `SIGTERM`). Resolves once the signal is sent, not when the process exits. */
78
+ kill: (signal?: number) => Promise<void>;
79
+ /** The process id inside the container, or `-1` when the runtime does not report one. */
80
+ pid: number;
81
+ /** Resize the PTY. Rejects for a process spawned without `pty`. */
82
+ resize: (cols: number, rows: number) => Promise<void>;
83
+ /** The process's stderr, or `null` under `pty` (merged into `stdout`). */
84
+ stderr: ReadableStream<Uint8Array> | null;
85
+ /** The process's stdin when spawned with `stdin: true`, else `null`. */
86
+ stdin: WritableStream<Uint8Array> | null;
87
+ /** The process's stdout (and, under `pty`, its stderr too). */
88
+ stdout: ReadableStream<Uint8Array> | null;
89
+ }
1
90
  /**
2
91
  * Public configuration types for `@lunora/container`.
3
92
  *
@@ -97,6 +186,16 @@ interface ContainerReadinessCheck {
97
186
  /**
98
187
  * The fields every container accepts, whichever scheduling policy it uses.
99
188
  */
189
+ /**
190
+ * Where `handle.backup()` stores directory backups for a `sandbox: true`
191
+ * container: an R2 bucket binding and an optional key prefix.
192
+ */
193
+ interface ContainerBackupStorage {
194
+ /** Name of an R2 bucket binding on the Worker env (`r2_buckets[].binding`). */
195
+ bucket: string;
196
+ /** Key prefix for the backup objects. A non-empty prefix ends in `/`. */
197
+ prefix?: string;
198
+ }
100
199
  interface ContainerConfigBase {
101
200
  /**
102
201
  * Hostnames the container may reach **even when {@link ContainerConfigBase.enableInternet}
@@ -109,6 +208,13 @@ interface ContainerConfigBase {
109
208
  * handle's `egress` controls adjust the lists at runtime.
110
209
  */
111
210
  allowedHosts?: ReadonlyArray<string>;
211
+ /**
212
+ * Where `handle.backup()` saves directory backups (requires `sandbox: true`).
213
+ * The backup and restore traffic goes through the `DirectoryBackupGateway`
214
+ * Worker entrypoint, which hands the container a grant for one object per
215
+ * operation, so the container never holds bucket credentials.
216
+ */
217
+ backups?: ContainerBackupStorage;
112
218
  /**
113
219
  * Build-time variables for a Dockerfile/Railpack image — wrangler's
114
220
  * `image_vars` (equivalent to `docker build --build-arg`). For *runtime*
@@ -204,6 +310,17 @@ interface ContainerConfigBase {
204
310
  * request doesn't pick one.
205
311
  */
206
312
  requiredPorts?: ReadonlyArray<number>;
313
+ /**
314
+ * Opt in to the Sandbox SDK helpers (`@cloudflare/sandbox`): structured
315
+ * file operations, directory backups to R2 and S3-compatible bucket mounts
316
+ * on a named instance. The image must ship the `sandbox-shim` helper at
317
+ * `/usr/local/bin/sandbox-shim`, copied from Cloudflare's shim-only
318
+ * `cloudflare/sandbox` image (`COPY --from=…`). Turning it on also makes codegen export the `S3Gateway` and
319
+ * `DirectoryBackupGateway` entrypoints from the worker, which the helpers
320
+ * route the container's storage traffic through. Must be a `true`/`false`
321
+ * literal, because codegen reads it. Defaults to `false`.
322
+ */
323
+ sandbox?: boolean;
207
324
  /**
208
325
  * Names of Worker secrets (from `wrangler secret` / `.dev.vars`) forwarded
209
326
  * into the container's environment at instance start. Each declared name
@@ -347,4 +464,4 @@ type NormalizedContainerImage = {
347
464
  * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
348
465
  */
349
466
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
350
- export { BuildImageSource as B, ContainerSnapshot as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerRuntimeInstanceType as a, DefaultScheduledContainerConfig as b, DurableObjectScheduledContainerConfig as c, ContainerDefinition as d, ContainerImageSource as e, ContainerConfig as f, ContainerConfigBase as g, ContainerInstanceType as h, ContainerNamedImageSource as i, ContainerReadinessCheck as j, ContainerRollout as k, CustomContainerInstanceType as l, NamedContainerInstanceType as m };
467
+ export { BuildImageSource as B, ContainerDefinition as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, SpawnResult as S, ContainerSpawnOptions as a, ContainerSnapshot as b, ContainerProcess as c, ContainerRuntimeInstanceType as d, ContainerSpawnRequest as e, DefaultScheduledContainerConfig as f, DurableObjectScheduledContainerConfig as g, ContainerImageSource as h, ContainerBackupStorage as i, ContainerConfig as j, ContainerConfigBase as k, ContainerInstanceType as l, ContainerNamedImageSource as m, ContainerReadinessCheck as n, ContainerRollout as o, CustomContainerInstanceType as p, NamedContainerInstanceType as q };