tina4-nodejs 3.13.141 → 3.13.143

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.
Files changed (67) hide show
  1. package/CLAUDE.md +28 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +817 -538
  4. package/packages/cli/src/bin.ts +4 -14
  5. package/packages/cli/src/commands/build.ts +2 -18
  6. package/packages/cli/src/commands/generate.ts +9 -24
  7. package/packages/cli/src/commands/init.ts +18 -8
  8. package/packages/cli/src/commands/lint.ts +3 -13
  9. package/packages/cli/src/commands/migrate.ts +2 -6
  10. package/packages/cli/src/commands/migrateRollback.ts +2 -6
  11. package/packages/cli/src/commands/migrateStatus.ts +2 -6
  12. package/packages/cli/src/commands/queue.ts +2 -30
  13. package/packages/cli/src/util.ts +90 -0
  14. package/packages/core/dist/index.js +592 -262
  15. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
  16. package/packages/core/gallery/queue/src/lib/queueDb.ts +11 -0
  17. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +2 -5
  18. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +2 -5
  19. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
  20. package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
  21. package/packages/core/src/csp.ts +138 -0
  22. package/packages/core/src/devAdmin.ts +3 -12
  23. package/packages/core/src/dispatchPipeline.ts +2 -18
  24. package/packages/core/src/errorOverlay.ts +80 -37
  25. package/packages/core/src/etag.ts +24 -0
  26. package/packages/core/src/feedback.ts +2 -11
  27. package/packages/core/src/index.ts +10 -0
  28. package/packages/core/src/middleware.ts +20 -6
  29. package/packages/core/src/response.ts +14 -3
  30. package/packages/core/src/server.ts +52 -15
  31. package/packages/core/src/session.ts +174 -71
  32. package/packages/core/src/sessionHandlers/respSessionHandler.ts +94 -0
  33. package/packages/core/src/sessionHandlers/valkeyHandler.ts +22 -73
  34. package/packages/core/src/sso.ts +1 -1
  35. package/packages/core/src/static.ts +1 -13
  36. package/packages/core/src/types.ts +8 -0
  37. package/packages/core/src/upstreamResponse.ts +31 -0
  38. package/packages/core/src/websocket.ts +48 -7
  39. package/packages/core/templates/errors/302.twig +1 -1
  40. package/packages/core/templates/errors/401.twig +2 -2
  41. package/packages/core/templates/errors/403.twig +1 -1
  42. package/packages/core/templates/errors/404.twig +1 -1
  43. package/packages/core/templates/errors/500.twig +1 -1
  44. package/packages/core/templates/errors/502.twig +2 -2
  45. package/packages/core/templates/errors/503.twig +2 -2
  46. package/packages/core/templates/errors/base.twig +4 -1
  47. package/packages/frond/dist/index.js +2 -0
  48. package/packages/frond/src/engine.ts +23 -0
  49. package/packages/orm/dist/index.js +593 -271
  50. package/packages/orm/src/graph/adapters/bolt.ts +8 -29
  51. package/packages/orm/src/graph/adapters/rowMapping.ts +57 -0
  52. package/packages/orm/src/graph/adapters/ultipa.ts +8 -30
  53. package/packages/swagger/dist/index.js +4 -4
  54. package/packages/swagger/src/ui.ts +7 -4
  55. package/types/cli/src/util.d.ts +28 -0
  56. package/types/core/src/csp.d.ts +42 -0
  57. package/types/core/src/etag.d.ts +9 -0
  58. package/types/core/src/index.d.ts +1 -0
  59. package/types/core/src/middleware.d.ts +6 -0
  60. package/types/core/src/session.d.ts +64 -21
  61. package/types/core/src/sessionHandlers/respSessionHandler.d.ts +52 -0
  62. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +2 -26
  63. package/types/core/src/types.d.ts +8 -0
  64. package/types/core/src/upstreamResponse.d.ts +10 -0
  65. package/types/core/src/websocket.d.ts +21 -0
  66. package/types/frond/src/engine.d.ts +7 -0
  67. package/types/orm/src/graph/adapters/rowMapping.d.ts +26 -0
@@ -5406,6 +5406,73 @@ var init_trustedProxy = __esm({
5406
5406
  }
5407
5407
  });
5408
5408
 
5409
+ // ../core/src/csp.ts
5410
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
5411
+ import { randomBytes as randomBytes2 } from "node:crypto";
5412
+ function generateNonce() {
5413
+ return randomBytes2(16).toString("base64");
5414
+ }
5415
+ function runWithCspNonce(nonce, fn) {
5416
+ return nonceStore.run({ nonce }, fn);
5417
+ }
5418
+ function setCurrentNonce(nonce) {
5419
+ const store = nonceStore.getStore();
5420
+ if (store) store.nonce = nonce;
5421
+ else nonceFallback = nonce;
5422
+ }
5423
+ function clearCurrentNonce() {
5424
+ setCurrentNonce(void 0);
5425
+ }
5426
+ function currentCspNonce() {
5427
+ const store = nonceStore.getStore();
5428
+ let value = store ? store.nonce : nonceFallback;
5429
+ if (!value) {
5430
+ value = generateNonce();
5431
+ if (store) store.nonce = value;
5432
+ else nonceFallback = value;
5433
+ }
5434
+ return value;
5435
+ }
5436
+ function cspNonce() {
5437
+ return currentCspNonce();
5438
+ }
5439
+ function injectNonceIntoCsp(csp, nonce) {
5440
+ const token = `'nonce-${nonce}'`;
5441
+ const directives = [];
5442
+ for (const rawPart of csp.split(";")) {
5443
+ const part = rawPart.trim();
5444
+ if (!part) continue;
5445
+ const spaceAt = part.search(/\s/);
5446
+ const name = (spaceAt === -1 ? part : part.slice(0, spaceAt)).toLowerCase();
5447
+ const value = spaceAt === -1 ? "" : part.slice(spaceAt + 1).trim();
5448
+ directives.push([name, value]);
5449
+ }
5450
+ const defaultEntry = directives.find(([name]) => name === "default-src");
5451
+ const defaultValue = defaultEntry && defaultEntry[1] ? defaultEntry[1] : "'self'";
5452
+ for (const directive of ["style-src", "script-src"]) {
5453
+ const existing = directives.find(([name]) => name === directive);
5454
+ if (existing) {
5455
+ if (!existing[1].split(/\s+/).includes(token)) {
5456
+ existing[1] = `${existing[1]} ${token}`.trim();
5457
+ }
5458
+ } else {
5459
+ directives.push([directive, `${defaultValue} ${token}`.trim()]);
5460
+ }
5461
+ }
5462
+ return directives.map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
5463
+ }
5464
+ function resolveCspHeader(nonce) {
5465
+ const csp = process.env.TINA4_CSP || "default-src 'self'";
5466
+ return injectNonceIntoCsp(csp, nonce);
5467
+ }
5468
+ var nonceStore, nonceFallback;
5469
+ var init_csp = __esm({
5470
+ "../core/src/csp.ts"() {
5471
+ "use strict";
5472
+ nonceStore = new AsyncLocalStorage2();
5473
+ }
5474
+ });
5475
+
5409
5476
  // ../frond/src/engine.ts
5410
5477
  var engine_exports = {};
5411
5478
  __export(engine_exports, {
@@ -5415,9 +5482,10 @@ __export(engine_exports, {
5415
5482
  expressionFormCache: () => expressionFormCache,
5416
5483
  filterChainCache: () => filterChainCache,
5417
5484
  pathParseCache: () => pathParseCache,
5485
+ setCspNonceProvider: () => setCspNonceProvider,
5418
5486
  setFormTokenSessionId: () => setFormTokenSessionId
5419
5487
  });
5420
- import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
5488
+ import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
5421
5489
  import { readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync4, realpathSync } from "node:fs";
5422
5490
  import { join as join5, resolve as resolve2, isAbsolute as isAbsolute2, sep } from "node:path";
5423
5491
  function jsonText(value) {
@@ -6484,6 +6552,9 @@ function _b64url(data) {
6484
6552
  function setFormTokenSessionId(sessionId) {
6485
6553
  _formTokenSessionId = sessionId || "";
6486
6554
  }
6555
+ function setCspNonceProvider(fn) {
6556
+ _cspNonceProvider = fn;
6557
+ }
6487
6558
  function _buildFormTokenJwt(descriptor = "") {
6488
6559
  const secret = process.env.TINA4_SECRET ?? "";
6489
6560
  const secretBytes = Buffer.byteLength(secret, "utf8");
@@ -6495,7 +6566,7 @@ function _buildFormTokenJwt(descriptor = "") {
6495
6566
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
6496
6567
  const header = { alg: "HS256", typ: "JWT" };
6497
6568
  const now = Math.floor(Date.now() / 1e3);
6498
- const payload = { type: "form", nonce: randomBytes2(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
6569
+ const payload = { type: "form", nonce: randomBytes3(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
6499
6570
  if (descriptor) {
6500
6571
  if (descriptor.includes("|")) {
6501
6572
  const [ctx, ref] = descriptor.split("|", 2);
@@ -6522,7 +6593,7 @@ function _generateFormToken(descriptor = "") {
6522
6593
  function _generateFormTokenValue(descriptor = "") {
6523
6594
  return new SafeString(_buildFormTokenJwt(descriptor));
6524
6595
  }
6525
- var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, Frond;
6596
+ var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FAST_FILTERS, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, JS_SAFE_RE, MEDIA_TYPE_RE, SANDBOX_EXPR_KEYWORDS, SANDBOX_IDENT_RE, SANDBOX_STRING_RE, UNSAFE_KEYS, BUILTIN_FILTERS, _formTokenSessionId, _cspNonceProvider, Frond;
6526
6597
  var init_engine = __esm({
6527
6598
  "../frond/src/engine.ts"() {
6528
6599
  "use strict";
@@ -6876,6 +6947,7 @@ var init_engine = __esm({
6876
6947
  js_escape: (v) => new SafeString(jsEscape(v))
6877
6948
  };
6878
6949
  _formTokenSessionId = "";
6950
+ _cspNonceProvider = () => "";
6879
6951
  Frond = class _Frond {
6880
6952
  // ── Class-level registries ──────────────────────────────────
6881
6953
  // Persist globals, filters, and tests across hot-reloads and at
@@ -7029,6 +7101,7 @@ var init_engine = __esm({
7029
7101
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
7030
7102
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
7031
7103
  this.globals.dump = (value) => renderDump(value);
7104
+ this.globals.csp_nonce = () => _cspNonceProvider();
7032
7105
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
7033
7106
  for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
7034
7107
  for (const [k, v] of _Frond.classTests) this.tests[k] = v;
@@ -8292,7 +8365,8 @@ async function getFrond() {
8292
8365
  const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
8293
8366
  let engine = _frondCache.get(dir);
8294
8367
  if (!engine) {
8295
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8368
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8369
+ setCspNonceProvider2(currentCspNonce);
8296
8370
  engine = new Frond2(dir);
8297
8371
  _frondCache.set(dir, engine);
8298
8372
  }
@@ -8303,7 +8377,8 @@ async function getFrameworkFrond() {
8303
8377
  const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
8304
8378
  if (!_frameworkFrond && fs2.existsSync(frameworkDir)) {
8305
8379
  try {
8306
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8380
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8381
+ setCspNonceProvider2(currentCspNonce);
8307
8382
  _frameworkFrond = new Frond2(frameworkDir);
8308
8383
  } catch {
8309
8384
  return null;
@@ -8403,6 +8478,7 @@ function createResponse(res) {
8403
8478
  return response;
8404
8479
  };
8405
8480
  response.raw = res;
8481
+ response.cspNonce = currentCspNonce();
8406
8482
  response.json = function(data, status2) {
8407
8483
  if (res.headersSent) return response;
8408
8484
  if (status2 !== void 0) res.statusCode = status2;
@@ -8548,7 +8624,8 @@ function createResponse(res) {
8548
8624
  };
8549
8625
  response.render = async function(templateName, data, status2, templateDir) {
8550
8626
  try {
8551
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8627
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
8628
+ setCspNonceProvider2(currentCspNonce);
8552
8629
  const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
8553
8630
  let engine = _frondCache.get(dir);
8554
8631
  if (!engine) {
@@ -8666,6 +8743,7 @@ var HEADER_TOKEN, _frondCache, _defaultTemplatesDir, _globalFrond, _frameworkFro
8666
8743
  var init_response = __esm({
8667
8744
  "../core/src/response.ts"() {
8668
8745
  "use strict";
8746
+ init_csp();
8669
8747
  HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
8670
8748
  _frondCache = /* @__PURE__ */ new Map();
8671
8749
  _defaultTemplatesDir = null;
@@ -8877,6 +8955,7 @@ var init_middleware = __esm({
8877
8955
  init_dotenv();
8878
8956
  init_router();
8879
8957
  init_trustedProxy();
8958
+ init_csp();
8880
8959
  init_response();
8881
8960
  MiddlewareChain = class {
8882
8961
  middlewares = [];
@@ -9309,12 +9388,18 @@ var init_middleware = __esm({
9309
9388
  * overrides, WITHOUT Strict-Transport-Security, which depends on the
9310
9389
  * request's scheme. Also what a transport rejection carries (ADR-0068
9311
9390
  * section 4), written before the scheme is known.
9391
+ *
9392
+ * Content-Security-Policy always carries a per-response nonce in style-src
9393
+ * AND script-src (ADR-0088), read from the request async-context (minted on
9394
+ * first access), so the framework's own inline content runs under the strict
9395
+ * default without 'unsafe-inline'. The nonce is random per response, so a
9396
+ * caller comparing this header must assert its STRUCTURE, never a byte string.
9312
9397
  */
9313
9398
  static canonicalHeaders() {
9314
9399
  return {
9315
9400
  "X-Frame-Options": process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
9316
9401
  "X-Content-Type-Options": "nosniff",
9317
- "Content-Security-Policy": process.env.TINA4_CSP ?? "default-src 'self'",
9402
+ "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
9318
9403
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
9319
9404
  "X-XSS-Protection": "0",
9320
9405
  "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
@@ -9356,7 +9441,7 @@ var init_middleware = __esm({
9356
9441
  static warnCspDefaultOnce() {
9357
9442
  if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
9358
9443
  _SecurityHeadersMiddleware.cspDefaultWarned = true;
9359
- const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. That default blocks runtime-injected inline styles, cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
9444
+ const message = `TINA4_CSP is not set, so Tina4 is serving the default Content-Security-Policy "default-src 'self'" on every response. The framework injects a per-response nonce into style-src and script-src, so its own inline <style>/<script> (and your templates using the csp_nonce() Frond global) work under this policy. The default still blocks cross-origin fonts/scripts/CDNs, data: URIs, and cross-origin WebSocket/XHR (e.g. a separate API or LiveKit host). If your app uses any of these, set TINA4_CSP to a policy that allows them (see https://tina4.com); to silence this notice without changing behaviour, set TINA4_CSP="default-src 'self'".`;
9360
9445
  try {
9361
9446
  Log.warning(message);
9362
9447
  } catch {
@@ -10361,6 +10446,21 @@ var init_portTakeover = __esm({
10361
10446
  }
10362
10447
  });
10363
10448
 
10449
+ // ../core/src/etag.ts
10450
+ function etagMatches(ifNoneMatch, etag) {
10451
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
10452
+ const target = strip(etag);
10453
+ return ifNoneMatch.split(",").some((candidate) => {
10454
+ const trimmed = candidate.trim();
10455
+ return trimmed === "*" || strip(trimmed) === target;
10456
+ });
10457
+ }
10458
+ var init_etag = __esm({
10459
+ "../core/src/etag.ts"() {
10460
+ "use strict";
10461
+ }
10462
+ });
10463
+
10364
10464
  // ../core/src/sessionHandlers/syncBridge.ts
10365
10465
  import { Worker } from "node:worker_threads";
10366
10466
  function getBridge(key, workerSource, workerData) {
@@ -10642,42 +10742,35 @@ var init_respClient = __esm({
10642
10742
  }
10643
10743
  });
10644
10744
 
10645
- // ../core/src/sessionHandlers/valkeyHandler.ts
10646
- var ValkeySessionHandler;
10647
- var init_valkeyHandler = __esm({
10648
- "../core/src/sessionHandlers/valkeyHandler.ts"() {
10745
+ // ../core/src/sessionHandlers/respSessionHandler.ts
10746
+ var RespSessionHandler;
10747
+ var init_respSessionHandler = __esm({
10748
+ "../core/src/sessionHandlers/respSessionHandler.ts"() {
10649
10749
  "use strict";
10650
10750
  init_respClient();
10651
- ValkeySessionHandler = class {
10652
- host;
10653
- port;
10654
- password;
10655
- prefix;
10656
- db;
10657
- constructor(config) {
10658
- this.host = config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1";
10659
- this.port = config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379);
10660
- this.password = config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "";
10661
- this.prefix = config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:";
10662
- this.db = config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0);
10751
+ RespSessionHandler = class {
10752
+ constructor(conn, label) {
10753
+ this.conn = conn;
10754
+ this.label = label;
10663
10755
  }
10756
+ conn;
10757
+ label;
10664
10758
  /**
10665
- * Execute a RESP command synchronously against the live Valkey server.
10759
+ * Execute a RESP command synchronously against the live server.
10666
10760
  *
10667
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
10668
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
10669
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
10670
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
10761
+ * Delegates to the shared {@link respCommandSync} transport: a genuine key
10762
+ * miss yields `""`, and a transport/connection FAILURE THROWS so the caller
10763
+ * can distinguish "not found" from "backend failed". Backend-failure parity.
10671
10764
  */
10672
10765
  execSync(args) {
10673
10766
  return respCommandSync(
10674
- { host: this.host, port: this.port, password: this.password, db: this.db },
10767
+ { host: this.conn.host, port: this.conn.port, password: this.conn.password, db: this.conn.db },
10675
10768
  args,
10676
- "Valkey"
10769
+ this.label
10677
10770
  );
10678
10771
  }
10679
10772
  key(sessionId) {
10680
- return `${this.prefix}${sessionId}`;
10773
+ return `${this.conn.prefix}${sessionId}`;
10681
10774
  }
10682
10775
  read(sessionId) {
10683
10776
  const raw = this.execSync(["GET", this.key(sessionId)]);
@@ -10688,7 +10781,7 @@ var init_valkeyHandler = __esm({
10688
10781
  return null;
10689
10782
  }
10690
10783
  }
10691
- write(sessionId, data, ttl) {
10784
+ write(sessionId, data, ttl = 0) {
10692
10785
  const json = JSON.stringify(data);
10693
10786
  if (ttl > 0) {
10694
10787
  this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
@@ -10703,6 +10796,29 @@ var init_valkeyHandler = __esm({
10703
10796
  }
10704
10797
  });
10705
10798
 
10799
+ // ../core/src/sessionHandlers/valkeyHandler.ts
10800
+ var ValkeySessionHandler;
10801
+ var init_valkeyHandler = __esm({
10802
+ "../core/src/sessionHandlers/valkeyHandler.ts"() {
10803
+ "use strict";
10804
+ init_respSessionHandler();
10805
+ ValkeySessionHandler = class extends RespSessionHandler {
10806
+ constructor(config) {
10807
+ super(
10808
+ {
10809
+ host: config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1",
10810
+ port: config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379),
10811
+ password: config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "",
10812
+ prefix: config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:",
10813
+ db: config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0)
10814
+ },
10815
+ "Valkey"
10816
+ );
10817
+ }
10818
+ };
10819
+ }
10820
+ });
10821
+
10706
10822
  // ../core/src/sessionHandlers/memcachedHandler.ts
10707
10823
  import { createHash as createHash4 } from "node:crypto";
10708
10824
  var MAX_KEY_BYTES, MAX_RELATIVE_EXPTIME, MemcachedSessionHandler;
@@ -11718,7 +11834,7 @@ __export(session_exports, {
11718
11834
  sessionCookieName: () => sessionCookieName,
11719
11835
  sessionStrictMode: () => sessionStrictMode
11720
11836
  });
11721
- import { createHash as createHash5, randomBytes as randomBytes3 } from "node:crypto";
11837
+ import { createHash as createHash5, randomBytes as randomBytes4 } from "node:crypto";
11722
11838
  import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "node:fs";
11723
11839
  import { join as join9 } from "node:path";
11724
11840
  function isValidSessionId(sessionId) {
@@ -11736,6 +11852,20 @@ function resolveBackend(name) {
11736
11852
  }
11737
11853
  return normalised;
11738
11854
  }
11855
+ function fingerprint(value) {
11856
+ try {
11857
+ return JSON.stringify(value);
11858
+ } catch {
11859
+ return void 0;
11860
+ }
11861
+ }
11862
+ function fingerprints(data) {
11863
+ const prints = /* @__PURE__ */ new Map();
11864
+ for (const [key, value] of Object.entries(data)) {
11865
+ prints.set(key, fingerprint(value));
11866
+ }
11867
+ return prints;
11868
+ }
11739
11869
  function isSecureScheme(forwardedProto, socketEncrypted) {
11740
11870
  const forwarded = (forwardedProto ?? "").trim();
11741
11871
  if (forwarded !== "") {
@@ -11768,7 +11898,7 @@ var init_session = __esm({
11768
11898
  "use strict";
11769
11899
  init_logger();
11770
11900
  init_dotenv();
11771
- init_respClient();
11901
+ init_respSessionHandler();
11772
11902
  init_valkeyHandler();
11773
11903
  init_memcachedHandler();
11774
11904
  init_mongoHandler();
@@ -11894,57 +12024,19 @@ var init_session = __esm({
11894
12024
  }
11895
12025
  }
11896
12026
  };
11897
- RedisSessionHandler = class {
11898
- host;
11899
- port;
11900
- password;
11901
- prefix;
11902
- db;
12027
+ RedisSessionHandler = class extends RespSessionHandler {
11903
12028
  constructor(config) {
11904
- this.host = config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1";
11905
- this.port = config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379);
11906
- this.password = config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "";
11907
- this.prefix = config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:";
11908
- this.db = config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0);
11909
- }
11910
- /**
11911
- * Execute a Redis command synchronously against the live server.
11912
- *
11913
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
11914
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
11915
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
11916
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
11917
- */
11918
- execSync(args) {
11919
- return respCommandSync(
11920
- { host: this.host, port: this.port, password: this.password, db: this.db },
11921
- args,
12029
+ super(
12030
+ {
12031
+ host: config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1",
12032
+ port: config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379),
12033
+ password: config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "",
12034
+ prefix: config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:",
12035
+ db: config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0)
12036
+ },
11922
12037
  "Redis"
11923
12038
  );
11924
12039
  }
11925
- key(sessionId) {
11926
- return `${this.prefix}${sessionId}`;
11927
- }
11928
- read(sessionId) {
11929
- const raw = this.execSync(["GET", this.key(sessionId)]);
11930
- if (!raw) return null;
11931
- try {
11932
- return JSON.parse(raw);
11933
- } catch {
11934
- return null;
11935
- }
11936
- }
11937
- write(sessionId, data, ttl = 0) {
11938
- const json = JSON.stringify(data);
11939
- if (ttl > 0) {
11940
- this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
11941
- } else {
11942
- this.execSync(["SET", this.key(sessionId), json]);
11943
- }
11944
- }
11945
- destroy(sessionId) {
11946
- this.execSync(["DEL", this.key(sessionId)]);
11947
- }
11948
12040
  };
11949
12041
  FLASH_PREFIX = "_flash_";
11950
12042
  Session = class {
@@ -11971,6 +12063,29 @@ var init_session = __esm({
11971
12063
  * Lets `start()` tell "no such session" from "the store is unreachable".
11972
12064
  */
11973
12065
  lastReadFailed = false;
12066
+ /**
12067
+ * What this request last saw of the stored record, one fingerprint per key:
12068
+ * taken by start() and again by every save. save() compares the live data
12069
+ * against it to find the keys THIS request changed.
12070
+ */
12071
+ loaded = /* @__PURE__ */ new Map();
12072
+ /**
12073
+ * True when this request is working on a record the store holds: start()
12074
+ * adopted one, or a save has written one since. A save never re-creates such
12075
+ * a record once another request has removed it.
12076
+ */
12077
+ stored = false;
12078
+ /**
12079
+ * True when clear() ran since the last save: the save then replaces the
12080
+ * stored record instead of merging into it.
12081
+ */
12082
+ cleared = false;
12083
+ /**
12084
+ * True once this request found its session ended by another request (the
12085
+ * record it loaded is gone). It stays ended for the rest of the request:
12086
+ * regenerate() mints nothing either.
12087
+ */
12088
+ ended = false;
11974
12089
  constructor(backend, config) {
11975
12090
  const backendType = resolveBackend(
11976
12091
  backend ?? config?.backend ?? process.env.TINA4_SESSION_BACKEND ?? "file"
@@ -12115,6 +12230,10 @@ var init_session = __esm({
12115
12230
  const now2 = Math.floor(Date.now() / 1e3);
12116
12231
  this.data = { _created: now2, _accessed: now2 };
12117
12232
  this.dirty = false;
12233
+ this.stored = false;
12234
+ this.loaded = /* @__PURE__ */ new Map();
12235
+ this.cleared = false;
12236
+ this.ended = false;
12118
12237
  return sessionId;
12119
12238
  }
12120
12239
  if (loaded) {
@@ -12124,14 +12243,22 @@ var init_session = __esm({
12124
12243
  this.data._accessed = now2;
12125
12244
  this.dirty = false;
12126
12245
  this.safeWrite(this.sessionId, this.data, this.ttl);
12246
+ this.stored = true;
12247
+ this.loaded = fingerprints(this.data);
12248
+ this.cleared = false;
12249
+ this.ended = false;
12127
12250
  return sessionId;
12128
12251
  }
12129
12252
  }
12130
- this.sessionId = randomBytes3(16).toString("hex");
12253
+ this.sessionId = randomBytes4(16).toString("hex");
12131
12254
  const now = Math.floor(Date.now() / 1e3);
12132
12255
  this.data = { _created: now, _accessed: now };
12133
12256
  this.dirty = false;
12134
12257
  this.safeWrite(this.sessionId, this.data, this.ttl);
12258
+ this.stored = false;
12259
+ this.loaded = /* @__PURE__ */ new Map();
12260
+ this.cleared = false;
12261
+ this.ended = false;
12135
12262
  return this.sessionId;
12136
12263
  }
12137
12264
  /**
@@ -12172,9 +12299,7 @@ var init_session = __esm({
12172
12299
  if (this.sessionId) {
12173
12300
  this.safeDestroy(this.sessionId);
12174
12301
  }
12175
- this.sessionId = null;
12176
- this.data = null;
12177
- this.dirty = false;
12302
+ this.forget();
12178
12303
  }
12179
12304
  /**
12180
12305
  * Get all session data (excluding internal keys).
@@ -12197,6 +12322,7 @@ var init_session = __esm({
12197
12322
  const now = Math.floor(Date.now() / 1e3);
12198
12323
  this.data = { _created: this.data._created, _accessed: now };
12199
12324
  this.dirty = true;
12325
+ this.cleared = true;
12200
12326
  this.save();
12201
12327
  }
12202
12328
  /**
@@ -12213,17 +12339,36 @@ var init_session = __esm({
12213
12339
  * session fixation — the pre-auth ID is destroyed and the data is carried
12214
12340
  * onto a fresh, unguessable ID. A backend destroy/write failure is logged
12215
12341
  * (never silent) but does not throw under the default policy.
12342
+ *
12343
+ * What is carried is the session as it is stored now with this request's
12344
+ * own changes applied, the same merge save() does. If another request ended
12345
+ * the session after this one loaded it (a logout, or a regenerate of its
12346
+ * own), it stays ended: nothing is carried, no id is minted and null is
12347
+ * returned, so no cookie goes out to replace the one that request sent.
12216
12348
  */
12217
12349
  regenerate() {
12350
+ if (this.ended) return null;
12351
+ if (this.sessionId && this.stored && this.data) {
12352
+ const current = this.safeRead(this.sessionId);
12353
+ if (!this.lastReadFailed) {
12354
+ if (!current) {
12355
+ this.forget();
12356
+ this.ended = true;
12357
+ return null;
12358
+ }
12359
+ if (!this.cleared) this.data = this.merged(current);
12360
+ }
12361
+ }
12218
12362
  const oldId = this.sessionId;
12219
12363
  const oldData = this.data;
12220
12364
  if (oldId) {
12221
12365
  this.safeDestroy(oldId);
12222
12366
  }
12223
- this.sessionId = randomBytes3(16).toString("hex");
12367
+ this.sessionId = randomBytes4(16).toString("hex");
12224
12368
  this.data = oldData ?? { _created: Math.floor(Date.now() / 1e3), _accessed: Math.floor(Date.now() / 1e3) };
12225
12369
  this.data._accessed = Math.floor(Date.now() / 1e3);
12226
12370
  this.dirty = true;
12371
+ this.stored = false;
12227
12372
  this.save();
12228
12373
  return this.sessionId;
12229
12374
  }
@@ -12299,17 +12444,84 @@ var init_session = __esm({
12299
12444
  *
12300
12445
  * Returns true on a successful persist, false if the backend was unreachable
12301
12446
  * (logged). The dirty flag is cleared only on success so a later save()
12302
- * retries once the backend recovers. A nothing-to-persist call returns true.
12447
+ * retries once the backend recovers. A call with no session returns true.
12448
+ *
12449
+ * Other requests on the same session run while this one does, so the save
12450
+ * writes only THIS request's changes, onto the record as it is stored now.
12451
+ * Writing back the whole snapshot start() loaded undid whatever another
12452
+ * request did in between: a logout (destroy, clear, regenerate) or a
12453
+ * privilege change made with set(). A record removed since this request
12454
+ * loaded it is never re-created: the session has ended, for this request
12455
+ * too. A request that has not loaded a stored record (a new session, the new
12456
+ * id regenerate() mints) writes its data whole, as before.
12457
+ *
12458
+ * A session the store already holds is re-written even when this request
12459
+ * changed nothing: that write re-stamps the backend deadline to
12460
+ * now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
12461
+ * rather than that long after its last CHANGE (ADR-0087 — a session's expiry
12462
+ * slides on activity). The re-write is the re-read, merged record, so sliding
12463
+ * the deadline is concurrent-safe: it never clobbers another request's change,
12464
+ * never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
12465
+ * never-expires deadline (ADR-0027). A request with no stored session and no
12466
+ * change writes nothing — there is no deadline to move.
12303
12467
  */
12304
12468
  save() {
12305
12469
  if (!this.sessionId || !this.data) return true;
12306
- if (!this.dirty) return true;
12307
- if (this.safeWrite(this.sessionId, this.data, this.ttl)) {
12470
+ if (!this.dirty && !this.stored) return true;
12471
+ let record = this.data;
12472
+ if (this.stored) {
12473
+ const current = this.safeRead(this.sessionId);
12474
+ if (this.lastReadFailed) return false;
12475
+ if (!current) {
12476
+ this.forget();
12477
+ this.ended = true;
12478
+ return true;
12479
+ }
12480
+ if (!this.cleared) record = this.merged(current);
12481
+ }
12482
+ if (this.safeWrite(this.sessionId, record, this.ttl)) {
12308
12483
  this.dirty = false;
12484
+ this.stored = true;
12485
+ this.cleared = false;
12486
+ this.loaded = fingerprints(this.data);
12309
12487
  return true;
12310
12488
  }
12311
12489
  return false;
12312
12490
  }
12491
+ /**
12492
+ * The stored record with this request's changes applied: every key whose
12493
+ * value differs from what this request loaded or last saved, and the
12494
+ * removal of every key it deleted. Keys it did not touch keep whatever is
12495
+ * stored now.
12496
+ */
12497
+ merged(current) {
12498
+ const data = this.data;
12499
+ const record = { ...current };
12500
+ for (const [key, value] of Object.entries(data)) {
12501
+ const print = fingerprint(value);
12502
+ if (print === void 0 || !this.loaded.has(key) || this.loaded.get(key) !== print) {
12503
+ record[key] = value;
12504
+ }
12505
+ }
12506
+ for (const key of this.loaded.keys()) {
12507
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
12508
+ delete record[key];
12509
+ }
12510
+ }
12511
+ return record;
12512
+ }
12513
+ /**
12514
+ * End the session for this request without touching the store: no id and
12515
+ * no data, so nothing more is written and no cookie goes out for it.
12516
+ */
12517
+ forget() {
12518
+ this.sessionId = null;
12519
+ this.data = null;
12520
+ this.dirty = false;
12521
+ this.stored = false;
12522
+ this.cleared = false;
12523
+ this.loaded = /* @__PURE__ */ new Map();
12524
+ }
12313
12525
  };
12314
12526
  }
12315
12527
  });
@@ -12354,14 +12566,6 @@ function headStripIntercept(rawReq, rawRes) {
12354
12566
  function isCompressibleContentType(contentType) {
12355
12567
  return COMPRESSIBLE_PREFIXES.some((prefix) => contentType.includes(prefix));
12356
12568
  }
12357
- function etagMatchesInm(ifNoneMatch, etag) {
12358
- const strip = (tag) => tag.trim().replace(/^W\//, "");
12359
- const target = strip(etag);
12360
- return ifNoneMatch.split(",").some((candidate) => {
12361
- const trimmed = candidate.trim();
12362
- return trimmed === "*" || strip(trimmed) === target;
12363
- });
12364
- }
12365
12569
  function maybeCompressResponse(body, rawReq, rawRes) {
12366
12570
  const acceptEncoding = String(rawReq.headers["accept-encoding"] ?? "");
12367
12571
  const contentTypeHeader = rawRes.getHeader("content-type");
@@ -12377,7 +12581,7 @@ function maybeCompressResponse(body, rawReq, rawRes) {
12377
12581
  }
12378
12582
  function responseIsNotModified(rawReq, rawRes, etag) {
12379
12583
  const ifNoneMatch = String(rawReq.headers["if-none-match"] ?? "");
12380
- if (ifNoneMatch) return etagMatchesInm(ifNoneMatch, etag);
12584
+ if (ifNoneMatch) return etagMatches(ifNoneMatch, etag);
12381
12585
  const lastModifiedHeader = rawRes.getHeader("last-modified");
12382
12586
  if (typeof lastModifiedHeader !== "string") return false;
12383
12587
  const ifModifiedSince = String(rawReq.headers["if-modified-since"] ?? "");
@@ -12505,6 +12709,7 @@ var init_dispatchPipeline = __esm({
12505
12709
  "../core/src/dispatchPipeline.ts"() {
12506
12710
  "use strict";
12507
12711
  init_logger();
12712
+ init_etag();
12508
12713
  COMPRESSIBLE_PREFIXES = [
12509
12714
  "text/",
12510
12715
  "application/json",
@@ -12600,18 +12805,11 @@ function isNotModified(req2, etag, stat) {
12600
12805
  }
12601
12806
  return false;
12602
12807
  }
12603
- function etagMatches(ifNoneMatch, etag) {
12604
- const strip = (tag) => tag.trim().replace(/^W\//, "");
12605
- const target = strip(etag);
12606
- return ifNoneMatch.split(",").some((candidate) => {
12607
- const trimmed = candidate.trim();
12608
- return trimmed === "*" || strip(trimmed) === target;
12609
- });
12610
- }
12611
12808
  var COMPRESSIBLE_PREFIXES2, MIME_TYPES;
12612
12809
  var init_static = __esm({
12613
12810
  "../core/src/static.ts"() {
12614
12811
  "use strict";
12812
+ init_etag();
12615
12813
  COMPRESSIBLE_PREFIXES2 = [
12616
12814
  "text/",
12617
12815
  "application/json",
@@ -12692,11 +12890,11 @@ function formatSourceBlock(filename, lineno) {
12692
12890
  const lines = readSourceLines(filename, lineno);
12693
12891
  if (lines.length === 0) return "";
12694
12892
  const rows = lines.map(([num, text, isError]) => {
12695
- const bg = isError ? `background:${ERROR_LINE_BG};` : "";
12893
+ const rowClass = isError ? "eo-line eo-line-err" : "eo-line";
12696
12894
  const marker = isError ? "&#x25b6;" : " ";
12697
- return `<div style="${bg}display:flex;padding:1px 0;"><span style="color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;">${num}</span><span style="color:${RED};width:1.2em;user-select:none;">${marker}</span><span style="color:${TEXT};white-space:pre-wrap;tab-size:4;">${esc(text)}</span></div>`;
12895
+ return `<div class="${rowClass}"><span class="eo-ln">${num}</span><span class="eo-marker">${marker}</span><span class="eo-code">${esc(text)}</span></div>`;
12698
12896
  }).join("\n");
12699
- return `<div style="background:${SURFACE};border-radius:6px;padding:12px;overflow-x:auto;font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:13px;line-height:1.6;">` + rows + `</div>`;
12897
+ return `<div class="eo-source">` + rows + `</div>`;
12700
12898
  }
12701
12899
  function formatFrame(frame, capturedAt = 0) {
12702
12900
  const source = frame.file && frame.line > 0 ? formatSourceBlock(frame.file, frame.line) : "";
@@ -12708,23 +12906,23 @@ function formatFrame(frame, capturedAt = 0) {
12708
12906
  if (mtime > capturedAt + 0.5) {
12709
12907
  const d = new Date(mtime * 1e3);
12710
12908
  const mtimeIso = `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
12711
- staleBadge = ` <span style="background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;">FILE MODIFIED @ ${mtimeIso} UTC &mdash; source may not match what failed</span>`;
12909
+ staleBadge = ` <span class="eo-stale">FILE MODIFIED @ ${mtimeIso} UTC &mdash; source may not match what failed</span>`;
12712
12910
  }
12713
12911
  } catch {
12714
12912
  }
12715
12913
  }
12716
- return `<div style="margin-bottom:16px;"><div style="margin-bottom:4px;"><span style="color:${BLUE};">${esc(frame.file)}</span><span style="color:${SUBTEXT};"> : </span><span style="color:${YELLOW};">${frame.line}</span><span style="color:${SUBTEXT};"> in </span><span style="color:${GREEN};">${esc(frame.func)}</span>` + staleBadge + `</div>` + source + `</div>`;
12914
+ return `<div class="eo-frame"><div class="eo-frame-head"><span class="eo-file">${esc(frame.file)}</span><span class="eo-sep"> : </span><span class="eo-lineno">${frame.line}</span><span class="eo-sep"> in </span><span class="eo-fn">${esc(frame.func)}</span>` + staleBadge + `</div>` + source + `</div>`;
12717
12915
  }
12718
12916
  function collapsible(title, content, openByDefault = false) {
12719
12917
  const open = openByDefault ? " open" : "";
12720
- return `<details style="margin-top:16px;"${open}><summary style="cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;">${esc(title)}</summary><div style="padding:8px 0;">${content}</div></details>`;
12918
+ return `<details class="eo-details"${open}><summary class="eo-summary">${esc(title)}</summary><div class="eo-details-body">${content}</div></details>`;
12721
12919
  }
12722
12920
  function table(pairs) {
12723
- if (pairs.length === 0) return `<span style="color:${SUBTEXT};">None</span>`;
12921
+ if (pairs.length === 0) return `<span class="eo-none">None</span>`;
12724
12922
  const rows = pairs.map(
12725
- ([key, val]) => `<tr><td style="color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;">${esc(key)}</td><td style="color:${TEXT};padding:4px 0;word-break:break-all;">${esc(val)}</td></tr>`
12923
+ ([key, val]) => `<tr><td class="eo-key">${esc(key)}</td><td class="eo-val">${esc(val)}</td></tr>`
12726
12924
  ).join("");
12727
- return `<table style="border-collapse:collapse;width:100%;">${rows}</table>`;
12925
+ return `<table class="eo-table">${rows}</table>`;
12728
12926
  }
12729
12927
  function renderErrorOverlay(error2, request) {
12730
12928
  const capturedAt = Date.now() / 1e3;
@@ -12737,7 +12935,7 @@ function renderErrorOverlay(error2, request) {
12737
12935
  }
12738
12936
  const hidden = frames.length - Math.min(frames.length, MAX_FRAMES);
12739
12937
  if (hidden > 0) {
12740
- framesHtml += `<div style="color:${SUBTEXT};padding:8px 0;font-size:13px;">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
12938
+ framesHtml += `<div class="eo-hidden-frames">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
12741
12939
  }
12742
12940
  const requestPairs = [];
12743
12941
  if (request != null) {
@@ -12774,37 +12972,72 @@ function renderErrorOverlay(error2, request) {
12774
12972
  ];
12775
12973
  const envSection = collapsible("Environment", table(envPairs));
12776
12974
  const stackSection = collapsible("Stack Trace", framesHtml, true);
12975
+ const nonce = currentCspNonce();
12777
12976
  return `<!DOCTYPE html>
12778
12977
  <html lang="en">
12779
12978
  <head>
12780
12979
  <meta charset="utf-8">
12781
12980
  <meta name="viewport" content="width=device-width,initial-scale=1">
12782
12981
  <title>Tina4 Error \u2014 ${esc(excType)}</title>
12783
- <style>
12784
- *{margin:0;padding:0;box-sizing:border-box;}
12785
- body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
12982
+ <style nonce="${nonce}">
12983
+ ${overlayStylesheet()}
12786
12984
  </style>
12787
12985
  </head>
12788
12986
  <body>
12789
- <div style="max-width:960px;margin:0 auto;">
12790
- <div style="margin-bottom:24px;">
12791
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
12792
- <span style="background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
12793
- <span style="color:${SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
12987
+ <div class="eo-wrap">
12988
+ <div class="eo-header">
12989
+ <div class="eo-badge-row">
12990
+ <span class="eo-badge">Error</span>
12991
+ <span class="eo-sub">Tina4 Debug Overlay</span>
12794
12992
  </div>
12795
- <h1 style="color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;">${esc(excType)}</h1>
12796
- <p style="color:${TEXT};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:${SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid ${RED};">${esc(excMsg)}</p>
12993
+ <h1 class="eo-type">${esc(excType)}</h1>
12994
+ <p class="eo-msg">${esc(excMsg)}</p>
12797
12995
  </div>
12798
12996
  ${stackSection}
12799
12997
  ${requestSection}
12800
12998
  ${envSection}
12801
- <div style="margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;">
12999
+ <div class="eo-footer">
12802
13000
  Tina4 Debug Overlay &mdash; This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
12803
13001
  </div>
12804
13002
  </div>
12805
13003
  </body>
12806
13004
  </html>`;
12807
13005
  }
13006
+ function overlayStylesheet() {
13007
+ return `
13008
+ *{margin:0;padding:0;box-sizing:border-box;}
13009
+ body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
13010
+ .eo-wrap{max-width:960px;margin:0 auto;}
13011
+ .eo-header{margin-bottom:24px;}
13012
+ .eo-badge-row{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
13013
+ .eo-badge{background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;}
13014
+ .eo-sub{color:${SUBTEXT};font-size:14px;}
13015
+ .eo-type{color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;}
13016
+ .eo-msg{color:${TEXT};font-size:18px;font-family:'SF Mono','Fira Code','Consolas',monospace;background:${SURFACE};padding:12px 16px;border-radius:6px;border-left:4px solid ${RED};}
13017
+ .eo-footer{margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;}
13018
+ .eo-source{background:${SURFACE};border-radius:6px;padding:12px;overflow-x:auto;font-family:'SF Mono','Fira Code','Consolas',monospace;font-size:13px;line-height:1.6;}
13019
+ .eo-line{display:flex;padding:1px 0;}
13020
+ .eo-line-err{background:${ERROR_LINE_BG};}
13021
+ .eo-ln{color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;}
13022
+ .eo-marker{color:${RED};width:1.2em;user-select:none;}
13023
+ .eo-code{color:${TEXT};white-space:pre-wrap;tab-size:4;}
13024
+ .eo-frame{margin-bottom:16px;}
13025
+ .eo-frame-head{margin-bottom:4px;}
13026
+ .eo-file{color:${BLUE};}
13027
+ .eo-sep{color:${SUBTEXT};}
13028
+ .eo-lineno{color:${YELLOW};}
13029
+ .eo-fn{color:${GREEN};}
13030
+ .eo-stale{background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;}
13031
+ .eo-details{margin-top:16px;}
13032
+ .eo-summary{cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;}
13033
+ .eo-details-body{padding:8px 0;}
13034
+ .eo-table{border-collapse:collapse;width:100%;}
13035
+ .eo-key{color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;}
13036
+ .eo-val{color:${TEXT};padding:4px 0;word-break:break-all;}
13037
+ .eo-none{color:${SUBTEXT};}
13038
+ .eo-hidden-frames{color:${SUBTEXT};padding:8px 0;font-size:13px;}
13039
+ `;
13040
+ }
12808
13041
  function isDebugMode() {
12809
13042
  return isTruthy(process.env.TINA4_DEBUG);
12810
13043
  }
@@ -12813,6 +13046,7 @@ var init_errorOverlay = __esm({
12813
13046
  "../core/src/errorOverlay.ts"() {
12814
13047
  "use strict";
12815
13048
  init_dotenv();
13049
+ init_csp();
12816
13050
  MAX_FRAMES = 50;
12817
13051
  SENSITIVE_KEY_RE = /password|passwd|secret|token|authorization|cookie|key/i;
12818
13052
  REDACTED2 = "[redacted]";
@@ -13188,6 +13422,25 @@ Generated by DevMailbox.seed() for development purposes.`,
13188
13422
  }
13189
13423
  });
13190
13424
 
13425
+ // ../core/src/upstreamResponse.ts
13426
+ async function relayUpstreamResponse(res, upstream) {
13427
+ const raw = await upstream.text();
13428
+ const status2 = upstream.status || 200;
13429
+ try {
13430
+ res.json(JSON.parse(raw), status2);
13431
+ } catch {
13432
+ res.raw.writeHead(status2, {
13433
+ "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
13434
+ });
13435
+ res.raw.end(raw);
13436
+ }
13437
+ }
13438
+ var init_upstreamResponse = __esm({
13439
+ "../core/src/upstreamResponse.ts"() {
13440
+ "use strict";
13441
+ }
13442
+ });
13443
+
13191
13444
  // ../core/src/metrics.ts
13192
13445
  import * as fs3 from "node:fs";
13193
13446
  import * as path2 from "node:path";
@@ -13403,6 +13656,7 @@ var init_feedback = __esm({
13403
13656
  "use strict";
13404
13657
  init_auth();
13405
13658
  init_devAdmin();
13659
+ init_upstreamResponse();
13406
13660
  RATE_LIMIT_WINDOW_SEC = 3600;
13407
13661
  RATE_LIMIT_MAX = 5;
13408
13662
  _rateLimitHits = /* @__PURE__ */ new Map();
@@ -13449,16 +13703,7 @@ var init_feedback = __esm({
13449
13703
  return;
13450
13704
  }
13451
13705
  clearTimeout(timer);
13452
- const raw = await upstream.text();
13453
- const status2 = upstream.status || 200;
13454
- try {
13455
- res.json(JSON.parse(raw), status2);
13456
- } catch {
13457
- res.raw.writeHead(status2, {
13458
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
13459
- });
13460
- res.raw.end(raw);
13461
- }
13706
+ await relayUpstreamResponse(res, upstream);
13462
13707
  };
13463
13708
  __feedbackDirname = dirname5(fileURLToPath2(import.meta.url));
13464
13709
  WIDGET_BUNDLE_PATH = resolve5(__feedbackDirname, "__feedback", "widget.js");
@@ -13508,6 +13753,38 @@ var init_version = __esm({
13508
13753
  }
13509
13754
  });
13510
13755
 
13756
+ // ../cli/src/util.ts
13757
+ function parseFlags(args, booleanFlags) {
13758
+ const flags = {};
13759
+ const positional = [];
13760
+ let i = 0;
13761
+ while (i < args.length) {
13762
+ const arg = args[i];
13763
+ if (arg.startsWith("--")) {
13764
+ const key = arg.slice(2);
13765
+ if (booleanFlags?.has(key)) {
13766
+ flags[key] = true;
13767
+ i += 1;
13768
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
13769
+ flags[key] = args[i + 1];
13770
+ i += 2;
13771
+ } else {
13772
+ flags[key] = true;
13773
+ i += 1;
13774
+ }
13775
+ } else {
13776
+ positional.push(arg);
13777
+ i += 1;
13778
+ }
13779
+ }
13780
+ return { flags, positional };
13781
+ }
13782
+ var init_util = __esm({
13783
+ "../cli/src/util.ts"() {
13784
+ "use strict";
13785
+ }
13786
+ });
13787
+
13511
13788
  // ../cli/src/commands/generate.ts
13512
13789
  var generate_exports = {};
13513
13790
  __export(generate_exports, {
@@ -13772,28 +14049,7 @@ function parseCliArgs(args) {
13772
14049
  "json",
13773
14050
  "dry-run"
13774
14051
  ]);
13775
- const flags = {};
13776
- const positional = [];
13777
- let i = 0;
13778
- while (i < args.length) {
13779
- if (args[i].startsWith("--")) {
13780
- const key = args[i].slice(2);
13781
- if (booleanFlags.has(key)) {
13782
- flags[key] = true;
13783
- i += 1;
13784
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
13785
- flags[key] = args[i + 1];
13786
- i += 2;
13787
- } else {
13788
- flags[key] = true;
13789
- i += 1;
13790
- }
13791
- } else {
13792
- positional.push(args[i]);
13793
- i += 1;
13794
- }
13795
- }
13796
- return { flags, positional };
14052
+ return parseFlags(args, booleanFlags);
13797
14053
  }
13798
14054
  function parseEvery(every) {
13799
14055
  if (!every || every === true) return 60;
@@ -14641,7 +14897,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
14641
14897
  `{% extends "base.twig" %}
14642
14898
  {% block title %}Login{% endblock %}
14643
14899
  {% block content %}
14644
- <div class="container mt-4" style="max-width:400px">
14900
+ {# No inline style attribute: a nonce lets this <style> block run under the
14901
+ strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
14902
+ that returns this response's nonce (ADR-0088). #}
14903
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
14904
+ <div class="container mt-4 auth-form">
14645
14905
  <h1>Login</h1>
14646
14906
  <form method="post" action="/api/auth/login">
14647
14907
  {{ form_token() }}
@@ -14665,7 +14925,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
14665
14925
  `{% extends "base.twig" %}
14666
14926
  {% block title %}Register{% endblock %}
14667
14927
  {% block content %}
14668
- <div class="container mt-4" style="max-width:400px">
14928
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
14929
+ <div class="container mt-4 auth-form">
14669
14930
  <h1>Register</h1>
14670
14931
  <form method="post" action="/api/auth/register">
14671
14932
  {{ form_token() }}
@@ -15354,6 +15615,7 @@ var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __res
15354
15615
  var init_generate = __esm({
15355
15616
  "../cli/src/commands/generate.ts"() {
15356
15617
  "use strict";
15618
+ init_util();
15357
15619
  FIELD_TYPE_MAP = {
15358
15620
  string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
15359
15621
  str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
@@ -15601,7 +15863,7 @@ import * as path3 from "node:path";
15601
15863
  import * as os from "node:os";
15602
15864
  import { spawnSync as spawnSync2 } from "node:child_process";
15603
15865
  import { createRequire as createRequire2 } from "node:module";
15604
- import { randomBytes as randomBytes4 } from "node:crypto";
15866
+ import { randomBytes as randomBytes5 } from "node:crypto";
15605
15867
  function reqSibling(pkg) {
15606
15868
  return req(`../../${pkg}/src/index.ts`);
15607
15869
  }
@@ -16923,7 +17185,7 @@ var init_mcp = __esm({
16923
17185
  // ── Session lifecycle + protocol negotiation ──────────────────
16924
17186
  /** Mint a new session id and remember it. Called on `initialize`. */
16925
17187
  openSession() {
16926
- const sid = randomBytes4(16).toString("hex");
17188
+ const sid = randomBytes5(16).toString("hex");
16927
17189
  this._sessions.set(sid, Date.now());
16928
17190
  return sid;
16929
17191
  }
@@ -18972,6 +19234,7 @@ __export(websocket_exports, {
18972
19234
  CLOSE_NORMAL: () => CLOSE_NORMAL,
18973
19235
  CLOSE_POLICY_VIOLATION: () => CLOSE_POLICY_VIOLATION,
18974
19236
  CLOSE_PROTOCOL_ERROR: () => CLOSE_PROTOCOL_ERROR,
19237
+ MessageAssembler: () => MessageAssembler,
18975
19238
  OP_BINARY: () => OP_BINARY,
18976
19239
  OP_CLOSE: () => OP_CLOSE,
18977
19240
  OP_CONTINUATION: () => OP_CONTINUATION,
@@ -19229,21 +19492,24 @@ function serveWebSocketRoute(req2, socket, head) {
19229
19492
  state.closed = true;
19230
19493
  void Promise.resolve().then(() => conn._onClose ? conn._onClose() : handler(conn, "close", "")).catch((e) => Log.error(`WebSocket close handler error: ${e.message}`)).finally(() => wsRouteManager.remove(conn.id));
19231
19494
  };
19495
+ const assembler = new MessageAssembler();
19232
19496
  socket.on("data", (chunk) => {
19233
19497
  buffer = Buffer.concat([buffer, chunk]);
19234
19498
  while (buffer.length > 0) {
19235
19499
  const frame = parseFrame(buffer);
19236
19500
  if (!frame) break;
19237
19501
  buffer = buffer.subarray(frame.bytesConsumed);
19238
- switch (frame.opcode) {
19502
+ const whole = assembler.push(frame);
19503
+ if (!whole) continue;
19504
+ switch (whole.opcode) {
19239
19505
  case OP_TEXT: {
19240
- const text = frame.payload.toString("utf-8");
19506
+ const text = whole.payload.toString("utf-8");
19241
19507
  void Promise.resolve().then(() => conn._onMessage ? conn._onMessage(text) : handler(conn, "message", text)).catch((e) => Log.error(`WebSocket message handler error: ${e.message}`));
19242
19508
  break;
19243
19509
  }
19244
19510
  case OP_PING:
19245
19511
  try {
19246
- socket.write(buildFrame(OP_PONG, frame.payload));
19512
+ socket.write(buildFrame(OP_PONG, whole.payload));
19247
19513
  } catch {
19248
19514
  }
19249
19515
  break;
@@ -19262,7 +19528,7 @@ function serveWebSocketRoute(req2, socket, head) {
19262
19528
  socket.on("error", fireClose);
19263
19529
  return true;
19264
19530
  }
19265
- var MAGIC_STRING, OP_CONTINUATION, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, CLOSE_NORMAL, CLOSE_GOING_AWAY, CLOSE_PROTOCOL_ERROR, CLOSE_POLICY_VIOLATION, WebSocketServer, WsRouteManager, wsRouteManager, DevReloadWsManager, devReloadWs;
19531
+ var MAGIC_STRING, OP_CONTINUATION, OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, CLOSE_NORMAL, CLOSE_GOING_AWAY, CLOSE_PROTOCOL_ERROR, CLOSE_POLICY_VIOLATION, MessageAssembler, WebSocketServer, WsRouteManager, wsRouteManager, DevReloadWsManager, devReloadWs;
19266
19532
  var init_websocket = __esm({
19267
19533
  "../core/src/websocket.ts"() {
19268
19534
  "use strict";
@@ -19281,6 +19547,30 @@ var init_websocket = __esm({
19281
19547
  CLOSE_GOING_AWAY = 1001;
19282
19548
  CLOSE_PROTOCOL_ERROR = 1002;
19283
19549
  CLOSE_POLICY_VIOLATION = 1008;
19550
+ MessageAssembler = class {
19551
+ parts = [];
19552
+ opcode = 0;
19553
+ push(frame) {
19554
+ if (frame.opcode >= 8) return { opcode: frame.opcode, payload: frame.payload };
19555
+ if (frame.opcode === OP_CONTINUATION) {
19556
+ if (!this.opcode) return null;
19557
+ this.parts.push(frame.payload);
19558
+ if (!frame.fin) return null;
19559
+ const whole = { opcode: this.opcode, payload: Buffer.concat(this.parts) };
19560
+ this.parts = [];
19561
+ this.opcode = 0;
19562
+ return whole;
19563
+ }
19564
+ if (frame.fin) {
19565
+ this.parts = [];
19566
+ this.opcode = 0;
19567
+ return { opcode: frame.opcode, payload: frame.payload };
19568
+ }
19569
+ this.opcode = frame.opcode;
19570
+ this.parts = [frame.payload];
19571
+ return null;
19572
+ }
19573
+ };
19284
19574
  WebSocketServer = class {
19285
19575
  port;
19286
19576
  server = null;
@@ -19723,6 +20013,15 @@ var init_websocket = __esm({
19723
20013
  this.emit("error", err, client);
19724
20014
  });
19725
20015
  }
20016
+ assemblers = /* @__PURE__ */ new WeakMap();
20017
+ assemblerFor(client) {
20018
+ let a = this.assemblers.get(client);
20019
+ if (!a) {
20020
+ a = new MessageAssembler();
20021
+ this.assemblers.set(client, a);
20022
+ }
20023
+ return a;
20024
+ }
19726
20025
  processBuffer(client, buffer, setBuffer) {
19727
20026
  let remaining = buffer;
19728
20027
  while (remaining.length > 0) {
@@ -19730,15 +20029,17 @@ var init_websocket = __esm({
19730
20029
  if (!frame) break;
19731
20030
  remaining = remaining.subarray(frame.bytesConsumed);
19732
20031
  client.lastActivity = Date.now();
19733
- switch (frame.opcode) {
20032
+ const whole = this.assemblerFor(client).push(frame);
20033
+ if (!whole) continue;
20034
+ switch (whole.opcode) {
19734
20035
  case OP_TEXT:
19735
- this.emit("message", client, frame.payload.toString("utf-8"));
20036
+ this.emit("message", client, whole.payload.toString("utf-8"));
19736
20037
  break;
19737
20038
  case OP_BINARY:
19738
- this.emit("message", client, frame.payload);
20039
+ this.emit("message", client, whole.payload);
19739
20040
  break;
19740
20041
  case OP_PING: {
19741
- const pongFrame = buildFrame(OP_PONG, frame.payload);
20042
+ const pongFrame = buildFrame(OP_PONG, whole.payload);
19742
20043
  try {
19743
20044
  client.socket.write(pongFrame);
19744
20045
  } catch {
@@ -24116,16 +24417,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
24116
24417
  return;
24117
24418
  }
24118
24419
  clearTimeout(timer);
24119
- const raw = await upstream.text();
24120
- const status2 = upstream.status || 200;
24121
- try {
24122
- res.json(JSON.parse(raw), status2);
24123
- } catch {
24124
- res.raw.writeHead(status2, {
24125
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
24126
- });
24127
- res.raw.end(raw);
24128
- }
24420
+ await relayUpstreamResponse(res, upstream);
24129
24421
  }
24130
24422
  function resolveDevEnvVar(key) {
24131
24423
  const live = process.env[key];
@@ -24567,6 +24859,7 @@ var init_devAdmin = __esm({
24567
24859
  "../core/src/devAdmin.ts"() {
24568
24860
  "use strict";
24569
24861
  init_devMailbox();
24862
+ init_upstreamResponse();
24570
24863
  init_dotenv();
24571
24864
  init_metrics();
24572
24865
  init_feedback();
@@ -24668,8 +24961,8 @@ var init_devAdmin = __esm({
24668
24961
  * Duplicate errors (same message) increment count and update last_seen.
24669
24962
  */
24670
24963
  static capture(errorType, message, traceback = "", file = "", line = 0) {
24671
- const fingerprint = `${errorType}|${message}|${file}|${line}`;
24672
- const existing = this.errors.find((e) => e.fingerprint === fingerprint);
24964
+ const fingerprint2 = `${errorType}|${message}|${file}|${line}`;
24965
+ const existing = this.errors.find((e) => e.fingerprint === fingerprint2);
24673
24966
  const now = (/* @__PURE__ */ new Date()).toISOString();
24674
24967
  if (existing) {
24675
24968
  existing.count = (existing.count || 1) + 1;
@@ -24682,7 +24975,7 @@ var init_devAdmin = __esm({
24682
24975
  message,
24683
24976
  stack: traceback || void 0,
24684
24977
  resolved: false,
24685
- ...{ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
24978
+ ...{ fingerprint: fingerprint2, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
24686
24979
  });
24687
24980
  if (this.errors.length > this.maxErrors) {
24688
24981
  this.errors = this.errors.slice(-this.maxErrors);
@@ -27361,7 +27654,7 @@ function swaggerEnabled() {
27361
27654
  }
27362
27655
  function createSwaggerRoutes(getSpec) {
27363
27656
  const serveUi = async (_req, res) => {
27364
- res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
27657
+ res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
27365
27658
  };
27366
27659
  return [
27367
27660
  {
@@ -27405,13 +27698,13 @@ var SWAGGER_UI_HTML;
27405
27698
  var init_ui = __esm({
27406
27699
  "../swagger/src/ui.ts"() {
27407
27700
  "use strict";
27408
- SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
27701
+ SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
27409
27702
  <html lang="en">
27410
27703
  <head>
27411
27704
  <meta charset="UTF-8">
27412
27705
  <title>Tina4 API Documentation</title>
27413
27706
  <link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
27414
- <style>
27707
+ <style nonce="${nonce}">
27415
27708
  body { margin: 0; background: #fafafa; }
27416
27709
  .topbar { display: none !important; }
27417
27710
  </style>
@@ -27419,7 +27712,7 @@ var init_ui = __esm({
27419
27712
  <body>
27420
27713
  <div id="swagger-ui"></div>
27421
27714
  <script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
27422
- <script>
27715
+ <script nonce="${nonce}">
27423
27716
  SwaggerUIBundle({
27424
27717
  url: "${specUrl}",
27425
27718
  dom_id: '#swagger-ui',
@@ -27459,7 +27752,7 @@ __export(sso_exports, {
27459
27752
  SsoError: () => SsoError,
27460
27753
  liveSessionIdentity: () => liveSessionIdentity
27461
27754
  });
27462
- import { createHash as createHash9, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
27755
+ import { createHash as createHash9, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
27463
27756
  function expiresAt(tokens) {
27464
27757
  const lifetime = Number(tokens.expires_in ?? 0);
27465
27758
  return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
@@ -27587,9 +27880,9 @@ var init_sso = __esm({
27587
27880
  async login(requestOrSession, returnTo = "/") {
27588
27881
  const session = this.session(requestOrSession);
27589
27882
  if (!session) throw new SsoError("SSO login requires a Tina4 Session");
27590
- const state = randomBytes5(32).toString("base64url");
27591
- const nonce = randomBytes5(32).toString("base64url");
27592
- const verifier = randomBytes5(64).toString("base64url");
27883
+ const state = randomBytes6(32).toString("base64url");
27884
+ const nonce = randomBytes6(32).toString("base64url");
27885
+ const verifier = randomBytes6(64).toString("base64url");
27593
27886
  const challenge = createHash9("sha256").update(verifier).digest("base64url");
27594
27887
  session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
27595
27888
  const metadata = await this.discover();
@@ -27817,7 +28110,7 @@ var init_docsAutoDiscovery = __esm({
27817
28110
  });
27818
28111
 
27819
28112
  // ../core/src/server.ts
27820
- import { randomBytes as randomBytes6 } from "node:crypto";
28113
+ import { randomBytes as randomBytes7 } from "node:crypto";
27821
28114
  import { resolve as resolve15, dirname as dirname11, join as join25, relative as relative10 } from "node:path";
27822
28115
  import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync16 } from "node:fs";
27823
28116
  import { isatty } from "node:tty";
@@ -27997,7 +28290,8 @@ function isDevMode() {
27997
28290
  }
27998
28291
  async function renderErrorPage(code, data, templatesDir) {
27999
28292
  try {
28000
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
28293
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
28294
+ setCspNonceProvider2(currentCspNonce);
28001
28295
  const templateFile = `errors/${code}.twig`;
28002
28296
  const getCachedFrond = (dir) => {
28003
28297
  let instance = frondCache.get(dir);
@@ -28113,6 +28407,7 @@ function resolveTemplate(pathname, templatesDir) {
28113
28407
  }
28114
28408
  function renderLandingPage(port = 7148) {
28115
28409
  const version = TINA4_VERSION;
28410
+ const nonce = currentCspNonce();
28116
28411
  const galleryItems = [
28117
28412
  { id: "rest-api", icon: "&#128640;", name: "REST API", desc: "A simple JSON API with GET and POST endpoints", accent: "accent-blue", tryUrl: "/api/gallery/hello" },
28118
28413
  { id: "orm", icon: "&#128451;", name: "ORM", desc: "Product model with CRUD endpoints", accent: "accent-green", tryUrl: "/api/gallery/products" },
@@ -28127,7 +28422,7 @@ function renderLandingPage(port = 7148) {
28127
28422
  const isDeployed = deployed[item.id] === true;
28128
28423
  const tryBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-try" target="_blank">Try It</a>` : "";
28129
28424
  const viewBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-view" target="_blank">View</a>` : "";
28130
- const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" onclick="deployGallery('${item.id}')">Deploy</button>`;
28425
+ const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" data-deploy="${item.id}">Deploy</button>`;
28131
28426
  return `<div class="gallery-card">
28132
28427
  <div class="accent ${item.accent}"></div>
28133
28428
  <div class="icon">${item.icon}</div>
@@ -28142,7 +28437,7 @@ function renderLandingPage(port = 7148) {
28142
28437
  <meta charset="utf-8">
28143
28438
  <meta name="viewport" content="width=device-width, initial-scale=1">
28144
28439
  <title>Tina4NodeJs</title>
28145
- <style>
28440
+ <style nonce="${nonce}">
28146
28441
  *{margin:0;padding:0;box-sizing:border-box}
28147
28442
  body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;flex-direction:column;align-items:center;position:relative}
28148
28443
  .bg-watermark{position:fixed;bottom:-5%;right:-5%;width:45%;opacity:0.04;pointer-events:none;z-index:0}
@@ -28181,6 +28476,11 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
28181
28476
  .gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
28182
28477
  @keyframes wiggle{0%{transform:rotate(0deg)}15%{transform:rotate(14deg)}30%{transform:rotate(-10deg)}45%{transform:rotate(8deg)}60%{transform:rotate(-4deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0deg)}}
28183
28478
  .star-wiggle{display:inline-block;transform-origin:center}
28479
+ .gallery-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
28480
+ .tok-comment{color:#64748b}
28481
+ .tok-kw{color:#c084fc}
28482
+ .tok-str{color:#4ade80}
28483
+ .tok-fn{color:#38bdf8}
28184
28484
  </style>
28185
28485
  </head>
28186
28486
  <body>
@@ -28206,23 +28506,23 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
28206
28506
  <div class="section">
28207
28507
  <div class="card">
28208
28508
  <h2>Getting Started</h2>
28209
- <pre class="code-block"><code><span style="color:#64748b">// app.ts</span>
28210
- <span style="color:#c084fc">import</span> { startServer, Router } <span style="color:#c084fc">from</span> <span style="color:#4ade80">"tina4-nodejs"</span>;
28509
+ <pre class="code-block"><code><span class="tok-comment">// app.ts</span>
28510
+ <span class="tok-kw">import</span> { startServer, Router } <span class="tok-kw">from</span> <span class="tok-str">"tina4-nodejs"</span>;
28211
28511
 
28212
- Router.get(<span style="color:#4ade80">"/hello"</span>, <span style="color:#c084fc">async</span> (<span style="color:#38bdf8">req</span>, <span style="color:#38bdf8">res</span>) =&gt; {
28213
- <span style="color:#c084fc">return</span> res.json({ message: <span style="color:#4ade80">"Hello World!"</span> });
28512
+ Router.get(<span class="tok-str">"/hello"</span>, <span class="tok-kw">async</span> (<span class="tok-fn">req</span>, <span class="tok-fn">res</span>) =&gt; {
28513
+ <span class="tok-kw">return</span> res.json({ message: <span class="tok-str">"Hello World!"</span> });
28214
28514
  });
28215
28515
 
28216
- startServer({ port: 7148 }); <span style="color:#64748b">// starts on port 7148</span></code></pre>
28516
+ startServer({ port: 7148 }); <span class="tok-comment">// starts on port 7148</span></code></pre>
28217
28517
  </div>
28218
28518
  </div>
28219
28519
  <div class="gallery">
28220
28520
  <h2 id="gallery">What You Can Build</h2>
28221
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;">
28521
+ <div class="gallery-cards">
28222
28522
  ${galleryCards}
28223
28523
  </div>
28224
28524
  </div>
28225
- <script>
28525
+ <script nonce="${nonce}">
28226
28526
  function deployGallery(name) {
28227
28527
  if (!confirm('Deploy the "' + name + '" gallery example into your project? This will copy files into src/.')) return;
28228
28528
  fetch('/__dev/api/gallery/deploy', {
@@ -28257,6 +28557,12 @@ function deployGallery(name) {
28257
28557
  }
28258
28558
  setTimeout(doWiggle,3000);
28259
28559
  })();
28560
+ // CSP-clean wiring: no inline onclick handlers \u2014 bind every Deploy button from
28561
+ // its data-deploy attribute (a nonce covers script elements, not event-handler
28562
+ // attributes).
28563
+ document.querySelectorAll('.gbtn-deploy[data-deploy]').forEach(function(btn){
28564
+ btn.addEventListener('click', function(){ deployGallery(btn.dataset.deploy); });
28565
+ });
28260
28566
  </script>
28261
28567
  </body>
28262
28568
  </html>`;
@@ -28473,7 +28779,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
28473
28779
  }
28474
28780
  }
28475
28781
  }
28476
- const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
28782
+ const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
28477
28783
  if (wantsJson(req2)) {
28478
28784
  const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
28479
28785
  res.raw.writeHead(500, { "Content-Type": "application/json" });
@@ -28544,7 +28850,7 @@ function serveStaticAsset(ctx) {
28544
28850
  return false;
28545
28851
  }
28546
28852
  async function serveNotFound(ctx) {
28547
- const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
28853
+ const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
28548
28854
  if (wantsJson(ctx.req)) {
28549
28855
  const body = negotiatedErrorBody(404, "Not Found", requestId);
28550
28856
  ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
@@ -28567,7 +28873,8 @@ async function buildDispatchContext(router, base) {
28567
28873
  const templatesDir = resolve15(root, "src/templates");
28568
28874
  let frondEngine = null;
28569
28875
  try {
28570
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
28876
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
28877
+ setCspNonceProvider2(currentCspNonce);
28571
28878
  frondEngine = new Frond2(templatesDir);
28572
28879
  } catch {
28573
28880
  }
@@ -28677,9 +28984,13 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
28677
28984
  }
28678
28985
  }
28679
28986
  async function runDispatch(ctx, rawReq, rawRes) {
28680
- const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes6(4).toString("hex");
28987
+ const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes7(4).toString("hex");
28681
28988
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
28682
- return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
28989
+ const requestNonce = generateNonce();
28990
+ return Log.runWithRequestId(
28991
+ requestId,
28992
+ () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
28993
+ );
28683
28994
  }
28684
28995
  function loopbackBindHosts(host) {
28685
28996
  const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
@@ -28806,7 +29117,8 @@ async function startServer(config) {
28806
29117
  let frondEngine = null;
28807
29118
  setDefaultTemplatesDir(templatesDir);
28808
29119
  try {
28809
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29120
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
29121
+ setCspNonceProvider2(currentCspNonce);
28810
29122
  frondEngine = new Frond2(templatesDir);
28811
29123
  router.addRoute({
28812
29124
  method: "GET",
@@ -28947,10 +29259,12 @@ async function startServer(config) {
28947
29259
  }
28948
29260
  });
28949
29261
  aiServer.on("error", (err) => {
28950
- if (err.code === "EADDRINUSE") {
29262
+ if (err?.code === "EADDRINUSE") {
28951
29263
  Log.warning(`Test port ${testPort} in use \u2014 skipping`);
28952
- aiServer = null;
29264
+ } else {
29265
+ Log.error(`Test port ${testPort} bind failed (${err?.code ?? err}) \u2014 skipping`);
28953
29266
  }
29267
+ aiServer = null;
28954
29268
  });
28955
29269
  aiServer.listen(testPort, host);
28956
29270
  }
@@ -29165,6 +29479,7 @@ var init_server = __esm({
29165
29479
  init_static();
29166
29480
  init_dotenv();
29167
29481
  init_errorOverlay();
29482
+ init_csp();
29168
29483
  init_health();
29169
29484
  init_rateLimiter();
29170
29485
  init_logger();
@@ -29945,7 +30260,7 @@ var init_mqttMessage = __esm({
29945
30260
  // ../core/src/mqtt.ts
29946
30261
  import net2 from "node:net";
29947
30262
  import tls from "node:tls";
29948
- import { randomBytes as randomBytes7 } from "node:crypto";
30263
+ import { randomBytes as randomBytes8 } from "node:crypto";
29949
30264
  import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
29950
30265
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
29951
30266
  var init_mqtt = __esm({
@@ -30033,7 +30348,7 @@ var init_mqtt = __esm({
30033
30348
  this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
30034
30349
  this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
30035
30350
  let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
30036
- if (cid === null || cid === "") cid = "tina4-" + randomBytes7(8).toString("hex");
30351
+ if (cid === null || cid === "") cid = "tina4-" + randomBytes8(8).toString("hex");
30037
30352
  this.clientId = cid;
30038
30353
  this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
30039
30354
  this.cleanSession = options.cleanSession ?? true;
@@ -31147,7 +31462,7 @@ var init_ssrf = __esm({
31147
31462
  import http from "node:http";
31148
31463
  import https from "node:https";
31149
31464
  import { URL as URL2 } from "node:url";
31150
- import { randomBytes as randomBytes8 } from "node:crypto";
31465
+ import { randomBytes as randomBytes9 } from "node:crypto";
31151
31466
  import { promises as fsp, createWriteStream } from "node:fs";
31152
31467
  import { basename as basename6 } from "node:path";
31153
31468
  import { pipeline } from "node:stream/promises";
@@ -31539,7 +31854,7 @@ var init_api = __esm({
31539
31854
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
31540
31855
  }
31541
31856
  const partContentType = guessContentType(uploadName);
31542
- const boundary = "----Tina4Boundary" + randomBytes8(16).toString("hex");
31857
+ const boundary = "----Tina4Boundary" + randomBytes9(16).toString("hex");
31543
31858
  const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
31544
31859
  const contentType = `multipart/form-data; boundary=${boundary}`;
31545
31860
  return this.execute("POST", this.buildUrl(path10), bodyBuffer, contentType, headers);
@@ -34503,7 +34818,7 @@ import {
34503
34818
  createHmac as createHmac3,
34504
34819
  createPrivateKey,
34505
34820
  createSign as createSign2,
34506
- randomBytes as randomBytes9
34821
+ randomBytes as randomBytes10
34507
34822
  } from "node:crypto";
34508
34823
  function encodeBase64Url(value) {
34509
34824
  return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
@@ -34583,7 +34898,7 @@ function encryptPayload(payload, subscription) {
34583
34898
  ]);
34584
34899
  const prkKey = hmac(authSecret, sharedSecret);
34585
34900
  const ikm = hkdfExpand(prkKey, keyInfo, 32);
34586
- const salt = randomBytes9(16);
34901
+ const salt = randomBytes10(16);
34587
34902
  const contentPrk = hmac(salt, ikm);
34588
34903
  const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
34589
34904
  const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
@@ -37340,6 +37655,7 @@ __export(src_exports2, {
37340
37655
  cacheStats: () => cacheStats,
37341
37656
  checkPassword: () => checkPassword,
37342
37657
  clearCache: () => clearCache,
37658
+ clearCurrentNonce: () => clearCurrentNonce,
37343
37659
  computeAcceptKey: () => computeAcceptKey,
37344
37660
  container: () => container,
37345
37661
  cors: () => cors,
@@ -37351,6 +37667,8 @@ __export(src_exports2, {
37351
37667
  createMessenger: () => createMessenger,
37352
37668
  createRequest: () => createRequest,
37353
37669
  createResponse: () => createResponse,
37670
+ cspNonce: () => cspNonce,
37671
+ currentCspNonce: () => currentCspNonce,
37354
37672
  decodeRequest: () => decodeRequest,
37355
37673
  defaultContext: () => defaultContext,
37356
37674
  defaultRouter: () => defaultRouter,
@@ -37376,6 +37694,7 @@ __export(src_exports2, {
37376
37694
  feedbackWhitelist: () => feedbackWhitelist,
37377
37695
  fts5Supported: () => fts5Supported,
37378
37696
  generateContext: () => generateContext,
37697
+ generateNonce: () => generateNonce,
37379
37698
  generateVapidKeys: () => generateVapidKeys,
37380
37699
  get: () => get,
37381
37700
  getDefaultDevServer: () => getDefaultDevServer,
@@ -37398,6 +37717,7 @@ __export(src_exports2, {
37398
37717
  httpReason: () => httpReason,
37399
37718
  inContainer: () => inContainer,
37400
37719
  injectFeedbackWidget: () => injectFeedbackWidget,
37720
+ injectNonceIntoCsp: () => injectNonceIntoCsp,
37401
37721
  installAll: () => installAll,
37402
37722
  installSelected: () => installSelected,
37403
37723
  isBannerSuppressed: () => isBannerSuppressed,
@@ -37452,6 +37772,7 @@ __export(src_exports2, {
37452
37772
  resetTrustedProxyCache: () => resetTrustedProxyCache,
37453
37773
  resolveAlgorithm: () => resolveAlgorithm,
37454
37774
  resolveClientIp: () => resolveClientIp,
37775
+ resolveCspHeader: () => resolveCspHeader,
37455
37776
  resolveFrameworkVersion: () => resolveFrameworkVersion,
37456
37777
  resolvePortAndHost: () => resolvePortAndHost,
37457
37778
  resolveStringMiddleware: () => resolveStringMiddleware,
@@ -37459,12 +37780,14 @@ __export(src_exports2, {
37459
37780
  responseCache: () => responseCache,
37460
37781
  runAll: () => runAll,
37461
37782
  runRouteMiddlewares: () => runRouteMiddlewares,
37783
+ runWithCspNonce: () => runWithCspNonce,
37462
37784
  runtimeDir: () => runtimeDir,
37463
37785
  saveUpload: () => saveUpload,
37464
37786
  schemaFromParams: () => schemaFromParams,
37465
37787
  selectablePids: () => selectablePids,
37466
37788
  serveWebSocketRoute: () => serveWebSocketRoute,
37467
37789
  sessionCookieName: () => sessionCookieName,
37790
+ setCurrentNonce: () => setCurrentNonce,
37468
37791
  setDefaultTemplatesDir: () => setDefaultTemplatesDir,
37469
37792
  setFrond: () => setFrond,
37470
37793
  showMenu: () => showMenu,
@@ -37531,6 +37854,7 @@ var init_src2 = __esm({
37531
37854
  init_wsdl();
37532
37855
  init_htmlElement();
37533
37856
  init_errorOverlay();
37857
+ init_csp();
37534
37858
  init_ai();
37535
37859
  init_sso();
37536
37860
  init_push();
@@ -41697,7 +42021,7 @@ __export(database_exports, {
41697
42021
  stripTrailingSemicolons: () => stripTrailingSemicolons,
41698
42022
  wrapWithCache: () => wrapWithCache
41699
42023
  });
41700
- import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
42024
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
41701
42025
  function stripTrailingSemicolons(sql) {
41702
42026
  if (!sql) return sql;
41703
42027
  let stripped = sql.replace(/\s+$/, "");
@@ -42111,9 +42435,9 @@ var init_database = __esm({
42111
42435
  leases = /* @__PURE__ */ new Map();
42112
42436
  discardedSlots = /* @__PURE__ */ new Set();
42113
42437
  repairs = /* @__PURE__ */ new Map();
42114
- leaseOwner = new AsyncLocalStorage2();
42115
- operationStore = new AsyncLocalStorage2();
42116
- txStore = new AsyncLocalStorage2();
42438
+ leaseOwner = new AsyncLocalStorage3();
42439
+ operationStore = new AsyncLocalStorage3();
42440
+ txStore = new AsyncLocalStorage3();
42117
42441
  /**
42118
42442
  * Create a Database wrapping an existing adapter.
42119
42443
  * For creating a Database from a URL, use the async static factories:
@@ -46864,7 +47188,7 @@ var init_seeder = __esm({
46864
47188
 
46865
47189
  // src/docstore.ts
46866
47190
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
46867
- import { randomBytes as randomBytes10 } from "node:crypto";
47191
+ import { randomBytes as randomBytes11 } from "node:crypto";
46868
47192
  import { mkdirSync as mkdirSync20 } from "node:fs";
46869
47193
  import { dirname as dirname14, isAbsolute as isAbsolute6, join as join32 } from "node:path";
46870
47194
  function iso(d) {
@@ -47221,8 +47545,8 @@ var init_docstore = __esm({
47221
47545
  OID_RE = /^[0-9a-fA-F]{24}$/;
47222
47546
  ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
47223
47547
  ObjectId = class _ObjectId {
47224
- static _counter = randomBytes10(3).readUIntBE(0, 3);
47225
- static _process = randomBytes10(5);
47548
+ static _counter = randomBytes11(3).readUIntBE(0, 3);
47549
+ static _process = randomBytes11(5);
47226
47550
  _bytes;
47227
47551
  constructor(oid) {
47228
47552
  if (oid === void 0 || oid === null) {
@@ -47718,6 +48042,36 @@ var init_shapes = __esm({
47718
48042
  }
47719
48043
  });
47720
48044
 
48045
+ // src/graph/adapters/rowMapping.ts
48046
+ function errorMessage(exc) {
48047
+ if (exc instanceof Error) return exc.message;
48048
+ return String(exc);
48049
+ }
48050
+ function nodeFromRow(row) {
48051
+ if (row === null || row === void 0) return null;
48052
+ return new GraphNode(
48053
+ String(row.id),
48054
+ row.labels ?? [],
48055
+ row.props ?? {}
48056
+ );
48057
+ }
48058
+ function edgeFromRow(row) {
48059
+ if (row === null || row === void 0) return null;
48060
+ return new GraphEdge(
48061
+ String(row.id),
48062
+ String(row.type),
48063
+ String(row.f),
48064
+ String(row.t),
48065
+ row.props ?? {}
48066
+ );
48067
+ }
48068
+ var init_rowMapping = __esm({
48069
+ "src/graph/adapters/rowMapping.ts"() {
48070
+ "use strict";
48071
+ init_shapes();
48072
+ }
48073
+ });
48074
+
47721
48075
  // src/graph/connectTimeout.ts
47722
48076
  function resolveGraphConnectTimeout() {
47723
48077
  const raw = (process.env[GRAPH_CONNECT_TIMEOUT_VARIABLE] ?? "").trim();
@@ -47757,15 +48111,12 @@ function propClause(properties) {
47757
48111
  for (const key of keys) params[`p_${key}`] = props[key];
47758
48112
  return { clause: `{${pairs}}`, params };
47759
48113
  }
47760
- function errorMessage(exc) {
47761
- if (exc instanceof Error) return exc.message;
47762
- return String(exc);
47763
- }
47764
48114
  var DRIVER_PACKAGE2, driver, UltipaClient, UltipaConnectError, UNBOUNDED_CONNECT_SECONDS, UltipaGraphAdapter;
47765
48115
  var init_ultipa = __esm({
47766
48116
  async "src/graph/adapters/ultipa.ts"() {
47767
48117
  "use strict";
47768
48118
  init_shapes();
48119
+ init_rowMapping();
47769
48120
  init_errors();
47770
48121
  init_connectTimeout2();
47771
48122
  DRIVER_PACKAGE2 = "tina4-ultipa";
@@ -47823,12 +48174,7 @@ var init_ultipa = __esm({
47823
48174
  }
47824
48175
  // -- portable node/edge/traverse core (GQL) ----------------------------
47825
48176
  nodeFromRow(row) {
47826
- if (row === null || row === void 0) return null;
47827
- return new GraphNode(
47828
- String(row.id),
47829
- row.labels ?? [],
47830
- row.props ?? {}
47831
- );
48177
+ return nodeFromRow(row);
47832
48178
  }
47833
48179
  async addNode(label, properties = null) {
47834
48180
  const { clause, params } = propClause(properties);
@@ -47842,15 +48188,7 @@ var init_ultipa = __esm({
47842
48188
  params.to_id = toId;
47843
48189
  const gql = `MATCH (a), (b) WHERE id(a) = $from_id AND id(b) = $to_id INSERT (a)-[e:\`${type2}\` ${clause}]->(b) RETURN id(e) AS id, type(e) AS type, id(a) AS f, id(b) AS t, properties(e) AS props`;
47844
48190
  const rows = (await this.run(gql, params, false)).dicts();
47845
- if (rows.length === 0) return null;
47846
- const row = rows[0];
47847
- return new GraphEdge(
47848
- String(row.id),
47849
- String(row.type),
47850
- String(row.f),
47851
- String(row.t),
47852
- row.props ?? {}
47853
- );
48191
+ return edgeFromRow(rows[0]);
47854
48192
  }
47855
48193
  async getNode(nodeId) {
47856
48194
  const gql = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -47915,10 +48253,6 @@ var bolt_exports = {};
47915
48253
  __export(bolt_exports, {
47916
48254
  BoltGraphAdapter: () => BoltGraphAdapter
47917
48255
  });
47918
- function errorMessage2(exc) {
47919
- if (exc instanceof Error) return exc.message;
47920
- return String(exc);
47921
- }
47922
48256
  function boltId(id) {
47923
48257
  const value = Number(id);
47924
48258
  return Number.isNaN(value) ? -1 : value;
@@ -47928,6 +48262,7 @@ var init_bolt = __esm({
47928
48262
  async "src/graph/adapters/bolt.ts"() {
47929
48263
  "use strict";
47930
48264
  init_shapes();
48265
+ init_rowMapping();
47931
48266
  init_errors();
47932
48267
  init_connectTimeout2();
47933
48268
  DRIVER_PACKAGE3 = "neo4j-driver";
@@ -47963,7 +48298,7 @@ var init_bolt = __esm({
47963
48298
  const result = await session.run(cypher, params ?? {});
47964
48299
  return result.records.map((record) => record.toObject());
47965
48300
  } catch (exc) {
47966
- this.lastError = errorMessage2(exc);
48301
+ this.lastError = errorMessage(exc);
47967
48302
  const code = exc?.code ?? "";
47968
48303
  const message = this.lastError.toLowerCase();
47969
48304
  if (code === "ServiceUnavailable" || code === neo4j.error?.SERVICE_UNAVAILABLE || message.includes("timed out") || message.includes("timeout")) {
@@ -47987,12 +48322,7 @@ var init_bolt = __esm({
47987
48322
  }
47988
48323
  // -- portable node/edge/traverse core (Cypher) -------------------------
47989
48324
  nodeFromRow(row) {
47990
- if (row === null || row === void 0) return null;
47991
- return new GraphNode(
47992
- String(row.id),
47993
- row.labels ?? [],
47994
- row.props ?? {}
47995
- );
48325
+ return nodeFromRow(row);
47996
48326
  }
47997
48327
  async addNode(label, properties = null) {
47998
48328
  const cypher = `CREATE (n:\`${label}\` $props) RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -48006,15 +48336,7 @@ var init_bolt = __esm({
48006
48336
  to_id: boltId(toId),
48007
48337
  props: properties ?? {}
48008
48338
  });
48009
- if (rows.length === 0) return null;
48010
- const row = rows[0];
48011
- return new GraphEdge(
48012
- String(row.id),
48013
- String(row.type),
48014
- String(row.f),
48015
- String(row.t),
48016
- row.props ?? {}
48017
- );
48339
+ return edgeFromRow(rows[0]);
48018
48340
  }
48019
48341
  async getNode(nodeId) {
48020
48342
  const cypher = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -48080,7 +48402,7 @@ function cleanProps(doc) {
48080
48402
  }
48081
48403
  return props;
48082
48404
  }
48083
- function errorMessage3(exc) {
48405
+ function errorMessage2(exc) {
48084
48406
  if (exc instanceof Error) return exc.message;
48085
48407
  return String(exc);
48086
48408
  }
@@ -48119,14 +48441,14 @@ var init_arango = __esm({
48119
48441
  this.db = new ArangoDatabase(config);
48120
48442
  }
48121
48443
  connectOrError(exc) {
48122
- const text = errorMessage3(exc).toLowerCase();
48444
+ const text = errorMessage2(exc).toLowerCase();
48123
48445
  if (text.includes("timed out") || text.includes("timeout") || text.includes("connection") || text.includes("econnrefused") || text.includes("etimedout") || text.includes("max retries")) {
48124
48446
  return new GraphConnectTimeout(
48125
48447
  `Graph connect to ${this.url.host}:${this.url.port} timed out (${GRAPH_CONNECT_TIMEOUT_VARIABLE}). Raise ${GRAPH_CONNECT_TIMEOUT_VARIABLE} if the server is simply slow, or set it to 0 to wait indefinitely.`,
48126
48448
  exc
48127
48449
  );
48128
48450
  }
48129
- return new GraphError(errorMessage3(exc), exc);
48451
+ return new GraphError(errorMessage2(exc), exc);
48130
48452
  }
48131
48453
  async ensureCollections() {
48132
48454
  if (this.ensured) return;
@@ -48137,7 +48459,7 @@ var init_arango = __esm({
48137
48459
  if (!await edges.exists()) await this.db.createEdgeCollection(EDGE_COLLECTION);
48138
48460
  this.ensured = true;
48139
48461
  } catch (exc) {
48140
- this.lastError = errorMessage3(exc);
48462
+ this.lastError = errorMessage2(exc);
48141
48463
  throw this.connectOrError(exc);
48142
48464
  }
48143
48465
  }
@@ -48147,7 +48469,7 @@ var init_arango = __esm({
48147
48469
  const cursor = await this.db.query({ query, bindVars: bind2 ?? {} });
48148
48470
  return await cursor.all();
48149
48471
  } catch (exc) {
48150
- this.lastError = errorMessage3(exc);
48472
+ this.lastError = errorMessage2(exc);
48151
48473
  throw this.connectOrError(exc);
48152
48474
  }
48153
48475
  }
@@ -48421,7 +48743,7 @@ var init_attachment = __esm({
48421
48743
  });
48422
48744
 
48423
48745
  // src/realtime/storage.ts
48424
- import { randomBytes as randomBytes11 } from "node:crypto";
48746
+ import { randomBytes as randomBytes12 } from "node:crypto";
48425
48747
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync18, unlinkSync as unlinkSync8, statSync as statSync19 } from "node:fs";
48426
48748
  import { resolve as resolve20, sep as sep8 } from "node:path";
48427
48749
  import { createRequire as createRequire8 } from "node:module";
@@ -48441,7 +48763,7 @@ function storageKey(filename = "") {
48441
48763
  const clean = raw.replace(UNSAFE, "").slice(0, 12);
48442
48764
  if (clean) ext = `.${clean}`;
48443
48765
  }
48444
- return `${randomBytes11(16).toString("hex")}${ext}`;
48766
+ return `${randomBytes12(16).toString("hex")}${ext}`;
48445
48767
  }
48446
48768
  function selectStorage(storage) {
48447
48769
  if (storage) return storage;