@lunora/container 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,224 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
3
+ const NAMED_INSTANCE_TYPES = /* @__PURE__ */ new Set(["basic", "lite", "standard-1", "standard-2", "standard-3", "standard-4"]);
4
+ const ENV_NAME_PATTERN = /^[A-Z_]\w*$/i;
5
+ const SLEEP_AFTER_PATTERN = /^\d+[smh]$/;
6
+ const DURATION_UNIT_SECONDS = { h: 3600, m: 60, s: 1 };
7
+ const parseDurationSeconds = (duration) => {
8
+ if (typeof duration === "number") {
9
+ return Math.floor(duration);
10
+ }
11
+ const match = SLEEP_AFTER_PATTERN.exec(duration);
12
+ if (match === null) {
13
+ throw new TypeError(`Invalid duration "${duration}" — expected a number of seconds or "<n>[smh]"`);
14
+ }
15
+ return Number(duration.slice(0, -1)) * (DURATION_UNIT_SECONDS[duration.slice(-1)] ?? 1);
16
+ };
17
+ const basename = (path) => {
18
+ const trimmed = path.endsWith("/") ? path.slice(0, -1) : path;
19
+ const separatorIndex = trimmed.lastIndexOf("/");
20
+ return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 1);
21
+ };
22
+ const dirname = (path) => {
23
+ const separatorIndex = path.lastIndexOf("/");
24
+ return separatorIndex === -1 ? "." : path.slice(0, separatorIndex) || "/";
25
+ };
26
+ const normalizeContainerImage = (image) => {
27
+ if (typeof image !== "string") {
28
+ if ("build" in image) {
29
+ const buildDirectory = image.build.endsWith("/") ? image.build.slice(0, -1) : image.build;
30
+ return { buildDir: buildDirectory, kind: "build" };
31
+ }
32
+ return { kind: "registry", reference: image.registry };
33
+ }
34
+ if (basename(image).startsWith("Dockerfile")) {
35
+ return { buildContext: dirname(image), dockerfilePath: image, kind: "dockerfile" };
36
+ }
37
+ const context = image.endsWith("/") ? image.slice(0, -1) : image;
38
+ return { buildContext: context, dockerfilePath: `${context}/Dockerfile`, kind: "dockerfile" };
39
+ };
40
+ const containerClassName = (exportName) => `${exportName.charAt(0).toUpperCase()}${exportName.slice(1)}Container`;
41
+ const containerBindingName = (exportName) => `CONTAINER_${exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "_").toUpperCase()}`;
42
+ const containerBuildTag = (exportName) => `lunora-${exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "-").toLowerCase()}:build`;
43
+ const assertValidImage = (image) => {
44
+ if (typeof image === "string") {
45
+ if (image.length === 0) {
46
+ throw new TypeError("defineContainer: `image` must be a non-empty path or a { registry } reference");
47
+ }
48
+ if (image.includes(":")) {
49
+ throw new TypeError(
50
+ `defineContainer: \`image\` string "${image}" looks like a registry reference — pass it as { registry: "${image}" } instead. Plain strings are local Dockerfile paths.`
51
+ );
52
+ }
53
+ return;
54
+ }
55
+ if ("build" in image) {
56
+ if (typeof image.build !== "string" || image.build.length === 0) {
57
+ throw new TypeError("defineContainer: `image.build` must be a non-empty source directory for Railpack to build");
58
+ }
59
+ return;
60
+ }
61
+ if (typeof image.registry !== "string" || image.registry.length === 0) {
62
+ throw new TypeError("defineContainer: `image.registry` must be a non-empty fully-qualified image reference");
63
+ }
64
+ };
65
+ const assertValidSecretsStore = (config, envNames, secretNames) => {
66
+ for (const [envName, binding] of Object.entries(config.secretsStore ?? {})) {
67
+ if (!ENV_NAME_PATTERN.test(envName)) {
68
+ throw new TypeError(`defineContainer: secretsStore env name "${envName}" is not a valid environment variable name`);
69
+ }
70
+ if (typeof binding !== "string" || binding.trim().length === 0) {
71
+ throw new TypeError(`defineContainer: \`secretsStore["${envName}"]\` must be a non-empty Secrets Store binding name`);
72
+ }
73
+ if (envNames.has(envName) || secretNames.has(envName)) {
74
+ throw new TypeError(`defineContainer: "${envName}" is declared in both \`secretsStore\` and \`env\`/\`secrets\` — pick one source for the value`);
75
+ }
76
+ }
77
+ };
78
+ const assertValidEnvAndSecrets = (config) => {
79
+ for (const name of Object.keys(config.env ?? {})) {
80
+ if (!ENV_NAME_PATTERN.test(name)) {
81
+ throw new TypeError(`defineContainer: env variable name "${name}" is not a valid environment variable name`);
82
+ }
83
+ }
84
+ for (const name of Object.keys(config.buildArgs ?? {})) {
85
+ if (!ENV_NAME_PATTERN.test(name)) {
86
+ throw new TypeError(`defineContainer: buildArg name "${name}" is not a valid environment variable name`);
87
+ }
88
+ }
89
+ const envNames = new Set(Object.keys(config.env ?? {}));
90
+ const secretNames = new Set(config.secrets);
91
+ for (const secret of config.secrets ?? []) {
92
+ if (!ENV_NAME_PATTERN.test(secret)) {
93
+ throw new TypeError(`defineContainer: secret name "${secret}" is not a valid environment variable name`);
94
+ }
95
+ if (envNames.has(secret)) {
96
+ throw new TypeError(
97
+ `defineContainer: "${secret}" is declared in both \`env\` and \`secrets\` — a secret would silently overwrite the static env value; pick one`
98
+ );
99
+ }
100
+ }
101
+ assertValidSecretsStore(config, envNames, secretNames);
102
+ };
103
+ const assertValidPort = (port, field) => {
104
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
105
+ throw new TypeError(`defineContainer: \`${field}\` must be an integer in 1–65535 (got ${String(port)})`);
106
+ }
107
+ };
108
+ const assertValidReadyOnChecks = (config) => {
109
+ for (const check of config.readyOn ?? []) {
110
+ if (typeof check.path !== "string" || check.path.trim().length === 0) {
111
+ throw new TypeError("defineContainer: `readyOn[].path` must be a non-empty HTTP path string");
112
+ }
113
+ if (check.path !== check.path.trim()) {
114
+ throw new TypeError("defineContainer: `readyOn[].path` must not have leading or trailing whitespace");
115
+ }
116
+ if (check.port !== void 0) {
117
+ assertValidPort(check.port, "readyOn[].port");
118
+ }
119
+ if (check.status !== void 0 && (!Number.isInteger(check.status) || check.status < 100 || check.status > 599)) {
120
+ throw new TypeError(`defineContainer: \`readyOn[].status\` must be an HTTP status code in 100–599 (got ${String(check.status)})`);
121
+ }
122
+ }
123
+ };
124
+ const assertValidHardTimeout = (hardTimeout) => {
125
+ if (hardTimeout === void 0) {
126
+ return;
127
+ }
128
+ if (typeof hardTimeout === "string") {
129
+ if (!SLEEP_AFTER_PATTERN.test(hardTimeout)) {
130
+ throw new TypeError(
131
+ `defineContainer: \`hardTimeout\` string "${hardTimeout}" must be a number of seconds followed by a unit, e.g. "30s", "5m", or "1h"`
132
+ );
133
+ }
134
+ } else if (!Number.isInteger(hardTimeout) || hardTimeout < 1) {
135
+ throw new TypeError(
136
+ `defineContainer: \`hardTimeout\` must be a positive integer number of seconds or a duration string like "5m" (got ${String(hardTimeout)})`
137
+ );
138
+ }
139
+ };
140
+ const assertValidEgressFields = (config) => {
141
+ for (const field of ["allowedHosts", "deniedHosts"]) {
142
+ const hosts = config[field];
143
+ if (hosts?.some((host) => typeof host !== "string" || host.trim().length === 0)) {
144
+ throw new TypeError(`defineContainer: \`${field}\` must be an array of non-empty hostname patterns`);
145
+ }
146
+ }
147
+ if (config.interceptHttps !== void 0 && typeof config.interceptHttps !== "boolean") {
148
+ throw new TypeError("defineContainer: `interceptHttps` must be a boolean, or omitted");
149
+ }
150
+ };
151
+ const assertValidContainerRuntimeFields = (config) => {
152
+ if (config.requiredPorts !== void 0) {
153
+ if (config.requiredPorts.length === 0) {
154
+ throw new TypeError("defineContainer: `requiredPorts` must be a non-empty array of ports, or omitted");
155
+ }
156
+ for (const port of config.requiredPorts) {
157
+ assertValidPort(port, "requiredPorts[]");
158
+ }
159
+ }
160
+ if (config.entrypoint !== void 0 && (config.entrypoint.length === 0 || config.entrypoint.some((part) => typeof part !== "string" || part.trim().length === 0))) {
161
+ throw new TypeError("defineContainer: `entrypoint` must be a non-empty array of non-empty strings, or omitted");
162
+ }
163
+ assertValidEgressFields(config);
164
+ if (config.pingEndpoint !== void 0 && (typeof config.pingEndpoint !== "string" || config.pingEndpoint.trim().length === 0)) {
165
+ throw new TypeError("defineContainer: `pingEndpoint` must be a non-empty path string");
166
+ }
167
+ for (const [key, value] of Object.entries(config.labels ?? {})) {
168
+ if (key.trim().length === 0 || typeof value !== "string") {
169
+ throw new TypeError("defineContainer: `labels` must be a record of non-empty keys to string values");
170
+ }
171
+ }
172
+ assertValidReadyOnChecks(config);
173
+ };
174
+ const defineContainer = (config) => {
175
+ assertValidImage(config.image);
176
+ if (config.defaultPort !== void 0) {
177
+ assertValidPort(config.defaultPort, "defaultPort");
178
+ }
179
+ const stepPercentage = config.rollout?.stepPercentage;
180
+ if (stepPercentage !== void 0 && (!Number.isInteger(stepPercentage) || stepPercentage < 1 || stepPercentage > 100)) {
181
+ throw new TypeError(`defineContainer: \`rollout.stepPercentage\` must be an integer in 1–100 (got ${String(stepPercentage)})`);
182
+ }
183
+ if (config.maxInstances !== void 0 && (!Number.isInteger(config.maxInstances) || config.maxInstances < 1)) {
184
+ throw new TypeError(`defineContainer: \`maxInstances\` must be a positive integer (got ${String(config.maxInstances)})`);
185
+ }
186
+ if (typeof config.instanceType === "string" && !NAMED_INSTANCE_TYPES.has(config.instanceType)) {
187
+ throw new TypeError(
188
+ `defineContainer: unknown \`instanceType\` "${config.instanceType}" — use one of ${[...NAMED_INSTANCE_TYPES].join(", ")}, or a custom { vcpu, memoryMib, diskMb } object`
189
+ );
190
+ }
191
+ if (typeof config.sleepAfter === "string") {
192
+ if (!SLEEP_AFTER_PATTERN.test(config.sleepAfter)) {
193
+ throw new TypeError(
194
+ `defineContainer: \`sleepAfter\` string "${config.sleepAfter}" must be a number of seconds followed by a unit, e.g. "30s", "5m", or "1h"`
195
+ );
196
+ }
197
+ } else if (config.sleepAfter !== void 0 && (!Number.isInteger(config.sleepAfter) || config.sleepAfter < 1)) {
198
+ throw new TypeError(
199
+ `defineContainer: \`sleepAfter\` must be a positive integer number of seconds or a duration string like "5m" (got ${String(config.sleepAfter)})`
200
+ );
201
+ }
202
+ assertValidHardTimeout(config.hardTimeout);
203
+ assertValidEnvAndSecrets(config);
204
+ assertValidContainerRuntimeFields(config);
205
+ return { ...config, isLunoraContainer: true };
206
+ };
207
+ const isContainerDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraContainer === true;
208
+ const resolveContainerEnvVariables = (definition, workerEnv, exportName) => {
209
+ const resolved = { ...definition.env };
210
+ for (const secret of definition.secrets ?? []) {
211
+ const value = workerEnv[secret];
212
+ if (typeof value !== "string") {
213
+ const label = exportName === void 0 ? "container" : `container "${exportName}"`;
214
+ throw new LunoraError(
215
+ "INTERNAL",
216
+ `${label}: declared secret "${secret}" is not set on the Worker environment. Add it to .dev.vars for local dev and run \`wrangler secret put ${secret}\` for production.`
217
+ );
218
+ }
219
+ resolved[secret] = value;
220
+ }
221
+ return resolved;
222
+ };
223
+
224
+ export { containerBindingName, containerBuildTag, containerClassName, defineContainer, isContainerDefinition, normalizeContainerImage, parseDurationSeconds, resolveContainerEnvVariables as resolveContainerEnvVars };
@@ -0,0 +1,220 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { containerBindingName } from './containerBindingName-BiTrAF1J.mjs';
3
+ import { a as applyJurisdiction } from './jurisdiction-CuPNcLDt.mjs';
4
+
5
+ const DEFAULT_POOL_SIZE = 3;
6
+ const DEFAULT_MAX_BACKOFF_MS = 3e4;
7
+ const DEFAULT_COLD_START_ATTEMPTS = 3;
8
+ const DEFAULT_COLD_START_BACKOFF_MS = 500;
9
+ const TARGET_PORT_HEADER = "cf-container-target-port";
10
+ const toRequest = (input, init, port) => {
11
+ const request = typeof input === "string" && input.startsWith("/") ? new Request(`http://container${input}`, init) : new Request(input, init);
12
+ if (port !== void 0) {
13
+ request.headers.set(TARGET_PORT_HEADER, String(port));
14
+ }
15
+ return request;
16
+ };
17
+ const sleep = async (ms) => {
18
+ if (ms <= 0) {
19
+ return;
20
+ }
21
+ await new Promise((resolve) => {
22
+ setTimeout(resolve, ms);
23
+ });
24
+ };
25
+ const COLD_START_ERROR_PATTERN = /no container instance|not listening|try again later|rate.?limit|provision/i;
26
+ const COLD_START_NO_INSTANCE_BODY = "no Container instance available";
27
+ const COLD_START_START_FAILURE_BODY = "Failed to start container:";
28
+ const COLD_START_SENTINEL_SCAN_BYTES = 1024;
29
+ const readBodyPrefix = async (response) => {
30
+ const stream = response.clone().body;
31
+ if (stream === null) {
32
+ return "";
33
+ }
34
+ const reader = stream.getReader();
35
+ const decoder = new TextDecoder();
36
+ let text = "";
37
+ try {
38
+ while (text.length < COLD_START_SENTINEL_SCAN_BYTES) {
39
+ const { done, value } = await reader.read();
40
+ if (done) {
41
+ break;
42
+ }
43
+ text += decoder.decode(value, { stream: true });
44
+ }
45
+ } finally {
46
+ await reader.cancel();
47
+ }
48
+ return text;
49
+ };
50
+ const isColdStartError = (error) => error instanceof Error && COLD_START_ERROR_PATTERN.test(error.message);
51
+ const isColdStartTransient = async (response) => {
52
+ if (response.status === 429) {
53
+ return true;
54
+ }
55
+ if (response.status !== 500 && response.status !== 503) {
56
+ return false;
57
+ }
58
+ try {
59
+ const body = await readBodyPrefix(response);
60
+ return body.includes(COLD_START_NO_INSTANCE_BODY) || body.startsWith(COLD_START_START_FAILURE_BODY);
61
+ } catch {
62
+ return false;
63
+ }
64
+ };
65
+ const coldStartRetryingHandle = (send, options = {}, port) => {
66
+ const attempts = Math.max(1, options.attempts ?? DEFAULT_COLD_START_ATTEMPTS);
67
+ const baseBackoff = options.backoffMs ?? DEFAULT_COLD_START_BACKOFF_MS;
68
+ const maxBackoff = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
69
+ return {
70
+ fetch: async (input, init) => {
71
+ const totalAttempts = typeof input === "string" ? attempts : 1;
72
+ let lastError;
73
+ for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
74
+ const isLastAttempt = attempt === totalAttempts - 1;
75
+ if (attempt > 0) {
76
+ await sleep(Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff));
77
+ }
78
+ try {
79
+ const response = await send(toRequest(input, init, port));
80
+ if (isLastAttempt || !await isColdStartTransient(response)) {
81
+ return response;
82
+ }
83
+ } catch (error) {
84
+ lastError = error;
85
+ if (isLastAttempt || !isColdStartError(error)) {
86
+ throw error;
87
+ }
88
+ }
89
+ }
90
+ throw lastError instanceof Error ? lastError : new Error("ctx.containers: cold-start retry exhausted");
91
+ },
92
+ port: (targetPort) => coldStartRetryingHandle(send, options, targetPort)
93
+ };
94
+ };
95
+ const handleFor = (namespace, instanceName, options) => coldStartRetryingHandle(async (request) => namespace.get(namespace.idFromName(instanceName)).fetch(request), options);
96
+ const lifecycleCall = async (stub, method, binding, argument) => {
97
+ const rpc = stub[method];
98
+ if (typeof rpc !== "function") {
99
+ throw new TypeError(`ctx.containers: the "${binding}" container DO does not expose ${method}() — is @lunora/container/do up to date?`);
100
+ }
101
+ return rpc(argument);
102
+ };
103
+ const egressControlsFor = (stub, binding) => {
104
+ return {
105
+ allow: async (hostname) => lifecycleCall(stub(), "allowHost", binding, hostname),
106
+ deny: async (hostname) => lifecycleCall(stub(), "denyHost", binding, hostname),
107
+ removeAllowed: async (hostname) => lifecycleCall(stub(), "removeAllowedHost", binding, hostname),
108
+ removeDenied: async (hostname) => lifecycleCall(stub(), "removeDeniedHost", binding, hostname),
109
+ setAllowed: async (hosts) => lifecycleCall(stub(), "setAllowedHosts", binding, [...hosts]),
110
+ setDenied: async (hosts) => lifecycleCall(stub(), "setDeniedHosts", binding, [...hosts])
111
+ };
112
+ };
113
+ const instanceHandleFor = (namespace, spec, instanceName, options) => {
114
+ const stub = () => namespace.get(namespace.idFromName(instanceName));
115
+ return {
116
+ ...coldStartRetryingHandle(async (request) => stub().fetch(request), options),
117
+ destroy: async () => lifecycleCall(stub(), "destroy", spec.binding),
118
+ egress: egressControlsFor(stub, spec.binding),
119
+ getState: async () => lifecycleCall(stub(), "getState", spec.binding),
120
+ renewActivityTimeout: async () => lifecycleCall(stub(), "renewActivityTimeout", spec.binding),
121
+ start: async (startOptions) => lifecycleCall(stub(), "start", spec.binding, startOptions),
122
+ stop: async (signal) => lifecycleCall(stub(), "stop", spec.binding, signal)
123
+ };
124
+ };
125
+ const randomPoolName = (size) => (
126
+ // eslint-disable-next-line sonarjs/pseudo-random -- load-balancing pick across interchangeable instances, not a security decision
127
+ `pool-${String(Math.floor(Math.random() * size))}`
128
+ );
129
+ const retryOnServerError = (response) => response.status >= 500;
130
+ const poolHandleFor = (namespace, spec, options = {}, port) => {
131
+ const size = options.size ?? spec.maxInstances ?? DEFAULT_POOL_SIZE;
132
+ const attempts = Math.max(1, options.attempts ?? 3);
133
+ const baseBackoff = options.backoffMs ?? 100;
134
+ const maxBackoff = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
135
+ const shouldRetry = options.retryOn ?? retryOnServerError;
136
+ return {
137
+ fetch: async (input, init) => {
138
+ const totalAttempts = typeof input === "string" ? attempts : 1;
139
+ let lastError;
140
+ for (let attempt = 0; attempt < totalAttempts; attempt += 1) {
141
+ if (attempt > 0) {
142
+ await sleep(Math.min(baseBackoff * 2 ** (attempt - 1), maxBackoff));
143
+ }
144
+ const request = toRequest(input, init, port);
145
+ try {
146
+ const response = await namespace.get(namespace.idFromName(randomPoolName(size))).fetch(request);
147
+ if (attempt === totalAttempts - 1 || !shouldRetry(response)) {
148
+ return response;
149
+ }
150
+ } catch (error) {
151
+ lastError = error;
152
+ }
153
+ }
154
+ throw lastError instanceof Error ? lastError : new Error(`ctx.containers.${spec.exportName}.pool(): all ${String(totalAttempts)} attempts failed`);
155
+ },
156
+ port: (targetPort) => poolHandleFor(namespace, spec, options, targetPort)
157
+ };
158
+ };
159
+ const accessorFor = (namespace, spec) => {
160
+ return {
161
+ any: (count, options) => handleFor(namespace, randomPoolName(count ?? spec.maxInstances ?? DEFAULT_POOL_SIZE), options),
162
+ get: (name, options) => instanceHandleFor(namespace, spec, name, options),
163
+ pool: (options) => poolHandleFor(namespace, spec, options)
164
+ };
165
+ };
166
+ const missingBindingAccessor = (spec) => {
167
+ const fail = () => {
168
+ throw new LunoraError(
169
+ "INTERNAL",
170
+ `ctx.containers.${spec.exportName}: no "${spec.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.`
171
+ );
172
+ };
173
+ return { any: fail, get: fail, pool: fail };
174
+ };
175
+ const createContainerContext = (env, specs, jurisdiction) => {
176
+ const containers = {};
177
+ for (const spec of specs) {
178
+ const binding = env[spec.binding];
179
+ containers[spec.exportName] = binding && typeof binding.idFromName === "function" && typeof binding.get === "function" ? accessorFor(applyJurisdiction(binding, jurisdiction), spec) : missingBindingAccessor(spec);
180
+ }
181
+ return containers;
182
+ };
183
+ const testNamespaceFor = (handler) => {
184
+ const stubFor = (name) => {
185
+ return {
186
+ allowHost: () => Promise.resolve(),
187
+ denyHost: () => Promise.resolve(),
188
+ destroy: () => Promise.resolve(),
189
+ fetch: (request) => Promise.resolve(handler(request, { name })),
190
+ getState: () => Promise.resolve({ lastChange: 0 }),
191
+ removeAllowedHost: () => Promise.resolve(),
192
+ removeDeniedHost: () => Promise.resolve(),
193
+ renewActivityTimeout: () => Promise.resolve(),
194
+ setAllowedHosts: () => Promise.resolve(),
195
+ setDeniedHosts: () => Promise.resolve(),
196
+ start: () => Promise.resolve(),
197
+ stop: () => Promise.resolve()
198
+ };
199
+ };
200
+ return { get: (id) => stubFor(String(id)), idFromName: (name) => name };
201
+ };
202
+ const createContainerTestContext = (handlers) => {
203
+ const containers = {};
204
+ for (const [exportName, handler] of Object.entries(handlers)) {
205
+ const namespace = testNamespaceFor(handler);
206
+ const spec = { binding: containerBindingName(exportName)};
207
+ containers[exportName] = {
208
+ // `.any()`/`.pool()` route to a fixed `pool-0` so the handler's
209
+ // `instance.name` is deterministic under test; the double doesn't
210
+ // simulate the random-pick or retry/backoff the real pool/cold-start
211
+ // path does (`attempts: 1` keeps a handler's own 5xx from looping).
212
+ any: () => handleFor(namespace, "pool-0", { attempts: 1 }),
213
+ get: (name) => instanceHandleFor(namespace, spec, name, { attempts: 1 }),
214
+ pool: () => handleFor(namespace, "pool-0", { attempts: 1 })
215
+ };
216
+ }
217
+ return containers;
218
+ };
219
+
220
+ export { createContainerContext, createContainerTestContext };
@@ -0,0 +1,13 @@
1
+ const applyJurisdiction = (namespace, jurisdiction) => {
2
+ if (jurisdiction === void 0) {
3
+ return namespace;
4
+ }
5
+ if (typeof namespace.jurisdiction !== "function") {
6
+ throw new TypeError(
7
+ `@lunora/container: Durable Object namespace does not support jurisdiction("${jurisdiction}") — update @cloudflare/workers-types or remove the jurisdiction option`
8
+ );
9
+ }
10
+ return namespace.jurisdiction(jurisdiction);
11
+ };
12
+
13
+ export { applyJurisdiction as a };