@lunora/container 1.0.0-alpha.8 → 1.0.0-alpha.81
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 +24 -5
- package/dist/bridge.d.mts +43 -33
- package/dist/bridge.d.ts +43 -33
- package/dist/bridge.mjs +1 -79
- package/dist/do/index.d.mts +353 -91
- package/dist/do/index.d.ts +353 -91
- package/dist/do/index.mjs +1 -328
- package/dist/index.d.mts +584 -153
- package/dist/index.d.ts +584 -153
- package/dist/index.mjs +1 -2
- package/dist/otel.d.mts +217 -0
- package/dist/otel.d.ts +217 -0
- package/dist/otel.mjs +1 -0
- package/dist/packem_shared/CONTAINER_EXEC_PATH-7Hcd6-zs.mjs +1 -0
- package/dist/packem_shared/CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU-D8Hy9Rxv.mjs +1 -0
- package/dist/packem_shared/ContainerProxy-CfXjS6ZI.mjs +27 -0
- package/dist/packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs +1 -0
- package/dist/packem_shared/LunoraSandboxContainer-DPnhJUu1.mjs +1 -0
- package/dist/packem_shared/abort-deadline-jwbW9Ala.mjs +1 -0
- package/dist/packem_shared/createContainerContext-p6QxWjsm.mjs +1 -0
- package/dist/packem_shared/exec-BYLW70sR.mjs +1 -0
- package/dist/packem_shared/jurisdiction-DtE9s70w.mjs +1 -0
- package/dist/packem_shared/jurisdiction.d-BwpauCoI.d.mts +471 -0
- package/dist/packem_shared/jurisdiction.d-BwpauCoI.d.ts +471 -0
- package/dist/packem_shared/sampling-D21rpu_s.mjs +1 -0
- package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.mts +110 -0
- package/dist/packem_shared/sandbox-types.d-DXlHnsEg.d.ts +110 -0
- package/dist/sandbox/index.d.mts +66 -0
- package/dist/sandbox/index.d.ts +66 -0
- package/dist/sandbox/index.mjs +1 -0
- package/package.json +12 -2
- package/dist/packem_shared/ContainerProxy-DWqUX_re.mjs +0 -1474
- package/dist/packem_shared/containerBindingName-D5KTrblH.mjs +0 -218
- package/dist/packem_shared/createContainerContext-Cibbh7AE.mjs +0 -228
- package/dist/packem_shared/types.d-BlNwNY44.d.mts +0 -250
- package/dist/packem_shared/types.d-BlNwNY44.d.ts +0 -250
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { containerBindingName, containerBuildTag, containerClassName, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVars } from './packem_shared/containerBindingName-D5KTrblH.mjs';
|
|
1
|
+
import{createContainerContext as r,createContainerTestContext as t,getContainer as a}from"./packem_shared/createContainerContext-p6QxWjsm.mjs";import{DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS as i,containerBindingName as C,containerBuildTag as _,containerClassName as E,defineContainer as s,isCloudflareRegistryDigest as I,isContainerDefinition as T,isManagedImage as N,normalizeContainerImage as m,resolveContainerEnvVars as g}from"./packem_shared/DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs";import{C as f}from"./packem_shared/exec-BYLW70sR.mjs";import{CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU as l,CUSTOM_INSTANCE_TYPE_LIMITS as x}from"./packem_shared/CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU-D8Hy9Rxv.mjs";export{f as CONTAINER_EXEC_PATH,l as CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU,x as CUSTOM_INSTANCE_TYPE_LIMITS,i as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,C as containerBindingName,_ as containerBuildTag,E as containerClassName,r as createContainerContext,t as createContainerTestContext,s as defineContainer,a as getContainer,I as isCloudflareRegistryDigest,T as isContainerDefinition,N as isManagedImage,m as normalizeContainerImage,g as resolveContainerEnvVars};
|
package/dist/otel.d.mts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An attribute value carried on a span or log.
|
|
3
|
+
*/
|
|
4
|
+
type ContainerAttributeValue = boolean | number | string;
|
|
5
|
+
/**
|
|
6
|
+
* A `fetch` implementation — defaults to the runtime global. The exporter passes
|
|
7
|
+
* an abort `signal` (for the per-request timeout) and, once the promise settles,
|
|
8
|
+
* cancels the response `body` so Node/undici can release the socket for
|
|
9
|
+
* keep-alive reuse instead of leaving it occupied by an unread stream. It reads
|
|
10
|
+
* `ok`/`status` to detect a rejected export and nothing else from the response.
|
|
11
|
+
*/
|
|
12
|
+
type OtelFetchLike = (input: string, init: {
|
|
13
|
+
body: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
method: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
body?: {
|
|
19
|
+
cancel: () => Promise<void>;
|
|
20
|
+
} | null;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* A single span the container process asks the exporter to record.
|
|
26
|
+
*/
|
|
27
|
+
interface ContainerSpanInput {
|
|
28
|
+
/** Attributes attached to the span (rendered under the OTLP `attributes` list). */
|
|
29
|
+
attributes?: Record<string, ContainerAttributeValue>;
|
|
30
|
+
/** Wall-clock millis when the operation ended. */
|
|
31
|
+
endMs: number;
|
|
32
|
+
/** When set, the span is marked errored with this message (and optional `error.type`). */
|
|
33
|
+
error?: {
|
|
34
|
+
message: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Span name — the operation being timed, e.g. `"transcode"`. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Wall-clock millis when the operation started. */
|
|
40
|
+
startMs: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A single log line the container process asks the exporter to record.
|
|
44
|
+
*/
|
|
45
|
+
interface ContainerLogInput {
|
|
46
|
+
/** Attributes attached to the log record. */
|
|
47
|
+
attributes?: Record<string, ContainerAttributeValue>;
|
|
48
|
+
/** Severity — defaults to `"info"`. */
|
|
49
|
+
level?: "debug" | "error" | "info" | "warn";
|
|
50
|
+
/** The log message body. */
|
|
51
|
+
message: string;
|
|
52
|
+
/** Wall-clock millis the line was emitted; defaults to now. */
|
|
53
|
+
ts?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Options for {@link createContainerTelemetry}.
|
|
57
|
+
*/
|
|
58
|
+
interface ContainerTelemetryOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Keep a span that ERRORED even when the inbound `traceparent` says the trace
|
|
61
|
+
* was sampled out — the **tail bias**, the same rule `@lunora/runtime`'s
|
|
62
|
+
* `sampling.alwaysSampleErrors` and the shard apply.
|
|
63
|
+
*
|
|
64
|
+
* Normally you do not set this: the dispatch's verdict arrives on the
|
|
65
|
+
* inbound request (see {@link ContainerTelemetryOptions.request}), so all
|
|
66
|
+
* three tiers agree because they were told the same thing. Set it to
|
|
67
|
+
* override that. With neither, the `LUNORA_SAMPLE_ERRORS` env var applies
|
|
68
|
+
* (`"0"` turns it off) — which is what a one-shot container with no request
|
|
69
|
+
* to read has — and failing that, the tier default `true`.
|
|
70
|
+
*
|
|
71
|
+
* Per SPAN, not per trace: the container is a long-running process with no
|
|
72
|
+
* dispatch boundary to re-decide at, so a span that already settled `ok`
|
|
73
|
+
* before a sibling failed is not retro-exported. That is the same shape the
|
|
74
|
+
* worker applies to its own dispatch events.
|
|
75
|
+
*/
|
|
76
|
+
alwaysSampleErrors?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Value of the `deployment.environment` resource attribute. Falls back to
|
|
79
|
+
* the `DEPLOYMENT_ENVIRONMENT` / `ENVIRONMENT` / `NODE_ENV` env vars **only
|
|
80
|
+
* when {@link ContainerTelemetryOptions.detectResources} is `true`** —
|
|
81
|
+
* unlike `serviceName`, env detection here is opt-in so a stray `NODE_ENV`
|
|
82
|
+
* never silently labels a deployment.
|
|
83
|
+
*/
|
|
84
|
+
deploymentEnvironment?: string;
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, auto-detect OTLP resource attributes from the container
|
|
87
|
+
* environment (`HOSTNAME`, `KUBERNETES_*`, `SERVICE_VERSION`, etc.).
|
|
88
|
+
* Explicit options and `resourceAttributes` win on collision.
|
|
89
|
+
*/
|
|
90
|
+
detectResources?: boolean;
|
|
91
|
+
/** Base OTLP collector endpoint; defaults to the `LUNORA_OTLP_ENDPOINT` env var. */
|
|
92
|
+
endpoint?: string;
|
|
93
|
+
/** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
|
|
94
|
+
fetch?: OtelFetchLike;
|
|
95
|
+
/** Extra headers merged onto every POST — e.g. deployment/org correlation. `content-type` is set by default. */
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
/** Called with any send failure so the caller can surface it; the export itself always swallows. */
|
|
98
|
+
onError?: (error: unknown) => void;
|
|
99
|
+
/**
|
|
100
|
+
* The inbound container request, read for the telemetry context the caller's
|
|
101
|
+
* Worker dispatch propagated onto it: the W3C `traceparent` and the
|
|
102
|
+
* tail-bias toggle.
|
|
103
|
+
*
|
|
104
|
+
* The one-call form of the per-request pattern. A container that serves many
|
|
105
|
+
* requests must build a telemetry instance per request anyway (the trace
|
|
106
|
+
* context differs each call), and passing the request means the handler does
|
|
107
|
+
* not have to know which headers carry what — which is the only way the
|
|
108
|
+
* toggle's header name would otherwise reach user code.
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* const telemetry = createContainerTelemetry({ request });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
request?: {
|
|
115
|
+
headers: {
|
|
116
|
+
get: (name: string) => null | string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/** Additional resource attributes merged onto every signal. */
|
|
120
|
+
resourceAttributes?: Record<string, ContainerAttributeValue>;
|
|
121
|
+
/** `service.name` resource attribute; defaults to the `LUNORA_SERVICE_NAME` env var then `"lunora-container"`. */
|
|
122
|
+
serviceName?: string;
|
|
123
|
+
/**
|
|
124
|
+
* `service.version` resource attribute. Falls back to `SERVICE_VERSION` /
|
|
125
|
+
* `CF_VERSION_METADATA` / `VERCEL_GIT_COMMIT_SHA` / `GITHUB_SHA` /
|
|
126
|
+
* `COMMIT_SHA` env vars **only when
|
|
127
|
+
* {@link ContainerTelemetryOptions.detectResources} is `true`**.
|
|
128
|
+
*/
|
|
129
|
+
serviceVersion?: string;
|
|
130
|
+
/** Per-POST timeout in ms; a collector that never responds aborts after this so a stuck send can't stall `flush()`. Defaults to {@link DEFAULT_TIMEOUT_MS} (10s). */
|
|
131
|
+
timeoutMs?: number;
|
|
132
|
+
/** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
|
|
133
|
+
token?: string;
|
|
134
|
+
/**
|
|
135
|
+
* W3C `traceparent` of the Worker RPC that invoked this container; defaults to
|
|
136
|
+
* the `LUNORA_TRACEPARENT` env var. When present (and well-formed) every span
|
|
137
|
+
* inherits its trace id and hangs off its span id, so container spans stitch
|
|
138
|
+
* under the Worker's trace instead of forming a fresh, disconnected trace, and
|
|
139
|
+
* every log record is stamped with the same ids so a request's container logs
|
|
140
|
+
* are reachable from its trace.
|
|
141
|
+
*
|
|
142
|
+
* It also carries the trace's settled **sampling verdict**, which this exporter
|
|
143
|
+
* OBEYS rather than re-derives: a `traceparent` whose flags say the trace was
|
|
144
|
+
* sampled out (`…-00`) suppresses span export, because the worker and shard
|
|
145
|
+
* spans of that trace were dropped and shipping ours would leave the collector
|
|
146
|
+
* holding the middle of a trace. An ERRORED span is the exception — see
|
|
147
|
+
* {@link ContainerTelemetryOptions.alwaysSampleErrors}. Logs are never sampled
|
|
148
|
+
* and are unaffected.
|
|
149
|
+
*
|
|
150
|
+
* `@lunora/container` stamps this trace context on every proxied fetch
|
|
151
|
+
* (`ctx.containers.<name>.…`), so a container that serves many requests
|
|
152
|
+
* should create a telemetry instance per request — the trace context differs
|
|
153
|
+
* each call, so a single process-lifetime instance can't carry it. Prefer
|
|
154
|
+
* {@link ContainerTelemetryOptions.request}, which takes this AND the
|
|
155
|
+
* tail-bias toggle off the same request:
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* // inside the container's request handler
|
|
159
|
+
* const telemetry = createContainerTelemetry({ request });
|
|
160
|
+
* await telemetry.trace("transcode", () => transcode(job));
|
|
161
|
+
* await telemetry.flush();
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* Pass this option explicitly only to override what the request carries. The
|
|
165
|
+
* `LUNORA_TRACEPARENT` env fallback fits a one-shot container that processes
|
|
166
|
+
* a single job per start, where there is no request to read (the value is
|
|
167
|
+
* fixed for the process).
|
|
168
|
+
*/
|
|
169
|
+
traceparent?: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The exporter handle {@link createContainerTelemetry} returns.
|
|
173
|
+
*/
|
|
174
|
+
interface ContainerTelemetry {
|
|
175
|
+
/** Record one log line (no-op when disabled). */
|
|
176
|
+
emitLog: (log: ContainerLogInput) => void;
|
|
177
|
+
/** Record one span (no-op when disabled). */
|
|
178
|
+
emitSpan: (span: ContainerSpanInput) => void;
|
|
179
|
+
/** True when an endpoint resolved and exports are actually sent. */
|
|
180
|
+
readonly enabled: boolean;
|
|
181
|
+
/** Drain the span/log buffers and await every in-flight send — call before the process exits, or buffered signals are lost. */
|
|
182
|
+
flush: () => Promise<void>;
|
|
183
|
+
/** Time `run()`, recording a span named `name` (ok, or errored if it throws). Always runs `run()`, even when disabled. */
|
|
184
|
+
trace: <T>(name: string, run: () => Promise<T>, attributes?: Record<string, ContainerAttributeValue>) => Promise<T>;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Create a zero-config OTLP exporter for the container process.
|
|
188
|
+
*
|
|
189
|
+
* ```ts
|
|
190
|
+
* const telemetry = createContainerTelemetry(); // reads LUNORA_OTLP_ENDPOINT / _TOKEN
|
|
191
|
+
* await telemetry.trace("transcode", () => transcode(job), { jobId: job.id });
|
|
192
|
+
* telemetry.emitLog({ level: "info", message: "done", attributes: { jobId: job.id } });
|
|
193
|
+
* await telemetry.flush(); // REQUIRED before the process exits — see below
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* `flush()` is mandatory, not an optimisation. Spans and log records are
|
|
197
|
+
* BATCHED: nothing leaves the process until the batcher's timer elapses or
|
|
198
|
+
* `flush()` drains it, so a job that finishes and exits inside that window
|
|
199
|
+
* reports NOTHING without it. A long job between flushes can also reach the
|
|
200
|
+
* batcher's item cap, which drops the OLDEST buffered records — flush at
|
|
201
|
+
* checkpoints, not only at exit.
|
|
202
|
+
*
|
|
203
|
+
* With no endpoint resolvable the returned exporter is disabled (`enabled ===
|
|
204
|
+
* false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
|
|
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.
|
|
211
|
+
* @param options Exporter options. Connection fields (`endpoint`, `token`,
|
|
212
|
+
* `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
|
|
213
|
+
* resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
|
|
214
|
+
* `detectResources: true`.
|
|
215
|
+
*/
|
|
216
|
+
declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
|
|
217
|
+
export { type ContainerAttributeValue, type ContainerLogInput, type ContainerSpanInput, type ContainerTelemetry, type ContainerTelemetryOptions, type OtelFetchLike, createContainerTelemetry };
|
package/dist/otel.d.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An attribute value carried on a span or log.
|
|
3
|
+
*/
|
|
4
|
+
type ContainerAttributeValue = boolean | number | string;
|
|
5
|
+
/**
|
|
6
|
+
* A `fetch` implementation — defaults to the runtime global. The exporter passes
|
|
7
|
+
* an abort `signal` (for the per-request timeout) and, once the promise settles,
|
|
8
|
+
* cancels the response `body` so Node/undici can release the socket for
|
|
9
|
+
* keep-alive reuse instead of leaving it occupied by an unread stream. It reads
|
|
10
|
+
* `ok`/`status` to detect a rejected export and nothing else from the response.
|
|
11
|
+
*/
|
|
12
|
+
type OtelFetchLike = (input: string, init: {
|
|
13
|
+
body: string;
|
|
14
|
+
headers: Record<string, string>;
|
|
15
|
+
method: string;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
body?: {
|
|
19
|
+
cancel: () => Promise<void>;
|
|
20
|
+
} | null;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
status: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* A single span the container process asks the exporter to record.
|
|
26
|
+
*/
|
|
27
|
+
interface ContainerSpanInput {
|
|
28
|
+
/** Attributes attached to the span (rendered under the OTLP `attributes` list). */
|
|
29
|
+
attributes?: Record<string, ContainerAttributeValue>;
|
|
30
|
+
/** Wall-clock millis when the operation ended. */
|
|
31
|
+
endMs: number;
|
|
32
|
+
/** When set, the span is marked errored with this message (and optional `error.type`). */
|
|
33
|
+
error?: {
|
|
34
|
+
message: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
};
|
|
37
|
+
/** Span name — the operation being timed, e.g. `"transcode"`. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** Wall-clock millis when the operation started. */
|
|
40
|
+
startMs: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A single log line the container process asks the exporter to record.
|
|
44
|
+
*/
|
|
45
|
+
interface ContainerLogInput {
|
|
46
|
+
/** Attributes attached to the log record. */
|
|
47
|
+
attributes?: Record<string, ContainerAttributeValue>;
|
|
48
|
+
/** Severity — defaults to `"info"`. */
|
|
49
|
+
level?: "debug" | "error" | "info" | "warn";
|
|
50
|
+
/** The log message body. */
|
|
51
|
+
message: string;
|
|
52
|
+
/** Wall-clock millis the line was emitted; defaults to now. */
|
|
53
|
+
ts?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Options for {@link createContainerTelemetry}.
|
|
57
|
+
*/
|
|
58
|
+
interface ContainerTelemetryOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Keep a span that ERRORED even when the inbound `traceparent` says the trace
|
|
61
|
+
* was sampled out — the **tail bias**, the same rule `@lunora/runtime`'s
|
|
62
|
+
* `sampling.alwaysSampleErrors` and the shard apply.
|
|
63
|
+
*
|
|
64
|
+
* Normally you do not set this: the dispatch's verdict arrives on the
|
|
65
|
+
* inbound request (see {@link ContainerTelemetryOptions.request}), so all
|
|
66
|
+
* three tiers agree because they were told the same thing. Set it to
|
|
67
|
+
* override that. With neither, the `LUNORA_SAMPLE_ERRORS` env var applies
|
|
68
|
+
* (`"0"` turns it off) — which is what a one-shot container with no request
|
|
69
|
+
* to read has — and failing that, the tier default `true`.
|
|
70
|
+
*
|
|
71
|
+
* Per SPAN, not per trace: the container is a long-running process with no
|
|
72
|
+
* dispatch boundary to re-decide at, so a span that already settled `ok`
|
|
73
|
+
* before a sibling failed is not retro-exported. That is the same shape the
|
|
74
|
+
* worker applies to its own dispatch events.
|
|
75
|
+
*/
|
|
76
|
+
alwaysSampleErrors?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Value of the `deployment.environment` resource attribute. Falls back to
|
|
79
|
+
* the `DEPLOYMENT_ENVIRONMENT` / `ENVIRONMENT` / `NODE_ENV` env vars **only
|
|
80
|
+
* when {@link ContainerTelemetryOptions.detectResources} is `true`** —
|
|
81
|
+
* unlike `serviceName`, env detection here is opt-in so a stray `NODE_ENV`
|
|
82
|
+
* never silently labels a deployment.
|
|
83
|
+
*/
|
|
84
|
+
deploymentEnvironment?: string;
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, auto-detect OTLP resource attributes from the container
|
|
87
|
+
* environment (`HOSTNAME`, `KUBERNETES_*`, `SERVICE_VERSION`, etc.).
|
|
88
|
+
* Explicit options and `resourceAttributes` win on collision.
|
|
89
|
+
*/
|
|
90
|
+
detectResources?: boolean;
|
|
91
|
+
/** Base OTLP collector endpoint; defaults to the `LUNORA_OTLP_ENDPOINT` env var. */
|
|
92
|
+
endpoint?: string;
|
|
93
|
+
/** Injectable `fetch` (tests / non-global runtimes). Defaults to `globalThis.fetch`. */
|
|
94
|
+
fetch?: OtelFetchLike;
|
|
95
|
+
/** Extra headers merged onto every POST — e.g. deployment/org correlation. `content-type` is set by default. */
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
/** Called with any send failure so the caller can surface it; the export itself always swallows. */
|
|
98
|
+
onError?: (error: unknown) => void;
|
|
99
|
+
/**
|
|
100
|
+
* The inbound container request, read for the telemetry context the caller's
|
|
101
|
+
* Worker dispatch propagated onto it: the W3C `traceparent` and the
|
|
102
|
+
* tail-bias toggle.
|
|
103
|
+
*
|
|
104
|
+
* The one-call form of the per-request pattern. A container that serves many
|
|
105
|
+
* requests must build a telemetry instance per request anyway (the trace
|
|
106
|
+
* context differs each call), and passing the request means the handler does
|
|
107
|
+
* not have to know which headers carry what — which is the only way the
|
|
108
|
+
* toggle's header name would otherwise reach user code.
|
|
109
|
+
*
|
|
110
|
+
* ```ts
|
|
111
|
+
* const telemetry = createContainerTelemetry({ request });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
request?: {
|
|
115
|
+
headers: {
|
|
116
|
+
get: (name: string) => null | string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/** Additional resource attributes merged onto every signal. */
|
|
120
|
+
resourceAttributes?: Record<string, ContainerAttributeValue>;
|
|
121
|
+
/** `service.name` resource attribute; defaults to the `LUNORA_SERVICE_NAME` env var then `"lunora-container"`. */
|
|
122
|
+
serviceName?: string;
|
|
123
|
+
/**
|
|
124
|
+
* `service.version` resource attribute. Falls back to `SERVICE_VERSION` /
|
|
125
|
+
* `CF_VERSION_METADATA` / `VERCEL_GIT_COMMIT_SHA` / `GITHUB_SHA` /
|
|
126
|
+
* `COMMIT_SHA` env vars **only when
|
|
127
|
+
* {@link ContainerTelemetryOptions.detectResources} is `true`**.
|
|
128
|
+
*/
|
|
129
|
+
serviceVersion?: string;
|
|
130
|
+
/** Per-POST timeout in ms; a collector that never responds aborts after this so a stuck send can't stall `flush()`. Defaults to {@link DEFAULT_TIMEOUT_MS} (10s). */
|
|
131
|
+
timeoutMs?: number;
|
|
132
|
+
/** Bearer token sent as an `Authorization: Bearer` header; defaults to the `LUNORA_OTLP_TOKEN` env var. */
|
|
133
|
+
token?: string;
|
|
134
|
+
/**
|
|
135
|
+
* W3C `traceparent` of the Worker RPC that invoked this container; defaults to
|
|
136
|
+
* the `LUNORA_TRACEPARENT` env var. When present (and well-formed) every span
|
|
137
|
+
* inherits its trace id and hangs off its span id, so container spans stitch
|
|
138
|
+
* under the Worker's trace instead of forming a fresh, disconnected trace, and
|
|
139
|
+
* every log record is stamped with the same ids so a request's container logs
|
|
140
|
+
* are reachable from its trace.
|
|
141
|
+
*
|
|
142
|
+
* It also carries the trace's settled **sampling verdict**, which this exporter
|
|
143
|
+
* OBEYS rather than re-derives: a `traceparent` whose flags say the trace was
|
|
144
|
+
* sampled out (`…-00`) suppresses span export, because the worker and shard
|
|
145
|
+
* spans of that trace were dropped and shipping ours would leave the collector
|
|
146
|
+
* holding the middle of a trace. An ERRORED span is the exception — see
|
|
147
|
+
* {@link ContainerTelemetryOptions.alwaysSampleErrors}. Logs are never sampled
|
|
148
|
+
* and are unaffected.
|
|
149
|
+
*
|
|
150
|
+
* `@lunora/container` stamps this trace context on every proxied fetch
|
|
151
|
+
* (`ctx.containers.<name>.…`), so a container that serves many requests
|
|
152
|
+
* should create a telemetry instance per request — the trace context differs
|
|
153
|
+
* each call, so a single process-lifetime instance can't carry it. Prefer
|
|
154
|
+
* {@link ContainerTelemetryOptions.request}, which takes this AND the
|
|
155
|
+
* tail-bias toggle off the same request:
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* // inside the container's request handler
|
|
159
|
+
* const telemetry = createContainerTelemetry({ request });
|
|
160
|
+
* await telemetry.trace("transcode", () => transcode(job));
|
|
161
|
+
* await telemetry.flush();
|
|
162
|
+
* ```
|
|
163
|
+
*
|
|
164
|
+
* Pass this option explicitly only to override what the request carries. The
|
|
165
|
+
* `LUNORA_TRACEPARENT` env fallback fits a one-shot container that processes
|
|
166
|
+
* a single job per start, where there is no request to read (the value is
|
|
167
|
+
* fixed for the process).
|
|
168
|
+
*/
|
|
169
|
+
traceparent?: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The exporter handle {@link createContainerTelemetry} returns.
|
|
173
|
+
*/
|
|
174
|
+
interface ContainerTelemetry {
|
|
175
|
+
/** Record one log line (no-op when disabled). */
|
|
176
|
+
emitLog: (log: ContainerLogInput) => void;
|
|
177
|
+
/** Record one span (no-op when disabled). */
|
|
178
|
+
emitSpan: (span: ContainerSpanInput) => void;
|
|
179
|
+
/** True when an endpoint resolved and exports are actually sent. */
|
|
180
|
+
readonly enabled: boolean;
|
|
181
|
+
/** Drain the span/log buffers and await every in-flight send — call before the process exits, or buffered signals are lost. */
|
|
182
|
+
flush: () => Promise<void>;
|
|
183
|
+
/** Time `run()`, recording a span named `name` (ok, or errored if it throws). Always runs `run()`, even when disabled. */
|
|
184
|
+
trace: <T>(name: string, run: () => Promise<T>, attributes?: Record<string, ContainerAttributeValue>) => Promise<T>;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Create a zero-config OTLP exporter for the container process.
|
|
188
|
+
*
|
|
189
|
+
* ```ts
|
|
190
|
+
* const telemetry = createContainerTelemetry(); // reads LUNORA_OTLP_ENDPOINT / _TOKEN
|
|
191
|
+
* await telemetry.trace("transcode", () => transcode(job), { jobId: job.id });
|
|
192
|
+
* telemetry.emitLog({ level: "info", message: "done", attributes: { jobId: job.id } });
|
|
193
|
+
* await telemetry.flush(); // REQUIRED before the process exits — see below
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* `flush()` is mandatory, not an optimisation. Spans and log records are
|
|
197
|
+
* BATCHED: nothing leaves the process until the batcher's timer elapses or
|
|
198
|
+
* `flush()` drains it, so a job that finishes and exits inside that window
|
|
199
|
+
* reports NOTHING without it. A long job between flushes can also reach the
|
|
200
|
+
* batcher's item cap, which drops the OLDEST buffered records — flush at
|
|
201
|
+
* checkpoints, not only at exit.
|
|
202
|
+
*
|
|
203
|
+
* With no endpoint resolvable the returned exporter is disabled (`enabled ===
|
|
204
|
+
* false`): `emitSpan`/`emitLog` no-op and `trace` still runs its work but records
|
|
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.
|
|
211
|
+
* @param options Exporter options. Connection fields (`endpoint`, `token`,
|
|
212
|
+
* `serviceName`, `traceparent`) always fall back to their `LUNORA_*` env var;
|
|
213
|
+
* resource fields (`serviceVersion`, `deploymentEnvironment`) only do so under
|
|
214
|
+
* `detectResources: true`.
|
|
215
|
+
*/
|
|
216
|
+
declare const createContainerTelemetry: (options?: ContainerTelemetryOptions) => ContainerTelemetry;
|
|
217
|
+
export { type ContainerAttributeValue, type ContainerLogInput, type ContainerSpanInput, type ContainerTelemetry, type ContainerTelemetryOptions, type OtelFetchLike, createContainerTelemetry };
|
package/dist/otel.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as X}from"./packem_shared/abort-deadline-jwbW9Ala.mjs";import{S as G,s as K}from"./packem_shared/sampling-D21rpu_s.mjs";const k={debug:5,error:17,fatal:21,info:9,log:9,trace:1,warn:13},N=e=>`${String(Math.round(e))}000000`,z=Array.from({length:256},(e,t)=>t.toString(16).padStart(2,"0")),y=512,x=new Uint8Array(y);let T=y;const U=(e,t,r)=>{let n="";for(let o=0;o<r;o+=1)n+=z[e[t+o]];return n},P=e=>{if(e>y){const r=new Uint8Array(e);return crypto.getRandomValues(r),U(r,0,e)}T+e>y&&(crypto.getRandomValues(x),T=0);const t=U(x,T,e);return T+=e,t},A=/^[0-9a-f]+$/,W=e=>{if(e==null)return;const t=e.split("-"),[r,n,o,s]=t;if(!(t.length<4||r===void 0||r.length!==2||!A.test(r)||r==="ff"||r==="00"&&t.length!==4||n===void 0||o===void 0||s===void 0||s.length!==2||!A.test(s)||n.length!==32||o.length!==16||!A.test(n)||!A.test(o)||n==="00000000000000000000000000000000"||o==="0000000000000000"))return{parentSpanId:o,sampled:(Number.parseInt(s,16)&1)===1,traceId:n}},h=(e,t)=>typeof t=="boolean"?{key:e,value:{boolValue:t}}:typeof t=="number"?Number.isFinite(t)?Number.isSafeInteger(t)?{key:e,value:{intValue:String(t)}}:{key:e,value:{doubleValue:t}}:{key:e,value:{stringValue:String(t)}}:{key:e,value:{stringValue:t}},D=e=>e===void 0?[]:Object.entries(e).map(([t,r])=>h(t,r)),q=(e,t,r)=>{const n={},o=new Map,s=(i,a)=>{const m=i.toLowerCase(),f=o.get(m);f===void 0?(o.set(m,i),n[i]=a):n[f]=a};for(const[i,a]of Object.entries(e))s(i,a);for(const[i,a]of Object.entries(t??{}))s(i,a);return r!==void 0&&r.length>0&&s("authorization",`Bearer ${r}`),n},C=e=>Array.isArray(e)?e:[e],H=(e,t)=>{const r={"service.name":e};for(const[n,o]of Object.entries(t??{}))r[n]=o;return Object.entries(r).map(([n,o])=>h(n,o))},J=(e,t,r,n)=>({resourceSpans:[{resource:{attributes:H(r,n)},scopeSpans:[{scope:{name:t},spans:C(e)}]}]}),Q=(e,t,r,n)=>({resourceLogs:[{resource:{attributes:H(r,n)},scopeLogs:[{logRecords:C(e),scope:{name:t}}]}]}),Z=512,ee=200,V=e=>{const t=e.maxItems??Z,r=e.maxDelayMs??ee;let n=[],o,s,i;const a=()=>{o!==void 0&&(clearTimeout(o),o=void 0)},m=async()=>{a();const d=n;n=[];const u=i;s=void 0,i=void 0;try{d.length>0&&await e.export(d)}catch{}finally{u?.()}},f=d=>{s===void 0&&(s=new Promise(u=>{i=u}),o=setTimeout(()=>{m()},r)),d?.(s)};return{add:(d,u)=>{for(n.push(d);n.length>t;)n.shift();f(u),n.length>=t&&m()},flush:async d=>{if(n.length===0){a();return}const u=m();return d?.(u),u},get size(){return n.length}}},te=e=>{const t={},r=e("SERVICE_VERSION")??e("CF_VERSION_METADATA")??e("VERCEL_GIT_COMMIT_SHA")??e("GITHUB_SHA")??e("COMMIT_SHA");r!==void 0&&(t["service.version"]=r);const n=e("DEPLOYMENT_ENVIRONMENT")??e("ENVIRONMENT")??e("NODE_ENV");return n!==void 0&&(t["deployment.environment"]=n),t},re=(e,t)=>{const r={},n=e("HOSTNAME")??e("COMPUTERNAME");n!==void 0&&(r["host.name"]=n);const o=e("KUBERNETES_POD_NAME")??(e("KUBERNETES_SERVICE_HOST")===void 0?void 0:e("HOSTNAME"));return o!==void 0&&(r["k8s.pod.name"]=o),t!==void 0&&Number.isFinite(t)&&(r["process.pid"]=t),r},B=(...e)=>{const t={};for(const r of e)if(r!==void 0)for(const[n,o]of Object.entries(r))t[n]=o;return t},ne=e=>e===void 0||e!=="0",oe=1e4,$=1,E=e=>process.env[e],se=()=>{const e=typeof process.pid!="number"?void 0:process.pid;return B(te(E),re(E,e))},ie=e=>e??(typeof globalThis.fetch=="function"?globalThis.fetch:void 0),ce=(e,t)=>{const r=D(e.attributes);e.error?.type!==void 0&&r.push(h("error.type",e.error.type));const n={attributes:r,endTimeUnixNano:N(e.endMs),flags:t?.sampled??!0?$:0,kind:1,name:e.name,...t===void 0?{}:{parentSpanId:t.parentSpanId},spanId:P(8),startTimeUnixNano:N(e.startMs),status:e.error===void 0?{code:1}:{code:2,message:e.error.message},traceId:t?.traceId??P(16)};return e.error&&(n.events=[{attributes:[h("exception.type",e.error.type??"Error"),h("exception.message",e.error.message)],name:"exception",timeUnixNano:N(e.endMs)}]),n},ae=(e,t,r)=>{const n=e.level??"info";return{attributes:D(e.attributes),body:{stringValue:e.message},severityNumber:k[n],severityText:n.toUpperCase(),timeUnixNano:N(e.ts??t),...r===void 0?{}:{flags:r.sampled?$:0,spanId:r.parentSpanId,traceId:r.traceId}}},Ee=(e={})=>{const t=e.endpoint??E("LUNORA_OTLP_ENDPOINT"),r=t!==void 0&&t.length>0,n=e.token??E("LUNORA_OTLP_TOKEN"),o=e.serviceName??E("LUNORA_SERVICE_NAME")??"lunora-container",s=e.request?.headers,i=W(e.traceparent??s?.get("traceparent")??E("LUNORA_TRACEPARENT")),a=i?.sampled!==!1,m=e.alwaysSampleErrors??ne(s?.get(G)??E("LUNORA_SAMPLE_ERRORS")),f=e.timeoutMs??oe,d=ie(e.fetch),u=q({"content-type":"application/json"},e.headers,n),j=e.detectResources===!0?se():void 0,S={};e.serviceVersion!==void 0&&(S["service.version"]=e.serviceVersion),e.deploymentEnvironment!==void 0&&(S["deployment.environment"]=e.deploymentEnvironment),e.resourceAttributes!==void 0&&Object.assign(S,e.resourceAttributes);const M=B(j,S);let p=t??"";for(;p.endsWith("/");)p=p.slice(0,-1);const F=`${p}/v1/traces`,Y=`${p}/v1/logs`,O=new Set,I=(c,R)=>{if(d===void 0)return e.onError?.(new TypeError("createContainerTelemetry: no `fetch` available — pass `fetch` in options for this runtime.")),Promise.resolve();const v=(async()=>{const l=X(void 0,f,()=>new DOMException(`OTLP export to ${c} timed out after ${String(f)}ms`,"TimeoutError"));try{const g=await d(c,{body:JSON.stringify(R),headers:u,method:"POST",signal:l.signal});g.ok||e.onError?.(new Error(`createContainerTelemetry: OTLP export to ${c} failed with status ${String(g.status)}.`));try{await g.body?.cancel()}catch{}}catch(g){e.onError?.(g)}finally{l.dispose()}})().finally(()=>{O.delete(v)});return O.add(v),v},L=V({export:c=>I(F,J(c,"@lunora/container",o,M))}),w=V({export:c=>I(Y,Q(c,"@lunora/container",o,M))}),b=c=>{!r||!K({isTraced:a,keepErrors:m},c.error!==void 0)||L.add(ce(c,i))};return{emitLog:c=>{r&&w.add(ae(c,Date.now(),i))},emitSpan:b,enabled:r,flush:async()=>{await Promise.allSettled([L.flush(),w.flush()]),await Promise.allSettled(O)},trace:async(c,R,_)=>{const v=Date.now();try{const l=await R();return b({attributes:_,endMs:Date.now(),name:c,startMs:v}),l}catch(l){throw b({attributes:_,endMs:Date.now(),error:{message:l instanceof Error?l.message:String(l),type:l instanceof Error?l.name:void 0},name:c,startMs:v}),l}}}};export{Ee as createContainerTelemetry};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./abort-deadline-jwbW9Ala.mjs";import{a as e,C as A,D as T,e as s,p as C}from"./exec-BYLW70sR.mjs";export{e as CONTAINER_EXEC_HEADER,A as CONTAINER_EXEC_PATH,T as DEFAULT_EXEC_MAX_OUTPUT_BYTES,s as execViaFetch,C as pathMatchesAnyDecoding};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=Object.freeze({diskMb:Object.freeze({max:2e4,min:1}),memoryMib:Object.freeze({max:12288,min:1}),vcpu:Object.freeze({max:4,min:1})}),M=3072;export{M as CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU,e as CUSTOM_INSTANCE_TYPE_LIMITS};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import{WorkerEntrypoint as D,DurableObject as $}from"cloudflare:workers";function W(i=9){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",e=new Uint8Array(i);crypto.getRandomValues(e);let n="";for(let s=0;s<i;s++)n+=t[e[s]%t.length];return n}function U(i){if(typeof i=="number")return i;if(typeof i=="string"){const t=i.match(/^(\d+)([smh])$/);if(!t)throw new Error(`invalid time expression ${i}`);const e=parseInt(t[1]),n=t[2];switch(n){case"s":return e;case"m":return e*60;case"h":return e*60*60;default:throw new Error(`unknown time unit ${n}`)}}throw new Error(`invalid type for a time expression: ${typeof i}`)}const B="there is no container instance that can be provided to this durable object",q="you are requesting too many containers per second",T="runtime signalled the container to exit:",S="container exited with unexpected exit code:",j="container is not listening",x="__CF_CONTAINER_STATE",N="OUTBOUND_CONFIGURATION",G=3,O=100,V=180*1e3,P=5e3,K="10m",E=300,I=8e3,M=2e4,k=33;function nt(i,t){return t}const b=new Map,C=new Map,A=new Map,X={SIGINT:2,SIGTERM:15,SIGKILL:9};function v(i,t){return(i instanceof Error?i.message:String(i)).toLowerCase().includes(t)}const H=i=>v(i,B),J=i=>v(i,q),Y=i=>v(i,T),_=i=>v(i,j),z=i=>v(i,S);function Z(i){return i instanceof Error?Y(i)?+i.message.toLowerCase().slice(i.message.toLowerCase().indexOf(T)+T.length+1):z(i)?+i.message.toLowerCase().slice(i.message.toLowerCase().indexOf(S)+S.length+1):null:null}function L(i,t){const e=new AbortController;if(i?.aborted)return e.abort(),e.signal;i?.addEventListener("abort",()=>e.abort());const n=setTimeout(()=>e.abort(),t);return e.signal.addEventListener("abort",()=>clearTimeout(n)),e.signal}function R(i,t){const e=i.split("*");if(e.length===1)return i===t;if(!t.startsWith(e[0])||!t.endsWith(e[e.length-1]))return!1;let n=e[0].length;for(let s=1;s<e.length-1;s++){const o=t.indexOf(e[s],n);if(o===-1)return!1;n=o+e[s].length}return n<=t.length-e[e.length-1].length}function F(i,t){return t.some(e=>R(e,i))}function Q(i){let t=i.length;for(;t>0&&i[t-1]===".";)t--;return i.slice(0,t)}class tt{storage;status;constructor(t){this.storage=t}async setRunning(){await this.setStatusAndupdate("running")}async setHealthy(){await this.setStatusAndupdate("healthy")}async setStopping(){await this.setStatusAndupdate("stopping")}async setStopped(){await this.setStatusAndupdate("stopped")}async setStoppedIfUnchanged(t){this.status===t&&await this.setStopped()}async setStoppedWithCode(t){this.status={status:"stopped_with_code",lastChange:Date.now(),exitCode:t},await this.update()}async getState(){if(!this.status){const t=await this.storage.get(x);t?this.status=t:(this.status={status:"stopped",lastChange:Date.now()},await this.update())}return this.status}async setStatusAndupdate(t){this.status={status:t,lastChange:Date.now()},await this.update()}async update(){if(!this.status)throw new Error("status should be init");await this.storage.put(x,this.status)}}class st extends D{async fetch(t){const e=new URL(t.url),n=Q(e.hostname),{className:s,containerId:o,outboundByHostOverrides:a,outboundHandlerOverride:c,enableInternet:d,allowedHosts:r,deniedHosts:h,interceptAll:f}=this.ctx.props,p={containerId:o,className:s};if(h&&F(n,h))return new Response("Origin is disallowed",{status:520});if(r&&!F(n,r))return new Response("Origin is disallowed",{status:520});const g=b.get(s);if(a&&g){const y=a[n]??Object.entries(a).find(([l])=>l!==n&&R(l,n))?.[1];if(y&&g[y.method])return g[y.method](t,this.env,{...p,params:y.params})}const w=A.get(s);if(w){const y=w[n]??Object.entries(w).find(([l])=>l!==n&&R(l,n))?.[1];if(y)return y(t,this.env,p)}if(!f)return r||d?fetch(t):new Response("Origin is disallowed",{status:520});if(c&&g?.[c.method])return g[c.method](t,this.env,{...p,params:c.params});const u=C.get(s);return u&&g?.[u]?g[u](t,this.env,p):r||d?fetch(t):new Response("Origin is disallowed",{status:520})}}class it extends ${static get outboundByHost(){return A.get(this.name)}static set outboundByHost(t){A.set(this.name,t)}static get outboundHandlers(){return b.get(this.name)}static set outboundHandlers(t){const e=b.get(this.name)??{};b.set(this.name,{...e,...t})}static get outbound(){const t=C.get(this.name);if(t)return b.get(this.name)?.[t]}static set outbound(t){const e="__outbound__",n=b.get(this.name)??{};b.set(this.name,{...n,[e]:t}),C.set(this.name,e)}static get outboundProxies(){return this.outboundHandlers}static set outboundProxies(t){this.outboundHandlers=t}static get outboundProxy(){return this.outbound}static set outboundProxy(t){this.outbound=t}defaultPort;requiredPorts;sleepAfter=K;envVars={};entrypoint;enableInternet=!0;labels={};interceptHttps=!1;allowedHosts;deniedHosts;pingEndpoint="ping";applyOutboundInterceptionPromise=Promise.resolve();usingInterception=!1;constructor(t,e,n){if(super(t,e),t.container===void 0)throw new Error("Containers have not been enabled for this Durable Object class. Have you correctly setup your Wrangler config? More info: https://developers.cloudflare.com/containers/get-started/#configuration");this.state=new tt(this.ctx.storage);const s=this.restoreOutboundConfiguration();this.ctx.blockConcurrencyWhile(async()=>{await this.scheduleNextAlarm(),this.renewActivityTimeout();const o=this.constructor;(s!==void 0||o.outboundByHost!==void 0||o.outbound!==void 0||o.outboundHandlers!==void 0||this.effectiveAllowedHosts!==void 0||this.effectiveDeniedHosts!==void 0)&&(this.usingInterception=!0),this.container.running&&(this.applyOutboundInterceptionPromise=this.applyOutboundInterception())}),this.container=t.container,n&&(n.defaultPort!==void 0&&(this.defaultPort=n.defaultPort),n.sleepAfter!==void 0&&(this.sleepAfter=n.sleepAfter),n.envVars!==void 0&&(this.envVars=n.envVars),n.entrypoint!==void 0&&(this.entrypoint=n.entrypoint),n.enableInternet!==void 0&&(this.enableInternet=n.enableInternet)),this.sql`
|
|
2
|
+
CREATE TABLE IF NOT EXISTS container_schedules (
|
|
3
|
+
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
4
|
+
callback TEXT NOT NULL,
|
|
5
|
+
payload TEXT,
|
|
6
|
+
type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed')),
|
|
7
|
+
time INTEGER NOT NULL,
|
|
8
|
+
delayInSeconds INTEGER,
|
|
9
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
10
|
+
)
|
|
11
|
+
`,this.container.running&&(this.monitor=this.container.monitor(),this.setupMonitorCallbacks())}async getState(){return{...await this.state.getState()}}async setOutboundHandler(t,...e){this.validateOutboundHandlerMethodName(t),this.outboundHandlerOverride=e.length===0?{method:t}:{method:t,params:e[0]},await this.refreshOutboundInterception()}async setOutboundByHost(t,e,...n){this.validateOutboundHandlerMethodName(e),this.outboundByHostOverrides[t]=n.length===0?{method:e}:{method:e,params:n[0]},await this.refreshOutboundInterception()}async removeOutboundByHost(t){delete this.outboundByHostOverrides[t],await this.refreshOutboundInterception()}async setOutboundByHosts(t){for(const e of Object.values(t)){const n=typeof e=="string"?e:e.method;this.validateOutboundHandlerMethodName(n)}this.outboundByHostOverrides=Object.fromEntries(Object.entries(t).map(([e,n])=>[e,typeof n=="string"?{method:n}:n])),await this.refreshOutboundInterception()}async setAllowedHosts(t){this.allowedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async setDeniedHosts(t){this.deniedHostsOverride=[...t],this.usingInterception=!0,await this.refreshOutboundInterception()}async allowHost(t){const e=this.effectiveAllowedHosts??[];e.includes(t)||(this.allowedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async denyHost(t){const e=this.effectiveDeniedHosts??[];e.includes(t)||(this.deniedHostsOverride=[...e,t]),this.usingInterception=!0,await this.refreshOutboundInterception()}async removeAllowedHost(t){this.allowedHostsOverride=(this.effectiveAllowedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async removeDeniedHost(t){this.deniedHostsOverride=(this.effectiveDeniedHosts??[]).filter(e=>e!==t),await this.refreshOutboundInterception()}async start(t,e){const n=e?.portToCheck??this.defaultPort??(this.requiredPorts?this.requiredPorts[0]:k),s=e?.waitInterval??E;await this.startContainerIfNotRunning({signal:e?.signal,waitInterval:s,retries:e?.retries??Math.ceil(I/s),portToCheck:n},t),this.setupMonitorCallbacks(),await this.ctx.blockConcurrencyWhile(async()=>{await this.onStart()})}async startAndWaitForPorts(t,e,n){let s,o,a;typeof t=="object"&&t!==null&&!Array.isArray(t)?(s=t.ports,o=t.cancellationOptions,a=t.startOptions):(s=t,o=e,a=n);const c=await this.getPortsToCheck(s);await this.syncPendingStoppedEvents(),o??={};const d=o.instanceGetTimeoutMS??I,r=o.waitInterval??E,h=Math.ceil(d/r),f={signal:o.abort,retries:h,waitInterval:r,portToCheck:c[0]},p=await this.startContainerIfNotRunning(f,a);let w=Math.ceil((o.portReadyTimeoutMS??M)/r)-p;for(const u of c)w=await this.waitForPort({signal:o.abort,waitInterval:r,retries:w,portToCheck:u});this.setupMonitorCallbacks(),await this.ctx.blockConcurrencyWhile(async()=>{await this.state.setHealthy(),await this.onStart()})}async waitForPort(t){const e=t.portToCheck,n=this.container.getTcpPort(e),s=new Promise(c=>{t.signal?.addEventListener("abort",()=>{c(!0)})}),o=t.waitInterval??E,a=t.retries??Math.ceil(M/o);for(let c=0;c<a;c++)try{const d=L(t.signal,P);await n.fetch(`http://${this.pingEndpoint}`,{signal:d});break}catch(d){const r=d instanceof Error?d.message:String(d);if(!this.container.running){try{await this.onError(new Error("Container crashed while checking for ports, did you start the container and setup the entrypoint correctly?"))}catch{}throw d}if(c===a-1){try{await this.onError(`Failed to verify port ${e} is available after ${(c+1)*o}ms, last error: ${r}`)}catch{}throw d}if(await Promise.any([new Promise(h=>setTimeout(h,o)),s]),t.signal?.aborted)throw new Error("Container request aborted.",{cause:d})}return a}async stop(t="SIGTERM"){this.container.running&&this.container.signal(typeof t=="string"?X[t]:t),await this.syncPendingStoppedEvents()}async destroy(){await this.container.destroy()}onStart(){}onStop(t){}async onActivityExpired(){console.log("Activity expired, signalling container to stop"),this.container.running&&await this.stop()}onError(t){throw console.error("Container error:",t),t}renewActivityTimeout(){const t=U(this.sleepAfter)*1e3;this.sleepAfterMs=Date.now()+t}decrementInflight(){this.inflightRequests=Math.max(0,this.inflightRequests-1),this.inflightRequests===0&&this.renewActivityTimeout()}async schedule(t,e,n){const s=W(9);if(typeof e!="string")throw new Error("Callback must be a string (method name)");if(typeof this[e]!="function")throw new Error(`this.${e} is not a function`);if(t instanceof Date){const o=Math.floor(t.getTime()/1e3);return this.sql`
|
|
12
|
+
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, time)
|
|
13
|
+
VALUES (${s}, ${e}, ${JSON.stringify(n)}, 'scheduled', ${o})
|
|
14
|
+
`,await this.scheduleNextAlarm(),{taskId:s,callback:e,payload:n,time:o,type:"scheduled"}}if(typeof t=="number"){const o=Math.floor(Date.now()/1e3+t);return this.sql`
|
|
15
|
+
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, delayInSeconds, time)
|
|
16
|
+
VALUES (${s}, ${e}, ${JSON.stringify(n)}, 'delayed', ${t}, ${o})
|
|
17
|
+
`,await this.scheduleNextAlarm(),{taskId:s,callback:e,payload:n,delayInSeconds:t,time:o,type:"delayed"}}throw new Error("Invalid schedule type. 'when' must be a Date or number of seconds")}async containerFetch(t,e,n){const{request:s,port:o}=this.requestAndPortFromContainerFetchArgs(t,e,n),a=await this.state.getState();if(!this.container.running||a.status!=="healthy")try{await this.startAndWaitForPorts(o,{abort:s.signal})}catch(r){return H(r)?new Response(`There is no Container instance available at this time.
|
|
18
|
+
This is likely because you have reached your max concurrent instance count (set in wrangler config) or are you currently provisioning the Container.
|
|
19
|
+
If you are deploying your Container for the first time, check your dashboard to see provisioning status, this may take a few minutes.`,{status:503}):J(r)?new Response(r instanceof Error?r.message:String(r),{status:429}):new Response(`Failed to start container: ${r instanceof Error?r.message:String(r)}`,{status:500})}const c=this.container.getTcpPort(o),d=s.url.replace("https:","http:");this.inflightRequests++;try{this.renewActivityTimeout();const r=await c.fetch(d,s);if(r.webSocket!==null){const h=r.webSocket,[f,p]=Object.values(new WebSocketPair);let g=!1;const w=()=>{g||(g=!0,this.decrementInflight())};return h.accept(),p.accept(),p.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;h.send(y)}catch{p.close(1011,"Failed to forward message to container")}}),h.addEventListener("message",async u=>{this.renewActivityTimeout();try{const y=u.data instanceof Blob?await u.data.arrayBuffer():u.data;p.send(y)}catch{h.close(1011,"Failed to forward message to client")}}),p.addEventListener("close",u=>{w();const y=u.code===1005||u.code===1006?1e3:u.code;h.close(y,u.reason)}),h.addEventListener("close",u=>{w();const y=u.code===1005||u.code===1006?1e3:u.code;p.close(y,u.reason)}),p.addEventListener("error",()=>{w(),h.close(1011,"Client WebSocket error")}),h.addEventListener("error",()=>{w(),p.close(1011,"Container WebSocket error")}),new Response(null,{status:r.status,webSocket:f,headers:r.headers})}if(r.body!==null){const{readable:h,writable:f}=new IdentityTransformStream;return r.body?.pipeTo(f).finally(()=>{this.decrementInflight()}),new Response(h,r)}return this.decrementInflight(),r}catch(r){if(this.decrementInflight(),!(r instanceof Error))throw r;return r.message.includes("Network connection lost.")?new Response("Container suddenly disconnected, try again",{status:500}):(console.error(`Error proxying request to container ${this.ctx.id}:`,r),new Response(`Error proxying request to container: ${r instanceof Error?r.message:String(r)}`,{status:500}))}}async fetch(t){if(this.defaultPort===void 0&&!t.headers.has("cf-container-target-port"))throw new Error("No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port with `container.fetch(switchPort(request, port))`.");let e=this.defaultPort;if(t.headers.has("cf-container-target-port")){const n=parseInt(t.headers.get("cf-container-target-port")??"");if(isNaN(n))throw new Error("port value from switchPort is not a number");e=n}return await this.containerFetch(t,e)}container;onStopCalled=!1;state;monitor;startInFlight;monitoredPromise;sleepAfterMs=0;inflightRequests=0;outboundByHostOverrides={};outboundHandlerOverride;allowedHostsOverride;deniedHostsOverride;hasInterceptAllRegistration=!1;validateOutboundHandlerMethodName(t){const e=b.get(this.constructor.name);if(!e||!(t in e))throw new Error(`Outbound handler method '${t}' not found in outboundHandlers for ${this.constructor.name}`)}get effectiveAllowedHosts(){return this.allowedHostsOverride??this.allowedHosts}get effectiveDeniedHosts(){return this.deniedHostsOverride??this.deniedHosts}getOutboundConfiguration(){return{outboundByHostOverrides:Object.keys(this.outboundByHostOverrides).length>0?this.outboundByHostOverrides:void 0,outboundHandlerOverride:this.outboundHandlerOverride,allowedHosts:this.effectiveAllowedHosts,deniedHosts:this.effectiveDeniedHosts,hasInterceptAllRegistration:this.hasInterceptAllRegistration||void 0}}persistOutboundConfiguration(t){this.ctx.storage.kv.put(N,{...t,allowedHosts:this.allowedHostsOverride,deniedHosts:this.deniedHostsOverride})}restoreOutboundConfiguration(){const t=this.ctx.storage.kv.get(N);if(t){if(this.outboundHandlerOverride=void 0,t.outboundHandlerOverride!==void 0)try{this.validateOutboundHandlerMethodName(t.outboundHandlerOverride.method),this.outboundHandlerOverride=t.outboundHandlerOverride}catch(e){console.warn("Ignoring invalid persisted outbound handler override:",e)}this.outboundByHostOverrides={};for(const[e,n]of Object.entries(t.outboundByHostOverrides??{}))try{this.validateOutboundHandlerMethodName(n.method),this.outboundByHostOverrides[e]=n}catch(s){console.warn(`Ignoring invalid persisted outbound override for ${e}:`,s)}return this.hasInterceptAllRegistration=t.hasInterceptAllRegistration===!0,t.allowedHosts&&(this.allowedHostsOverride=t.allowedHosts),t.deniedHosts&&(this.deniedHostsOverride=t.deniedHosts),this.getOutboundConfiguration()}}needsCatchAllInterception(){return this.constructor.outbound!==void 0||this.outboundHandlerOverride!==void 0}hasMutableOutboundConfiguration(){return Object.keys(this.outboundByHostOverrides).length>0||this.allowedHostsOverride!==void 0||this.deniedHostsOverride!==void 0}shouldInterceptAllOutbound(){return this.hasInterceptAllRegistration||this.needsCatchAllInterception()||this.effectiveAllowedHosts!==void 0||this.effectiveDeniedHosts!==void 0||this.hasMutableOutboundConfiguration()}getStaticOutboundByHostKeys(){const t=this.constructor;return t.outboundByHost?Object.keys(t.outboundByHost):[]}getHostsToIntercept(){const t=new Set,e=this.constructor;if(e.outboundByHost)for(const n of Object.keys(e.outboundByHost))t.add(n);for(const n of Object.keys(this.outboundByHostOverrides))t.add(n);return[...t]}async refreshOutboundInterception(){this.usingInterception&&(this.applyOutboundInterceptionPromise=this.applyOutboundInterception(),await this.applyOutboundInterceptionPromise)}async applyOutboundInterception(){const t=this.ctx;if(t.exports===void 0)throw new Error("ctx.exports is undefined, please try to update your compatibility date or export ContainerProxy from the containers package in your worker entrypoint");if(t.exports.ContainerProxy===void 0)throw new Error("ctx.exports.ContainerProxy is undefined, export ContainerProxy from the containers package in your worker entrypoint");const e=this.shouldInterceptAllOutbound();e&&(this.hasInterceptAllRegistration=e);const n=this.getOutboundConfiguration();this.persistOutboundConfiguration(n);const s=this.getHostsToIntercept(),o={enableInternet:this.enableInternet,containerId:this.ctx.id.toString(),className:this.constructor.name,outboundByHostOverrides:n.outboundByHostOverrides,outboundHandlerOverride:n.outboundHandlerOverride,allowedHosts:n.allowedHosts,deniedHosts:n.deniedHosts,interceptAll:e},a=t.exports.ContainerProxy({props:o});if(e){for(const c of this.getStaticOutboundByHostKeys())await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a);this.interceptHttps&&await this.container.interceptOutboundHttps("*",a),await this.container.interceptAllOutboundHttp(a)}else for(const c of s)await this.container.interceptOutboundHttp(c,a),this.interceptHttps&&await this.container.interceptOutboundHttps(c,a)}sql(t,...e){const n=t.reduce((s,o,a)=>s+o+(a<e.length?"?":""),"");return[...this.ctx.storage.sql.exec(n,...e)]}requestAndPortFromContainerFetchArgs(t,e,n){let s,o;if(t instanceof Request)s=t,o=typeof e=="number"?e:void 0;else{const a=typeof t=="string"?t:t.toString(),c=typeof e=="number"?{}:e||{};o=typeof e=="number"?e:typeof n=="number"?n:void 0,s=new Request(a,c)}if(o??=this.defaultPort,o===void 0)throw new Error("No port specified for container fetch. Set defaultPort or specify a port parameter.");return{request:s,port:o}}async getPortsToCheck(t){return t!==void 0?Array.isArray(t)?t:[t]:this.requiredPorts&&this.requiredPorts.length>0?[...this.requiredPorts]:[this.defaultPort??k]}async startContainerIfNotRunning(t,e){if(this.startInFlight)return this.startInFlight;if(this.container.running)return this.monitor||(this.monitor=this.container.monitor()),0;const n=this.doStartContainer(t,e);this.startInFlight=n;try{return await n}finally{this.startInFlight===n&&(this.startInFlight=void 0)}}async doStartContainer(t,e){const n=new Promise(a=>{t.signal?.addEventListener("abort",()=>{a(!0)})}),s=t.waitInterval??E,o=t.retries??Math.ceil(I/s);for(let a=0;a<o;a++){const c=e?.envVars??this.envVars,d=e?.entrypoint??this.entrypoint,r=e?.enableInternet??this.enableInternet,h=e?.labels??this.labels,f={enableInternet:r};c&&Object.keys(c).length>0&&(f.env=c),d&&(f.entrypoint=d),h&&Object.keys(h).length>0&&(f.labels=h);const p=e?.containerSnapshot,g=e?.image??this.image,w=e?.instance??this.instance;p?f.containerSnapshot=p:g&&(f.image=g),w&&(f.instance=w),this.renewActivityTimeout();const u=async()=>{const l=await this.monitor?.catch(m=>m);if(typeof l=="number"){const m=new Error(`Container exited before we could determine the container health, exit code: ${l}`);await this.state.setStoppedWithCode(l),this.monitor=void 0;try{await this.onError(m)}catch{}throw m}else if(!H(l)){await this.state.setStopped(),this.monitor=void 0;try{await this.onError(l)}catch{}throw l}};a>0&&!this.container.running&&await u(),await this.scheduleNextAlarm(),this.container.running?await this.scheduleNextAlarm():(await this.refreshOutboundInterception(),this.container.start(f),this.monitor=this.container.monitor(),await this.state.setRunning()),this.renewActivityTimeout();const y=this.container.getTcpPort(t.portToCheck);try{const l=L(t.signal,P);return await y.fetch("http://containerstarthealthcheck",{signal:l}),a}catch(l){if(_(l)&&this.container.running)return a;if(!this.container.running&&_(l)&&await u(),await Promise.any([new Promise(m=>setTimeout(m,t.waitInterval)),n]),t.signal?.aborted)throw new Error("Aborted waiting for container to start as we received a cancellation signal",{cause:l});if(o===a+1)throw l instanceof Error&&l.message.includes("Network connection lost")&&this.ctx.abort(),await u(),await this.state.setStopped(),this.monitor=void 0,new Error(B,{cause:l});continue}}throw new Error(`Container did not start after ${o*s}ms`)}setupMonitorCallbacks(){const t=this.monitor;!t||this.monitoredPromise===t||(this.monitoredPromise=t,t.then(async()=>{await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(0)})}).catch(async e=>{if(this.monitor!==t)return;if(H(e)){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()});return}const n=Z(e);if(n!==null){await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStoppedWithCode(n)});return}if(await this.ctx.blockConcurrencyWhile(async()=>{this.monitor===t&&await this.state.setStopped()}),this.monitor===t)try{await this.onError(e)}catch{}}).finally(()=>{this.monitor===t&&(this.monitoredPromise=void 0,this.monitor=void 0)}))}deleteSchedules(t){this.sql`DELETE FROM container_schedules WHERE callback = ${t}`}async alarm(t){if(t!==void 0&&t.isRetry&&t.retryCount>G){((Number(this.sql`SELECT COUNT(*) as count FROM container_schedules`[0]?.count)||0)>0||this.container.running)&&await this.scheduleNextAlarm();return}const e=this.sql`
|
|
20
|
+
SELECT * FROM container_schedules;
|
|
21
|
+
`;let n=Date.now()+V;const s=Date.now()/1e3;for(const d of e){if(d.time>s)continue;const r=this[d.callback];if(!r||typeof r!="function"){console.error(`Callback ${d.callback} not found or is not a function`);continue}const h=this.getSchedule(d.id);try{const f=d.payload?JSON.parse(d.payload):void 0;await r.call(this,f,await h)}catch(f){console.error(`Error executing scheduled callback "${d.callback}":`,f)}this.sql`DELETE FROM container_schedules WHERE id = ${d.id}`}const o=this.sql`
|
|
22
|
+
SELECT * FROM container_schedules;
|
|
23
|
+
`,a=Math.min(...o.map(d=>d.time*1e3));if(!this.container.running){await this.syncPendingStoppedEvents(),o.length==0?await this.ctx.storage.deleteAlarm():await this.ctx.storage.setAlarm(a);return}if(this.isActivityExpired()){await this.onActivityExpired(),this.renewActivityTimeout(),await this.ctx.storage.setAlarm(Date.now()+O);return}n=Math.min(a,n,this.sleepAfterMs);const c=Math.max(n,Date.now()+O);await this.ctx.storage.setAlarm(c)}async syncPendingStoppedEvents(){const t=await this.state.getState();if(!this.container.running&&(t.status==="healthy"||t.status==="running")){await this.callOnStop({exitCode:0,reason:"exit"},t);return}if(!this.container.running&&t.status==="stopped_with_code"){await this.callOnStop({exitCode:t.exitCode??0,reason:"exit"},t);return}}async callOnStop(t,e){if(this.onStopCalled)return;this.onStopCalled=!0;const n=this.onStop(t);n instanceof Promise?await n.finally(()=>{this.onStopCalled=!1}):this.onStopCalled=!1,await this.state.setStoppedIfUnchanged(e)}async scheduleNextAlarm(t=1e3){const e=Date.now()+Math.max(t,O),n=await this.ctx.storage.getAlarm();n!==null&&n<=e||(await this.ctx.storage.setAlarm(e),await this.ctx.storage.sync())}async listSchedules(t){const e=this.sql`
|
|
24
|
+
SELECT * FROM container_schedules WHERE callback = ${t} LIMIT 1
|
|
25
|
+
`;return!e||e.length===0?[]:e.map(this.toSchedule)}toSchedule(t){let e;try{e=JSON.parse(t.payload)}catch(n){console.error(`Error parsing payload for schedule ${t.id}:`,n),e=void 0}return t.type==="delayed"?{taskId:t.id,callback:t.callback,payload:e,type:"delayed",time:t.time,delayInSeconds:t.delayInSeconds}:{taskId:t.id,callback:t.callback,payload:e,type:"scheduled",time:t.time}}async getSchedule(t){const e=this.sql`
|
|
26
|
+
SELECT * FROM container_schedules WHERE id = ${t} LIMIT 1
|
|
27
|
+
`;if(!e||e.length===0)return;const n=e[0];return this.toSchedule(n)}isActivityExpired(){return this.inflightRequests>0?(this.renewActivityTimeout(),!1):this.sleepAfterMs<=Date.now()}}export{it as Container,st as ContainerProxy,nt as outboundParams};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as g}from"@lunora/errors";import{CUSTOM_INSTANCE_TYPE_LIMITS as E,CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU as l}from"./CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU-D8Hy9Rxv.mjs";const c=new Set(["basic","lite","standard-1","standard-2","standard-3","standard-4"]),u=new Set(["lite","standard-1","standard-2","standard-3","standard-4"]),y="cloudflare/",T=e=>e.startsWith(y),C=["maxInstances","rollout"],p=100,f=128,a=/^[A-Z_]\w*$/i,h=/^\d+[smh]$/,v={h:3600,m:60,s:1},U=e=>{if(typeof e=="number")return Math.floor(e);if(h.exec(e)===null)throw new TypeError(`Invalid duration "${e}" — expected a number of seconds or "<n>[smh]"`);return Number(e.slice(0,-1))*(v[e.slice(-1)]??1)},$=e=>{const t=e.endsWith("/")?e.slice(0,-1):e,n=t.lastIndexOf("/");return n===-1?t:t.slice(n+1)},S=e=>{const t=e.lastIndexOf("/");return t===-1?".":e.slice(0,t)||"/"},L=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($(e).startsWith("Dockerfile"))return{buildContext:S(e),dockerfilePath:e,kind:"dockerfile"};const t=e.endsWith("/")?e.slice(0,-1):e;return{buildContext:t,dockerfilePath:`${t}/Dockerfile`,kind:"dockerfile"}},B=e=>`${e.charAt(0).toUpperCase()}${e.slice(1)}Container`,H=e=>`CONTAINER_${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"_").toUpperCase()}`,W=e=>`lunora-${e.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g,"-").toLowerCase()}:build`,b=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")},N=(e,t,n)=>{for(const[r,o]of Object.entries(e.secretsStore??{})){if(!a.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`)}},I=e=>{for(const r of Object.keys(e.env??{}))if(!a.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(!a.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(!a.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`)}N(e,t,n)},d=(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)})`)},k=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&&d(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)})`)}},m=(e,t)=>{if(e!==void 0){if(typeof e=="string"){if(!h.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)})`)}},P=e=>{const{backups:t}=e;if(t!==void 0){if(e.sandbox!==!0)throw new TypeError("defineContainer: `backups` needs `sandbox: true` — directory backups are a Sandbox SDK helper");if(typeof t.bucket!="string"||t.bucket.trim().length===0)throw new TypeError("defineContainer: `backups.bucket` must name an R2 bucket binding");if(t.prefix!==void 0&&(typeof t.prefix!="string"||t.prefix.length>0&&!t.prefix.endsWith("/")))throw new TypeError("defineContainer: `backups.prefix` must be a string ending in `/`, or omitted")}},_=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")},A=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)d(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.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");if(k(e),e.sandbox!==void 0&&typeof e.sandbox!="boolean")throw new TypeError("defineContainer: `sandbox` must be a boolean, or omitted");P(e)},O=e=>typeof e=="string"&&e.startsWith("registry.cloudflare.com/")&&e.includes("@sha256:"),M=(e,t)=>{if(e.length===0||e.length>f)throw new TypeError(`defineContainer: image name "${e}" must be 1–${String(f)} characters`);if(typeof t=="string"){b(t);return}if(!O(t.registry))throw new TypeError(`defineContainer: \`images["${e}"].registry\` must be a digest-pinned (@sha256) reference under registry.cloudflare.com — push other registries' images there first`)},x=e=>{for(const[r,{max:o,min:s}]of Object.entries(E)){const i=e[r];if(typeof i!="number"||Number.isNaN(i)||i<s||i>o){const w=r==="vcpu"?' — below 1 vCPU use a named runtime size such as "lite" or "standard-1"':"";throw new TypeError(`defineContainer: a custom \`instanceType\` under schedulingPolicy "durable_object" needs ${r} in ${String(s)}–${String(o)} (got ${String(i)})${w}`)}}const{memoryMib:t,vcpu:n}=e;if(t<n*l)throw new TypeError(`defineContainer: a custom \`instanceType\` needs ≥ ${String(l)} MiB memory per vCPU (got ${String(t)} MiB for ${String(n)} vCPU)`)},D=e=>{for(const r of C)if(e[r]!==void 0)throw new TypeError(`defineContainer: \`${r}\` is not supported with schedulingPolicy "durable_object"`);const t=Object.entries(e.images??{});if(t.length>p)throw new TypeError(`defineContainer: \`images\` holds at most ${String(p)} entries (got ${String(t.length)})`);for(const[r,o]of t)M(r,o);if(e.image!==void 0&&!Object.hasOwn(e.images??{},e.image)&&!T(e.image))throw new TypeError(`defineContainer: \`image\` "${e.image}" must name an entry of \`images\` or a Cloudflare-managed "${y}…" image`);const{instanceType:n}=e;if(typeof n=="string"&&!u.has(n))throw new TypeError(`defineContainer: \`instanceType\` "${n}" is not a runtime size — use one of ${[...u].join(", ")}, or a full { vcpu, memoryMib, diskMb } object`);typeof n=="object"&&x(n)},j=e=>{if(e.images!==void 0)throw new TypeError('defineContainer: `images` needs schedulingPolicy "durable_object"');b(e.image);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"&&!c.has(e.instanceType))throw new TypeError(`defineContainer: unknown \`instanceType\` "${e.instanceType}" — use one of ${[...c].join(", ")}, or a custom { vcpu, memoryMib, diskMb } object`)},q=e=>{const t=e.schedulingPolicy;if(e.schedulingPolicy==="durable_object")D(e);else if(t===void 0||t==="default")j(e);else throw new TypeError(`defineContainer: unknown \`schedulingPolicy\` ${JSON.stringify(t)} — use "default" or "durable_object"`);return e.defaultPort!==void 0&&d(e.defaultPort,"defaultPort"),m(e.sleepAfter,"sleepAfter"),m(e.hardTimeout,"hardTimeout"),I(e),A(e),{...e,isLunoraContainer:!0}},z=e=>typeof e=="object"&&e!==null&&e.isLunoraContainer===!0,Y=(e,t,n)=>{const r={...e.env};for(const o of e.secrets??[]){const s=t[o];if(typeof s!="string"){const i=n===void 0?"container":`container "${n}"`;throw new g("INTERNAL",`${i}: 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]=s}return r};export{C as DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS,H as containerBindingName,W as containerBuildTag,B as containerClassName,q as defineContainer,O as isCloudflareRegistryDigest,z as isContainerDefinition,T as isManagedImage,L as normalizeContainerImage,U as parseDurationSeconds,Y as resolveContainerEnvVars};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SandboxFileError as u,SandboxBackupError as d,SandboxS3MountError as l,SandboxProtocolError as p,Files as w,DirectoryBackup as f,S3Mount as b}from"@cloudflare/sandbox";import{LunoraError as s}from"@lunora/errors";import{LunoraContainer as y}from"../do/index.mjs";const m={EACCES:"FORBIDDEN",EBUSY:"CONFLICT",EEXIST:"CONFLICT",ENOENT:"NOT_FOUND",ENOTEMPTY:"CONFLICT",EPERM:"FORBIDDEN"},N={BACKUP_INTEGRITY:"CONFLICT",BACKUP_NOT_FOUND:"NOT_FOUND",BACKUP_TRANSFER:"INTERNAL"},E={S3_MOUNT_BUSY:"CONFLICT",S3_MOUNT_CONFLICT:"CONFLICT",S3_MOUNT_FAILED:"INTERNAL",S3_MOUNT_INCOMPATIBLE:"BAD_REQUEST"},h=(n,t)=>{if(u.is(n)){const e=n.destination===void 0?n.path:`${n.path} → ${n.destination}`;return new s(m[n.code]??"BAD_REQUEST",`${t}: ${n.operation} ${e} failed with ${n.code}: ${n.detail}`,{cause:n,data:{errno:n.code,operation:n.operation,path:n.path,...n.destination===void 0?{}:{destination:n.destination}}})}return d.is(n)?new s(N[n.code]??"INTERNAL",`${t}: ${n.operation} of ${n.path} failed (${n.code}): ${n.detail}`,{cause:n,data:{operation:n.operation,path:n.path,reason:n.code}}):l.is(n)?new s(E[n.code]??"INTERNAL",`${t}: ${n.operation} at ${n.path} failed (${n.code}): ${n.detail}`,{cause:n,data:{operation:n.operation,path:n.path,reason:n.code}}):p.is(n)?new s("INTERNAL",`${t}: the container did not answer the sandbox helper protocol: ${n.detail}`,{cause:n,hint:"The image needs `sandbox-shim` at `/usr/local/bin/sandbox-shim` in a version matching `@cloudflare/sandbox`. Copy it from Cloudflare's `cloudflare/sandbox` image (`COPY --from=docker.io/cloudflare/sandbox:<version> /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim`)."}):n},k=(n,t)=>{if(n.body===null)return t(),n;const{readable:e,writable:i}=new TransformStream;return n.body.pipeTo(i).finally(t).catch(()=>{}),new Response(e,n)};class O extends y{#t;#e;#n;#i;constructor(t,e,i,a,o){super(t,e,i,a,o),this.#t=i.backups}async lunoraReadFile(t,e={}){const{container:i,release:a}=await this.lunoraAcquire("files.readFile");try{return k(await this.files(i).readFile(t,e),a)}catch(o){throw a(),h(o,this.label("files.readFile"))}}async lunoraWriteFile(t,e,i={}){await this.withContainer("files.writeFile",async a=>this.files(a).writeFile(t,e,i))}async lunoraStat(t,e={}){return this.withContainer("files.stat",async i=>this.files(i).stat(t,e))}async lunoraReadDirectory(t,e={}){return this.withContainer("files.readDirectory",async i=>this.files(i).readDirectory(t,e))}async lunoraMkdir(t,e={}){await this.withContainer("files.mkdir",async i=>this.files(i).mkdir(t,e))}async lunoraRename(t,e,i={}){await this.withContainer("files.rename",async a=>this.files(a).rename(t,e,i))}async lunoraRemove(t,e={}){await this.withContainer("files.remove",async i=>this.files(i).remove(t,e))}async lunoraBackup(t,e={}){const i={dir:t,...e.exclude===void 0?{}:{exclude:[...e.exclude]},...e.gitignore===void 0?{}:{gitignore:e.gitignore},...e.name===void 0?{}:{name:e.name}};return this.withContainer("backup",async a=>this.backups(a).backup(i))}async lunoraRestore(t,e={}){const i=e.directory===void 0?{}:{dir:e.directory};await this.withContainer("restore",async a=>this.backups(a).restore(t,i))}async lunoraDeleteBackup(t){const e=this.ctx.container;if(e===void 0)throw new s("INTERNAL",`${this.label("deleteBackup")}: this Durable Object has no container binding`);try{await this.backups(e).delete(t)}catch(i){throw h(i,this.label("deleteBackup"))}}async lunoraMount(t){if(this.usingInterception)throw new s("BAD_REQUEST",`${this.label("mount")}: bucket mounts cannot be combined with an egress policy (allowedHosts, deniedHosts, interceptHttps, outbound handlers or runtime egress controls) on the same container`);const e={bucket:t.bucket,credentials:{type:"static",...this.mountCredentials(t)},endpoint:t.endpoint,region:t.region,type:"s3"},i={access:t.access,mountPath:t.path,source:e,...t.keyPrefix===void 0?{}:{keyPrefix:t.keyPrefix},...t.s3fsOptions===void 0?{}:{s3fsOptions:t.s3fsOptions}};await this.withContainer("mount",async a=>this.mounts(a).mount(i))}async lunoraInspectMount(t){return this.withContainer("inspectMount",async e=>this.mounts(e).inspect(t))}async lunoraUnmount(t){await this.withContainer("unmount",async e=>this.mounts(e).unmount(t))}async beforeContainerStart(){await super.beforeContainerStart();const t=this.ctx.container;this.#t!==void 0&&this.usingInterception&&t!==void 0&&await this.backups(t).intercept()}async withContainer(t,e){const{container:i,release:a}=await this.lunoraAcquire(t);try{return await e(i)}catch(o){throw h(o,this.label(t))}finally{a()}}files(t){return this.#e??=new w(t),this.#e}backups(t){if(this.#t===void 0)throw new s("BAD_REQUEST",`container "${this.lunoraName}": backups need \`backups: { bucket }\` in lunora/containers.ts`);const{bucket:e}=this.#t;if(typeof this.env[e]?.get!="function")throw new s("INTERNAL",`container "${this.lunoraName}": backups.bucket "${e}" is not an R2 bucket binding on the Worker env — add an r2_buckets entry with binding "${e}" to wrangler.jsonc`);return this.#n??=new f(t,this.workerExport("DirectoryBackupGateway"),{binding:this.#t.bucket,...this.#t.prefix===void 0?{}:{prefix:this.#t.prefix}}),this.#n}mounts(t){return this.#i??=new b(t,this.workerExport("S3Gateway")),this.#i}workerExport(t){const e=this.ctx.exports?.[t];if(e===void 0)throw new s("INTERNAL",`container "${this.lunoraName}": the worker does not export ${t}. Re-export the generated containers file from the worker entry (\`export * from "./lunora/_generated/containers.js"\`).`);return e}mountCredentials(t){const e=c=>{const r=this.env[c];if(typeof r!="string"||r.length===0)throw new s("INTERNAL",`${this.label("mount")}: credential secret "${c}" is not set on the Worker env — add it with \`wrangler secret put ${c}\` (or .dev.vars)`);return r},{accessKeyIdSecret:i,secretAccessKeySecret:a,sessionTokenSecret:o}=t.credentials;return{accessKeyId:e(i),secretAccessKey:e(a),...o===void 0?{}:{sessionToken:e(o)}}}label(t){return`container "${this.lunoraName}": ${t}()`}}export{O as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i=(o,e,t)=>{if(e===void 0)return{dispose:()=>{},signal:o};const r=new AbortController,n=setTimeout(()=>{r.abort(t())},e);return{dispose:()=>{clearTimeout(n)},signal:o===void 0?r.signal:AbortSignal.any([o,r.signal])}};export{i as a};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as N}from"@lunora/errors";import{e as I,p as J,a as x,r as G}from"./exec-BYLW70sR.mjs";import{S as K}from"./sampling-D21rpu_s.mjs";import{containerBindingName as $}from"./DURABLE_OBJECT_POLICY_FORBIDDEN_KEYS-gYi8RBUO.mjs";import{a as Z}from"./jurisdiction-DtE9s70w.mjs";import"./CUSTOM_INSTANCE_MIN_MEMORY_MIB_PER_VCPU-D8Hy9Rxv.mjs";const ee=(e,t)=>{if(e.length===0)throw new TypeError("spawn: `command` must be a non-empty string");return{command:e,...t.args===void 0?{}:{args:[...t.args]},...t.cwd===void 0?{}:{cwd:t.cwd},...t.env===void 0?{}:{env:{...t.env}},...t.pty===void 0?{}:{pty:{...t.pty}},...t.stdin===void 0?{}:{stdin:t.stdin},...t.timeoutMs===void 0?{}:{timeoutMs:t.timeoutMs}}},te=(e,t)=>{const{control:r}=e,o=r.exitCode();if(t!==void 0){const n=()=>{Promise.resolve(r.kill()).catch(()=>{})};t.aborted?n():(t.addEventListener("abort",n,{once:!0}),o.finally(()=>{t.removeEventListener("abort",n)}).catch(()=>{}))}return{exitCode:o,isPty:e.isPty,kill:async n=>{await r.kill(n)},pid:e.pid,resize:async(n,s)=>{await r.resize(n,s)},stderr:e.stderr,stdin:e.stdin,stdout:e.stdout}},re=1048576,oe=80,ne=24,B=(e,t)=>{const r=Number(e.searchParams.get(t));return Number.isInteger(r)&&r>0?r:void 0},se={pair:()=>{const{WebSocketPair:e}=globalThis;if(e===void 0)throw new TypeError("terminal(): WebSocketPair is not available — open a terminal from a Worker or Durable Object");const t=new e;return[t[0],t[1]]},upgrade:e=>new Response(void 0,{status:101,webSocket:e})},ae=e=>{try{const t=JSON.parse(e);return Number.isInteger(t.cols)&&Number.isInteger(t.rows)?{cols:t.cols,rows:t.rows}:void 0}catch{return}},ie=async e=>{if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer())},ce=e=>e instanceof ArrayBuffer||ArrayBuffer.isView(e)?e.byteLength:e instanceof Blob?e.size:0,le=async(e,t)=>{const r=e.stdout?.getReader();try{for(;r!==void 0;){const{done:n,value:s}=await r.read();if(n)break;t.send(s)}const o=await e.exitCode;t.close(1e3,`exited with code ${String(o)}`)}catch{t.close(1011,"terminal stream failed")}},de=async(e,t,r={},o=se)=>{if(t.headers.get("upgrade")?.toLowerCase()!=="websocket")return new Response("terminal(): expected a WebSocket upgrade request",{status:426});const n=new URL(t.url),{command:s="sh",cols:c,rows:a,...i}=r,u=await e(s,{...i,env:{TERM:"xterm-256color",...i.env},pty:{cols:c??B(n,"cols")??oe,rows:a??B(n,"rows")??ne},stdin:!0}),[v,m]=o.pair(),d=u.stdin?.getWriter(),f=()=>{u.kill().catch(()=>{}),d?.close().catch(()=>{})};let p=Promise.resolve();const h=y=>{p=p.then(y).catch(f)};let R=0;const S=(y,g)=>{if(R+=y,R>re){m.close(1009,"terminal input backlog too large"),f();return}h(async()=>{try{const E=await g;E!==void 0&&await d?.write(E)}finally{R-=y}})};return m.accept(),m.addEventListener("message",({data:y})=>{if(typeof y!="string"){S(ce(y),ie(y));return}const g=ae(y);if(g===void 0){const E=new TextEncoder().encode(y);S(E.byteLength,Promise.resolve(E))}else h(async()=>u.resize(g.cols,g.rows).catch(()=>{}))}),m.addEventListener("close",f),m.addEventListener("error",f),le(u,m).catch(()=>{}),o.upgrade(v)},ue={EEXIST:"CONFLICT",ENOENT:"NOT_FOUND",ENOTEMPTY:"CONFLICT"},w=(e,t,r)=>new N(ue[e]??"BAD_REQUEST",`test container: ${t} ${r} failed with ${e}`,{data:{errno:e,operation:t,path:r}}),T=(e,t={})=>{const r=e.startsWith("/")?e:`${t.cwd??""}/${e}`;if(!r.startsWith("/"))throw new TypeError(`test container: "${e}" is relative and no absolute \`cwd\` was given`);const o=[];for(const n of r.split("/"))n===".."?o.pop():n!==""&&n!=="."&&o.push(n);return`/${o.join("/")}`},C=e=>e.slice(0,e.lastIndexOf("/"))||"/",fe=async e=>typeof e=="string"?new TextEncoder().encode(e):e instanceof ArrayBuffer?Uint8Array.from(new Uint8Array(e)):ArrayBuffer.isView(e)?new Uint8Array(e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength)):new Uint8Array(await new Response(e).arrayBuffer()),L=(e,t)=>{const r=new Date;return{accessedAt:r,changedAt:r,gid:0,modifiedAt:r,mode:e==="directory"?16877:33188,size:BigInt(t),type:e,uid:0}},ye=()=>{const e=new Map,t=new Set(["/"]),r=s=>e.has(s)||t.has(s),o=s=>[...e.keys(),...t].filter(c=>c!==s&&C(c)===s).toSorted((c,a)=>c.localeCompare(a)),n=(s,c)=>{const a=C(s);if(e.has(a))throw w("ENOTDIR",c,s);if(!t.has(a))throw w("ENOENT",c,s)};return{mkdir:async(s,c={})=>{const a=T(s,c);if(c.recursive===!0){let i="";for(const u of a.split("/").filter(Boolean)){if(i=`${i}/${u}`,e.has(i))throw w("ENOTDIR","mkdir",i);t.add(i)}return}if(r(a))throw w("EEXIST","mkdir",a);n(a,"mkdir"),t.add(a)},readDirectory:async(s,c)=>{const a=T(s,c);if(e.has(a))throw w("ENOTDIR","readDirectory",a);if(!t.has(a))throw w("ENOENT","readDirectory",a);return o(a).map(i=>({name:i.slice(i.lastIndexOf("/")+1),type:t.has(i)?"directory":"file"}))},readFile:async(s,c)=>{const a=T(s,c),i=e.get(a);if(i===void 0)throw w(t.has(a)?"EISDIR":"ENOENT","readFile",a);return new Response(new Uint8Array(i))},remove:async(s,c={})=>{const a=T(s,c);if(!e.delete(a)){if(!t.has(a)){if(c.force!==!0)throw w("ENOENT","remove",a);return}if(c.recursive!==!0)throw w(o(a).length>0?"ENOTEMPTY":"EISDIR","remove",a);for(const i of[...e.keys(),...t])(i===a||i.startsWith(`${a}/`))&&(e.delete(i),t.delete(i));t.add("/")}},rename:async(s,c,a)=>{const i=T(s,a),u=T(c,a);if(!r(i))throw w("ENOENT","rename",i);if(n(u,"rename"),u.startsWith(`${i}/`))throw w("EINVAL","rename",i);const v=[...e],m=[...t];for(const[d,f]of v)(d===i||d.startsWith(`${i}/`))&&(e.delete(d),e.set(u+d.slice(i.length),f));for(const d of m)(d===i||d.startsWith(`${i}/`))&&(t.delete(d),t.add(u+d.slice(i.length)))},stat:async(s,c)=>{const a=T(s,c),i=e.get(a);if(i!==void 0)return L("file",i.byteLength);if(t.has(a))return L("directory",4096);throw w("ENOENT","stat",a)},writeFile:async(s,c,a)=>{const i=T(s,a);if(t.has(i))throw w("EISDIR","writeFile",i);n(i,"writeFile"),e.set(i,await fe(c))}}},k=3,U=3e4,me=3,we=500,he="cf-container-target-port",M="__lunora",Ee=e=>{const t=e.indexOf(";");return t===-1?e:e.slice(0,t)},ve=e=>{const t=e.split("/").find(r=>r!=="")??"";return Ee(t).toLowerCase()},H=(e,t)=>{const r=typeof e=="string"?e:e.url,o=URL.parse(r.startsWith("/")?`http://container${r}`:r);if(o!==null&&J(o.pathname,n=>ve(n)===M))throw new N("BAD_REQUEST",`${t}: \`/${M}/*\` is reserved for Lunora's own container routes and cannot be reached with \`fetch\`. Use \`exec\` to run a command.`)},W=(e,t)=>{if(typeof e!="string"){const o=new Request(e,t);return o.headers.delete(x),[o,void 0]}if(t?.headers===void 0)return[e,t];const r=new Headers(t.headers);return r.delete(x),[e,{...t,headers:r}]},z=(e,t,r,o)=>{const n=typeof e=="string"&&e.startsWith("/")?new Request(`http://container${e}`,t):new Request(e,t);return r!==void 0&&n.headers.set(he,String(r)),o?.traceparent!==void 0&&n.headers.set("traceparent",o.traceparent),o?.sampleErrors!==void 0&&n.headers.set(K,o.sampleErrors?"1":"0"),n},j=async e=>{e<=0||await new Promise(t=>{setTimeout(t,e)})},pe=/no container instance|not listening|try again later|rate.?limit|provision/i,Te="no Container instance available",Re="Failed to start container:",ge=1024,Ae=async e=>(await G(e.clone().body,ge)).text,X=e=>e instanceof Error&&pe.test(e.message),V=async e=>{if(e.status===429)return!0;if(e.status!==500&&e.status!==503)return!1;try{const t=await Ae(e);return t.includes(Te)||t.startsWith(Re)}catch{return!1}},O=e=>`ctx.containers.${e.exportName}`,_=(e,t,r={},o,n)=>{const s=Math.max(1,r.attempts??me),c=r.backoffMs??we,a=r.maxBackoffMs??U,i=async(u,v)=>{const m=typeof u=="string"?s:1;let d;for(let f=0;f<m;f+=1){const p=f===m-1;f>0&&await j(Math.min(c*2**(f-1),a));try{const h=await e(z(u,v,o,n),v?.signal);if(p||!await V(h))return h}catch(h){if(d=h,p||!X(h))throw h}}throw d instanceof Error?d:new Error("ctx.containers: cold-start retry exhausted")};return{exec:I(i,t),fetch:async(u,v)=>(H(u,t),i(...W(u,v))),port:u=>_(e,t,r,u,n)}},Se=async(e,t)=>{if(t.aborted)throw e.catch(()=>{}),t.reason;const r=new AbortController,o=new Promise((n,s)=>{t.addEventListener("abort",()=>{s(t.reason)},{once:!0,signal:r.signal})});try{return await Promise.race([e,o])}finally{r.abort(),e.catch(()=>{})}},F=async(e,t,r)=>{if(!t.headers.has(x))return e.fetch(t);if(typeof e.lunoraExec!="function")throw new TypeError("ctx.containers: this container DO does not expose lunoraExec() — is @lunora/container/do up to date?");t.headers.delete(x);const o=e.lunoraExec(new Request(t.url,{body:await t.text(),headers:t.headers,method:t.method}));return r?Se(o,r):o},Y=(e,t,r,o,n)=>_(async(s,c)=>F(e.get(e.idFromName(t)),s,c),r,o,void 0,n),Ne=new Set(["lunoraBackup","lunoraDeleteBackup","lunoraInspectMount","lunoraMkdir","lunoraMount","lunoraReadDirectory","lunoraReadFile","lunoraRemove","lunoraRename","lunoraRestore","lunoraStat","lunoraUnmount","lunoraWriteFile"]),l=async(e,t,r,...o)=>{const n=e[t];if(typeof n!="function")throw Ne.has(t)?new N("BAD_REQUEST",`ctx.containers: the "${r}" container DO does not expose ${t}() — sandbox helpers need \`sandbox: true\` in its defineContainer() definition`):new TypeError(`ctx.containers: the "${r}" container DO does not expose ${t}() — is @lunora/container/do up to date?`);return n(...o)},Oe=(e,t)=>({backup:async(r,o)=>l(e(),"lunoraBackup",t,r,o),deleteBackup:async r=>l(e(),"lunoraDeleteBackup",t,r),files:{mkdir:async(r,o)=>l(e(),"lunoraMkdir",t,r,o),readDirectory:async(r,o)=>l(e(),"lunoraReadDirectory",t,r,o),readFile:async(r,o)=>l(e(),"lunoraReadFile",t,r,o),remove:async(r,o)=>l(e(),"lunoraRemove",t,r,o),rename:async(r,o,n)=>l(e(),"lunoraRename",t,r,o,n),stat:async(r,o)=>l(e(),"lunoraStat",t,r,o),writeFile:async(r,o,n)=>l(e(),"lunoraWriteFile",t,r,o,n)},inspectMount:async r=>l(e(),"lunoraInspectMount",t,r),mount:async r=>l(e(),"lunoraMount",t,r),restore:async(r,o)=>l(e(),"lunoraRestore",t,r,o),unmount:async r=>l(e(),"lunoraUnmount",t,r)}),De=(e,t)=>({allow:async r=>l(e(),"allowHost",t,r),deny:async r=>l(e(),"denyHost",t,r),removeAllowed:async r=>l(e(),"removeAllowedHost",t,r),removeDenied:async r=>l(e(),"removeDeniedHost",t,r),setAllowed:async r=>l(e(),"setAllowedHosts",t,[...r]),setDenied:async r=>l(e(),"setDeniedHosts",t,[...r])}),Q=(e,t,r,o,n)=>{const s=()=>e.get(e.idFromName(r)),c=async(a,i={})=>te(await l(s(),"lunoraSpawn",t.binding,ee(a,i)),i.signal);return{..._(async(a,i)=>F(s(),a,i),O(t),o,void 0,n),...Oe(s,t.binding),destroy:async()=>l(s(),"destroy",t.binding),egress:De(s,t.binding),getState:async()=>l(s(),"getState",t.binding),renewActivityTimeout:async()=>l(s(),"renewActivityTimeout",t.binding),snapshot:async a=>l(s(),"lunoraSnapshot",t.binding,a),spawn:c,start:async a=>l(s(),"start",t.binding,a),stop:async a=>l(s(),"stop",t.binding,a),terminal:async(a,i)=>de(c,a,i)}},D="pool-",P=e=>`${D}${String(Math.floor(Math.random()*e))}`,q=(e,t)=>{if(e.startsWith(D))throw new N("BAD_REQUEST",`${O(t)}.get(${JSON.stringify(e)}): the "${D}" 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 "${D}".`)},xe=e=>e.status>=500,b=(e,t,r={},o,n)=>{const s=r.size??t.maxInstances??k,c=Math.max(1,r.attempts??3),a=r.backoffMs??100,i=r.maxBackoffMs??U,u=`${O(t)}.pool()`,v=(d,f)=>async(p,h)=>{const R=typeof p=="string"?c:1;let S;for(let y=0;y<R;y+=1){y>0&&await j(Math.min(a*2**(y-1),i));const g=z(p,h,o,n);try{const E=await F(e.get(e.idFromName(P(s))),g,h?.signal);if(y===R-1||!await d(E))return E}catch(E){if(S=E,!f(E))throw E}}throw S instanceof Error?S:new Error(`ctx.containers.${t.exportName}.pool(): all ${String(R)} attempts failed`)},m=v(r.retryOn??xe,()=>!0);return{exec:I(v(V,X),u),fetch:async(d,f)=>(H(d,u),m(...W(d,f))),port:d=>b(e,t,r,d,n)}},ke=(e,t,r)=>({any:(o,n)=>Y(e,P(o??t.maxInstances??k),O(t),n,r),get:(o,n)=>(q(o,t),Q(e,t,o,n,r)),pool:o=>b(e,t,o,void 0,r)}),_e=e=>{const t=()=>{throw new N("INTERNAL",`ctx.containers.${e.exportName}: no "${e.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:t,get:t,pool:t}},Fe=(e,t,r,o,n)=>{const s={};for(const c of t){const a=e[c.binding];s[c.exportName]=a&&typeof a.idFromName=="function"&&typeof a.get=="function"?ke(Z(a,r),c,{...n===void 0?{}:{sampleErrors:n},...o===void 0?{}:{traceparent:o}}):_e(c)}return s},$e=(e,t,r,o={})=>{const{jurisdiction:n,...s}=o,c={binding:$(t),exportName:t};return Fe(e,[c],n)[t].get(r,s)},A=e=>()=>Promise.reject(new N("NOT_IMPLEMENTED",`createContainerTestContext: ${e}() needs a real container — the test double has no processes or buckets. Stub the handle method in your test instead.`)),Pe=e=>{const t=new Map,r=o=>{const n=t.get(o)??ye();return t.set(o,n),{allowHost:()=>Promise.resolve(),denyHost:()=>Promise.resolve(),destroy:()=>Promise.resolve(),fetch:s=>Promise.resolve(e(s,{name:o})),lunoraBackup:A("backup"),lunoraDeleteBackup:A("deleteBackup"),lunoraExec:s=>Promise.resolve(e(s,{name:o})),lunoraInspectMount:A("inspectMount"),lunoraMkdir:n.mkdir,lunoraMount:A("mount"),lunoraReadDirectory:n.readDirectory,lunoraReadFile:n.readFile,lunoraRemove:n.remove,lunoraRename:n.rename,lunoraRestore:A("restore"),lunoraSnapshot:s=>Promise.resolve({id:`test-snapshot-${o}`,size:0,...s?.name===void 0?{}:{name:s.name}}),lunoraSpawn:A("spawn"),lunoraStat:n.stat,lunoraUnmount:A("unmount"),lunoraWriteFile:n.writeFile,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=>r(String(o)),idFromName:o=>o}},Ue=e=>{const t={};for(const[r,o]of Object.entries(e)){const n=Pe(o),s={binding:$(r),exportName:r};t[r]={any:(c,a)=>Y(n,P(c??s.maxInstances??k),O(s),{attempts:1,...a}),get:(c,a)=>(q(c,s),Q(n,s,c,{attempts:1,...a})),pool:c=>b(n,s,{...c,attempts:1})}}return t};export{Fe as createContainerContext,Ue as createContainerTestContext,$e as getContainer};
|