@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 +1 -1
- package/dist/do/index.d.mts +66 -14
- package/dist/do/index.d.ts +66 -14
- package/dist/do/index.mjs +1 -1
- package/dist/index.d.mts +17 -3
- package/dist/index.d.ts +17 -3
- package/dist/index.mjs +1 -1
- package/dist/otel.d.mts +5 -0
- package/dist/otel.d.ts +5 -0
- package/dist/packem_shared/CONTAINER_EXEC_PATH-ZfG5wgai.mjs +1 -0
- package/dist/packem_shared/createContainerContext-CLvjjJLs.mjs +1 -0
- package/dist/packem_shared/exec-WbCOLswq.mjs +1 -0
- package/dist/packem_shared/{jurisdiction.d-rC2ejtvT.d.ts → jurisdiction.d-DPvNG2h5.d.mts} +2 -2
- package/dist/packem_shared/{jurisdiction.d-rC2ejtvT.d.mts → jurisdiction.d-DPvNG2h5.d.ts} +2 -2
- package/package.json +2 -2
- package/dist/packem_shared/CONTAINER_EXEC_PATH-CAqlIhLq.mjs +0 -1
- package/dist/packem_shared/createContainerContext-DRyINB-P.mjs +0 -1
- package/dist/packem_shared/exec-KlR4eMip.mjs +0 -1
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`.
|
|
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({
|
package/dist/do/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
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()`).
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as ContainerDefinition, D as DurableObjectJurisdiction } from "../packem_shared/jurisdiction.d-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
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()`).
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
/**
|
|
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-
|
|
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-
|
|
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
|
-
/**
|
|
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-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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};
|