@lunora/container 1.0.0-alpha.63 → 1.0.0-alpha.64

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.
@@ -26,6 +26,22 @@ interface CustomContainerInstanceType {
26
26
  * `ContainerInstanceType` is part of the experimental `@lunora/container` API and may change without a major version bump.
27
27
  */
28
28
  type ContainerInstanceType = CustomContainerInstanceType | NamedContainerInstanceType;
29
+ /**
30
+ * An instance size a Durable Object can pick at start under the
31
+ * `durable_object` scheduling policy. The runtime does not accept `basic` (or
32
+ * the legacy `dev`/`standard` aliases), and a custom size must set all three
33
+ * fields — it is passed to `ctx.container.start({ instance })` as-is.
34
+ */
35
+ type ContainerRuntimeInstanceType = Exclude<NamedContainerInstanceType, "basic"> | Required<CustomContainerInstanceType>;
36
+ /**
37
+ * One named image under the `durable_object` scheduling policy. A `string` is
38
+ * a local Dockerfile path or directory, built and uploaded by `wrangler deploy`
39
+ * (wrangler `dockerfile` + `build_context`); `{ registry }` must be a
40
+ * digest-pinned (`@sha256:…`) reference under `registry.cloudflare.com/`,
41
+ * the Cloudflare managed registry — Cloudflare does not accept Docker Hub, ECR
42
+ * or Artifact Registry references here.
43
+ */
44
+ type ContainerNamedImageSource = RegistryImageSource | string;
29
45
  /**
30
46
  * Rolling-deploy tuning for a container.
31
47
  */
@@ -73,17 +89,17 @@ type ContainerImageSource = BuildImageSource | RegistryImageSource | string;
73
89
  interface ContainerReadinessCheck {
74
90
  /** HTTP path probed on the container, e.g. `"/ready"` (a leading slash is optional). */
75
91
  path: string;
76
- /** Port to probe. Defaults to {@link ContainerConfig.defaultPort}. */
92
+ /** Port to probe. Defaults to {@link ContainerConfigBase.defaultPort}. */
77
93
  port?: number;
78
94
  /** HTTP status that means "ready". Defaults to `200`. */
79
95
  status?: number;
80
96
  }
81
97
  /**
82
- * `ContainerConfig` is part of the experimental `@lunora/container` API and may change without a major version bump.
98
+ * The fields every container accepts, whichever scheduling policy it uses.
83
99
  */
84
- interface ContainerConfig {
100
+ interface ContainerConfigBase {
85
101
  /**
86
- * Hostnames the container may reach **even when {@link ContainerConfig.enableInternet}
102
+ * Hostnames the container may reach **even when {@link ContainerConfigBase.enableInternet}
87
103
  * is `false`** — an egress allow-list (Cloudflare's `allowedHosts`). Glob
88
104
  * patterns like `*.stripe.com` are supported. Pair with `enableInternet:
89
105
  * false` to deny all egress except these hosts (the firewall pattern
@@ -96,29 +112,29 @@ interface ContainerConfig {
96
112
  /**
97
113
  * Build-time variables for a Dockerfile/Railpack image — wrangler's
98
114
  * `image_vars` (equivalent to `docker build --build-arg`). For *runtime*
99
- * values use {@link ContainerConfig.env} / {@link ContainerConfig.secrets}.
115
+ * values use {@link ContainerConfigBase.env} / {@link ContainerConfigBase.secrets}.
100
116
  * Ignored for a pre-built `{ registry }` image.
101
117
  */
102
118
  buildArgs?: Readonly<Record<string, string>>;
103
119
  /**
104
120
  * The port the container listens on. Worker → container requests target
105
121
  * this port. Locally the Dockerfile must also `EXPOSE` it. For a
106
- * multi-port container also declare {@link ContainerConfig.requiredPorts}
122
+ * multi-port container also declare {@link ContainerConfigBase.requiredPorts}
107
123
  * and route per request with the handle's `.port(n)`.
108
124
  */
109
125
  defaultPort?: number;
110
126
  /**
111
127
  * Hostnames the container may **never** reach — an egress deny-list
112
128
  * (Cloudflare's `deniedHosts`). Overrides everything else, including
113
- * `enableInternet: true` and {@link ContainerConfig.allowedHosts}. Glob
129
+ * `enableInternet: true` and {@link ContainerConfigBase.allowedHosts}. Glob
114
130
  * patterns like `*.evil.com` are supported.
115
131
  */
116
132
  deniedHosts?: ReadonlyArray<string>;
117
133
  /**
118
134
  * Whether the container may open outbound internet connections. Defaults
119
135
  * to `true` — the platform default. Note that container egress is billed
120
- * per GB by Cloudflare. Combine with {@link ContainerConfig.allowedHosts} /
121
- * {@link ContainerConfig.deniedHosts} for a precise egress firewall.
136
+ * per GB by Cloudflare. Combine with {@link ContainerConfigBase.allowedHosts} /
137
+ * {@link ContainerConfigBase.deniedHosts} for a precise egress firewall.
122
138
  */
123
139
  enableInternet?: boolean;
124
140
  /**
@@ -129,14 +145,14 @@ interface ContainerConfig {
129
145
  entrypoint?: ReadonlyArray<string>;
130
146
  /**
131
147
  * Static environment variables passed to the container on every start.
132
- * For secret values use {@link ContainerConfig.secrets} instead so they
148
+ * For secret values use {@link ContainerConfigBase.secrets} instead so they
133
149
  * flow through Worker Secrets rather than source code.
134
150
  */
135
151
  env?: Readonly<Record<string, string>>;
136
152
  /**
137
153
  * Hard cap on how long an instance may run, measured from start regardless
138
154
  * of activity — a runaway-cost backstop on top of the idle
139
- * {@link ContainerConfig.sleepAfter}. Same grammar as `sleepAfter`
155
+ * {@link ContainerConfigBase.sleepAfter}. Same grammar as `sleepAfter`
140
156
  * (`"30s"`, `"5m"`, `"1h"`, or a plain number of seconds). When it elapses,
141
157
  * the `LunoraContainer.onHardTimeoutExpired` hook runs (default: `stop()`,
142
158
  * which sends SIGTERM and does not escalate — a container that ignores the
@@ -144,13 +160,6 @@ interface ContainerConfig {
144
160
  * `destroy()`). (Upstream cloudflare/containers#85.)
145
161
  */
146
162
  hardTimeout?: number | string;
147
- /** Image source — a local Dockerfile path/directory or a registry reference. */
148
- image: ContainerImageSource;
149
- /**
150
- * Resource class for each instance: a named Cloudflare instance type or a
151
- * custom `{ vcpu, memoryMib, diskMb }` object.
152
- */
153
- instanceType?: ContainerInstanceType;
154
163
  /**
155
164
  * Intercept the container's outbound **HTTPS** traffic so the egress
156
165
  * allow/deny lists apply to TLS connections too (Cloudflare's
@@ -166,11 +175,6 @@ interface ContainerConfig {
166
175
  * `start({ labels })`.
167
176
  */
168
177
  labels?: Readonly<Record<string, string>>;
169
- /**
170
- * Maximum number of concurrently *running* instances. Stopped (slept)
171
- * containers don't count. Also the default pool size for `.any()`.
172
- */
173
- maxInstances?: number;
174
178
  /**
175
179
  * Override for the wrangler `containers[].name` identifier. Defaults to
176
180
  * wrangler's own default (worker name + class name + environment).
@@ -196,17 +200,10 @@ interface ContainerConfig {
196
200
  * Ports the container must be listening on before it's considered ready
197
201
  * (Cloudflare's `requiredPorts`) — for multi-port containers. Start-up
198
202
  * waits for every listed port, and the handle's `.port(n)` routes a request
199
- * to any of them; {@link ContainerConfig.defaultPort} is the target when a
203
+ * to any of them; {@link ContainerConfigBase.defaultPort} is the target when a
200
204
  * request doesn't pick one.
201
205
  */
202
206
  requiredPorts?: ReadonlyArray<number>;
203
- /**
204
- * Rolling-deploy tuning. `stepPercentage` is the share of instances updated
205
- * per rollout step (wrangler `rollout_step_percentage`); `gracePeriodSeconds`
206
- * is how long an active instance is left running before it's eligible for
207
- * update (wrangler `rollout_active_grace_period`).
208
- */
209
- rollout?: ContainerRollout;
210
207
  /**
211
208
  * Names of Worker secrets (from `wrangler secret` / `.dev.vars`) forwarded
212
209
  * into the container's environment at instance start. Each declared name
@@ -220,7 +217,7 @@ interface ContainerConfig {
220
217
  * binding name*. Each binding is resolved with its async `.get()` the first
221
218
  * time the instance starts, then injected as that env var — e.g.
222
219
  * `{ STRIPE_KEY: "STRIPE_SECRET" }` runs `env.STRIPE_SECRET.get()` and sets
223
- * `STRIPE_KEY` inside the container. Unlike {@link ContainerConfig.secrets}
220
+ * `STRIPE_KEY` inside the container. Unlike {@link ContainerConfigBase.secrets}
224
221
  * (plain Worker text secrets), this pulls from a `secrets_store_secrets`
225
222
  * binding. A name already used by `env`/`secrets` is rejected at authoring
226
223
  * time; a missing binding or unreadable value fails the start. Applies
@@ -236,13 +233,95 @@ interface ContainerConfig {
236
233
  */
237
234
  sleepAfter?: number | string;
238
235
  }
236
+ /**
237
+ * A container under the `default` scheduling policy: one application-wide
238
+ * image and instance size, set in wrangler and rolled out by Cloudflare on
239
+ * deploy.
240
+ */
241
+ interface DefaultScheduledContainerConfig extends ContainerConfigBase {
242
+ /** Image source — a local Dockerfile path/directory or a registry reference. */
243
+ image: ContainerImageSource;
244
+ /**
245
+ * Resource class for each instance: a named Cloudflare instance type or a
246
+ * custom `{ vcpu, memoryMib, diskMb }` object.
247
+ */
248
+ instanceType?: ContainerInstanceType;
249
+ /**
250
+ * Maximum number of concurrently *running* instances. Stopped (slept)
251
+ * containers don't count. Also the default pool size for `.any()`.
252
+ */
253
+ maxInstances?: number;
254
+ /**
255
+ * Rolling-deploy tuning. `stepPercentage` is the share of instances updated
256
+ * per rollout step (wrangler `rollout_step_percentage`); `gracePeriodSeconds`
257
+ * is how long an active instance is left running before it's eligible for
258
+ * update (wrangler `rollout_active_grace_period`).
259
+ */
260
+ rollout?: ContainerRollout;
261
+ /**
262
+ * Where the image and instance size are configured. `"default"` (the
263
+ * default) sets them once in wrangler; `"durable_object"` is the
264
+ * per-instance policy.
265
+ * Immutable once deployed: switching policies means a new container
266
+ * application, which replaces every running instance.
267
+ */
268
+ schedulingPolicy?: "default";
269
+ }
270
+ /**
271
+ * A container under the `durable_object` scheduling policy (Cloudflare public
272
+ * beta): each instance picks its image and size when it starts, via
273
+ * `ctx.containers.<name>.get(id).start({ image, instanceType })`, and can save
274
+ * and restore its filesystem with `snapshot()` / `start({ snapshot })`.
275
+ * Instances keep the image they started with across deploys — there is no
276
+ * application-wide rollout, and no `maxInstances` cap (running instances count
277
+ * against the account limit).
278
+ */
279
+ interface DurableObjectScheduledContainerConfig extends ContainerConfigBase {
280
+ /**
281
+ * The image a start uses when it names none — including the implicit start a
282
+ * `fetch`/`exec` triggers. Either a key of `images`
283
+ * or a Cloudflare-managed image such as `"cloudflare/debian-trixie"`.
284
+ * Without it, an instance must be started explicitly with an `image` (or a
285
+ * `snapshot`) before it can serve requests.
286
+ */
287
+ image?: string;
288
+ /**
289
+ * Named images an instance can start from, keyed by the name `start({ image })`
290
+ * takes (wrangler `containers[].images`). Up to 100, each name 1–128
291
+ * characters. Updating the map does not restart running instances.
292
+ */
293
+ images?: Readonly<Record<string, ContainerNamedImageSource>>;
294
+ /**
295
+ * The instance size a start uses when it names none. Cloudflare's runtime
296
+ * default is `"lite"`.
297
+ */
298
+ instanceType?: ContainerRuntimeInstanceType;
299
+ /** Selects the per-instance policy. Immutable once deployed. */
300
+ schedulingPolicy: "durable_object";
301
+ }
302
+ /**
303
+ * `ContainerConfig` is part of the experimental `@lunora/container` API and may change without a major version bump.
304
+ */
305
+ type ContainerConfig = DefaultScheduledContainerConfig | DurableObjectScheduledContainerConfig;
239
306
  /**
240
307
  * The value `defineContainer` returns: the validated config plus a brand the
241
308
  * codegen discovery and the generated Container DO class key on.
242
309
  */
243
- interface ContainerDefinition extends ContainerConfig {
310
+ type ContainerDefinition = ContainerConfig & {
244
311
  /** Brand marking a value as a Lunora container definition. */
245
312
  readonly isLunoraContainer: true;
313
+ };
314
+ /**
315
+ * A saved container filesystem, as `snapshot()` returns it. Plain data: store
316
+ * it anywhere and pass it back to `start({ snapshot })`, from the same or
317
+ * another instance of the same container. Tied to the image it was taken
318
+ * from; Cloudflare keeps it 30 days, refreshed on each restore.
319
+ */
320
+ interface ContainerSnapshot {
321
+ id: string;
322
+ name?: string;
323
+ /** Size in bytes. */
324
+ size: number;
246
325
  }
247
326
  /**
248
327
  * A normalized image source, as written into `wrangler.jsonc`.
@@ -268,4 +347,4 @@ type NormalizedContainerImage = {
268
347
  * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
269
348
  */
270
349
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
271
- export { BuildImageSource as B, ContainerDefinition as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerConfig as a, ContainerImageSource as b, ContainerInstanceType as c, ContainerReadinessCheck as d, ContainerRollout as e, CustomContainerInstanceType as f, NamedContainerInstanceType as g };
350
+ export { BuildImageSource as B, ContainerSnapshot as C, DurableObjectJurisdiction as D, NormalizedContainerImage as N, RegistryImageSource as R, ContainerRuntimeInstanceType as a, DefaultScheduledContainerConfig as b, DurableObjectScheduledContainerConfig as c, ContainerDefinition as d, ContainerImageSource as e, ContainerConfig as f, ContainerConfigBase as g, ContainerInstanceType as h, ContainerNamedImageSource as i, ContainerReadinessCheck as j, ContainerRollout as k, CustomContainerInstanceType as l, NamedContainerInstanceType as m };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/container",
3
- "version": "1.0.0-alpha.63",
3
+ "version": "1.0.0-alpha.64",
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.47"
59
+ "@lunora/errors": "1.0.0-alpha.48"
60
60
  },
61
61
  "engines": {
62
62
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
1
- import{LunoraError as f}from"@lunora/errors";const d=new Set(["basic","lite","standard-1","standard-2","standard-3","standard-4"]),s=/^[A-Z_]\w*$/i,c=/^\d+[smh]$/,u={h:3600,m:60,s:1},g=e=>{if(typeof e=="number")return Math.floor(e);if(c.exec(e)===null)throw new TypeError(`Invalid duration "${e}" — expected a number of seconds or "<n>[smh]"`);return Number(e.slice(0,-1))*(u[e.slice(-1)]??1)},y=e=>{const t=e.endsWith("/")?e.slice(0,-1):e,n=t.lastIndexOf("/");return n===-1?t:t.slice(n+1)},m=e=>{const t=e.lastIndexOf("/");return t===-1?".":e.slice(0,t)||"/"},$=e=>{if(typeof e!="string")return"build"in e?{buildDir:e.build.endsWith("/")?e.build.slice(0,-1):e.build,kind:"build"}:{kind:"registry",reference:e.registry};if(y(e).startsWith("Dockerfile"))return{buildContext:m(e),dockerfilePath:e,kind:"dockerfile"};const t=e.endsWith("/")?e.slice(0,-1):e;return{buildContext:t,dockerfilePath:`${t}/Dockerfile`,kind:"dockerfile"}},k=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}Container`,S=e=>`CONTAINER_${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"_").toUpperCase()}`,N=e=>`lunora-${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"-").toLowerCase()}:build`,h=e=>{if(typeof e=="string"){if(e.length===0)throw new TypeError("defineContainer: `image` must be a non-empty path or a { registry } reference");if(e.includes(":"))throw new TypeError(`defineContainer: \`image\` string "${e}" looks like a registry reference — pass it as { registry: "${e}" } instead. Plain strings are local Dockerfile paths.`);return}if("build"in e){if(typeof e.build!="string"||e.build.length===0)throw new TypeError("defineContainer: `image.build` must be a non-empty source directory for Railpack to build");return}if(typeof e.registry!="string"||e.registry.length===0)throw new TypeError("defineContainer: `image.registry` must be a non-empty fully-qualified image reference")},b=(e,t,n)=>{for(const[r,o]of Object.entries(e.secretsStore??{})){if(!s.test(r))throw new TypeError(`defineContainer: secretsStore env name "${r}" is not a valid environment variable name`);if(typeof o!="string"||o.trim().length===0)throw new TypeError(`defineContainer: \`secretsStore["${r}"]\` must be a non-empty Secrets Store binding name`);if(t.has(r)||n.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`secretsStore\` and \`env\`/\`secrets\` — pick one source for the value`)}},w=e=>{for(const r of Object.keys(e.env??{}))if(!s.test(r))throw new TypeError(`defineContainer: env variable name "${r}" is not a valid environment variable name`);for(const r of Object.keys(e.buildArgs??{}))if(!s.test(r))throw new TypeError(`defineContainer: buildArg name "${r}" is not a valid environment variable name`);const t=new Set(Object.keys(e.env??{})),n=new Set(e.secrets);for(const r of e.secrets??[]){if(!s.test(r))throw new TypeError(`defineContainer: secret name "${r}" is not a valid environment variable name`);if(t.has(r))throw new TypeError(`defineContainer: "${r}" is declared in both \`env\` and \`secrets\` — a secret would silently overwrite the static env value; pick one`)}b(e,t,n)},i=(e,t)=>{if(!Number.isInteger(e)||e<1||e>65535)throw new TypeError(`defineContainer: \`${t}\` must be an integer in 1–65535 (got ${String(e)})`)},v=e=>{for(const t of e.readyOn??[]){if(typeof t.path!="string"||t.path.trim().length===0)throw new TypeError("defineContainer: `readyOn[].path` must be a non-empty HTTP path string");if(t.path!==t.path.trim())throw new TypeError("defineContainer: `readyOn[].path` must not have leading or trailing whitespace");if(t.port!==void 0&&i(t.port,"readyOn[].port"),t.status!==void 0&&(!Number.isInteger(t.status)||t.status<100||t.status>599))throw new TypeError(`defineContainer: \`readyOn[].status\` must be an HTTP status code in 100–599 (got ${String(t.status)})`)}},l=(e,t)=>{if(e!==void 0){if(typeof e=="string"){if(!c.test(e))throw new TypeError(`defineContainer: \`${t}\` string "${e}" must be a number of seconds followed by a unit, e.g. "30s", "5m", or "1h"`)}else if(!Number.isInteger(e)||e<1)throw new TypeError(`defineContainer: \`${t}\` must be a positive integer number of seconds or a duration string like "5m" (got ${String(e)})`)}},E=e=>{for(const t of["allowedHosts","deniedHosts"])if(e[t]?.some(r=>typeof r!="string"||r.trim().length===0))throw new TypeError(`defineContainer: \`${t}\` must be an array of non-empty hostname patterns`);if(e.interceptHttps!==void 0&&typeof e.interceptHttps!="boolean")throw new TypeError("defineContainer: `interceptHttps` must be a boolean, or omitted")},T=e=>{if(e.requiredPorts!==void 0){if(e.requiredPorts.length===0)throw new TypeError("defineContainer: `requiredPorts` must be a non-empty array of ports, or omitted");for(const t of e.requiredPorts)i(t,"requiredPorts[]")}if(e.entrypoint!==void 0&&(e.entrypoint.length===0||e.entrypoint.some(t=>typeof t!="string"||t.trim().length===0)))throw new TypeError("defineContainer: `entrypoint` must be a non-empty array of non-empty strings, or omitted");if(E(e),e.pingEndpoint!==void 0&&(typeof e.pingEndpoint!="string"||e.pingEndpoint.trim().length===0))throw new TypeError("defineContainer: `pingEndpoint` must be a non-empty path string");for(const[t,n]of Object.entries(e.labels??{}))if(t.trim().length===0||typeof n!="string")throw new TypeError("defineContainer: `labels` must be a record of non-empty keys to string values");v(e)},P=e=>{h(e.image),e.defaultPort!==void 0&&i(e.defaultPort,"defaultPort");const t=e.rollout?.stepPercentage;if(t!==void 0&&(!Number.isInteger(t)||t<1||t>100))throw new TypeError(`defineContainer: \`rollout.stepPercentage\` must be an integer in 1–100 (got ${String(t)})`);const n=e.rollout?.gracePeriodSeconds;if(n!==void 0&&(!Number.isInteger(n)||n<0))throw new TypeError(`defineContainer: \`rollout.gracePeriodSeconds\` must be a non-negative integer (got ${String(n)})`);if(e.maxInstances!==void 0&&(!Number.isInteger(e.maxInstances)||e.maxInstances<1))throw new TypeError(`defineContainer: \`maxInstances\` must be a positive integer (got ${String(e.maxInstances)})`);if(typeof e.instanceType=="string"&&!d.has(e.instanceType))throw new TypeError(`defineContainer: unknown \`instanceType\` "${e.instanceType}" — use one of ${[...d].join(", ")}, or a custom { vcpu, memoryMib, diskMb } object`);return l(e.sleepAfter,"sleepAfter"),l(e.hardTimeout,"hardTimeout"),w(e),T(e),{...e,isLunoraContainer:!0}},I=e=>typeof e=="object"&&e!==null&&e.isLunoraContainer===!0,A=(e,t,n)=>{const r={...e.env};for(const o of e.secrets??[]){const a=t[o];if(typeof a!="string"){const p=n===void 0?"container":`container "${n}"`;throw new f("INTERNAL",`${p}: declared secret "${o}" is not set on the Worker environment. Add it to .dev.vars for local dev and run \`wrangler secret put ${o}\` for production.`)}r[o]=a}return r};export{S as containerBindingName,N as containerBuildTag,k as containerClassName,P as defineContainer,I as isContainerDefinition,$ as normalizeContainerImage,g as parseDurationSeconds,A as resolveContainerEnvVars};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
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};