@lunora/container 1.0.0-alpha.63 → 1.0.0-alpha.65
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/README.md +17 -0
- package/dist/do/index.d.mts +140 -18
- package/dist/do/index.d.ts +140 -18
- package/dist/do/index.mjs +1 -1
- package/dist/index.d.mts +160 -12
- package/dist/index.d.ts +160 -12
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/CONTAINER_EXEC_PATH-7Hcd6-zs.mjs +1 -0
- package/dist/packem_shared/{ContainerProxy-fTW_pXWY.mjs → ContainerProxy-CfXjS6ZI.mjs} +7 -7
- 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/exec-BYLW70sR.mjs +1 -0
- package/dist/packem_shared/{jurisdiction.d-DPvNG2h5.d.mts → jurisdiction.d-t4mtuqYW.d.mts} +228 -32
- package/dist/packem_shared/{jurisdiction.d-DPvNG2h5.d.ts → jurisdiction.d-t4mtuqYW.d.ts} +228 -32
- 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/CONTAINER_EXEC_PATH-ZfG5wgai.mjs +0 -1
- package/dist/packem_shared/containerBindingName-C7SonAIK.mjs +0 -1
- package/dist/packem_shared/createContainerContext-CLvjjJLs.mjs +0 -1
- package/dist/packem_shared/exec-WbCOLswq.mjs +0 -1
package/README.md
CHANGED
|
@@ -181,6 +181,23 @@ export const job = defineContainer({
|
|
|
181
181
|
});
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Per-instance images and snapshots
|
|
185
|
+
|
|
186
|
+
`schedulingPolicy: "durable_object"` (Cloudflare public beta) lets each instance pick its image and size at start, and save and restore its filesystem:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
export const agentComputer = defineContainer({
|
|
190
|
+
schedulingPolicy: "durable_object",
|
|
191
|
+
images: { base: "./containers/base" },
|
|
192
|
+
image: "base",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const computer = ctx.containers.agentComputer.get(taskId);
|
|
196
|
+
await computer.start({ image: "base", instanceType: "standard-2" });
|
|
197
|
+
const saved = await computer.snapshot();
|
|
198
|
+
await ctx.containers.agentComputer.get(otherId).start({ snapshot: saved });
|
|
199
|
+
```
|
|
200
|
+
|
|
184
201
|
### Calling Lunora from inside a container
|
|
185
202
|
|
|
186
203
|
Container code calls back into your app's functions with the bridge client (any JS runtime), over the Worker's HTTP RPC endpoint:
|
package/dist/do/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-
|
|
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
|
*/
|
|
@@ -345,7 +345,7 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
345
345
|
* When the counter transitions to 0, renew the activity timeout so the
|
|
346
346
|
* inactivity window starts fresh from the moment the last request completes.
|
|
347
347
|
*/
|
|
348
|
-
|
|
348
|
+
protected decrementInflight(): void;
|
|
349
349
|
/**
|
|
350
350
|
* Schedule a task to be executed in the future.
|
|
351
351
|
*
|
|
@@ -387,10 +387,17 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
387
387
|
private onStopCalled;
|
|
388
388
|
private state;
|
|
389
389
|
private monitor;
|
|
390
|
-
|
|
390
|
+
protected startInFlight?: Promise<unknown>;
|
|
391
391
|
private monitoredPromise;
|
|
392
392
|
private sleepAfterMs;
|
|
393
|
-
|
|
393
|
+
protected inflightRequests: number;
|
|
394
|
+
/** Patched (Lunora): the `durable_object`-policy image reference and size `start()` forwards. */
|
|
395
|
+
protected image?: string;
|
|
396
|
+
protected instance?: string | {
|
|
397
|
+
vcpu: number;
|
|
398
|
+
memoryMib: number;
|
|
399
|
+
diskMb: number;
|
|
400
|
+
};
|
|
394
401
|
private outboundByHostOverrides;
|
|
395
402
|
private outboundHandlerOverride?;
|
|
396
403
|
private allowedHostsOverride?;
|
|
@@ -483,7 +490,68 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
483
490
|
getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
|
|
484
491
|
private isActivityExpired;
|
|
485
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
|
+
}
|
|
531
|
+
/** What an explicit `start()` chose that later restarts must reuse. */
|
|
532
|
+
interface StartOverride {
|
|
533
|
+
/** Replaces the declared env, `secrets` and `secretsStore` wholesale. */
|
|
534
|
+
envVars?: Record<string, string>;
|
|
535
|
+
/** A key of the definition's `images`, or a managed `cloudflare/…` image. */
|
|
536
|
+
image?: string;
|
|
537
|
+
instanceType?: ContainerRuntimeInstanceType;
|
|
538
|
+
}
|
|
486
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
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The `start()` options `LunoraContainer` accepts on top of the base's: the
|
|
550
|
+
* `durable_object`-policy image, size and snapshot to restore.
|
|
551
|
+
*/
|
|
552
|
+
type LunoraStartOptions = NonNullable<Parameters<Container["start"]>[0]> & Pick<StartOverride, "image" | "instanceType"> & {
|
|
553
|
+
snapshot?: ContainerSnapshot;
|
|
554
|
+
};
|
|
487
555
|
/**
|
|
488
556
|
* Base class for the generated Container DO classes. Applies a
|
|
489
557
|
* `defineContainer` definition onto `@cloudflare/containers`' `Container`:
|
|
@@ -502,14 +570,14 @@ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
|
|
|
502
570
|
* ```
|
|
503
571
|
*/
|
|
504
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;
|
|
505
575
|
/**
|
|
506
576
|
* Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
|
|
507
577
|
* schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
|
|
508
578
|
* to the same region as the root shard. `undefined` ⇒ un-pinned.
|
|
509
579
|
*/
|
|
510
580
|
private readonly lunoraJurisdiction?;
|
|
511
|
-
/** The `lunora/containers.ts` export name, for lifecycle log correlation. */
|
|
512
|
-
private readonly lunoraName;
|
|
513
581
|
/** Default port the readiness probes target when a check omits its own `port`. */
|
|
514
582
|
private readonly lunoraDefaultPort?;
|
|
515
583
|
/** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
|
|
@@ -522,12 +590,16 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
522
590
|
private readonly lunoraSecretsStore?;
|
|
523
591
|
/** The definition's `env` + `secrets`, as resolved at construction — the env a start uses absent an override. */
|
|
524
592
|
private readonly lunoraDeclaredEnv;
|
|
525
|
-
/** The persisted
|
|
526
|
-
private
|
|
593
|
+
/** The persisted start override, once loaded. See {@link START_OVERRIDE_KEY}. */
|
|
594
|
+
private lunoraOverride?;
|
|
527
595
|
/** Whether the override field reflects storage (read once per DO instance, then kept in step). */
|
|
528
|
-
private
|
|
596
|
+
private lunoraOverrideLoaded;
|
|
529
597
|
/** Memoised Secrets Store resolution: the resolved `name → value` map, fetched once. */
|
|
530
598
|
private lunoraSecretsStoreResolved?;
|
|
599
|
+
/** Whether the definition uses the `durable_object` scheduling policy. */
|
|
600
|
+
private readonly lunoraDurableObjectScheduled;
|
|
601
|
+
/** The definition's default image name and instance size (`durable_object` policy only). */
|
|
602
|
+
private readonly lunoraDefaultSelection;
|
|
531
603
|
/**
|
|
532
604
|
* Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
|
|
533
605
|
* across a start: a bump means the run that start snapshotted is over,
|
|
@@ -535,6 +607,15 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
535
607
|
*/
|
|
536
608
|
private lunoraStops;
|
|
537
609
|
constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
|
|
610
|
+
/**
|
|
611
|
+
* Save the running container's filesystem (`durable_object` policy only).
|
|
612
|
+
* The handle is plain data: store it and pass it to `start({ snapshot })` to
|
|
613
|
+
* restore it here or in another instance. Memory and processes are not
|
|
614
|
+
* captured — a restored container runs its entrypoint again.
|
|
615
|
+
*/
|
|
616
|
+
lunoraSnapshot(options?: {
|
|
617
|
+
name?: string;
|
|
618
|
+
}): Promise<ContainerSnapshot>;
|
|
538
619
|
/**
|
|
539
620
|
* HTTP entry for every request to this Durable Object. Refuses the reserved
|
|
540
621
|
* `/__lunora/*` namespace unconditionally: `exec` reaches it only through
|
|
@@ -562,6 +643,14 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
562
643
|
* handle's target-port header the way the base `fetch` does.
|
|
563
644
|
*/
|
|
564
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>;
|
|
565
654
|
/**
|
|
566
655
|
* The start path `containerFetch` takes (and the one an app can call itself).
|
|
567
656
|
* Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
|
|
@@ -589,10 +678,10 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
589
678
|
* return early on a live container, reporting a start whose env never took
|
|
590
679
|
* effect. `stop()` it first, or `destroy()` it to drop the override.
|
|
591
680
|
*/
|
|
592
|
-
override start(
|
|
681
|
+
override start(options?: LunoraStartOptions, waitOptions?: Parameters<Container<Env>["start"]>[1]): Promise<void>;
|
|
593
682
|
/**
|
|
594
|
-
* Stop the container and forget this instance's
|
|
595
|
-
*
|
|
683
|
+
* Stop the container and forget this instance's start override, so the next
|
|
684
|
+
* start uses the declared env, image and size again.
|
|
596
685
|
*/
|
|
597
686
|
override destroy(): Promise<void>;
|
|
598
687
|
override onActivityExpired(): Promise<void>;
|
|
@@ -617,6 +706,24 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
617
706
|
generation?: number;
|
|
618
707
|
}): Promise<void>;
|
|
619
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>;
|
|
620
727
|
/** A 403 for a path under the reserved namespace, else `undefined`. */
|
|
621
728
|
private refuseReserved;
|
|
622
729
|
/**
|
|
@@ -711,19 +818,34 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
711
818
|
* `secretsStore`.
|
|
712
819
|
*/
|
|
713
820
|
private resolveSecretsStoreEnv;
|
|
714
|
-
/** The persisted
|
|
715
|
-
private
|
|
821
|
+
/** The persisted start override, read from storage once per instance lifetime. */
|
|
822
|
+
private readOverride;
|
|
716
823
|
/**
|
|
717
|
-
*
|
|
718
|
-
*
|
|
824
|
+
* Validate an explicit start's choices and persist them — merged over the
|
|
825
|
+
* stored override, so `start({ instanceType })` alone keeps a chosen image.
|
|
826
|
+
* Everything is checked before the one write, so a rejected start stores
|
|
827
|
+
* nothing. A start whose choices differ from what a running (or starting)
|
|
828
|
+
* container has is REJECTED rather than silently joined: the base would
|
|
829
|
+
* return early on a live container, reporting a start that never took
|
|
830
|
+
* effect. A snapshot restore only applies to a fresh start, so it is refused
|
|
831
|
+
* on a busy one.
|
|
719
832
|
*/
|
|
720
|
-
private
|
|
833
|
+
private recordStartOverride;
|
|
721
834
|
/**
|
|
722
835
|
* Set `envVars` for the start about to happen: the persisted override when
|
|
723
836
|
* there is one (the Secrets Store is not read), else the declared env plus
|
|
724
837
|
* the resolved Secrets Store values.
|
|
725
838
|
*/
|
|
726
839
|
private applyStartEnv;
|
|
840
|
+
/**
|
|
841
|
+
* Resolve the image reference and instance size for the start about to
|
|
842
|
+
* happen (`durable_object` policy only): the persisted explicit choice, else
|
|
843
|
+
* the definition default. A fresh boot with neither an image nor a snapshot
|
|
844
|
+
* would be rejected by the runtime, so it fails here with the fix instead.
|
|
845
|
+
*/
|
|
846
|
+
private applyStartSelection;
|
|
847
|
+
/** Map an image name to the reference `ctx.container.start()` takes, via `ctx.container.images`. */
|
|
848
|
+
private resolveImage;
|
|
727
849
|
/**
|
|
728
850
|
* Block until every `readyOn` probe responds with its expected status, or
|
|
729
851
|
* throw once the readiness budget is spent. Probes run in parallel and hit
|
package/dist/do/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-
|
|
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
|
*/
|
|
@@ -345,7 +345,7 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
345
345
|
* When the counter transitions to 0, renew the activity timeout so the
|
|
346
346
|
* inactivity window starts fresh from the moment the last request completes.
|
|
347
347
|
*/
|
|
348
|
-
|
|
348
|
+
protected decrementInflight(): void;
|
|
349
349
|
/**
|
|
350
350
|
* Schedule a task to be executed in the future.
|
|
351
351
|
*
|
|
@@ -387,10 +387,17 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
387
387
|
private onStopCalled;
|
|
388
388
|
private state;
|
|
389
389
|
private monitor;
|
|
390
|
-
|
|
390
|
+
protected startInFlight?: Promise<unknown>;
|
|
391
391
|
private monitoredPromise;
|
|
392
392
|
private sleepAfterMs;
|
|
393
|
-
|
|
393
|
+
protected inflightRequests: number;
|
|
394
|
+
/** Patched (Lunora): the `durable_object`-policy image reference and size `start()` forwards. */
|
|
395
|
+
protected image?: string;
|
|
396
|
+
protected instance?: string | {
|
|
397
|
+
vcpu: number;
|
|
398
|
+
memoryMib: number;
|
|
399
|
+
diskMb: number;
|
|
400
|
+
};
|
|
394
401
|
private outboundByHostOverrides;
|
|
395
402
|
private outboundHandlerOverride?;
|
|
396
403
|
private allowedHostsOverride?;
|
|
@@ -483,7 +490,68 @@ declare class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
|
|
|
483
490
|
getSchedule<T = string>(id: string): Promise<Schedule<T> | undefined>;
|
|
484
491
|
private isActivityExpired;
|
|
485
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
|
+
}
|
|
531
|
+
/** What an explicit `start()` chose that later restarts must reuse. */
|
|
532
|
+
interface StartOverride {
|
|
533
|
+
/** Replaces the declared env, `secrets` and `secretsStore` wholesale. */
|
|
534
|
+
envVars?: Record<string, string>;
|
|
535
|
+
/** A key of the definition's `images`, or a managed `cloudflare/…` image. */
|
|
536
|
+
image?: string;
|
|
537
|
+
instanceType?: ContainerRuntimeInstanceType;
|
|
538
|
+
}
|
|
486
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
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The `start()` options `LunoraContainer` accepts on top of the base's: the
|
|
550
|
+
* `durable_object`-policy image, size and snapshot to restore.
|
|
551
|
+
*/
|
|
552
|
+
type LunoraStartOptions = NonNullable<Parameters<Container["start"]>[0]> & Pick<StartOverride, "image" | "instanceType"> & {
|
|
553
|
+
snapshot?: ContainerSnapshot;
|
|
554
|
+
};
|
|
487
555
|
/**
|
|
488
556
|
* Base class for the generated Container DO classes. Applies a
|
|
489
557
|
* `defineContainer` definition onto `@cloudflare/containers`' `Container`:
|
|
@@ -502,14 +570,14 @@ type DurableObjectContext = ConstructorParameters<typeof Container>[0];
|
|
|
502
570
|
* ```
|
|
503
571
|
*/
|
|
504
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;
|
|
505
575
|
/**
|
|
506
576
|
* Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
|
|
507
577
|
* schema's `.jurisdiction("…")`). Used to pin the best-effort lifecycle report
|
|
508
578
|
* to the same region as the root shard. `undefined` ⇒ un-pinned.
|
|
509
579
|
*/
|
|
510
580
|
private readonly lunoraJurisdiction?;
|
|
511
|
-
/** The `lunora/containers.ts` export name, for lifecycle log correlation. */
|
|
512
|
-
private readonly lunoraName;
|
|
513
581
|
/** Default port the readiness probes target when a check omits its own `port`. */
|
|
514
582
|
private readonly lunoraDefaultPort?;
|
|
515
583
|
/** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
|
|
@@ -522,12 +590,16 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
522
590
|
private readonly lunoraSecretsStore?;
|
|
523
591
|
/** The definition's `env` + `secrets`, as resolved at construction — the env a start uses absent an override. */
|
|
524
592
|
private readonly lunoraDeclaredEnv;
|
|
525
|
-
/** The persisted
|
|
526
|
-
private
|
|
593
|
+
/** The persisted start override, once loaded. See {@link START_OVERRIDE_KEY}. */
|
|
594
|
+
private lunoraOverride?;
|
|
527
595
|
/** Whether the override field reflects storage (read once per DO instance, then kept in step). */
|
|
528
|
-
private
|
|
596
|
+
private lunoraOverrideLoaded;
|
|
529
597
|
/** Memoised Secrets Store resolution: the resolved `name → value` map, fetched once. */
|
|
530
598
|
private lunoraSecretsStoreResolved?;
|
|
599
|
+
/** Whether the definition uses the `durable_object` scheduling policy. */
|
|
600
|
+
private readonly lunoraDurableObjectScheduled;
|
|
601
|
+
/** The definition's default image name and instance size (`durable_object` policy only). */
|
|
602
|
+
private readonly lunoraDefaultSelection;
|
|
531
603
|
/**
|
|
532
604
|
* Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
|
|
533
605
|
* across a start: a bump means the run that start snapshotted is over,
|
|
@@ -535,6 +607,15 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
535
607
|
*/
|
|
536
608
|
private lunoraStops;
|
|
537
609
|
constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
|
|
610
|
+
/**
|
|
611
|
+
* Save the running container's filesystem (`durable_object` policy only).
|
|
612
|
+
* The handle is plain data: store it and pass it to `start({ snapshot })` to
|
|
613
|
+
* restore it here or in another instance. Memory and processes are not
|
|
614
|
+
* captured — a restored container runs its entrypoint again.
|
|
615
|
+
*/
|
|
616
|
+
lunoraSnapshot(options?: {
|
|
617
|
+
name?: string;
|
|
618
|
+
}): Promise<ContainerSnapshot>;
|
|
538
619
|
/**
|
|
539
620
|
* HTTP entry for every request to this Durable Object. Refuses the reserved
|
|
540
621
|
* `/__lunora/*` namespace unconditionally: `exec` reaches it only through
|
|
@@ -562,6 +643,14 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
562
643
|
* handle's target-port header the way the base `fetch` does.
|
|
563
644
|
*/
|
|
564
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>;
|
|
565
654
|
/**
|
|
566
655
|
* The start path `containerFetch` takes (and the one an app can call itself).
|
|
567
656
|
* Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
|
|
@@ -589,10 +678,10 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
589
678
|
* return early on a live container, reporting a start whose env never took
|
|
590
679
|
* effect. `stop()` it first, or `destroy()` it to drop the override.
|
|
591
680
|
*/
|
|
592
|
-
override start(
|
|
681
|
+
override start(options?: LunoraStartOptions, waitOptions?: Parameters<Container<Env>["start"]>[1]): Promise<void>;
|
|
593
682
|
/**
|
|
594
|
-
* Stop the container and forget this instance's
|
|
595
|
-
*
|
|
683
|
+
* Stop the container and forget this instance's start override, so the next
|
|
684
|
+
* start uses the declared env, image and size again.
|
|
596
685
|
*/
|
|
597
686
|
override destroy(): Promise<void>;
|
|
598
687
|
override onActivityExpired(): Promise<void>;
|
|
@@ -617,6 +706,24 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
617
706
|
generation?: number;
|
|
618
707
|
}): Promise<void>;
|
|
619
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>;
|
|
620
727
|
/** A 403 for a path under the reserved namespace, else `undefined`. */
|
|
621
728
|
private refuseReserved;
|
|
622
729
|
/**
|
|
@@ -711,19 +818,34 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
|
|
|
711
818
|
* `secretsStore`.
|
|
712
819
|
*/
|
|
713
820
|
private resolveSecretsStoreEnv;
|
|
714
|
-
/** The persisted
|
|
715
|
-
private
|
|
821
|
+
/** The persisted start override, read from storage once per instance lifetime. */
|
|
822
|
+
private readOverride;
|
|
716
823
|
/**
|
|
717
|
-
*
|
|
718
|
-
*
|
|
824
|
+
* Validate an explicit start's choices and persist them — merged over the
|
|
825
|
+
* stored override, so `start({ instanceType })` alone keeps a chosen image.
|
|
826
|
+
* Everything is checked before the one write, so a rejected start stores
|
|
827
|
+
* nothing. A start whose choices differ from what a running (or starting)
|
|
828
|
+
* container has is REJECTED rather than silently joined: the base would
|
|
829
|
+
* return early on a live container, reporting a start that never took
|
|
830
|
+
* effect. A snapshot restore only applies to a fresh start, so it is refused
|
|
831
|
+
* on a busy one.
|
|
719
832
|
*/
|
|
720
|
-
private
|
|
833
|
+
private recordStartOverride;
|
|
721
834
|
/**
|
|
722
835
|
* Set `envVars` for the start about to happen: the persisted override when
|
|
723
836
|
* there is one (the Secrets Store is not read), else the declared env plus
|
|
724
837
|
* the resolved Secrets Store values.
|
|
725
838
|
*/
|
|
726
839
|
private applyStartEnv;
|
|
840
|
+
/**
|
|
841
|
+
* Resolve the image reference and instance size for the start about to
|
|
842
|
+
* happen (`durable_object` policy only): the persisted explicit choice, else
|
|
843
|
+
* the definition default. A fresh boot with neither an image nor a snapshot
|
|
844
|
+
* would be rejected by the runtime, so it fails here with the fix instead.
|
|
845
|
+
*/
|
|
846
|
+
private applyStartSelection;
|
|
847
|
+
/** Map an image name to the reference `ctx.container.start()` takes, via `ctx.container.images`. */
|
|
848
|
+
private resolveImage;
|
|
727
849
|
/**
|
|
728
850
|
* Block until every `readyOn` probe responds with its expected status, or
|
|
729
851
|
* throw once the readiness budget is spent. Probes run in parallel and hit
|
package/dist/do/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{LunoraError as u}from"@lunora/errors";import{a as R}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{resolveContainerEnvVars as g,parseDurationSeconds as y}from"../packem_shared/containerBindingName-C7SonAIK.mjs";import{C as S,p as N}from"../packem_shared/exec-WbCOLswq.mjs";import{a as O}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{Container as _}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";import{ContainerProxy as W,outboundParams as Y}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";const T="lunora",A=(r,t,n,e)=>({container:r,event:n,instance:t,level:n==="error"?"error":"info",message:e,source:T,ts:Date.now(),type:"container"}),i=(r,t,n,e)=>{const s=A(r,t,n,e),a=JSON.stringify(s);return n==="error"?console.error(a):console.log(a),s},P="__lunora_admin__:recordContainerEvent",w="__root__",I=r=>{if(r===null||typeof r!="object")return!1;const t=r;return typeof t.get=="function"&&typeof t.idFromName=="function"},x=(r,t)=>{const n=O(r,t);return typeof n.getByName=="function"?n.getByName(w):n.get(n.idFromName(w))},L=async(r,t,n)=>{try{const e=r??{},s=e.SHARD;if(!I(s))return;const a=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.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:P}),headers:{authorization:`Bearer ${a}`,"content-type":"application/json"},method:"POST"});await x(s,n).fetch(o)}catch{}},f=500,E=3e4,d="__lunoraHardTimeoutGeneration",h="__lunoraEnvOverride",C=(r,t)=>{if(r===void 0||t===void 0)return r===t;const n=Object.keys(r);return n.length===Object.keys(t).length&&n.every(e=>Object.hasOwn(t,e)&&r[e]===t[e])},m="__lunora",D=/[/\\]/u,H=r=>{const t=r.indexOf(";");return t===-1?r:r.slice(0,t)},$=r=>N(r,n=>n.split(D).some(e=>H(e).toLowerCase()===m)),b=r=>{const t=r instanceof Request?r.url:String(r);return URL.parse(t,"https://container")?.pathname??t},V="cf-container-target-port";class k extends _{lunoraJurisdiction;lunoraName;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraDeclaredEnv;lunoraEnvOverride;lunoraEnvOverrideLoaded=!1;lunoraSecretsStoreResolved;lunoraStops=0;constructor(t,n,e,s,a){const o=g(e,n,s);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=s??"container",this.lunoraJurisdiction=a,this.lunoraDefaultPort=e.defaultPort,this.lunoraReadyOn=e.readyOn?[...e.readyOn]:[],this.lunoraHardTimeoutSeconds=e.hardTimeout===void 0?void 0:y(e.hardTimeout),this.lunoraSecretsStore=e.secretsStore}async fetch(t){return this.refuseReserved(new URL(t.url).pathname)??super.fetch(t)}async containerFetch(...t){const n=this.refuseReserved(b(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!==S)return new Response(`container "${this.lunoraName}": lunoraExec only serves POST ${S}.`,{status:400});const e=t.headers.get(V),s=e===null?this.defaultPort:Number.parseInt(e,10);return s===void 0||Number.isNaN(s)?new Response(`container "${this.lunoraName}": exec needs a port — set \`defaultPort\` or exec through \`.port(n)\`.`,{status:400}):(await this.awaitReadinessGate(),super.containerFetch(t,s))}async startAndWaitForPorts(...t){await this.applyStartEnv();const n=this.lunoraStops,e=this.beginStart();await super.startAndWaitForPorts(...t),await this.afterContainerStart(e&&this.lunoraStops===n)}async start(...t){const[n]=t;n?.envVars!==void 0&&await this.persistEnvOverride({...n.envVars}),await this.applyStartEnv();const e=this.lunoraStops,s=this.beginStart();await super.start(...t),await this.afterContainerStart(s&&this.lunoraStops===e)}async destroy(){await super.destroy(),await this.ctx.storage.delete(h),this.lunoraEnvOverride=void 0}async onActivityExpired(){const t=i(this.lunoraName,this.instanceId(),"sleep");this.surfaceInStudioLogs(t),await super.onActivityExpired()}onError(t){const n=i(this.lunoraName,this.instanceId(),"error",t instanceof Error?t.message:String(t));return this.surfaceInStudioLogs(n),super.onError(t)}async onStart(){const t=i(this.lunoraName,this.instanceId(),"start");this.surfaceInStudioLogs(t),await super.onStart()}async onHardTimeoutExpired(t){const n=await this.ctx.storage.get(d);if(t?.generation!==void 0&&t.generation!==n||this.ctx.container?.running!==!0)return;const e=i(this.lunoraName,this.instanceId(),"stop","hard timeout reached");this.surfaceInStudioLogs(e),await this.stop()}async onStop(t){const n=i(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($(t))return new Response(`container "${this.lunoraName}": /${m}/* 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(s){throw this.lunoraReadiness===e&&(this.lunoraReadiness=void 0),s}}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(d)??0)+1;await this.ctx.storage.put(d,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[s,a]of Object.entries(t)){const o=n[a];if(o===void 0||typeof o.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${s}" points at binding "${a}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${a}".`);const c=await o.get();if(typeof c!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${a}" (env "${s}") did not resolve to a string value.`);e[s]=c}return e})().catch(n=>{throw this.lunoraSecretsStoreResolved=void 0,n}),this.lunoraSecretsStoreResolved)}async readEnvOverride(){return this.lunoraEnvOverrideLoaded||(this.lunoraEnvOverride=await this.ctx.storage.get(h),this.lunoraEnvOverrideLoaded=!0),this.lunoraEnvOverride}async persistEnvOverride(t){const n=await this.readEnvOverride();if((this.ctx.container?.running===!0||this.startInFlight!==void 0)&&!C(n,t))throw new u("CONFLICT",`container "${this.lunoraName}": start({ envVars }) on an instance that is already running with a different env — the running container keeps its own. stop() it first (or destroy() it to also drop the override), then start again.`);await this.ctx.storage.put(h,t),this.lunoraEnvOverride=t}async applyStartEnv(){const t=await this.readEnvOverride();this.envVars=t===void 0?{...this.lunoraDeclaredEnv,...await this.resolveSecretsStoreEnv()}:{...t}}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const n=Date.now()+E;await Promise.all(this.lunoraReadyOn.map(async e=>this.awaitReadinessCheck(t,e,n)))}async awaitReadinessCheck(t,n,e){const s=n.port??this.lunoraDefaultPort;if(s===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 a=n.status??200,o=n.path.startsWith("/")?n.path:`/${n.path}`,c=t.getTcpPort(s);for(;;){const l=Math.max(f,e-Date.now()),p=R(void 0,l,()=>new DOMException(`readiness probe timed out after ${String(l)}ms`,"TimeoutError"));try{if((await c.fetch(`http://container${o}`,{signal:p.signal})).status===a)return}catch{}finally{p.dispose()}if(Date.now()>=e)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${n.path}" (port ${String(s)}) did not return ${String(a)} within ${String(E)}ms`);await new Promise(v=>{setTimeout(v,f)})}}surfaceInStudioLogs(t){L(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{W as ContainerProxy,k as LunoraContainer,Y 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};
|