@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 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:
@@ -1,5 +1,5 @@
1
- import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DPvNG2h5.mjs";
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
- private decrementInflight;
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
- private startInFlight;
390
+ protected startInFlight?: Promise<unknown>;
391
391
  private monitoredPromise;
392
392
  private sleepAfterMs;
393
- private inflightRequests;
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 `start({ envVars })` override, once loaded. See {@link ENV_OVERRIDE_KEY}. */
526
- private lunoraEnvOverride?;
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 lunoraEnvOverrideLoaded;
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(...args: Parameters<Container<Env>["start"]>): Promise<void>;
681
+ override start(options?: LunoraStartOptions, waitOptions?: Parameters<Container<Env>["start"]>[1]): Promise<void>;
593
682
  /**
594
- * Stop the container and forget this instance's `start({ envVars })`
595
- * override, so the next start uses the declared env again.
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 `start({ envVars })` override, read from storage once per instance lifetime. */
715
- private readEnvOverride;
821
+ /** The persisted start override, read from storage once per instance lifetime. */
822
+ private readOverride;
716
823
  /**
717
- * Record a `start({ envVars })` override, refusing one that differs from
718
- * the env a running (or starting) container already has — see {@link start}.
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 persistEnvOverride;
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
@@ -1,5 +1,5 @@
1
- import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DPvNG2h5.js";
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
- private decrementInflight;
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
- private startInFlight;
390
+ protected startInFlight?: Promise<unknown>;
391
391
  private monitoredPromise;
392
392
  private sleepAfterMs;
393
- private inflightRequests;
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 `start({ envVars })` override, once loaded. See {@link ENV_OVERRIDE_KEY}. */
526
- private lunoraEnvOverride?;
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 lunoraEnvOverrideLoaded;
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(...args: Parameters<Container<Env>["start"]>): Promise<void>;
681
+ override start(options?: LunoraStartOptions, waitOptions?: Parameters<Container<Env>["start"]>[1]): Promise<void>;
593
682
  /**
594
- * Stop the container and forget this instance's `start({ envVars })`
595
- * override, so the next start uses the declared env again.
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 `start({ envVars })` override, read from storage once per instance lifetime. */
715
- private readEnvOverride;
821
+ /** The persisted start override, read from storage once per instance lifetime. */
822
+ private readOverride;
716
823
  /**
717
- * Record a `start({ envVars })` override, refusing one that differs from
718
- * the env a running (or starting) container already has — see {@link start}.
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 persistEnvOverride;
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};