@lunora/container 1.0.0-alpha.55 → 1.0.0-alpha.57

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
@@ -127,7 +127,7 @@ export const worker = defineContainer({
127
127
 
128
128
  ### Secrets and Secrets Store
129
129
 
130
- `secrets` forwards plain Worker secrets into the container env; `secretsStore` maps a _container env-var name → Cloudflare [Secrets Store](https://developers.cloudflare.com/secrets-store/) binding name_ and resolves each with its async `.get()` at first start (memoised). A collision with `env`/`secrets` is rejected at authoring time; a missing binding fails the start — the same fail-closed stance as `secrets`. Like `env`/`secrets`, these injected values only apply to implicit starts or a bare `start()`; a per-instance `start({ envVars })` replaces the env set wholesale (and skips Secrets Store resolution entirely).
130
+ `secrets` forwards plain Worker secrets into the container env; `secretsStore` maps a _container env-var name → Cloudflare [Secrets Store](https://developers.cloudflare.com/secrets-store/) binding name_ and resolves each with its async `.get()` at first start (memoised). A collision with `env`/`secrets` is rejected at authoring time; a missing binding fails the start — the same fail-closed stance as `secrets`. A per-instance `start({ envVars })` replaces the env set wholesale (and skips Secrets Store resolution entirely), and is persisted for that instance: later starts, including implicit restarts, reuse it until `destroy()`. The override cannot change while the container is running or still starting: a differing `start({ envVars })` is rejected with `CONFLICT` in either case, so `stop()` it (and let any start in flight finish) first.
131
131
 
132
132
  ```ts
133
133
  export const worker = defineContainer({
@@ -1,4 +1,4 @@
1
- import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-rC2ejtvT.mjs";
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DPvNG2h5.mjs";
2
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
@@ -520,7 +520,13 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
520
520
  private readonly lunoraReadyOn;
521
521
  /** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
522
522
  private readonly lunoraSecretsStore?;
523
- /** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
523
+ /** The definition's `env` + `secrets`, as resolved at construction — the env a start uses absent an override. */
524
+ private readonly lunoraDeclaredEnv;
525
+ /** The persisted `start({ envVars })` override, once loaded. See {@link ENV_OVERRIDE_KEY}. */
526
+ private lunoraEnvOverride?;
527
+ /** Whether the override field reflects storage (read once per DO instance, then kept in step). */
528
+ private lunoraEnvOverrideLoaded;
529
+ /** Memoised Secrets Store resolution: the resolved `name → value` map, fetched once. */
524
530
  private lunoraSecretsStoreResolved?;
525
531
  /**
526
532
  * Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
@@ -530,12 +536,32 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
530
536
  private lunoraStops;
531
537
  constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
532
538
  /**
533
- * Proxy entry for every `ctx.containers.<name>` fetch. The base starts the
534
- * container for this request through {@link startAndWaitForPorts}, which is
535
- * where the `secretsStore` resolution lives — a request that finds the
536
- * container already healthy needs no resolution at all.
539
+ * HTTP entry for every request to this Durable Object. Refuses the reserved
540
+ * `/__lunora/*` namespace unconditionally: `exec` reaches it only through
541
+ * the {@link lunoraExec} RPC, so no header, path spelling, or inbound request
542
+ * an app forwards as-is (`env.CONTAINER_X.get(id).fetch(request)`) opens it.
543
+ * The client-side path guard is the first line; this is the second.
544
+ */
545
+ override fetch(request: Request): Promise<Response>;
546
+ /**
547
+ * Proxy entry for every `ctx.containers.<name>` fetch — and a public RPC on
548
+ * the stub in its own right, so it applies the same `/__lunora/*` refusal
549
+ * as {@link fetch} rather than trusting that every caller came through it.
550
+ * The base starts the container for this request through
551
+ * {@link startAndWaitForPorts}, which is where the `secretsStore` resolution
552
+ * lives — a request that finds the container already healthy needs no
553
+ * resolution at all.
537
554
  */
538
555
  override containerFetch(...args: Parameters<Container<Env>["containerFetch"]>): Promise<Response>;
556
+ /**
557
+ * The exec entry: `handle.exec` delivers its `POST /__lunora/exec` here, as
558
+ * an RPC, never over {@link fetch}. An RPC can only be invoked by code that
559
+ * holds the Durable Object binding, which is what makes the exec route
560
+ * unreachable from request content. Accepts exactly the exec route and
561
+ * nothing else under the reserved namespace, and honours a `.port(n)`
562
+ * handle's target-port header the way the base `fetch` does.
563
+ */
564
+ lunoraExec(request: Request): Promise<Response>;
539
565
  /**
540
566
  * The start path `containerFetch` takes (and the one an app can call itself).
541
567
  * Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
@@ -548,16 +574,27 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
548
574
  */
549
575
  override startAndWaitForPorts(...args: Parameters<Container<Env>["startAndWaitForPorts"]>): Promise<void>;
550
576
  /**
551
- * Explicit start (`ctx.containers.<name>.get(id).start()`). Resolves the
552
- * `secretsStore` bindings into `envVars` first, mirroring
553
- * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
554
- * env set wholesale (base behavior), so the injected values only apply to a
555
- * bare `start()` — same as the static `env`/`secrets`. When the caller
556
- * supplies its own `envVars` we skip resolution entirely: those values would
557
- * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
558
- * that never uses them.
577
+ * Explicit start (`ctx.containers.<name>.get(id).start()`).
578
+ *
579
+ * A per-instance `start({ envVars })` REPLACES the declared env (`env`,
580
+ * `secrets`, `secretsStore` — the Secrets Store is not even read) and is
581
+ * PERSISTED: every later start of this instance, explicit or implicit (a
582
+ * `fetch`/`exec` after the container slept, crashed or hit `hardTimeout`),
583
+ * boots with the same variables until `destroy()`. That is what makes
584
+ * `start({ envVars: {} })` a credential-free sandbox rather than one that
585
+ * gets its credentials back on the next restart.
586
+ *
587
+ * A start with `envVars` that differ from what the instance is running (or
588
+ * starting) with is REJECTED rather than silently joined: the base would
589
+ * return early on a live container, reporting a start whose env never took
590
+ * effect. `stop()` it first, or `destroy()` it to drop the override.
559
591
  */
560
592
  override start(...args: Parameters<Container<Env>["start"]>): Promise<void>;
593
+ /**
594
+ * Stop the container and forget this instance's `start({ envVars })`
595
+ * override, so the next start uses the declared env again.
596
+ */
597
+ override destroy(): Promise<void>;
561
598
  override onActivityExpired(): Promise<void>;
562
599
  override onError(error: unknown): unknown;
563
600
  override onStart(): Promise<void>;
@@ -580,6 +617,8 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
580
617
  generation?: number;
581
618
  }): Promise<void>;
582
619
  override onStop(parameters: StopParams): Promise<void>;
620
+ /** A 403 for a path under the reserved namespace, else `undefined`. */
621
+ private refuseReserved;
583
622
  /**
584
623
  * Arm the hard timeout and block on the `readyOn` probes — the work that has
585
624
  * to happen once per real start, **outside** the base's start gate.
@@ -672,6 +711,19 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
672
711
  * `secretsStore`.
673
712
  */
674
713
  private resolveSecretsStoreEnv;
714
+ /** The persisted `start({ envVars })` override, read from storage once per instance lifetime. */
715
+ private readEnvOverride;
716
+ /**
717
+ * Record a `start({ envVars })` override, refusing one that differs from
718
+ * the env a running (or starting) container already has — see {@link start}.
719
+ */
720
+ private persistEnvOverride;
721
+ /**
722
+ * Set `envVars` for the start about to happen: the persisted override when
723
+ * there is one (the Secrets Store is not read), else the declared env plus
724
+ * the resolved Secrets Store values.
725
+ */
726
+ private applyStartEnv;
675
727
  /**
676
728
  * Block until every `readyOn` probe responds with its expected status, or
677
729
  * throw once the readiness budget is spent. Probes run in parallel and hit
@@ -1,4 +1,4 @@
1
- import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-rC2ejtvT.js";
1
+ import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-DPvNG2h5.js";
2
2
  import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
3
3
  /**
4
4
  * ContainerStartOptions as they come from worker types
@@ -520,7 +520,13 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
520
520
  private readonly lunoraReadyOn;
521
521
  /** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
522
522
  private readonly lunoraSecretsStore?;
523
- /** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
523
+ /** The definition's `env` + `secrets`, as resolved at construction — the env a start uses absent an override. */
524
+ private readonly lunoraDeclaredEnv;
525
+ /** The persisted `start({ envVars })` override, once loaded. See {@link ENV_OVERRIDE_KEY}. */
526
+ private lunoraEnvOverride?;
527
+ /** Whether the override field reflects storage (read once per DO instance, then kept in step). */
528
+ private lunoraEnvOverrideLoaded;
529
+ /** Memoised Secrets Store resolution: the resolved `name → value` map, fetched once. */
524
530
  private lunoraSecretsStoreResolved?;
525
531
  /**
526
532
  * Count of runs observed to have ENDED, bumped by the `onStop` hook. Read
@@ -530,12 +536,32 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
530
536
  private lunoraStops;
531
537
  constructor(context: DurableObjectContext, env: Env, definition: ContainerDefinition, exportName?: string, jurisdiction?: DurableObjectJurisdiction);
532
538
  /**
533
- * Proxy entry for every `ctx.containers.<name>` fetch. The base starts the
534
- * container for this request through {@link startAndWaitForPorts}, which is
535
- * where the `secretsStore` resolution lives — a request that finds the
536
- * container already healthy needs no resolution at all.
539
+ * HTTP entry for every request to this Durable Object. Refuses the reserved
540
+ * `/__lunora/*` namespace unconditionally: `exec` reaches it only through
541
+ * the {@link lunoraExec} RPC, so no header, path spelling, or inbound request
542
+ * an app forwards as-is (`env.CONTAINER_X.get(id).fetch(request)`) opens it.
543
+ * The client-side path guard is the first line; this is the second.
544
+ */
545
+ override fetch(request: Request): Promise<Response>;
546
+ /**
547
+ * Proxy entry for every `ctx.containers.<name>` fetch — and a public RPC on
548
+ * the stub in its own right, so it applies the same `/__lunora/*` refusal
549
+ * as {@link fetch} rather than trusting that every caller came through it.
550
+ * The base starts the container for this request through
551
+ * {@link startAndWaitForPorts}, which is where the `secretsStore` resolution
552
+ * lives — a request that finds the container already healthy needs no
553
+ * resolution at all.
537
554
  */
538
555
  override containerFetch(...args: Parameters<Container<Env>["containerFetch"]>): Promise<Response>;
556
+ /**
557
+ * The exec entry: `handle.exec` delivers its `POST /__lunora/exec` here, as
558
+ * an RPC, never over {@link fetch}. An RPC can only be invoked by code that
559
+ * holds the Durable Object binding, which is what makes the exec route
560
+ * unreachable from request content. Accepts exactly the exec route and
561
+ * nothing else under the reserved namespace, and honours a `.port(n)`
562
+ * handle's target-port header the way the base `fetch` does.
563
+ */
564
+ lunoraExec(request: Request): Promise<Response>;
539
565
  /**
540
566
  * The start path `containerFetch` takes (and the one an app can call itself).
541
567
  * Resolves the `secretsStore` bindings into `envVars` first — `doStartContainer`
@@ -548,16 +574,27 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
548
574
  */
549
575
  override startAndWaitForPorts(...args: Parameters<Container<Env>["startAndWaitForPorts"]>): Promise<void>;
550
576
  /**
551
- * Explicit start (`ctx.containers.<name>.get(id).start()`). Resolves the
552
- * `secretsStore` bindings into `envVars` first, mirroring
553
- * {@link containerFetch}. A per-instance `start({ envVars })` replaces the
554
- * env set wholesale (base behavior), so the injected values only apply to a
555
- * bare `start()` — same as the static `env`/`secrets`. When the caller
556
- * supplies its own `envVars` we skip resolution entirely: those values would
557
- * be discarded anyway, so a missing/unreadable binding shouldn't fail a start
558
- * that never uses them.
577
+ * Explicit start (`ctx.containers.<name>.get(id).start()`).
578
+ *
579
+ * A per-instance `start({ envVars })` REPLACES the declared env (`env`,
580
+ * `secrets`, `secretsStore` — the Secrets Store is not even read) and is
581
+ * PERSISTED: every later start of this instance, explicit or implicit (a
582
+ * `fetch`/`exec` after the container slept, crashed or hit `hardTimeout`),
583
+ * boots with the same variables until `destroy()`. That is what makes
584
+ * `start({ envVars: {} })` a credential-free sandbox rather than one that
585
+ * gets its credentials back on the next restart.
586
+ *
587
+ * A start with `envVars` that differ from what the instance is running (or
588
+ * starting) with is REJECTED rather than silently joined: the base would
589
+ * return early on a live container, reporting a start whose env never took
590
+ * effect. `stop()` it first, or `destroy()` it to drop the override.
559
591
  */
560
592
  override start(...args: Parameters<Container<Env>["start"]>): Promise<void>;
593
+ /**
594
+ * Stop the container and forget this instance's `start({ envVars })`
595
+ * override, so the next start uses the declared env again.
596
+ */
597
+ override destroy(): Promise<void>;
561
598
  override onActivityExpired(): Promise<void>;
562
599
  override onError(error: unknown): unknown;
563
600
  override onStart(): Promise<void>;
@@ -580,6 +617,8 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
580
617
  generation?: number;
581
618
  }): Promise<void>;
582
619
  override onStop(parameters: StopParams): Promise<void>;
620
+ /** A 403 for a path under the reserved namespace, else `undefined`. */
621
+ private refuseReserved;
583
622
  /**
584
623
  * Arm the hard timeout and block on the `readyOn` probes — the work that has
585
624
  * to happen once per real start, **outside** the base's start gate.
@@ -672,6 +711,19 @@ declare class LunoraContainer<Env = unknown> extends Container<Env> {
672
711
  * `secretsStore`.
673
712
  */
674
713
  private resolveSecretsStoreEnv;
714
+ /** The persisted `start({ envVars })` override, read from storage once per instance lifetime. */
715
+ private readEnvOverride;
716
+ /**
717
+ * Record a `start({ envVars })` override, refusing one that differs from
718
+ * the env a running (or starting) container already has — see {@link start}.
719
+ */
720
+ private persistEnvOverride;
721
+ /**
722
+ * Set `envVars` for the start about to happen: the persisted override when
723
+ * there is one (the Secrets Store is not read), else the declared env plus
724
+ * the resolved Secrets Store values.
725
+ */
726
+ private applyStartEnv;
675
727
  /**
676
728
  * Block until every `readyOn` probe responds with its expected status, or
677
729
  * 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 f}from"../packem_shared/abort-deadline-jwbW9Ala.mjs";import{resolveContainerEnvVars as g,parseDurationSeconds as m}from"../packem_shared/containerBindingName-C7SonAIK.mjs";import{a as y}from"../packem_shared/jurisdiction-DtE9s70w.mjs";import{Container as R}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";import{ContainerProxy as D,outboundParams as M}from"../packem_shared/ContainerProxy-fTW_pXWY.mjs";const E="lunora",N=(o,t,n,e)=>({container:o,event:n,instance:t,level:n==="error"?"error":"info",message:e,source:E,ts:Date.now(),type:"container"}),i=(o,t,n,e)=>{const r=N(o,t,n,e),s=JSON.stringify(r);return n==="error"?console.error(s):console.log(s),r},T="__lunora_admin__:recordContainerEvent",S="__root__",_=o=>{if(o===null||typeof o!="object")return!1;const t=o;return typeof t.get=="function"&&typeof t.idFromName=="function"},O=(o,t)=>{const n=y(o,t);return typeof n.getByName=="function"?n.getByName(S):n.get(n.idFromName(S))},I=async(o,t,n)=>{try{const e=o??{},r=e.SHARD;if(!_(r))return;const s=typeof e.LUNORA_ADMIN_TOKEN=="string"?e.LUNORA_ADMIN_TOKEN:void 0;if(!s||s.length===0)return;const a=new Request("https://shard.internal/rpc",{body:JSON.stringify({args:{event:t},functionPath:T}),headers:{authorization:`Bearer ${s}`,"content-type":"application/json"},method:"POST"});await O(r,n).fetch(a)}catch{}},v=500,w=3e4,h="__lunoraHardTimeoutGeneration";class L extends R{lunoraJurisdiction;lunoraName;lunoraDefaultPort;lunoraHardTimeoutSeconds;lunoraReadiness;lunoraReadyOn;lunoraSecretsStore;lunoraSecretsStoreResolved;lunoraStops=0;constructor(t,n,e,r,s){super(t,n,{defaultPort:e.defaultPort,entrypoint:e.entrypoint?[...e.entrypoint]:void 0,envVars:g(e,n,r),sleepAfter:e.sleepAfter}),e.enableInternet!==void 0&&(this.enableInternet=e.enableInternet),e.requiredPorts!==void 0&&(this.requiredPorts=[...e.requiredPorts]),e.interceptHttps!==void 0&&(this.interceptHttps=e.interceptHttps),e.allowedHosts!==void 0&&(this.allowedHosts=[...e.allowedHosts]),e.deniedHosts!==void 0&&(this.deniedHosts=[...e.deniedHosts]),e.pingEndpoint!==void 0&&(this.pingEndpoint=e.pingEndpoint),e.labels!==void 0&&(this.labels={...e.labels}),this.lunoraName=r??"container",this.lunoraJurisdiction=s,this.lunoraDefaultPort=e.defaultPort,this.lunoraReadyOn=e.readyOn?[...e.readyOn]:[],this.lunoraHardTimeoutSeconds=e.hardTimeout===void 0?void 0:m(e.hardTimeout),this.lunoraSecretsStore=e.secretsStore}async containerFetch(...t){return await this.awaitReadinessGate(),super.containerFetch(...t)}async startAndWaitForPorts(...t){await this.resolveSecretsStoreEnv();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.resolveSecretsStoreEnv();const e=this.lunoraStops,r=this.beginStart();await super.start(...t),await this.afterContainerStart(r&&this.lunoraStops===e)}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(h);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)}async afterContainerStart(t){const n=this.lunoraReadiness;if(n!==void 0){await n;return}const e=(async()=>{t||await this.armHardTimeout(),await this.awaitContainerReadiness()})();this.lunoraReadiness=e;try{await e}catch(r){throw this.lunoraReadiness===e&&(this.lunoraReadiness=void 0),r}}beginStart(){const t=this.ctx.container?.running===!0;return t||(this.lunoraReadiness=void 0),t}async awaitReadinessGate(){if(this.lunoraReadyOn.length===0)return;if(this.lunoraReadiness===void 0){const{status:n}=await this.getState();if(n!=="healthy")return;this.lunoraReadiness=this.awaitContainerReadiness()}const t=this.lunoraReadiness;try{await t}catch(n){throw this.lunoraReadiness===t&&(this.lunoraReadiness=void 0),n}}async armHardTimeout(){if(this.lunoraHardTimeoutSeconds===void 0)return;const t=(await this.ctx.storage.get(h)??0)+1;await this.ctx.storage.put(h,t),await this.schedule(this.lunoraHardTimeoutSeconds,"onHardTimeoutExpired",{generation:t})}async resolveSecretsStoreEnv(){const t=this.lunoraSecretsStore;t!==void 0&&(this.lunoraSecretsStoreResolved??=(async()=>{const n=this.env,e={};for(const[r,s]of Object.entries(t)){const a=n[s];if(a===void 0||typeof a.get!="function")throw new u("INTERNAL",`container "${this.lunoraName}": secretsStore env "${r}" points at binding "${s}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${s}".`);const c=await a.get();if(typeof c!="string")throw new TypeError(`container "${this.lunoraName}": Secrets Store binding "${s}" (env "${r}") did not resolve to a string value.`);e[r]=c}this.envVars={...this.envVars,...e}})().catch(n=>{throw this.lunoraSecretsStoreResolved=void 0,n}),await this.lunoraSecretsStoreResolved)}async awaitContainerReadiness(){if(this.lunoraReadyOn.length===0)return;const{container:t}=this.ctx;if(t===void 0)return;const n=Date.now()+w;await Promise.all(this.lunoraReadyOn.map(async e=>this.awaitReadinessCheck(t,e,n)))}async awaitReadinessCheck(t,n,e){const r=n.port??this.lunoraDefaultPort;if(r===void 0)throw new u("INTERNAL",`container "${this.lunoraName}": readyOn check "${n.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`);const s=n.status??200,a=n.path.startsWith("/")?n.path:`/${n.path}`,c=t.getTcpPort(r);for(;;){const d=Math.max(v,e-Date.now()),l=f(void 0,d,()=>new DOMException(`readiness probe timed out after ${String(d)}ms`,"TimeoutError"));try{if((await c.fetch(`http://container${a}`,{signal:l.signal})).status===s)return}catch{}finally{l.dispose()}if(Date.now()>=e)throw new u("INTERNAL",`container "${this.lunoraName}": readiness check "${n.path}" (port ${String(r)}) did not return ${String(s)} within ${String(w)}ms`);await new Promise(p=>{setTimeout(p,v)})}}surfaceInStudioLogs(t){I(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{D as ContainerProxy,L as LunoraContainer,M as outboundParams};
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};
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-rC2ejtvT.mjs";
2
- export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-rC2ejtvT.mjs";
1
+ import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-DPvNG2h5.mjs";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-DPvNG2h5.mjs";
3
3
  /**
4
4
  * Per-call options for `ContainerHandle.exec`.
5
5
  */
@@ -75,7 +75,19 @@ interface ContainerStartOptions {
75
75
  enableInternet?: boolean;
76
76
  /** Override the container entrypoint. */
77
77
  entrypoint?: string[];
78
- /** Per-instance environment, merged over the definition's `env`/secrets. */
78
+ /**
79
+ * Per-instance environment. REPLACES the definition's `env`, `secrets` and
80
+ * `secretsStore` (the Secrets Store is not read), so pass every variable the
81
+ * container needs. It is persisted for the instance: every later start —
82
+ * including the implicit restart a `fetch`/`exec` triggers after the
83
+ * container slept, crashed or hit `hardTimeout` — uses it, until
84
+ * `destroy()`. The override cannot change while the container is running
85
+ * or still starting: a start whose `envVars` differ is rejected with
86
+ * `CONFLICT` in either case, so `stop()` it (and let any start in flight
87
+ * finish) first. So
88
+ * `start({ envVars: {} })` is a sandbox without the declared credentials,
89
+ * for as long as the instance exists.
90
+ */
79
91
  envVars?: Record<string, string>;
80
92
  /** Metadata labels attached for metrics/observability. */
81
93
  labels?: Record<string, string>;
@@ -99,6 +111,8 @@ interface ContainerStubLike {
99
111
  destroy?: () => Promise<void>;
100
112
  fetch: (input: Request) => Promise<Response>;
101
113
  getState?: () => Promise<ContainerInstanceState>;
114
+ /** The container DO's exec entry (`LunoraContainer.lunoraExec`), the only way a request reaches `/__lunora/exec`. */
115
+ lunoraExec?: (request: Request) => Promise<Response>;
102
116
  removeAllowedHost?: (hostname: string) => Promise<void>;
103
117
  removeDeniedHost?: (hostname: string) => Promise<void>;
104
118
  renewActivityTimeout?: () => Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-rC2ejtvT.js";
2
- export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-rC2ejtvT.js";
1
+ import { D as DurableObjectJurisdiction, a as ContainerConfig, C as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/jurisdiction.d-DPvNG2h5.js";
2
+ export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/jurisdiction.d-DPvNG2h5.js";
3
3
  /**
4
4
  * Per-call options for `ContainerHandle.exec`.
5
5
  */
@@ -75,7 +75,19 @@ interface ContainerStartOptions {
75
75
  enableInternet?: boolean;
76
76
  /** Override the container entrypoint. */
77
77
  entrypoint?: string[];
78
- /** Per-instance environment, merged over the definition's `env`/secrets. */
78
+ /**
79
+ * Per-instance environment. REPLACES the definition's `env`, `secrets` and
80
+ * `secretsStore` (the Secrets Store is not read), so pass every variable the
81
+ * container needs. It is persisted for the instance: every later start —
82
+ * including the implicit restart a `fetch`/`exec` triggers after the
83
+ * container slept, crashed or hit `hardTimeout` — uses it, until
84
+ * `destroy()`. The override cannot change while the container is running
85
+ * or still starting: a start whose `envVars` differ is rejected with
86
+ * `CONFLICT` in either case, so `stop()` it (and let any start in flight
87
+ * finish) first. So
88
+ * `start({ envVars: {} })` is a sandbox without the declared credentials,
89
+ * for as long as the instance exists.
90
+ */
79
91
  envVars?: Record<string, string>;
80
92
  /** Metadata labels attached for metrics/observability. */
81
93
  labels?: Record<string, string>;
@@ -99,6 +111,8 @@ interface ContainerStubLike {
99
111
  destroy?: () => Promise<void>;
100
112
  fetch: (input: Request) => Promise<Response>;
101
113
  getState?: () => Promise<ContainerInstanceState>;
114
+ /** The container DO's exec entry (`LunoraContainer.lunoraExec`), the only way a request reaches `/__lunora/exec`. */
115
+ lunoraExec?: (request: Request) => Promise<Response>;
102
116
  removeAllowedHost?: (hostname: string) => Promise<void>;
103
117
  removeDeniedHost?: (hostname: string) => Promise<void>;
104
118
  renewActivityTimeout?: () => Promise<void>;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createContainerContext as o,createContainerTestContext as r}from"./packem_shared/createContainerContext-DRyINB-P.mjs";import{containerBindingName as a,containerBuildTag as i,containerClassName as C,defineContainer as m,isContainerDefinition as s,normalizeContainerImage as c,resolveContainerEnvVars as f}from"./packem_shared/containerBindingName-C7SonAIK.mjs";import{C as l}from"./packem_shared/exec-KlR4eMip.mjs";export{l as CONTAINER_EXEC_PATH,a as containerBindingName,i as containerBuildTag,C as containerClassName,o as createContainerContext,r as createContainerTestContext,m as defineContainer,s as isContainerDefinition,c as normalizeContainerImage,f as resolveContainerEnvVars};
1
+ import{createContainerContext as o,createContainerTestContext as r}from"./packem_shared/createContainerContext-CLvjjJLs.mjs";import{containerBindingName as a,containerBuildTag as i,containerClassName as C,defineContainer as m,isContainerDefinition as s,normalizeContainerImage as c,resolveContainerEnvVars as f}from"./packem_shared/containerBindingName-C7SonAIK.mjs";import{C as l}from"./packem_shared/exec-WbCOLswq.mjs";export{l as CONTAINER_EXEC_PATH,a as containerBindingName,i as containerBuildTag,C as containerClassName,o as createContainerContext,r as createContainerTestContext,m as defineContainer,s as isContainerDefinition,c as normalizeContainerImage,f as resolveContainerEnvVars};
package/dist/otel.d.mts CHANGED
@@ -203,6 +203,11 @@ interface ContainerTelemetry {
203
203
  * With no endpoint resolvable the returned exporter is disabled (`enabled ===
204
204
  * false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
205
205
  * nothing — so the same code runs unchanged locally and in the cloud.
206
+ *
207
+ * With a trace anchor (`traceparent`, the `request`'s header, or
208
+ * `LUNORA_TRACEPARENT`) every span hangs off it and every log is stamped with
209
+ * it. Without one each span starts its own trace — so in a long-running server,
210
+ * create the instance per request (`{ request }`), not once per process.
206
211
  * @param options Exporter options. Connection fields (`endpoint`, `token`,
207
212
  * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
208
213
  * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
package/dist/otel.d.ts CHANGED
@@ -203,6 +203,11 @@ interface ContainerTelemetry {
203
203
  * With no endpoint resolvable the returned exporter is disabled (`enabled ===
204
204
  * false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
205
205
  * nothing — so the same code runs unchanged locally and in the cloud.
206
+ *
207
+ * With a trace anchor (`traceparent`, the `request`'s header, or
208
+ * `LUNORA_TRACEPARENT`) every span hangs off it and every log is stamped with
209
+ * it. Without one each span starts its own trace — so in a long-running server,
210
+ * create the instance per request (`{ request }`), not once per process.
206
211
  * @param options Exporter options. Connection fields (`endpoint`, `token`,
207
212
  * `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
208
213
  * resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
@@ -0,0 +1 @@
1
+ import"./abort-deadline-jwbW9Ala.mjs";import{a as s,C as t,e as A,p as C}from"./exec-WbCOLswq.mjs";export{s as CONTAINER_EXEC_HEADER,t as CONTAINER_EXEC_PATH,A as execViaFetch,C as pathMatchesAnyDecoding};
@@ -0,0 +1 @@
1
+ import{LunoraError as R}from"@lunora/errors";import{e as C,p as j,a as g,r as X}from"./exec-WbCOLswq.mjs";import{S as Y}from"./sampling-D21rpu_s.mjs";import{containerBindingName as q}from"./containerBindingName-C7SonAIK.mjs";import{a as z}from"./jurisdiction-DtE9s70w.mjs";const S=3,F=3e4,G=3,J=500,K="cf-container-target-port",O="__lunora",Q=t=>{const e=t.indexOf(";");return e===-1?t:t.slice(0,e)},V=t=>{const e=t.split("/").find(o=>o!=="")??"";return Q(e).toLowerCase()},L=(t,e)=>{const o=typeof t=="string"?t:t.url,r=URL.parse(o.startsWith("/")?`http://container${o}`:o);if(r!==null&&j(r.pathname,n=>V(n)===O))throw new R("BAD_REQUEST",`${e}: \`/${O}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},b=(t,e)=>{if(typeof t!="string"){const r=new Request(t,e);return r.headers.delete(g),[r,void 0]}if(e?.headers===void 0)return[t,e];const o=new Headers(e.headers);return o.delete(g),[t,{...e,headers:o}]},H=(t,e,o,r)=>{const n=typeof t=="string"&&t.startsWith("/")?new Request(`http://container${t}`,e):new Request(t,e);return o!==void 0&&n.headers.set(K,String(o)),r?.traceparent!==void 0&&n.headers.set("traceparent",r.traceparent),r?.sampleErrors!==void 0&&n.headers.set(Y,r.sampleErrors?"1":"0"),n},M=async t=>{t<=0||await new Promise(e=>{setTimeout(e,t)})},Z=/no container instance|not listening|try again later|rate.?limit|provision/i,tt="no Container instance available",et="Failed to start container:",ot=1024,rt=async t=>(await X(t.clone().body,ot)).text,$=t=>t instanceof Error&&Z.test(t.message),B=async t=>{if(t.status===429)return!0;if(t.status!==500&&t.status!==503)return!1;try{const e=await rt(t);return e.includes(tt)||e.startsWith(et)}catch{return!1}},E=t=>`ctx.containers.${t.exportName}`,p=(t,e,o={},r,n)=>{const s=Math.max(1,o.attempts??G),a=o.backoffMs??J,c=o.maxBackoffMs??F,A=async(l,h)=>{const T=typeof l=="string"?s:1;let d;for(let f=0;f<T;f+=1){const u=f===T-1;f>0&&await M(Math.min(a*2**(f-1),c));try{const m=await t(H(l,h,r,n),h?.signal);if(u||!await B(m))return m}catch(m){if(d=m,u||!$(m))throw m}}throw d instanceof Error?d:new Error("ctx.containers: cold-start retry exhausted")};return{exec:C(A,e),fetch:async(l,h)=>(L(l,e),A(...b(l,h))),port:l=>p(t,e,o,l,n)}},nt=async(t,e)=>{if(e.aborted)throw t.catch(()=>{}),e.reason;const o=new AbortController,r=new Promise((n,s)=>{e.addEventListener("abort",()=>{s(e.reason)},{once:!0,signal:o.signal})});try{return await Promise.race([t,r])}finally{o.abort(),t.catch(()=>{})}},N=async(t,e,o)=>{if(!e.headers.has(g))return t.fetch(e);if(typeof t.lunoraExec!="function")throw new TypeError("ctx.containers: this container DO does not expose lunoraExec() — is @lunora/container/do up to date?");e.headers.delete(g);const r=t.lunoraExec(new Request(e.url,{body:await e.text(),headers:e.headers,method:e.method}));return o?nt(r,o):r},k=(t,e,o,r,n)=>p(async(s,a)=>N(t.get(t.idFromName(e)),s,a),o,r,void 0,n),i=async(t,e,o,r)=>{const n=t[e];if(typeof n!="function")throw new TypeError(`ctx.containers: the "${o}" container DO does not expose ${e}() — is @lunora/container/do up to date?`);return n(r)},st=(t,e)=>({allow:async o=>i(t(),"allowHost",e,o),deny:async o=>i(t(),"denyHost",e,o),removeAllowed:async o=>i(t(),"removeAllowedHost",e,o),removeDenied:async o=>i(t(),"removeDeniedHost",e,o),setAllowed:async o=>i(t(),"setAllowedHosts",e,[...o]),setDenied:async o=>i(t(),"setDeniedHosts",e,[...o])}),I=(t,e,o,r,n)=>{const s=()=>t.get(t.idFromName(o));return{...p(async(a,c)=>N(s(),a,c),E(e),r,void 0,n),destroy:async()=>i(s(),"destroy",e.binding),egress:st(s,e.binding),getState:async()=>i(s(),"getState",e.binding),renewActivityTimeout:async()=>i(s(),"renewActivityTimeout",e.binding),start:async a=>i(s(),"start",e.binding,a),stop:async a=>i(s(),"stop",e.binding,a)}},v="pool-",P=t=>`${v}${String(Math.floor(Math.random()*t))}`,U=(t,e)=>{if(t.startsWith(v))throw new R("BAD_REQUEST",`${E(e)}.get(${JSON.stringify(t)}): the "${v}" prefix is reserved for the instances .any()/.pool() pick, so this name would share a container (disk, lifecycle) with a pool member. Pick a name that does not start with "${v}".`)},at=t=>t.status>=500,D=(t,e,o={},r,n)=>{const s=o.size??e.maxInstances??S,a=Math.max(1,o.attempts??3),c=o.backoffMs??100,A=o.maxBackoffMs??F,l=`${E(e)}.pool()`,h=(d,f)=>async(u,m)=>{const _=typeof u=="string"?a:1;let x;for(let w=0;w<_;w+=1){w>0&&await M(Math.min(c*2**(w-1),A));const W=H(u,m,r,n);try{const y=await N(t.get(t.idFromName(P(s))),W,m?.signal);if(w===_-1||!await d(y))return y}catch(y){if(x=y,!f(y))throw y}}throw x instanceof Error?x:new Error(`ctx.containers.${e.exportName}.pool(): all ${String(_)} attempts failed`)},T=h(o.retryOn??at,()=>!0);return{exec:C(h(B,$),l),fetch:async(d,f)=>(L(d,l),T(...b(d,f))),port:d=>D(t,e,o,d,n)}},ct=(t,e,o)=>({any:(r,n)=>k(t,P(r??e.maxInstances??S),E(e),n,o),get:(r,n)=>(U(r,e),I(t,e,r,n,o)),pool:r=>D(t,e,r,void 0,o)}),it=t=>{const e=()=>{throw new R("INTERNAL",`ctx.containers.${t.exportName}: no "${t.binding}" Durable Object binding found. Run \`lunora dev\` (or \`lunora deploy\`) to reconcile wrangler.jsonc, and make sure the worker entry re-exports the generated container classes.`)};return{any:e,get:e,pool:e}},ut=(t,e,o,r,n)=>{const s={};for(const a of e){const c=t[a.binding];s[a.exportName]=c&&typeof c.idFromName=="function"&&typeof c.get=="function"?ct(z(c,o),a,{...n===void 0?{}:{sampleErrors:n},...r===void 0?{}:{traceparent:r}}):it(a)}return s},lt=t=>{const e=o=>({allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:r=>Promise.resolve(t(r,{name:o})),lunoraExec:r=>Promise.resolve(t(r,{name:o})),getState:()=>Promise.resolve({lastChange:0}),removeAllowedHost:()=>Promise.resolve(),removeDeniedHost:()=>Promise.resolve(),renewActivityTimeout:()=>Promise.resolve(),setAllowedHosts:()=>Promise.resolve(),setDeniedHosts:()=>Promise.resolve(),start:()=>Promise.resolve(),stop:()=>Promise.resolve()});return{get:o=>e(String(o)),idFromName:o=>o}},wt=t=>{const e={};for(const[o,r]of Object.entries(t)){const n=lt(r),s={binding:q(o),exportName:o};e[o]={any:(a,c)=>k(n,P(a??s.maxInstances??S),E(s),{attempts:1,...c}),get:(a,c)=>(U(a,s),I(n,s,a,{attempts:1,...c})),pool:a=>D(n,s,{...a,attempts:1})}}return e};export{ut as createContainerContext,wt as createContainerTestContext};
@@ -0,0 +1 @@
1
+ import{LunoraError as d}from"@lunora/errors";import{a as w}from"./abort-deadline-jwbW9Ala.mjs";const E=async(o,t,r)=>{if(o===null)return{overflowed:!1,text:""};const e=o.getReader(),n=new TextDecoder,a=()=>{e.cancel().catch(()=>{})};r?.addEventListener("abort",a);let s="",c=0,u=!1;try{for(;r?.aborted!==!0;){const{done:f,value:i}=await e.read();if(f)break;if(c+i.byteLength>t){u=!0,s+=n.decode(i.subarray(0,t-c),{stream:!0});break}c+=i.byteLength,s+=n.decode(i,{stream:!0})}s+=n.decode()}finally{r?.removeEventListener("abort",a),e.cancel().catch(()=>{})}if(r?.aborted===!0)throw r.reason;return{overflowed:u,text:s}},l="/__lunora/exec",h="x-lunora-container-exec",x=5,b=o=>{try{return decodeURIComponent(o)}catch{return o.replaceAll(/%[\da-f]{2}/giu,t=>{try{return decodeURIComponent(t)}catch{return t}})}},O=(o,t)=>{let r=o;for(let e=0;e<=x;e+=1){if(t(r))return!0;const n=b(r);if(n===r)return!1;r=n}return!0},m=512,v=1e6,p="<redacted>",g=(o,t)=>{if(t===void 0)return o;let r=o;for(const e of Object.values(t))e.length>0&&(r=r.split(e).join(p));return r},T=o=>{if(o===null||typeof o!="object")return;const{code:t,stderr:r,stdout:e}=o;if(typeof t=="number")return{code:t,stderr:typeof r=="string"?r:"",stdout:typeof e=="string"?e:""}},A=(o,t)=>w(o,t,()=>new d("INTERNAL",`ctx.containers: exec timed out after ${String(t)}ms`)),R=(o,t)=>async(r,e={})=>{if(typeof r!="string"||r.length===0)throw new d("BAD_REQUEST",`${t}: exec requires a non-empty \`command\``);const n=e.maxOutputBytes??v;if(!Number.isSafeInteger(n)||n<=0)throw new d("BAD_REQUEST",`${t}: exec \`maxOutputBytes\` must be a positive whole number of bytes, got ${String(n)}`);const a=A(e.signal,e.timeoutMs);try{const s=await o(l,{body:JSON.stringify({args:e.args??[],command:r,...e.cwd===void 0?{}:{cwd:e.cwd},...e.env===void 0?{}:{env:e.env},...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}}),headers:{"content-type":"application/json",[h]:"1"},method:"POST",...a.signal===void 0?{}:{signal:a.signal}});if(!s.ok){const i=await E(s.body,m,a.signal).then(y=>g(y.text,e.env),()=>"<unreadable body>");throw new d("INTERNAL",`${t}: exec failed — the container answered ${String(s.status)} for POST ${l}. Does it serve that route? Body: ${i}`)}const c=await E(s.body,n,a.signal);if(c.overflowed)throw new d("INTERNAL",`${t}: exec response exceeded ${String(n)} bytes and was abandoned — it is buffered whole to be parsed, so an unbounded one would exhaust the isolate. Cap the command's output, or raise \`maxOutputBytes\`.`);let u;try{u=JSON.parse(c.text)}catch{throw new d("INTERNAL",`${t}: exec response was not JSON — expected {"code","stdout","stderr"} from POST ${l}`)}const f=T(u);if(f===void 0)throw new d("INTERNAL",`${t}: exec response is missing a numeric \`code\` — expected {"code","stdout","stderr"} from POST ${l}`);return f}finally{a.dispose()}};export{l as C,h as a,R as e,O as p,E as r};
@@ -225,8 +225,8 @@ interface ContainerConfig {
225
225
  * binding. A name already used by `env`/`secrets` is rejected at authoring
226
226
  * time; a missing binding or unreadable value fails the start. Applies
227
227
  * to the default start (the `ctx.containers` proxy path and a bare
228
- * `start()`); a per-instance `start({ envVars })` replaces the env set
229
- * wholesale, as it does for `env`/`secrets`. (Upstream
228
+ * `start()`); a persisted per-instance `start({ envVars })` replaces the env
229
+ * set wholesale, as it does for `env`/`secrets`. (Upstream
230
230
  * cloudflare/containers#96.)
231
231
  */
232
232
  secretsStore?: Readonly<Record<string, string>>;
@@ -225,8 +225,8 @@ interface ContainerConfig {
225
225
  * binding. A name already used by `env`/`secrets` is rejected at authoring
226
226
  * time; a missing binding or unreadable value fails the start. Applies
227
227
  * to the default start (the `ctx.containers` proxy path and a bare
228
- * `start()`); a per-instance `start({ envVars })` replaces the env set
229
- * wholesale, as it does for `env`/`secrets`. (Upstream
228
+ * `start()`); a persisted per-instance `start({ envVars })` replaces the env
229
+ * set wholesale, as it does for `env`/`secrets`. (Upstream
230
230
  * cloudflare/containers#96.)
231
231
  */
232
232
  secretsStore?: Readonly<Record<string, string>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/container",
3
- "version": "1.0.0-alpha.55",
3
+ "version": "1.0.0-alpha.57",
4
4
  "description": "Cloudflare Containers for Lunora: defineContainer, generated Container DO classes, and the ctx.containers action surface",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@lunora/errors": "1.0.0-alpha.40"
59
+ "@lunora/errors": "1.0.0-alpha.41"
60
60
  },
61
61
  "engines": {
62
62
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import"./abort-deadline-jwbW9Ala.mjs";import{C as r,e as t}from"./exec-KlR4eMip.mjs";export{r as CONTAINER_EXEC_PATH,t as execViaFetch};
@@ -1 +0,0 @@
1
- import{LunoraError as D}from"@lunora/errors";import{e as N,r as k}from"./exec-KlR4eMip.mjs";import{S as I}from"./sampling-D21rpu_s.mjs";import{containerBindingName as U}from"./containerBindingName-C7SonAIK.mjs";import{a as q}from"./jurisdiction-DtE9s70w.mjs";const p=3,C=3e4,W=3,j=500,Y="cf-container-target-port",v="__lunora",F=t=>t.split("/").find(e=>e!=="")??"",P=(t,e)=>{const o=typeof t=="string"?t:t.url,n=URL.parse(o.startsWith("/")?`http://container${o}`:o);if(n===null)return;const{pathname:r}=n;let s=r;try{s=decodeURIComponent(r)}catch{}if(F(r)===v||F(s)===v)throw new D("BAD_REQUEST",`${e}: \`/${v}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},L=(t,e,o,n)=>{const r=typeof t=="string"&&t.startsWith("/")?new Request(`http://container${t}`,e):new Request(t,e);return o!==void 0&&r.headers.set(Y,String(o)),n?.traceparent!==void 0&&r.headers.set("traceparent",n.traceparent),n?.sampleErrors!==void 0&&r.headers.set(I,n.sampleErrors?"1":"0"),r},O=async t=>{t<=0||await new Promise(e=>{setTimeout(e,t)})},z=/no container instance|not listening|try again later|rate.?limit|provision/i,G="no Container instance available",K="Failed to start container:",V=1024,J=async t=>(await k(t.clone().body,V)).text,H=t=>t instanceof Error&&z.test(t.message),b=async t=>{if(t.status===429)return!0;if(t.status!==500&&t.status!==503)return!1;try{const e=await J(t);return e.includes(G)||e.startsWith(K)}catch{return!1}},T=t=>`ctx.containers.${t.exportName}`,S=(t,e,o={},n,r)=>{const s=Math.max(1,o.attempts??W),a=o.backoffMs??j,c=o.maxBackoffMs??C,g=async(l,y)=>{const E=typeof l=="string"?s:1;let d;for(let m=0;m<E;m+=1){const u=m===E-1;m>0&&await O(Math.min(a*2**(m-1),c));try{const f=await t(L(l,y,n,r));if(u||!await b(f))return f}catch(f){if(d=f,u||!H(f))throw f}}throw d instanceof Error?d:new Error("ctx.containers: cold-start retry exhausted")};return{exec:N(g,e),fetch:async(l,y)=>(P(l,e),g(l,y)),port:l=>S(t,e,o,l,r)}},M=(t,e,o,n,r)=>S(async s=>t.get(t.idFromName(e)).fetch(s),o,n,void 0,r),i=async(t,e,o,n)=>{const r=t[e];if(typeof r!="function")throw new TypeError(`ctx.containers: the "${o}" container DO does not expose ${e}() — is @lunora/container/do up to date?`);return r(n)},Q=(t,e)=>({allow:async o=>i(t(),"allowHost",e,o),deny:async o=>i(t(),"denyHost",e,o),removeAllowed:async o=>i(t(),"removeAllowedHost",e,o),removeDenied:async o=>i(t(),"removeDeniedHost",e,o),setAllowed:async o=>i(t(),"setAllowedHosts",e,[...o]),setDenied:async o=>i(t(),"setDeniedHosts",e,[...o])}),B=(t,e,o,n,r)=>{const s=()=>t.get(t.idFromName(o));return{...S(async a=>s().fetch(a),T(e),n,void 0,r),destroy:async()=>i(s(),"destroy",e.binding),egress:Q(s,e.binding),getState:async()=>i(s(),"getState",e.binding),renewActivityTimeout:async()=>i(s(),"renewActivityTimeout",e.binding),start:async a=>i(s(),"start",e.binding,a),stop:async a=>i(s(),"stop",e.binding,a)}},R=t=>`pool-${String(Math.floor(Math.random()*t))}`,X=t=>t.status>=500,x=(t,e,o={},n,r)=>{const s=o.size??e.maxInstances??p,a=Math.max(1,o.attempts??3),c=o.backoffMs??100,g=o.maxBackoffMs??C,l=`${T(e)}.pool()`,y=(d,m)=>async(u,f)=>{const A=typeof u=="string"?a:1;let _;for(let w=0;w<A;w+=1){w>0&&await O(Math.min(c*2**(w-1),g));const $=L(u,f,n,r);try{const h=await t.get(t.idFromName(R(s))).fetch($);if(w===A-1||!await d(h))return h}catch(h){if(_=h,!m(h))throw h}}throw _ instanceof Error?_:new Error(`ctx.containers.${e.exportName}.pool(): all ${String(A)} attempts failed`)},E=y(o.retryOn??X,()=>!0);return{exec:N(y(b,H),l),fetch:async(d,m)=>(P(d,l),E(d,m)),port:d=>x(t,e,o,d,r)}},Z=(t,e,o)=>({any:(n,r)=>M(t,R(n??e.maxInstances??p),T(e),r,o),get:(n,r)=>B(t,e,n,r,o),pool:n=>x(t,e,n,void 0,o)}),tt=t=>{const e=()=>{throw new D("INTERNAL",`ctx.containers.${t.exportName}: no "${t.binding}" Durable Object binding found. Run \`lunora dev\` (or \`lunora deploy\`) to reconcile wrangler.jsonc, and make sure the worker entry re-exports the generated container classes.`)};return{any:e,get:e,pool:e}},ct=(t,e,o,n,r)=>{const s={};for(const a of e){const c=t[a.binding];s[a.exportName]=c&&typeof c.idFromName=="function"&&typeof c.get=="function"?Z(q(c,o),a,{...r===void 0?{}:{sampleErrors:r},...n===void 0?{}:{traceparent:n}}):tt(a)}return s},et=t=>{const e=o=>({allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:n=>Promise.resolve(t(n,{name:o})),getState:()=>Promise.resolve({lastChange:0}),removeAllowedHost:()=>Promise.resolve(),removeDeniedHost:()=>Promise.resolve(),renewActivityTimeout:()=>Promise.resolve(),setAllowedHosts:()=>Promise.resolve(),setDeniedHosts:()=>Promise.resolve(),start:()=>Promise.resolve(),stop:()=>Promise.resolve()});return{get:o=>e(String(o)),idFromName:o=>o}},it=t=>{const e={};for(const[o,n]of Object.entries(t)){const r=et(n),s={binding:U(o),exportName:o};e[o]={any:(a,c)=>M(r,R(a??s.maxInstances??p),T(s),{attempts:1,...c}),get:(a,c)=>B(r,s,a,{attempts:1,...c}),pool:a=>x(r,s,{...a,attempts:1})}}return e};export{ct as createContainerContext,it as createContainerTestContext};
@@ -1 +0,0 @@
1
- import{LunoraError as d}from"@lunora/errors";import{a as E}from"./abort-deadline-jwbW9Ala.mjs";const w=async(o,t,r)=>{if(o===null)return{overflowed:!1,text:""};const e=o.getReader(),n=new TextDecoder,a=()=>{e.cancel().catch(()=>{})};r?.addEventListener("abort",a);let s="",c=0,u=!1;try{for(;r?.aborted!==!0;){const{done:f,value:i}=await e.read();if(f)break;if(c+i.byteLength>t){u=!0,s+=n.decode(i.subarray(0,t-c),{stream:!0});break}c+=i.byteLength,s+=n.decode(i,{stream:!0})}s+=n.decode()}finally{r?.removeEventListener("abort",a),e.cancel().catch(()=>{})}if(r?.aborted===!0)throw r.reason;return{overflowed:u,text:s}},l="/__lunora/exec",h=512,b=1e6,v="<redacted>",x=(o,t)=>{if(t===void 0)return o;let r=o;for(const e of Object.values(t))e.length>0&&(r=r.split(e).join(v));return r},m=o=>{if(o===null||typeof o!="object")return;const{code:t,stderr:r,stdout:e}=o;if(typeof t=="number")return{code:t,stderr:typeof r=="string"?r:"",stdout:typeof e=="string"?e:""}},g=(o,t)=>E(o,t,()=>new d("INTERNAL",`ctx.containers: exec timed out after ${String(t)}ms`)),N=(o,t)=>async(r,e={})=>{if(typeof r!="string"||r.length===0)throw new d("BAD_REQUEST",`${t}: exec requires a non-empty \`command\``);const n=e.maxOutputBytes??b;if(!Number.isSafeInteger(n)||n<=0)throw new d("BAD_REQUEST",`${t}: exec \`maxOutputBytes\` must be a positive whole number of bytes, got ${String(n)}`);const a=g(e.signal,e.timeoutMs);try{const s=await o(l,{body:JSON.stringify({args:e.args??[],command:r,...e.cwd===void 0?{}:{cwd:e.cwd},...e.env===void 0?{}:{env:e.env},...e.timeoutMs===void 0?{}:{timeoutMs:e.timeoutMs}}),headers:{"content-type":"application/json"},method:"POST",...a.signal===void 0?{}:{signal:a.signal}});if(!s.ok){const i=await w(s.body,h,a.signal).then(y=>x(y.text,e.env),()=>"<unreadable body>");throw new d("INTERNAL",`${t}: exec failed — the container answered ${String(s.status)} for POST ${l}. Does it serve that route? Body: ${i}`)}const c=await w(s.body,n,a.signal);if(c.overflowed)throw new d("INTERNAL",`${t}: exec response exceeded ${String(n)} bytes and was abandoned — it is buffered whole to be parsed, so an unbounded one would exhaust the isolate. Cap the command's output, or raise \`maxOutputBytes\`.`);let u;try{u=JSON.parse(c.text)}catch{throw new d("INTERNAL",`${t}: exec response was not JSON — expected {"code","stdout","stderr"} from POST ${l}`)}const f=m(u);if(f===void 0)throw new d("INTERNAL",`${t}: exec response is missing a numeric \`code\` — expected {"code","stdout","stderr"} from POST ${l}`);return f}finally{a.dispose()}};export{l as C,N as e,w as r};