@lunora/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from './LunoraError-Bpb9EFJ3.mjs';
2
+
1
3
  const RPC_ENDPOINT = "/_lunora/rpc";
2
4
  const buildIdentityHeaders = (options) => {
3
5
  const headers = { "content-type": "application/json" };
@@ -19,12 +21,12 @@ const fanOutRelation = async (options, body, label) => {
19
21
  })
20
22
  );
21
23
  if (!response.ok) {
22
- throw new Error(`cross-shard relation ${label} failed: worker returned ${String(response.status)}`);
24
+ throw new LunoraError(`cross-shard relation ${label} failed: worker returned ${String(response.status)}`);
23
25
  }
24
26
  const result = await response.json();
25
27
  if (typeof result.failed === "number" && result.failed > 0) {
26
28
  const reached = (typeof result.ok === "number" ? result.ok : 0) + result.failed;
27
- throw new Error(
29
+ throw new LunoraError(
28
30
  `cross-shard relation ${label} failed on ${String(result.failed)} of ${String(reached)} shard(s) — refusing to return a partial result`
29
31
  );
30
32
  }
@@ -1,5 +1,5 @@
1
- import { LunoraError } from './LunoraError-CL0aOtpo.mjs';
2
- import { resolveShard } from './resolveShard-DDkzWtrU.mjs';
1
+ import { LunoraError } from './LunoraError-Bpb9EFJ3.mjs';
2
+ import { resolveShard } from './applyJurisdiction-BkZtTkct.mjs';
3
3
 
4
4
  const createStaticShardRegistry = (table_to_keys) => {
5
5
  return {
@@ -547,7 +547,16 @@ const mergeTopK = (values, strategy) => {
547
547
  }
548
548
  }
549
549
  const direction = strategy.direction ?? "desc";
550
- collected.sort((a, b) => direction === "asc" ? a.score - b.score : b.score - a.score);
550
+ const ascending = (x, y) => {
551
+ if (x < y) {
552
+ return -1;
553
+ }
554
+ if (x > y) {
555
+ return 1;
556
+ }
557
+ return 0;
558
+ };
559
+ collected.sort((a, b) => direction === "asc" ? ascending(a.score, b.score) : ascending(b.score, a.score));
551
560
  return collected.slice(0, strategy.k).map((entry) => entry.row);
552
561
  };
553
562
  const mergeShardResults = (values, strategy) => {
@@ -1,4 +1,15 @@
1
+ import { LunoraError } from './LunoraError-Bpb9EFJ3.mjs';
2
+
1
3
  const DEFAULT_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
4
+ const htmlCspFor = (frameOptions) => {
5
+ const parts = ["base-uri 'none'", "object-src 'none'"];
6
+ if (frameOptions === "DENY") {
7
+ parts.push("frame-ancestors 'none'");
8
+ } else if (frameOptions === "SAMEORIGIN") {
9
+ parts.push("frame-ancestors 'self'");
10
+ }
11
+ return parts.join("; ");
12
+ };
2
13
  const DEFAULT_PERMISSIONS_POLICY = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
3
14
  const DEFAULT_CORS_HEADERS = ["Authorization", "Content-Type", "X-D1-Bookmark", "X-Lunora-Mutation-Id"];
4
15
  const DEFAULT_CORS_METHODS = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"];
@@ -13,14 +24,14 @@ const resolveHstsHeader = (hsts) => {
13
24
  const includeSubDomains = config.includeSubDomains ?? true;
14
25
  return `max-age=${String(maxAge)}${includeSubDomains ? "; includeSubDomains" : ""}${config.preload ? "; preload" : ""}`;
15
26
  };
16
- const resolveCspHeader = (csp) => {
27
+ const resolveCspHeader = (csp, htmlDefault) => {
17
28
  if (csp === false) {
18
29
  return void 0;
19
30
  }
20
31
  if (typeof csp === "string") {
21
- return { htmlToo: true, value: csp };
32
+ return { htmlValue: csp, value: csp };
22
33
  }
23
- return { htmlToo: false, value: DEFAULT_CSP };
34
+ return { htmlValue: htmlDefault, value: DEFAULT_CSP };
24
35
  };
25
36
  const resolveHeaders = (input) => {
26
37
  if (input === false) {
@@ -35,11 +46,12 @@ const resolveHeaders = (input) => {
35
46
  };
36
47
  }
37
48
  const options = input === void 0 || input === true ? {} : input;
49
+ const frameOptions = options.frameOptions === false ? void 0 : options.frameOptions ?? "SAMEORIGIN";
38
50
  return {
39
51
  coop: "same-origin",
40
- csp: resolveCspHeader(options.csp),
52
+ csp: resolveCspHeader(options.csp, htmlCspFor(frameOptions)),
41
53
  enabled: true,
42
- frameOptions: options.frameOptions === false ? void 0 : options.frameOptions ?? "SAMEORIGIN",
54
+ frameOptions,
43
55
  hsts: resolveHstsHeader(options.hsts),
44
56
  permissionsPolicy: options.permissionsPolicy === false ? void 0 : options.permissionsPolicy ?? DEFAULT_PERMISSIONS_POLICY,
45
57
  referrerPolicy: options.referrerPolicy === false ? void 0 : options.referrerPolicy ?? "strict-origin-when-cross-origin"
@@ -65,10 +77,14 @@ const resolveCors = (input) => {
65
77
  if (typeof origins === "function") {
66
78
  isAllowed = origins;
67
79
  isExplicitlyAllowed = origins;
80
+ const credentialsNote = allowCredentials ? " AND reflects matching origins with credentials (`allowCredentials: true`)" : "";
81
+ console.warn(
82
+ `@lunora/runtime: security.cors uses a custom \`allowedOrigins\` predicate. It is trusted by the CSRF and WebSocket origin checks${credentialsNote} — ensure it matches ONLY trusted origins by exact equality; an over-broad predicate (e.g. \`() => true\`, or \`endsWith\`/\`includes\` checks) defeats the allowlist.`
83
+ );
68
84
  } else {
69
85
  const originsList = origins;
70
86
  if (originsList.includes("*") && allowCredentials) {
71
- throw new Error(
87
+ throw new LunoraError(
72
88
  '@lunora/runtime: security.cors cannot combine a wildcard origin ("*") with allowCredentials: true — browsers reject it and it defeats the allowlist.'
73
89
  );
74
90
  }
@@ -152,6 +168,20 @@ const enforceOrigin = (request, resolved) => {
152
168
  { headers: { "content-type": "application/json" }, status: 403 }
153
169
  );
154
170
  };
171
+ const enforceWebSocketOrigin = (request, resolved) => {
172
+ if (!resolved.csrf.enabled || !request.headers.get("cookie")) {
173
+ return void 0;
174
+ }
175
+ const selfOrigin = new URL(request.url).origin;
176
+ const source = originOf(request.headers.get("origin"));
177
+ if (source !== void 0 && isTrustedOrigin(source, selfOrigin, resolved)) {
178
+ return void 0;
179
+ }
180
+ return Response.json(
181
+ { error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin websocket upgrade rejected" } },
182
+ { headers: { "content-type": "application/json" }, status: 403 }
183
+ );
184
+ };
155
185
  const corsResponseHeaders = (origin, cors) => {
156
186
  const headers = new Headers();
157
187
  headers.set("access-control-allow-origin", origin);
@@ -199,8 +229,11 @@ const applyBaselineHeaders = (headers, request, response, config) => {
199
229
  if (config.coop !== void 0) {
200
230
  setIfAbsent(headers, "cross-origin-opener-policy", config.coop);
201
231
  }
202
- if (config.csp !== void 0 && (config.csp.htmlToo || !isHtmlResponse(response))) {
203
- setIfAbsent(headers, "content-security-policy", config.csp.value);
232
+ if (config.csp !== void 0) {
233
+ const cspValue = isHtmlResponse(response) ? config.csp.htmlValue : config.csp.value;
234
+ if (cspValue !== void 0) {
235
+ setIfAbsent(headers, "content-security-policy", cspValue);
236
+ }
204
237
  }
205
238
  };
206
239
  const applyCorsHeaders = (headers, request, cors) => {
@@ -230,4 +263,4 @@ const decorateResponse = (response, request, resolved) => {
230
263
  return new Response(response.body, { headers, status: response.status, statusText: response.statusText });
231
264
  };
232
265
 
233
- export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity };
266
+ export { decorateResponse, enforceOrigin, enforceWebSocketOrigin, handleCorsPreflight, resolveSecurity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.21",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/runtime"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "README.md",
30
30
  "LICENSE.md",
31
31
  "__assets__"
@@ -45,6 +45,9 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
+ "dependencies": {
49
+ "@lunora/errors": "1.0.0-alpha.2"
50
+ },
48
51
  "engines": {
49
52
  "node": "^22.15.0 || >=24.11.0"
50
53
  }
@@ -1,46 +0,0 @@
1
- class LunoraError extends Error {
2
- code;
3
- status;
4
- constructor(message, options) {
5
- super(message, { cause: options?.cause });
6
- this.name = "LunoraError";
7
- this.code = options?.code ?? "INTERNAL";
8
- this.status = options?.status ?? 500;
9
- }
10
- toResponse() {
11
- const body = { error: { code: this.code, message: this.message } };
12
- return Response.json(body, {
13
- headers: { "content-type": "application/json" },
14
- status: this.status
15
- });
16
- }
17
- }
18
- const hasErrorShape = (error, name) => {
19
- if (!error || typeof error !== "object") {
20
- return false;
21
- }
22
- const candidate = error;
23
- return candidate.name === name && typeof candidate.code === "string" && typeof candidate.status === "number" && typeof candidate.message === "string";
24
- };
25
- const isStructuralConflictError = (error) => hasErrorShape(error, "ConflictError");
26
- const isStructuralLunoraError = (error) => hasErrorShape(error, "LunoraError");
27
- const toErrorResponse = (error) => {
28
- if (error instanceof LunoraError) {
29
- return error.toResponse();
30
- }
31
- if (isStructuralLunoraError(error) || isStructuralConflictError(error)) {
32
- const body2 = { error: { code: error.code, message: error.message } };
33
- return Response.json(body2, {
34
- headers: { "content-type": "application/json" },
35
- status: error.status
36
- });
37
- }
38
- console.error("[lunora] unhandled error:", error);
39
- const body = { error: { code: "INTERNAL", message: "Internal error" } };
40
- return Response.json(body, {
41
- headers: { "content-type": "application/json" },
42
- status: 500
43
- });
44
- };
45
-
46
- export { LunoraError, isStructuralConflictError, isStructuralLunoraError, toErrorResponse };
@@ -1,9 +0,0 @@
1
- const resolveShard = (namespace, shardKey) => {
2
- if (typeof namespace.getByName === "function") {
3
- return namespace.getByName(shardKey);
4
- }
5
- const id = namespace.idFromName(shardKey);
6
- return namespace.get(id);
7
- };
8
-
9
- export { resolveShard };