@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/do/index.d.mts +76 -4
- package/dist/do/index.d.ts +76 -4
- package/dist/do/index.mjs +1 -1
- package/dist/index.d.mts +108 -8
- package/dist/index.d.ts +108 -8
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-B9DkqZkf.mjs +1 -0
- package/dist/packem_shared/LunoraSandboxContainer-DPnhJUu1.mjs +1 -0
- package/dist/packem_shared/createContainerContext-CcW1olml.mjs +1 -0
- package/dist/packem_shared/{jurisdiction.d-CoLtDVr_.d.mts → jurisdiction.d-t4mtuqYW.d.mts} +118 -1
- package/dist/packem_shared/{jurisdiction.d-CoLtDVr_.d.ts → jurisdiction.d-t4mtuqYW.d.ts} +118 -1
- package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.mts +110 -0
- package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.ts +110 -0
- package/dist/sandbox/index.d.mts +66 -0
- package/dist/sandbox/index.d.ts +66 -0
- package/dist/sandbox/index.mjs +1 -0
- package/package.json +7 -2
- package/dist/packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs +0 -1
- package/dist/packem_shared/createContainerContext-DP-kZaQ1.mjs +0 -1
package/dist/do/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
1
|
+
import { d as ContainerRuntimeInstanceType, C as ContainerDefinition, D as DurableObjectJurisdiction, b as ContainerSnapshot, e as ContainerSpawnRequest } from "../packem_shared/jurisdiction.d-t4mtuqYW.mjs";
|
|
2
|
+
import { DurableObject, WorkerEntrypoint, RpcTarget } from 'cloudflare:workers';
|
|
3
3
|
/**
|
|
4
4
|
* ContainerStartOptions as they come from worker types
|
|
5
5
|
*/
|
|
@@ -490,6 +490,44 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
490
490
|
getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
|
|
491
491
|
private isActivityExpired;
|
|
492
492
|
}
|
|
493
|
+
/** The runtime's `ExecProcess`, structurally — only what a spawned process uses. */
|
|
494
|
+
interface NativeProcess {
|
|
495
|
+
exitCode: Promise<number>;
|
|
496
|
+
isPty?: boolean;
|
|
497
|
+
kill: (signal?: number) => void;
|
|
498
|
+
pid?: number;
|
|
499
|
+
resize?: (cols: number, rows: number) => void;
|
|
500
|
+
stderr?: ReadableStream | null;
|
|
501
|
+
stdin?: WritableStream | null;
|
|
502
|
+
stdout?: ReadableStream | null;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Control half of a spawned process, returned to the caller as an RPC stub.
|
|
506
|
+
* The streams travel next to it in {@link SpawnedProcess}: a stream can cross
|
|
507
|
+
* RPC only once, so it is handed over directly rather than exposed through a
|
|
508
|
+
* property that could be read twice.
|
|
509
|
+
*/
|
|
510
|
+
declare class ContainerProcessControl extends RpcTarget {
|
|
511
|
+
#private;
|
|
512
|
+
constructor(process: NativeProcess, exit: Promise<number>);
|
|
513
|
+
/** Resolves with the exit code once the process ends. */
|
|
514
|
+
exitCode(): Promise<number>;
|
|
515
|
+
/** Send `signal` (default `SIGTERM`) to the process. A no-op once it has exited. */
|
|
516
|
+
kill(signal?: number): void;
|
|
517
|
+
/** Resize a PTY process's terminal. Throws for a process spawned without `pty`. */
|
|
518
|
+
resize(cols: number, rows: number): void;
|
|
519
|
+
}
|
|
520
|
+
/** What `lunoraSpawn` returns over RPC. */
|
|
521
|
+
interface SpawnedProcess {
|
|
522
|
+
control: ContainerProcessControl;
|
|
523
|
+
isPty: boolean;
|
|
524
|
+
pid: number;
|
|
525
|
+
/** `null` under `pty`, where stderr is merged into the terminal output. */
|
|
526
|
+
stderr: ReadableStream<Uint8Array> | null;
|
|
527
|
+
/** A writable stdin when the request asked for `stdin: true`, else `null`. */
|
|
528
|
+
stdin: WritableStream<Uint8Array> | null;
|
|
529
|
+
stdout: ReadableStream<Uint8Array> | null;
|
|
530
|
+
}
|
|
493
531
|
/** What an explicit `start()` chose that later restarts must reuse. */
|
|
494
532
|
interface StartOverride {
|
|
495
533
|
/** Replaces the declared env, `secrets` and `secretsStore` wholesale. */
|
|
@@ -499,6 +537,14 @@ interface StartOverride {
|
|
|
499
537
|
instanceType?: ContainerRuntimeInstanceType;
|
|
500
538
|
}
|
|
501
539
|
type DurableObjectContext = ConstructorParameters<typeof Container>[0];
|
|
540
|
+
/** The runtime's `ctx.container`, once known to be present. */
|
|
541
|
+
type ContainerRuntime = NonNullable<DurableObjectContext["container"]>;
|
|
542
|
+
/** A running container plus the release for the in-flight count taken on it. See `LunoraContainer.lunoraAcquire`. */
|
|
543
|
+
interface AcquiredContainer {
|
|
544
|
+
container: ContainerRuntime;
|
|
545
|
+
/** Drop the in-flight count. Idempotent. */
|
|
546
|
+
release: () => void;
|
|
547
|
+
}
|
|
502
548
|
/**
|
|
503
549
|
* The `start()` options `LunoraContainer` accepts on top of the base's: the
|
|
504
550
|
* `durable_object`-policy image, size and snapshot to restore.
|
|
@@ -524,14 +570,14 @@ type LunoraStartOptions = NonNullable<Parameters<Container["start"]>[0]> & Pick<
|
|
|
524
570
|
* ```
|
|
525
571
|
*/
|
|
526
572
|
declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
573
|
+
/** The `lunora/containers.ts` export name, for lifecycle log correlation and error messages. */
|
|
574
|
+
protected readonly lunoraName: string;
|
|
527
575
|
/**
|
|
528
576
|
* Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
|
|
529
577
|
* schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
|
|
530
578
|
* to the same region as the root shard. `undefined` ⇒ un-pinned.
|
|
531
579
|
*/
|
|
532
580
|
private readonly lunoraJurisdiction?;
|
|
533
|
-
/** The `lunora/containers.ts` export name, for lifecycle log correlation. */
|
|
534
|
-
private readonly lunoraName;
|
|
535
581
|
/** Default port the readiness probes target when a check omits its own `port`. */
|
|
536
582
|
private readonly lunoraDefaultPort?;
|
|
537
583
|
/** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
|
|
@@ -597,6 +643,14 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
597
643
|
* handle's target-port header the way the base `fetch` does.
|
|
598
644
|
*/
|
|
599
645
|
lunoraExec(request: Request): Promise<Response>;
|
|
646
|
+
/**
|
|
647
|
+
* Start a process and stream it (`handle.spawn()`), through the runtime's
|
|
648
|
+
* native `ctx.container.exec()`. Starts the container first when it is not
|
|
649
|
+
* running, and counts the process in flight until it exits, so `sleepAfter`
|
|
650
|
+
* cannot stop the container under it. The process's streams and a control
|
|
651
|
+
* stub (`exitCode` / `kill` / `resize`) cross back over RPC.
|
|
652
|
+
*/
|
|
653
|
+
lunoraSpawn(request: ContainerSpawnRequest): Promise<SpawnedProcess>;
|
|
600
654
|
/**
|
|
601
655
|
* The start path `containerFetch` takes (and the one an app can call itself).
|
|
602
656
|
* Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
|
|
@@ -652,6 +706,24 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
652
706
|
generation?: number;
|
|
653
707
|
}): Promise<void>;
|
|
654
708
|
override onStop(parameters: StopParams): Promise<void>;
|
|
709
|
+
/**
|
|
710
|
+
* Get the container running for an operation that talks to it through
|
|
711
|
+
* `ctx.container` rather than over HTTP (`spawn`, and the sandbox helpers),
|
|
712
|
+
* and count the operation in flight so `sleepAfter` cannot stop the
|
|
713
|
+
* container underneath it. Goes through the same readiness gate a proxied
|
|
714
|
+
* request does, and through {@link start} when the container is stopped —
|
|
715
|
+
* `start()` returns once the container runs, so a container that serves no
|
|
716
|
+
* port starts here too. The caller must call `release` when the operation
|
|
717
|
+
* ends, including when the result is a stream that outlives this call.
|
|
718
|
+
*/
|
|
719
|
+
protected lunoraAcquire(operation: string): Promise<AcquiredContainer>;
|
|
720
|
+
/**
|
|
721
|
+
* Runs on every start path after the env and image are settled and before
|
|
722
|
+
* the base starts the container — which is also before the base installs
|
|
723
|
+
* its outbound interception. A subclass that has to register an outbound
|
|
724
|
+
* intercept ahead of the base's catch-all does it here. No-op by default.
|
|
725
|
+
*/
|
|
726
|
+
protected beforeContainerStart(): Promise<void>;
|
|
655
727
|
/** A 403 for a path under the reserved namespace, else `undefined`. */
|
|
656
728
|
private refuseReserved;
|
|
657
729
|
/**
|
package/dist/do/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
1
|
+
import { d as ContainerRuntimeInstanceType, C as ContainerDefinition, D as DurableObjectJurisdiction, b as ContainerSnapshot, e as ContainerSpawnRequest } from "../packem_shared/jurisdiction.d-t4mtuqYW.js";
|
|
2
|
+
import { DurableObject, WorkerEntrypoint, RpcTarget } from 'cloudflare:workers';
|
|
3
3
|
/**
|
|
4
4
|
* ContainerStartOptions as they come from worker types
|
|
5
5
|
*/
|
|
@@ -490,6 +490,44 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
490
490
|
getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
|
|
491
491
|
private isActivityExpired;
|
|
492
492
|
}
|
|
493
|
+
/** The runtime's `ExecProcess`, structurally — only what a spawned process uses. */
|
|
494
|
+
interface NativeProcess {
|
|
495
|
+
exitCode: Promise<number>;
|
|
496
|
+
isPty?: boolean;
|
|
497
|
+
kill: (signal?: number) => void;
|
|
498
|
+
pid?: number;
|
|
499
|
+
resize?: (cols: number, rows: number) => void;
|
|
500
|
+
stderr?: ReadableStream | null;
|
|
501
|
+
stdin?: WritableStream | null;
|
|
502
|
+
stdout?: ReadableStream | null;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Control half of a spawned process, returned to the caller as an RPC stub.
|
|
506
|
+
* The streams travel next to it in {@link SpawnedProcess}: a stream can cross
|
|
507
|
+
* RPC only once, so it is handed over directly rather than exposed through a
|
|
508
|
+
* property that could be read twice.
|
|
509
|
+
*/
|
|
510
|
+
declare class ContainerProcessControl extends RpcTarget {
|
|
511
|
+
#private;
|
|
512
|
+
constructor(process: NativeProcess, exit: Promise<number>);
|
|
513
|
+
/** Resolves with the exit code once the process ends. */
|
|
514
|
+
exitCode(): Promise<number>;
|
|
515
|
+
/** Send `signal` (default `SIGTERM`) to the process. A no-op once it has exited. */
|
|
516
|
+
kill(signal?: number): void;
|
|
517
|
+
/** Resize a PTY process's terminal. Throws for a process spawned without `pty`. */
|
|
518
|
+
resize(cols: number, rows: number): void;
|
|
519
|
+
}
|
|
520
|
+
/** What `lunoraSpawn` returns over RPC. */
|
|
521
|
+
interface SpawnedProcess {
|
|
522
|
+
control: ContainerProcessControl;
|
|
523
|
+
isPty: boolean;
|
|
524
|
+
pid: number;
|
|
525
|
+
/** `null` under `pty`, where stderr is merged into the terminal output. */
|
|
526
|
+
stderr: ReadableStream<Uint8Array> | null;
|
|
527
|
+
/** A writable stdin when the request asked for `stdin: true`, else `null`. */
|
|
528
|
+
stdin: WritableStream<Uint8Array> | null;
|
|
529
|
+
stdout: ReadableStream<Uint8Array> | null;
|
|
530
|
+
}
|
|
493
531
|
/** What an explicit `start()` chose that later restarts must reuse. */
|
|
494
532
|
interface StartOverride {
|
|
495
533
|
/** Replaces the declared env, `secrets` and `secretsStore` wholesale. */
|
|
@@ -499,6 +537,14 @@ interface StartOverride {
|
|
|
499
537
|
instanceType?: ContainerRuntimeInstanceType;
|
|
500
538
|
}
|
|
501
539
|
type DurableObjectContext = ConstructorParameters<typeof Container>[0];
|
|
540
|
+
/** The runtime's `ctx.container`, once known to be present. */
|
|
541
|
+
type ContainerRuntime = NonNullable<DurableObjectContext["container"]>;
|
|
542
|
+
/** A running container plus the release for the in-flight count taken on it. See `LunoraContainer.lunoraAcquire`. */
|
|
543
|
+
interface AcquiredContainer {
|
|
544
|
+
container: ContainerRuntime;
|
|
545
|
+
/** Drop the in-flight count. Idempotent. */
|
|
546
|
+
release: () => void;
|
|
547
|
+
}
|
|
502
548
|
/**
|
|
503
549
|
* The `start()` options `LunoraContainer` accepts on top of the base's: the
|
|
504
550
|
* `durable_object`-policy image, size and snapshot to restore.
|
|
@@ -524,14 +570,14 @@ type LunoraStartOptions = NonNullable<Parameters<Container["start"]>[0]> & Pick<
|
|
|
524
570
|
* ```
|
|
525
571
|
*/
|
|
526
572
|
declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
573
|
+
/** The `lunora/containers.ts` export name, for lifecycle log correlation and error messages. */
|
|
574
|
+
protected readonly lunoraName: string;
|
|
527
575
|
/**
|
|
528
576
|
* Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
|
|
529
577
|
* schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
|
|
530
578
|
* to the same region as the root shard. `undefined` ⇒ un-pinned.
|
|
531
579
|
*/
|
|
532
580
|
private readonly lunoraJurisdiction?;
|
|
533
|
-
/** The `lunora/containers.ts` export name, for lifecycle log correlation. */
|
|
534
|
-
private readonly lunoraName;
|
|
535
581
|
/** Default port the readiness probes target when a check omits its own `port`. */
|
|
536
582
|
private readonly lunoraDefaultPort?;
|
|
537
583
|
/** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
|
|
@@ -597,6 +643,14 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
597
643
|
* handle's target-port header the way the base `fetch` does.
|
|
598
644
|
*/
|
|
599
645
|
lunoraExec(request: Request): Promise<Response>;
|
|
646
|
+
/**
|
|
647
|
+
* Start a process and stream it (`handle.spawn()`), through the runtime's
|
|
648
|
+
* native `ctx.container.exec()`. Starts the container first when it is not
|
|
649
|
+
* running, and counts the process in flight until it exits, so `sleepAfter`
|
|
650
|
+
* cannot stop the container under it. The process's streams and a control
|
|
651
|
+
* stub (`exitCode` / `kill` / `resize`) cross back over RPC.
|
|
652
|
+
*/
|
|
653
|
+
lunoraSpawn(request: ContainerSpawnRequest): Promise<SpawnedProcess>;
|
|
600
654
|
/**
|
|
601
655
|
* The start path `containerFetch` takes (and the one an app can call itself).
|
|
602
656
|
* Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
|
|
@@ -652,6 +706,24 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
652
706
|
generation?: number;
|
|
653
707
|
}): Promise<void>;
|
|
654
708
|
override onStop(parameters: StopParams): Promise<void>;
|
|
709
|
+
/**
|
|
710
|
+
* Get the container running for an operation that talks to it through
|
|
711
|
+
* `ctx.container` rather than over HTTP (`spawn`, and the sandbox helpers),
|
|
712
|
+
* and count the operation in flight so `sleepAfter` cannot stop the
|
|
713
|
+
* container underneath it. Goes through the same readiness gate a proxied
|
|
714
|
+
* request does, and through {@link start} when the container is stopped —
|
|
715
|
+
* `start()` returns once the container runs, so a container that serves no
|
|
716
|
+
* port starts here too. The caller must call `release` when the operation
|
|
717
|
+
* ends, including when the result is a stream that outlives this call.
|
|
718
|
+
*/
|
|
719
|
+
protected lunoraAcquire(operation: string): Promise<AcquiredContainer>;
|
|
720
|
+
/**
|
|
721
|
+
* Runs on every start path after the env and image are settled and before
|
|
722
|
+
* the base starts the container — which is also before the base installs
|
|
723
|
+
* its outbound interception. A subclass that has to register an outbound
|
|
724
|
+
* intercept ahead of the base's catch-all does it here. No-op by default.
|
|
725
|
+
*/
|
|
726
|
+
protected beforeContainerStart(): Promise<void>;
|
|
655
727
|
/** A 403 for a path under the reserved namespace, else `undefined`. */
|
|
656
728
|
private refuseReserved;
|
|
657
729
|
/**
|
package/dist/do/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as u}from"@lunora/errors";import{a as $}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{isManagedImage as P,resolveContainerEnvVars as I,parseDurationSeconds as L}from"../packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-Dk6cuK7G.mjs";import{D as C,C as O,p as H}from"../packem_shared/exec-BYLW70sR.mjs";import{a as j}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{Container as V}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";import{ContainerProxy as dt,outboundParams as lt}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";const M="lunora",U=(a,t,n,e)=>({container:a,event:n,instance:t,level:n==="error"?"error":"info",message:e,source:M,ts:Date.now(),type:"container"}),p=(a,t,n,e)=>{const r=U(a,t,n,e),s=JSON.stringify(r);return n==="error"?console.error(s):console.log(s),r},F=async(a,t,n,e)=>{const r=t.maxOutputBytes??C,s=await a([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...n,...t.env},stderr:"pipe",stdout:"pipe"}),o=new AbortController;let i=!1;const c={overflowed:!1,timedOut:!1},m=()=>c.timedOut,v=s.exitCode.finally(()=>{i=!0}),S=()=>{o.abort(),i||(i=!0,s.kill())},A=t.timeoutMs===void 0?void 0:setTimeout(()=>{c.timedOut=!0,S()},t.timeoutMs);let y=0;const R=async l=>{if(l===null)return"";const d=l.getReader(),f=new TextDecoder,h=()=>{d.cancel().catch(()=>{})};let E="";o.signal.addEventListener("abort",h);try{for(;!o.signal.aborted;){const{done:D,value:T}=await d.read();if(D)break;if(y+=T.byteLength,y>r){c.overflowed=!0,S();break}E+=f.decode(T,{stream:!0})}return E+f.decode()}finally{o.signal.removeEventListener("abort",h),d.cancel().catch(()=>{})}};try{const[l,d]=await Promise.allSettled([R(s.stdout),R(s.stderr)]);if(c.overflowed)return new Response(`container "${e}": exec output exceeded ${String(r)} bytes; the process was killed`,{status:413});if(c.timedOut)return new Response(`container "${e}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});if(l.status==="rejected")throw l.reason;if(d.status==="rejected")throw d.reason;const f=await v;if(m())return new Response(`container "${e}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});const h=JSON.stringify({code:f,stderr:d.value,stdout:l.value});return new TextEncoder().encode(h).byteLength>r?new Response(`container "${e}": exec output exceeded ${String(r)} bytes once encoded`,{status:413}):new Response(h,{headers:{"content-type":"application/json"}})}finally{clearTimeout(A)}},k=async(a,t,n,e)=>{let r;try{r=await t.json()}catch{return new Response(`container "${e}": exec body must be JSON`,{status:400})}return typeof r.command!="string"||r.command.length===0?new Response(`container "${e}": exec requires a non-empty \`command\``,{status:400}):F(a,r,n,e)},B="__lunora_admin__:recordContainerEvent",N="__root__",J=a=>{if(a===null||typeof a!="object")return!1;const t=a;return typeof t.get=="function"&&typeof t.idFromName=="function"},G=(a,t)=>{const n=j(a,t);return typeof n.getByName=="function"?n.getByName(N):n.get(n.idFromName(N))},Q=async(a,t,n)=>{try{const e=a??{},r=e.SHARD;if(!J(r))return;const s=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(!s||s.length===0)return;const o=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:B}),headers:{authorization:`Bearer ${s}`,"content-type":"application/json"},method:"POST"});await G(r,n).fetch(o)}catch{}},g="__lunoraStartOverride",K=(a,t)=>{if(a===void 0||t===void 0)return a===t;const n=Object.keys(a);return n.length===Object.keys(t).length&&n.every(e=>Object.hasOwn(t,e)&&a[e]===t[e])},W=(a,t)=>K(a.envVars,t.envVars)&&a.image===t.image&&JSON.stringify(a.instanceType)===JSON.stringify(t.instanceType),z=(a,t,n)=>{if(P(t))return t;const e=a[t];if(e===void 0){const r=Object.keys(a);throw new u("BAD_REQUEST",`container "${n}": no image named "${t}" — ${r.length===0?"the container declares no images":`declared images: ${r.join(", ")}`}`)}return e},_=500,x=3e4,w="__lunoraHardTimeoutGeneration",b="__lunora",X=/[/\\]/u,Y=a=>{const t=a.indexOf(";");return t===-1?a:a.slice(0,t)},q=a=>H(a,n=>n.split(X).some(e=>Y(e).toLowerCase()===b)),Z=a=>{const t=a instanceof Request?a.url:String(a);return URL.parse(t,"https://container")?.pathname??t},tt="cf-container-target-port";class it extends V{lunoraJurisdiction;lunoraName;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraDeclaredEnv;lunoraOverride;lunoraOverrideLoaded=!1;lunoraSecretsStoreResolved;lunoraDurableObjectScheduled;lunoraDefaultSelection;lunoraStops=0;constructor(t,n,e,r,s){const o=I(e,n,r);super(t,n,{defaultPort:e.defaultPort,entrypoint:e.entrypoint?[...e.entrypoint]:void 0,envVars:o,sleepAfter:e.sleepAfter}),this.lunoraDeclaredEnv={...o},e.enableInternet!==void 0&&(this.enableInternet=e.enableInternet),e.requiredPorts!==void 0&&(this.requiredPorts=[...e.requiredPorts]),e.interceptHttps!==void 0&&(this.interceptHttps=e.interceptHttps),e.allowedHosts!==void 0&&(this.allowedHosts=[...e.allowedHosts]),e.deniedHosts!==void 0&&(this.deniedHosts=[...e.deniedHosts]),e.pingEndpoint!==void 0&&(this.pingEndpoint=e.pingEndpoint),e.labels!==void 0&&(this.labels={...e.labels}),this.lunoraName=r??"container",this.lunoraJurisdiction=s,this.lunoraDefaultPort=e.defaultPort,this.lunoraReadyOn=e.readyOn?[...e.readyOn]:[],this.lunoraHardTimeoutSeconds=e.hardTimeout===void 0?void 0:L(e.hardTimeout),this.lunoraSecretsStore=e.secretsStore,this.lunoraDurableObjectScheduled=e.schedulingPolicy==="durable_object",this.lunoraDefaultSelection=e.schedulingPolicy==="durable_object"?{image:e.image,instanceType:e.instanceType}:{}}async lunoraSnapshot(t={}){if(!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshots need schedulingPolicy "durable_object"`);const{container:n}=this.ctx;if(n?.running!==!0||typeof n.snapshotContainer!="function")throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshot() needs a running container — start it first`);const{id:e,name:r,size:s}=await n.snapshotContainer({...t.name===void 0?{}:{name:t.name}});return{id:e,size:s,...r===void 0?{}:{name:r}}}async fetch(t){return this.refuseReserved(new URL(t.url).pathname)??super.fetch(t)}async containerFetch(...t){const n=this.refuseReserved(Z(t[0]));return n!==void 0?n:(await this.awaitReadinessGate(),super.containerFetch(...t))}async lunoraExec(t){const{pathname:n}=new URL(t.url);if(t.method!=="POST"||n!==O)return new Response(`container "${this.lunoraName}": lunoraExec only serves POST ${O}.`,{status:400});const e=t.headers.get(tt),r=e===null?this.defaultPort:Number.parseInt(e,10);if(r===void 0||Number.isNaN(r))return new Response(`container "${this.lunoraName}": exec needs a port — set \`defaultPort\` or exec through \`.port(n)\`.`,{status:400});await this.awaitReadinessGate();const{container:s}=this.ctx;if(typeof s?.exec!="function")return super.containerFetch(t,r);await this.startAndWaitForPorts(r),this.inflightRequests+=1,this.renewActivityTimeout();try{return await k(s.exec.bind(s),t,this.envVars??{},this.lunoraName)}finally{this.decrementInflight()}}async startAndWaitForPorts(...t){await this.applyStartEnv(),await this.applyStartSelection();const n=this.lunoraStops,e=this.beginStart();await super.startAndWaitForPorts(...t),await this.afterContainerStart(e&&this.lunoraStops===n)}async start(t,n){const{image:e,instanceType:r,snapshot:s,...o}=t??{};await this.recordStartOverride({envVars:t?.envVars,image:e,instanceType:r},s),await this.applyStartEnv(),await this.applyStartSelection(s!==void 0);const i=this.lunoraStops,c=this.beginStart();await super.start({...o,...s===void 0?{}:{containerSnapshot:s}},n),await this.afterContainerStart(c&&this.lunoraStops===i)}async destroy(){await super.destroy(),await this.ctx.storage.delete(g),this.lunoraOverride=void 0}async onActivityExpired(){const t=p(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const n=p(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(n),super.onError(t)}async onStart(){const t=p(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart()}async onHardTimeoutExpired(t){const n=await this.ctx.storage.get(w);if(t?.generation!==void 0&&t.generation!==n||this.ctx.container?.running!==!0)return;const e=p(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(e),await this.stop()}async onStop(t){const n=p(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(n),this.lunoraReadiness=void 0,this.lunoraStops+=1,await super.onStop(t)}refuseReserved(t){if(q(t))return new Response(`container "${this.lunoraName}": /${b}/* is reserved for Lunora's own container routes; use exec.`,{status:403})}async afterContainerStart(t){const n=this.lunoraReadiness;if(n!==void 0){await n;return}const e=(async()=>{t||await this.armHardTimeout(),await this.awaitContainerReadiness()})();this.lunoraReadiness=e;try{await e}catch(r){throw this.lunoraReadiness===e&&(this.lunoraReadiness=void 0),r}}beginStart(){const t=this.ctx.container?.running===!0;return t||(this.lunoraReadiness=void 0),t}async awaitReadinessGate(){if(this.lunoraReadyOn.length===0)return;if(this.lunoraReadiness===void 0){const{status:n}=await this.getState();if(n!=="healthy")return;this.lunoraReadiness=this.awaitContainerReadiness()}const t=this.lunoraReadiness;try{await t}catch(n){throw this.lunoraReadiness===t&&(this.lunoraReadiness=void 0),n}}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(w)??0)+1;await this.ctx.storage.put(w,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;return t===void 0?{}:(this.lunoraSecretsStoreResolved??=(async()=>{const n=this.env,e={};for(const[r,s]of Object.entries(t)){const o=n[s];if(o===void 0||typeof o.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${r}" points at binding "${s}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${s}".`);const i=await o.get();if(typeof i!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${s}" (env "${r}") did not resolve to a string value.`);e[r]=i}return e})().catch(n=>{throw this.lunoraSecretsStoreResolved=void 0,n}),this.lunoraSecretsStoreResolved)}async readOverride(){return this.lunoraOverrideLoaded||(this.lunoraOverride=await this.ctx.storage.get(g),this.lunoraOverrideLoaded=!0),this.lunoraOverride??{}}async recordStartOverride(t,n){const e=t.image!==void 0||t.instanceType!==void 0;if((e||n!==void 0)&&!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start({ image | instanceType | snapshot }) needs schedulingPolicy "durable_object" — a default-policy container's image and size are set in lunora/containers.ts`);if(t.image!==void 0&&n!==void 0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start() takes an image or a snapshot, not both — the snapshot is the filesystem`);t.image!==void 0&&this.resolveImage(t.image);const r=await this.readOverride(),s={...r,...t.envVars===void 0?{}:{envVars:{...t.envVars}},...t.image===void 0?{}:{image:t.image},...t.instanceType===void 0?{}:{instanceType:t.instanceType}};if((this.ctx.container?.running===!0||this.startInFlight!==void 0)&&(n!==void 0||!W({...this.lunoraDefaultSelection,...r},{...this.lunoraDefaultSelection,...s})))throw new u("CONFLICT",`container "${this.lunoraName}": start() on an instance that is already running with a different env, image or size (or a snapshot to restore) — the running container keeps its own. stop() it first (or destroy() it to also drop the override), then start again.`);(e||t.envVars!==void 0)&&(await this.ctx.storage.put(g,s),this.lunoraOverride=s)}async applyStartEnv(){const{envVars:t}=await this.readOverride();this.envVars=t===void 0?{...this.lunoraDeclaredEnv,...await this.resolveSecretsStoreEnv()}:{...t}}async applyStartSelection(t=!1){if(!this.lunoraDurableObjectScheduled)return;const n={...this.lunoraDefaultSelection,...await this.readOverride()};if(n.image===void 0&&!t&&this.ctx.container?.running!==!0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": no image to start — pass start({ image }) or start({ snapshot }), or set a default \`image\` in lunora/containers.ts`);this.instance=n.instanceType,this.image=n.image===void 0?void 0:this.resolveImage(n.image)}resolveImage(t){return z(this.ctx.container?.images??{},t,this.lunoraName)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const n=Date.now()+x;await Promise.all(this.lunoraReadyOn.map(async e=>this.awaitReadinessCheck(t,e,n)))}async awaitReadinessCheck(t,n,e){const r=n.port??this.lunoraDefaultPort;if(r===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${n.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const s=n.status??200,o=n.path.startsWith("/")?n.path:`/${n.path}`,i=t.getTcpPort(r);for(;;){const c=Math.max(_,e-Date.now()),m=$(void 0,c,()=>new DOMException(`readiness probe timed out after ${String(c)}ms`,"TimeoutError"));try{if((await i.fetch(`http://container${o}`,{signal:m.signal})).status===s)return}catch{}finally{m.dispose()}if(Date.now()>=e)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${n.path}" (port ${String(r)}) did not return ${String(s)} within ${String(x)}ms`);await new Promise(v=>{setTimeout(v,_)})}}surfaceInStudioLogs(t){Q(this.env,t,this.lunoraJurisdiction).catch(()=>{})}instanceId(){try{const{id:t}=this.ctx;return typeof t?.toString=="function"?t.toString():"unknown"}catch{return"unknown"}}}export{dt as ContainerProxy,it as LunoraContainer,lt as outboundParams};
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{a as A}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{isManagedImage as C,resolveContainerEnvVars as I,parseDurationSeconds as L}from"../packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-B9DkqZkf.mjs";import{D as H,C as O,p as M}from"../packem_shared/exec-BYLW70sR.mjs";import{a as j}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{RpcTarget as V}from"cloudflare:workers";import{Container as U}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";import{ContainerProxy as wt,outboundParams as ft}from"../packem_shared/ContainerProxy-CfXjS6ZI.mjs";const k="lunora",B=(s,t,e,n)=>({container:s,event:e,instance:t,level:e==="error"?"error":"info",message:n,source:k,ts:Date.now(),type:"container"}),p=(s,t,e,n)=>{const r=B(s,t,e,n),a=JSON.stringify(r);return e==="error"?console.error(a):console.log(a),r},F=async(s,t,e,n)=>{const r=t.maxOutputBytes??H,a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},stderr:"pipe",stdout:"pipe"}),o=new AbortController;let i=!1;const c={overflowed:!1,timedOut:!1},v=()=>c.timedOut,m=a.exitCode.finally(()=>{i=!0}),y=()=>{o.abort(),i||(i=!0,a.kill())},$=t.timeoutMs===void 0?void 0:setTimeout(()=>{c.timedOut=!0,y()},t.timeoutMs);let S=0;const R=async l=>{if(l===null)return"";const d=l.getReader(),w=new TextDecoder,h=()=>{d.cancel().catch(()=>{})};let E="";o.signal.addEventListener("abort",h);try{for(;!o.signal.aborted;){const{done:P,value:T}=await d.read();if(P)break;if(S+=T.byteLength,S>r){c.overflowed=!0,y();break}E+=w.decode(T,{stream:!0})}return E+w.decode()}finally{o.signal.removeEventListener("abort",h),d.cancel().catch(()=>{})}};try{const[l,d]=await Promise.allSettled([R(a.stdout),R(a.stderr)]);if(c.overflowed)return new Response(`container "${n}": exec output exceeded ${String(r)} bytes; the process was killed`,{status:413});if(c.timedOut)return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});if(l.status==="rejected")throw l.reason;if(d.status==="rejected")throw d.reason;const w=await m;if(v())return new Response(`container "${n}": exec timed out after ${String(t.timeoutMs)}ms`,{status:504});const h=JSON.stringify({code:w,stderr:d.value,stdout:l.value});return new TextEncoder().encode(h).byteLength>r?new Response(`container "${n}": exec output exceeded ${String(r)} bytes once encoded`,{status:413}):new Response(h,{headers:{"content-type":"application/json"}})}finally{clearTimeout($)}},J=async(s,t,e,n)=>{let r;try{r=await t.json()}catch{return new Response(`container "${n}": exec body must be JSON`,{status:400})}return typeof r.command!="string"||r.command.length===0?new Response(`container "${n}": exec requires a non-empty \`command\``,{status:400}):F(s,r,e,n)},z="__lunora_admin__:recordContainerEvent",N="__root__",G=s=>{if(s===null||typeof s!="object")return!1;const t=s;return typeof t.get=="function"&&typeof t.idFromName=="function"},Q=(s,t)=>{const e=j(s,t);return typeof e.getByName=="function"?e.getByName(N):e.get(e.idFromName(N))},W=async(s,t,e)=>{try{const n=s??{},r=n.SHARD;if(!G(r))return;const a=typeof n.LUNORA_ADMIN_TOKEN=="string"?n.LUNORA_ADMIN_TOKEN:void 0;if(!a||a.length===0)return;const o=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:z}),headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},method:"POST"});await Q(r,e).fetch(o)}catch{}},K=1048576,_=s=>{if(s==null)return null;const{readable:t,writable:e}=new TransformStream(void 0,new ByteLengthQueuingStrategy({highWaterMark:K}));return s.pipeTo(e).catch(()=>{}),t};class Y extends V{#t;#e;constructor(t,e){super(),this.#t=t,this.#e=e}async exitCode(){return this.#e}kill(t){this.#t.kill(t)}resize(t,e){if(this.#t.isPty!==!0||typeof this.#t.resize!="function")throw new TypeError("resize() needs a process spawned with `pty`");this.#t.resize(t,e)}}const X=async(s,t,e,n)=>{const r=t.pty===void 0?void 0:{cols:t.pty.cols??80,rows:t.pty.rows??24};let a;try{a=await s([t.command,...t.args??[]],{...t.cwd===void 0?{}:{cwd:t.cwd},env:{...e,...t.env},...r===void 0?{stderr:"pipe"}:{pty:r},...t.stdin===void 0||t.stdin===!1?{}:{stdin:t.stdin===!0?"pipe":t.stdin},stdout:"pipe"})}catch(c){throw n(),c}const o=A(void 0,t.timeoutMs,()=>new DOMException(`spawn timed out after ${String(t.timeoutMs)}ms`,"TimeoutError"));o.signal?.addEventListener("abort",()=>{a.kill()});const i=a.exitCode.finally(()=>{o.dispose(),n()});return i.catch(()=>{}),{control:new Y(a,i),isPty:a.isPty===!0,pid:a.pid??-1,stderr:_(a.stderr),stdin:t.stdin===!0?a.stdin??null:null,stdout:_(a.stdout)}},f="__lunoraStartOverride",Z=(s,t)=>{if(s===void 0||t===void 0)return s===t;const e=Object.keys(s);return e.length===Object.keys(t).length&&e.every(n=>Object.hasOwn(t,n)&&s[n]===t[n])},q=(s,t)=>Z(s.envVars,t.envVars)&&s.image===t.image&&JSON.stringify(s.instanceType)===JSON.stringify(t.instanceType),tt=(s,t,e)=>{if(C(t))return t;const n=s[t];if(n===void 0){const r=Object.keys(s);throw new u("BAD_REQUEST",`container "${e}": no image named "${t}" — ${r.length===0?"the container declares no images":`declared images: ${r.join(", ")}`}`)}return n},x=500,b=3e4,g="__lunoraHardTimeoutGeneration",D="__lunora",et=/[/\\]/u,nt=s=>{const t=s.indexOf(";");return t===-1?s:s.slice(0,t)},rt=s=>M(s,e=>e.split(et).some(n=>nt(n).toLowerCase()===D)),at=s=>{const t=s instanceof Request?s.url:String(s);return URL.parse(t,"https://container")?.pathname??t},st="cf-container-target-port";class pt extends U{lunoraName;lunoraJurisdiction;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraDeclaredEnv;lunoraOverride;lunoraOverrideLoaded=!1;lunoraSecretsStoreResolved;lunoraDurableObjectScheduled;lunoraDefaultSelection;lunoraStops=0;constructor(t,e,n,r,a){const o=I(n,e,r);super(t,e,{defaultPort:n.defaultPort,entrypoint:n.entrypoint?[...n.entrypoint]:void 0,envVars:o,sleepAfter:n.sleepAfter}),this.lunoraDeclaredEnv={...o},n.enableInternet!==void 0&&(this.enableInternet=n.enableInternet),n.requiredPorts!==void 0&&(this.requiredPorts=[...n.requiredPorts]),n.interceptHttps!==void 0&&(this.interceptHttps=n.interceptHttps),n.allowedHosts!==void 0&&(this.allowedHosts=[...n.allowedHosts]),n.deniedHosts!==void 0&&(this.deniedHosts=[...n.deniedHosts]),n.pingEndpoint!==void 0&&(this.pingEndpoint=n.pingEndpoint),n.labels!==void 0&&(this.labels={...n.labels}),this.lunoraName=r??"container",this.lunoraJurisdiction=a,this.lunoraDefaultPort=n.defaultPort,this.lunoraReadyOn=n.readyOn?[...n.readyOn]:[],this.lunoraHardTimeoutSeconds=n.hardTimeout===void 0?void 0:L(n.hardTimeout),this.lunoraSecretsStore=n.secretsStore,this.lunoraDurableObjectScheduled=n.schedulingPolicy==="durable_object",this.lunoraDefaultSelection=n.schedulingPolicy==="durable_object"?{image:n.image,instanceType:n.instanceType}:{}}async lunoraSnapshot(t={}){if(!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshots need schedulingPolicy "durable_object"`);const{container:e}=this.ctx;if(e?.running!==!0||typeof e.snapshotContainer!="function")throw new u("BAD_REQUEST",`container "${this.lunoraName}": snapshot() needs a running container — start it first`);const{id:n,name:r,size:a}=await e.snapshotContainer({...t.name===void 0?{}:{name:t.name}});return{id:n,size:a,...r===void 0?{}:{name:r}}}async fetch(t){return this.refuseReserved(new URL(t.url).pathname)??super.fetch(t)}async containerFetch(...t){const e=this.refuseReserved(at(t[0]));return e!==void 0?e:(await this.awaitReadinessGate(),super.containerFetch(...t))}async lunoraExec(t){const{pathname:e}=new URL(t.url);if(t.method!=="POST"||e!==O)return new Response(`container "${this.lunoraName}": lunoraExec only serves POST ${O}.`,{status:400});const n=t.headers.get(st),r=n===null?this.defaultPort:Number.parseInt(n,10);if(r===void 0||Number.isNaN(r))return new Response(`container "${this.lunoraName}": exec needs a port — set \`defaultPort\` or exec through \`.port(n)\`.`,{status:400});await this.awaitReadinessGate();const{container:a}=this.ctx;if(typeof a?.exec!="function")return super.containerFetch(t,r);await this.startAndWaitForPorts(r),this.inflightRequests+=1,this.renewActivityTimeout();try{return await J(a.exec.bind(a),t,this.envVars??{},this.lunoraName)}finally{this.decrementInflight()}}async lunoraSpawn(t){const{container:e,release:n}=await this.lunoraAcquire("spawn");return X(e.exec.bind(e),t,this.envVars??{},n)}async startAndWaitForPorts(...t){await this.applyStartEnv(),await this.applyStartSelection(),await this.beforeContainerStart();const e=this.lunoraStops,n=this.beginStart();await super.startAndWaitForPorts(...t),await this.afterContainerStart(n&&this.lunoraStops===e)}async start(t,e){const{image:n,instanceType:r,snapshot:a,...o}=t??{};await this.recordStartOverride({envVars:t?.envVars,image:n,instanceType:r},a),await this.applyStartEnv(),await this.applyStartSelection(a!==void 0),await this.beforeContainerStart();const i=this.lunoraStops,c=this.beginStart();await super.start({...o,...a===void 0?{}:{containerSnapshot:a}},e),await this.afterContainerStart(c&&this.lunoraStops===i)}async destroy(){await super.destroy(),await this.ctx.storage.delete(f),this.lunoraOverride=void 0}async onActivityExpired(){const t=p(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const e=p(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(e),super.onError(t)}async onStart(){const t=p(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart()}async onHardTimeoutExpired(t){const e=await this.ctx.storage.get(g);if(t?.generation!==void 0&&t.generation!==e||this.ctx.container?.running!==!0)return;const n=p(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(n),await this.stop()}async onStop(t){const e=p(this.lunoraName,this.instanceId(),"stop",`${t.reason} (exit ${String(t.exitCode)})`);this.surfaceInStudioLogs(e),this.lunoraReadiness=void 0,this.lunoraStops+=1,await super.onStop(t)}async lunoraAcquire(t){const{container:e}=this.ctx;if(typeof e?.exec!="function")throw new u("NOT_IMPLEMENTED",`container "${this.lunoraName}": ${t}() needs the runtime's native ctx.container.exec(), which this host does not provide`);await this.awaitReadinessGate(),e.running||await this.start(),this.inflightRequests+=1,this.renewActivityTimeout();let n=!1;return{container:e,release:()=>{n||(n=!0,this.decrementInflight())}}}async beforeContainerStart(){}refuseReserved(t){if(rt(t))return new Response(`container "${this.lunoraName}": /${D}/* is reserved for Lunora's own container routes; use exec.`,{status:403})}async afterContainerStart(t){const e=this.lunoraReadiness;if(e!==void 0){await e;return}const n=(async()=>{t||await this.armHardTimeout(),await this.awaitContainerReadiness()})();this.lunoraReadiness=n;try{await n}catch(r){throw this.lunoraReadiness===n&&(this.lunoraReadiness=void 0),r}}beginStart(){const t=this.ctx.container?.running===!0;return t||(this.lunoraReadiness=void 0),t}async awaitReadinessGate(){if(this.lunoraReadyOn.length===0)return;if(this.lunoraReadiness===void 0){const{status:e}=await this.getState();if(e!=="healthy")return;this.lunoraReadiness=this.awaitContainerReadiness()}const t=this.lunoraReadiness;try{await t}catch(e){throw this.lunoraReadiness===t&&(this.lunoraReadiness=void 0),e}}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(g)??0)+1;await this.ctx.storage.put(g,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;return t===void 0?{}:(this.lunoraSecretsStoreResolved??=(async()=>{const e=this.env,n={};for(const[r,a]of Object.entries(t)){const o=e[a];if(o===void 0||typeof o.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${r}" points at binding "${a}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${a}".`);const i=await o.get();if(typeof i!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${a}" (env "${r}") did not resolve to a string value.`);n[r]=i}return n})().catch(e=>{throw this.lunoraSecretsStoreResolved=void 0,e}),this.lunoraSecretsStoreResolved)}async readOverride(){return this.lunoraOverrideLoaded||(this.lunoraOverride=await this.ctx.storage.get(f),this.lunoraOverrideLoaded=!0),this.lunoraOverride??{}}async recordStartOverride(t,e){const n=t.image!==void 0||t.instanceType!==void 0;if((n||e!==void 0)&&!this.lunoraDurableObjectScheduled)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start({ image | instanceType | snapshot }) needs schedulingPolicy "durable_object" — a default-policy container's image and size are set in lunora/containers.ts`);if(t.image!==void 0&&e!==void 0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": start() takes an image or a snapshot, not both — the snapshot is the filesystem`);t.image!==void 0&&this.resolveImage(t.image);const r=await this.readOverride(),a={...r,...t.envVars===void 0?{}:{envVars:{...t.envVars}},...t.image===void 0?{}:{image:t.image},...t.instanceType===void 0?{}:{instanceType:t.instanceType}};if((this.ctx.container?.running===!0||this.startInFlight!==void 0)&&(e!==void 0||!q({...this.lunoraDefaultSelection,...r},{...this.lunoraDefaultSelection,...a})))throw new u("CONFLICT",`container "${this.lunoraName}": start() on an instance that is already running with a different env, image or size (or a snapshot to restore) — the running container keeps its own. stop() it first (or destroy() it to also drop the override), then start again.`);(n||t.envVars!==void 0)&&(await this.ctx.storage.put(f,a),this.lunoraOverride=a)}async applyStartEnv(){const{envVars:t}=await this.readOverride();this.envVars=t===void 0?{...this.lunoraDeclaredEnv,...await this.resolveSecretsStoreEnv()}:{...t}}async applyStartSelection(t=!1){if(!this.lunoraDurableObjectScheduled)return;const e={...this.lunoraDefaultSelection,...await this.readOverride()};if(e.image===void 0&&!t&&this.ctx.container?.running!==!0)throw new u("BAD_REQUEST",`container "${this.lunoraName}": no image to start — pass start({ image }) or start({ snapshot }), or set a default \`image\` in lunora/containers.ts`);this.instance=e.instanceType,this.image=e.image===void 0?void 0:this.resolveImage(e.image)}resolveImage(t){return tt(this.ctx.container?.images??{},t,this.lunoraName)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const e=Date.now()+b;await Promise.all(this.lunoraReadyOn.map(async n=>this.awaitReadinessCheck(t,n,e)))}async awaitReadinessCheck(t,e,n){const r=e.port??this.lunoraDefaultPort;if(r===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${e.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const a=e.status??200,o=e.path.startsWith("/")?e.path:`/${e.path}`,i=t.getTcpPort(r);for(;;){const c=Math.max(x,n-Date.now()),v=A(void 0,c,()=>new DOMException(`readiness probe timed out after ${String(c)}ms`,"TimeoutError"));try{if((await i.fetch(`http://container${o}`,{signal:v.signal})).status===a)return}catch{}finally{v.dispose()}if(Date.now()>=n)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${e.path}" (port ${String(r)}) did not return ${String(a)} within ${String(b)}ms`);await new Promise(m=>{setTimeout(m,x)})}}surfaceInStudioLogs(t){W(this.env,t,this.lunoraJurisdiction).catch(()=>{})}instanceId(){try{const{id:t}=this.ctx;return typeof t?.toString=="function"?t.toString():"unknown"}catch{return"unknown"}}}export{wt as ContainerProxy,pt as LunoraContainer,ft as outboundParams};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { B as BuildImageSource,
|
|
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.mjs";
|
|
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.mjs";
|
|
3
|
+
import { b as ContainerBackupOptions, C as ContainerFileOptions, c as ContainerMountRequest, a as ContainerFileContent, d as ContainerSandboxControls } from "./packem_shared/sandbox-types.d-DXlHnsEg.mjs";
|
|
4
|
+
export type { e as ContainerFiles, f as ContainerMountCredentials } from "./packem_shared/sandbox-types.d-DXlHnsEg.mjs";
|
|
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,
|
|
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 };
|