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
@@ -13695,6 +13695,36 @@ var init_shapes = __esm({
13695
13695
  }
13696
13696
  });
13697
13697
 
13698
+ // ../orm/src/graph/adapters/rowMapping.ts
13699
+ function errorMessage(exc) {
13700
+ if (exc instanceof Error) return exc.message;
13701
+ return String(exc);
13702
+ }
13703
+ function nodeFromRow(row) {
13704
+ if (row === null || row === void 0) return null;
13705
+ return new GraphNode(
13706
+ String(row.id),
13707
+ row.labels ?? [],
13708
+ row.props ?? {}
13709
+ );
13710
+ }
13711
+ function edgeFromRow(row) {
13712
+ if (row === null || row === void 0) return null;
13713
+ return new GraphEdge(
13714
+ String(row.id),
13715
+ String(row.type),
13716
+ String(row.f),
13717
+ String(row.t),
13718
+ row.props ?? {}
13719
+ );
13720
+ }
13721
+ var init_rowMapping = __esm({
13722
+ "../orm/src/graph/adapters/rowMapping.ts"() {
13723
+ "use strict";
13724
+ init_shapes();
13725
+ }
13726
+ });
13727
+
13698
13728
  // ../orm/src/graph/connectTimeout.ts
13699
13729
  function resolveGraphConnectTimeout() {
13700
13730
  const raw = (process.env[GRAPH_CONNECT_TIMEOUT_VARIABLE] ?? "").trim();
@@ -13734,15 +13764,12 @@ function propClause(properties) {
13734
13764
  for (const key of keys) params[`p_${key}`] = props[key];
13735
13765
  return { clause: `{${pairs}}`, params };
13736
13766
  }
13737
- function errorMessage(exc) {
13738
- if (exc instanceof Error) return exc.message;
13739
- return String(exc);
13740
- }
13741
13767
  var DRIVER_PACKAGE, driver, UltipaClient, UltipaConnectError, UNBOUNDED_CONNECT_SECONDS, UltipaGraphAdapter;
13742
13768
  var init_ultipa = __esm({
13743
13769
  async "../orm/src/graph/adapters/ultipa.ts"() {
13744
13770
  "use strict";
13745
13771
  init_shapes();
13772
+ init_rowMapping();
13746
13773
  init_errors();
13747
13774
  init_connectTimeout2();
13748
13775
  DRIVER_PACKAGE = "tina4-ultipa";
@@ -13800,12 +13827,7 @@ var init_ultipa = __esm({
13800
13827
  }
13801
13828
  // -- portable node/edge/traverse core (GQL) ----------------------------
13802
13829
  nodeFromRow(row) {
13803
- if (row === null || row === void 0) return null;
13804
- return new GraphNode(
13805
- String(row.id),
13806
- row.labels ?? [],
13807
- row.props ?? {}
13808
- );
13830
+ return nodeFromRow(row);
13809
13831
  }
13810
13832
  async addNode(label, properties = null) {
13811
13833
  const { clause, params } = propClause(properties);
@@ -13819,15 +13841,7 @@ var init_ultipa = __esm({
13819
13841
  params.to_id = toId;
13820
13842
  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`;
13821
13843
  const rows = (await this.run(gql, params, false)).dicts();
13822
- if (rows.length === 0) return null;
13823
- const row = rows[0];
13824
- return new GraphEdge(
13825
- String(row.id),
13826
- String(row.type),
13827
- String(row.f),
13828
- String(row.t),
13829
- row.props ?? {}
13830
- );
13844
+ return edgeFromRow(rows[0]);
13831
13845
  }
13832
13846
  async getNode(nodeId) {
13833
13847
  const gql = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -13892,10 +13906,6 @@ var bolt_exports = {};
13892
13906
  __export(bolt_exports, {
13893
13907
  BoltGraphAdapter: () => BoltGraphAdapter
13894
13908
  });
13895
- function errorMessage2(exc) {
13896
- if (exc instanceof Error) return exc.message;
13897
- return String(exc);
13898
- }
13899
13909
  function boltId(id) {
13900
13910
  const value = Number(id);
13901
13911
  return Number.isNaN(value) ? -1 : value;
@@ -13905,6 +13915,7 @@ var init_bolt = __esm({
13905
13915
  async "../orm/src/graph/adapters/bolt.ts"() {
13906
13916
  "use strict";
13907
13917
  init_shapes();
13918
+ init_rowMapping();
13908
13919
  init_errors();
13909
13920
  init_connectTimeout2();
13910
13921
  DRIVER_PACKAGE2 = "neo4j-driver";
@@ -13940,7 +13951,7 @@ var init_bolt = __esm({
13940
13951
  const result = await session.run(cypher, params ?? {});
13941
13952
  return result.records.map((record) => record.toObject());
13942
13953
  } catch (exc) {
13943
- this.lastError = errorMessage2(exc);
13954
+ this.lastError = errorMessage(exc);
13944
13955
  const code = exc?.code ?? "";
13945
13956
  const message = this.lastError.toLowerCase();
13946
13957
  if (code === "ServiceUnavailable" || code === neo4j.error?.SERVICE_UNAVAILABLE || message.includes("timed out") || message.includes("timeout")) {
@@ -13964,12 +13975,7 @@ var init_bolt = __esm({
13964
13975
  }
13965
13976
  // -- portable node/edge/traverse core (Cypher) -------------------------
13966
13977
  nodeFromRow(row) {
13967
- if (row === null || row === void 0) return null;
13968
- return new GraphNode(
13969
- String(row.id),
13970
- row.labels ?? [],
13971
- row.props ?? {}
13972
- );
13978
+ return nodeFromRow(row);
13973
13979
  }
13974
13980
  async addNode(label, properties = null) {
13975
13981
  const cypher = `CREATE (n:\`${label}\` $props) RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -13983,15 +13989,7 @@ var init_bolt = __esm({
13983
13989
  to_id: boltId(toId),
13984
13990
  props: properties ?? {}
13985
13991
  });
13986
- if (rows.length === 0) return null;
13987
- const row = rows[0];
13988
- return new GraphEdge(
13989
- String(row.id),
13990
- String(row.type),
13991
- String(row.f),
13992
- String(row.t),
13993
- row.props ?? {}
13994
- );
13992
+ return edgeFromRow(rows[0]);
13995
13993
  }
13996
13994
  async getNode(nodeId) {
13997
13995
  const cypher = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -14057,7 +14055,7 @@ function cleanProps(doc) {
14057
14055
  }
14058
14056
  return props;
14059
14057
  }
14060
- function errorMessage3(exc) {
14058
+ function errorMessage2(exc) {
14061
14059
  if (exc instanceof Error) return exc.message;
14062
14060
  return String(exc);
14063
14061
  }
@@ -14096,14 +14094,14 @@ var init_arango = __esm({
14096
14094
  this.db = new ArangoDatabase(config);
14097
14095
  }
14098
14096
  connectOrError(exc) {
14099
- const text = errorMessage3(exc).toLowerCase();
14097
+ const text = errorMessage2(exc).toLowerCase();
14100
14098
  if (text.includes("timed out") || text.includes("timeout") || text.includes("connection") || text.includes("econnrefused") || text.includes("etimedout") || text.includes("max retries")) {
14101
14099
  return new GraphConnectTimeout(
14102
14100
  `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.`,
14103
14101
  exc
14104
14102
  );
14105
14103
  }
14106
- return new GraphError(errorMessage3(exc), exc);
14104
+ return new GraphError(errorMessage2(exc), exc);
14107
14105
  }
14108
14106
  async ensureCollections() {
14109
14107
  if (this.ensured) return;
@@ -14114,7 +14112,7 @@ var init_arango = __esm({
14114
14112
  if (!await edges.exists()) await this.db.createEdgeCollection(EDGE_COLLECTION);
14115
14113
  this.ensured = true;
14116
14114
  } catch (exc) {
14117
- this.lastError = errorMessage3(exc);
14115
+ this.lastError = errorMessage2(exc);
14118
14116
  throw this.connectOrError(exc);
14119
14117
  }
14120
14118
  }
@@ -14124,7 +14122,7 @@ var init_arango = __esm({
14124
14122
  const cursor = await this.db.query({ query, bindVars: bind2 ?? {} });
14125
14123
  return await cursor.all();
14126
14124
  } catch (exc) {
14127
- this.lastError = errorMessage3(exc);
14125
+ this.lastError = errorMessage2(exc);
14128
14126
  throw this.connectOrError(exc);
14129
14127
  }
14130
14128
  }
@@ -17432,6 +17430,73 @@ var init_trustedProxy = __esm({
17432
17430
  }
17433
17431
  });
17434
17432
 
17433
+ // src/csp.ts
17434
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
17435
+ import { randomBytes as randomBytes4 } from "node:crypto";
17436
+ function generateNonce() {
17437
+ return randomBytes4(16).toString("base64");
17438
+ }
17439
+ function runWithCspNonce(nonce, fn) {
17440
+ return nonceStore.run({ nonce }, fn);
17441
+ }
17442
+ function setCurrentNonce(nonce) {
17443
+ const store = nonceStore.getStore();
17444
+ if (store) store.nonce = nonce;
17445
+ else nonceFallback = nonce;
17446
+ }
17447
+ function clearCurrentNonce() {
17448
+ setCurrentNonce(void 0);
17449
+ }
17450
+ function currentCspNonce() {
17451
+ const store = nonceStore.getStore();
17452
+ let value = store ? store.nonce : nonceFallback;
17453
+ if (!value) {
17454
+ value = generateNonce();
17455
+ if (store) store.nonce = value;
17456
+ else nonceFallback = value;
17457
+ }
17458
+ return value;
17459
+ }
17460
+ function cspNonce() {
17461
+ return currentCspNonce();
17462
+ }
17463
+ function injectNonceIntoCsp(csp, nonce) {
17464
+ const token = `'nonce-${nonce}'`;
17465
+ const directives = [];
17466
+ for (const rawPart of csp.split(";")) {
17467
+ const part = rawPart.trim();
17468
+ if (!part) continue;
17469
+ const spaceAt = part.search(/\s/);
17470
+ const name = (spaceAt === -1 ? part : part.slice(0, spaceAt)).toLowerCase();
17471
+ const value = spaceAt === -1 ? "" : part.slice(spaceAt + 1).trim();
17472
+ directives.push([name, value]);
17473
+ }
17474
+ const defaultEntry = directives.find(([name]) => name === "default-src");
17475
+ const defaultValue = defaultEntry && defaultEntry[1] ? defaultEntry[1] : "'self'";
17476
+ for (const directive of ["style-src", "script-src"]) {
17477
+ const existing = directives.find(([name]) => name === directive);
17478
+ if (existing) {
17479
+ if (!existing[1].split(/\s+/).includes(token)) {
17480
+ existing[1] = `${existing[1]} ${token}`.trim();
17481
+ }
17482
+ } else {
17483
+ directives.push([directive, `${defaultValue} ${token}`.trim()]);
17484
+ }
17485
+ }
17486
+ return directives.map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
17487
+ }
17488
+ function resolveCspHeader(nonce) {
17489
+ const csp = process.env.TINA4_CSP || "default-src 'self'";
17490
+ return injectNonceIntoCsp(csp, nonce);
17491
+ }
17492
+ var nonceStore, nonceFallback;
17493
+ var init_csp = __esm({
17494
+ "src/csp.ts"() {
17495
+ "use strict";
17496
+ nonceStore = new AsyncLocalStorage3();
17497
+ }
17498
+ });
17499
+
17435
17500
  // ../frond/src/engine.ts
17436
17501
  var engine_exports = {};
17437
17502
  __export(engine_exports, {
@@ -17441,9 +17506,10 @@ __export(engine_exports, {
17441
17506
  expressionFormCache: () => expressionFormCache,
17442
17507
  filterChainCache: () => filterChainCache,
17443
17508
  pathParseCache: () => pathParseCache,
17509
+ setCspNonceProvider: () => setCspNonceProvider,
17444
17510
  setFormTokenSessionId: () => setFormTokenSessionId
17445
17511
  });
17446
- import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
17512
+ import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes5 } from "node:crypto";
17447
17513
  import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync6, realpathSync } from "node:fs";
17448
17514
  import { join as join10, resolve as resolve6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17449
17515
  function jsonText(value) {
@@ -18510,6 +18576,9 @@ function _b64url(data) {
18510
18576
  function setFormTokenSessionId(sessionId) {
18511
18577
  _formTokenSessionId = sessionId || "";
18512
18578
  }
18579
+ function setCspNonceProvider(fn) {
18580
+ _cspNonceProvider = fn;
18581
+ }
18513
18582
  function _buildFormTokenJwt(descriptor = "") {
18514
18583
  const secret = process.env.TINA4_SECRET ?? "";
18515
18584
  const secretBytes = Buffer.byteLength(secret, "utf8");
@@ -18521,7 +18590,7 @@ function _buildFormTokenJwt(descriptor = "") {
18521
18590
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
18522
18591
  const header = { alg: "HS256", typ: "JWT" };
18523
18592
  const now = Math.floor(Date.now() / 1e3);
18524
- const payload = { type: "form", nonce: randomBytes4(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
18593
+ const payload = { type: "form", nonce: randomBytes5(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
18525
18594
  if (descriptor) {
18526
18595
  if (descriptor.includes("|")) {
18527
18596
  const [ctx, ref] = descriptor.split("|", 2);
@@ -18548,7 +18617,7 @@ function _generateFormToken(descriptor = "") {
18548
18617
  function _generateFormTokenValue(descriptor = "") {
18549
18618
  return new SafeString(_buildFormTokenJwt(descriptor));
18550
18619
  }
18551
- 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;
18620
+ 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;
18552
18621
  var init_engine = __esm({
18553
18622
  "../frond/src/engine.ts"() {
18554
18623
  "use strict";
@@ -18902,6 +18971,7 @@ var init_engine = __esm({
18902
18971
  js_escape: (v) => new SafeString(jsEscape(v))
18903
18972
  };
18904
18973
  _formTokenSessionId = "";
18974
+ _cspNonceProvider = () => "";
18905
18975
  Frond = class _Frond {
18906
18976
  // ── Class-level registries ──────────────────────────────────
18907
18977
  // Persist globals, filters, and tests across hot-reloads and at
@@ -19055,6 +19125,7 @@ var init_engine = __esm({
19055
19125
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
19056
19126
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
19057
19127
  this.globals.dump = (value) => renderDump(value);
19128
+ this.globals.csp_nonce = () => _cspNonceProvider();
19058
19129
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
19059
19130
  for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
19060
19131
  for (const [k, v] of _Frond.classTests) this.tests[k] = v;
@@ -20318,7 +20389,8 @@ async function getFrond() {
20318
20389
  const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
20319
20390
  let engine = _frondCache.get(dir);
20320
20391
  if (!engine) {
20321
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20392
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20393
+ setCspNonceProvider2(currentCspNonce);
20322
20394
  engine = new Frond2(dir);
20323
20395
  _frondCache.set(dir, engine);
20324
20396
  }
@@ -20329,7 +20401,8 @@ async function getFrameworkFrond() {
20329
20401
  const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
20330
20402
  if (!_frameworkFrond && fs2.existsSync(frameworkDir)) {
20331
20403
  try {
20332
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20404
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20405
+ setCspNonceProvider2(currentCspNonce);
20333
20406
  _frameworkFrond = new Frond2(frameworkDir);
20334
20407
  } catch {
20335
20408
  return null;
@@ -20429,6 +20502,7 @@ function createResponse(res) {
20429
20502
  return response;
20430
20503
  };
20431
20504
  response.raw = res;
20505
+ response.cspNonce = currentCspNonce();
20432
20506
  response.json = function(data, status2) {
20433
20507
  if (res.headersSent) return response;
20434
20508
  if (status2 !== void 0) res.statusCode = status2;
@@ -20574,7 +20648,8 @@ function createResponse(res) {
20574
20648
  };
20575
20649
  response.render = async function(templateName, data, status2, templateDir) {
20576
20650
  try {
20577
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20651
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20652
+ setCspNonceProvider2(currentCspNonce);
20578
20653
  const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
20579
20654
  let engine = _frondCache.get(dir);
20580
20655
  if (!engine) {
@@ -20692,6 +20767,7 @@ var HEADER_TOKEN, _frondCache, _defaultTemplatesDir, _globalFrond, _frameworkFro
20692
20767
  var init_response = __esm({
20693
20768
  "src/response.ts"() {
20694
20769
  "use strict";
20770
+ init_csp();
20695
20771
  HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
20696
20772
  _frondCache = /* @__PURE__ */ new Map();
20697
20773
  _defaultTemplatesDir = null;
@@ -20903,6 +20979,7 @@ var init_middleware = __esm({
20903
20979
  init_dotenv();
20904
20980
  init_router();
20905
20981
  init_trustedProxy();
20982
+ init_csp();
20906
20983
  init_response();
20907
20984
  MiddlewareChain = class {
20908
20985
  middlewares = [];
@@ -21335,12 +21412,18 @@ var init_middleware = __esm({
21335
21412
  * overrides, WITHOUT Strict-Transport-Security, which depends on the
21336
21413
  * request's scheme. Also what a transport rejection carries (ADR-0068
21337
21414
  * section 4), written before the scheme is known.
21415
+ *
21416
+ * Content-Security-Policy always carries a per-response nonce in style-src
21417
+ * AND script-src (ADR-0088), read from the request async-context (minted on
21418
+ * first access), so the framework's own inline content runs under the strict
21419
+ * default without 'unsafe-inline'. The nonce is random per response, so a
21420
+ * caller comparing this header must assert its STRUCTURE, never a byte string.
21338
21421
  */
21339
21422
  static canonicalHeaders() {
21340
21423
  return {
21341
21424
  "X-Frame-Options": process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
21342
21425
  "X-Content-Type-Options": "nosniff",
21343
- "Content-Security-Policy": process.env.TINA4_CSP ?? "default-src 'self'",
21426
+ "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
21344
21427
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
21345
21428
  "X-XSS-Protection": "0",
21346
21429
  "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
@@ -21382,7 +21465,7 @@ var init_middleware = __esm({
21382
21465
  static warnCspDefaultOnce() {
21383
21466
  if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
21384
21467
  _SecurityHeadersMiddleware.cspDefaultWarned = true;
21385
- 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'".`;
21468
+ 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'".`;
21386
21469
  try {
21387
21470
  Log.warning(message);
21388
21471
  } catch {
@@ -22387,6 +22470,21 @@ var init_portTakeover = __esm({
22387
22470
  }
22388
22471
  });
22389
22472
 
22473
+ // src/etag.ts
22474
+ function etagMatches(ifNoneMatch, etag) {
22475
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
22476
+ const target = strip(etag);
22477
+ return ifNoneMatch.split(",").some((candidate) => {
22478
+ const trimmed = candidate.trim();
22479
+ return trimmed === "*" || strip(trimmed) === target;
22480
+ });
22481
+ }
22482
+ var init_etag = __esm({
22483
+ "src/etag.ts"() {
22484
+ "use strict";
22485
+ }
22486
+ });
22487
+
22390
22488
  // src/sessionHandlers/syncBridge.ts
22391
22489
  import { Worker } from "node:worker_threads";
22392
22490
  function getBridge(key, workerSource, workerData) {
@@ -22668,42 +22766,35 @@ var init_respClient = __esm({
22668
22766
  }
22669
22767
  });
22670
22768
 
22671
- // src/sessionHandlers/valkeyHandler.ts
22672
- var ValkeySessionHandler;
22673
- var init_valkeyHandler = __esm({
22674
- "src/sessionHandlers/valkeyHandler.ts"() {
22769
+ // src/sessionHandlers/respSessionHandler.ts
22770
+ var RespSessionHandler;
22771
+ var init_respSessionHandler = __esm({
22772
+ "src/sessionHandlers/respSessionHandler.ts"() {
22675
22773
  "use strict";
22676
22774
  init_respClient();
22677
- ValkeySessionHandler = class {
22678
- host;
22679
- port;
22680
- password;
22681
- prefix;
22682
- db;
22683
- constructor(config) {
22684
- this.host = config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1";
22685
- this.port = config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379);
22686
- this.password = config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "";
22687
- this.prefix = config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:";
22688
- this.db = config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0);
22775
+ RespSessionHandler = class {
22776
+ constructor(conn, label) {
22777
+ this.conn = conn;
22778
+ this.label = label;
22689
22779
  }
22780
+ conn;
22781
+ label;
22690
22782
  /**
22691
- * Execute a RESP command synchronously against the live Valkey server.
22783
+ * Execute a RESP command synchronously against the live server.
22692
22784
  *
22693
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
22694
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
22695
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
22696
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
22785
+ * Delegates to the shared {@link respCommandSync} transport: a genuine key
22786
+ * miss yields `""`, and a transport/connection FAILURE THROWS so the caller
22787
+ * can distinguish "not found" from "backend failed". Backend-failure parity.
22697
22788
  */
22698
22789
  execSync(args) {
22699
22790
  return respCommandSync(
22700
- { host: this.host, port: this.port, password: this.password, db: this.db },
22791
+ { host: this.conn.host, port: this.conn.port, password: this.conn.password, db: this.conn.db },
22701
22792
  args,
22702
- "Valkey"
22793
+ this.label
22703
22794
  );
22704
22795
  }
22705
22796
  key(sessionId) {
22706
- return `${this.prefix}${sessionId}`;
22797
+ return `${this.conn.prefix}${sessionId}`;
22707
22798
  }
22708
22799
  read(sessionId) {
22709
22800
  const raw = this.execSync(["GET", this.key(sessionId)]);
@@ -22714,7 +22805,7 @@ var init_valkeyHandler = __esm({
22714
22805
  return null;
22715
22806
  }
22716
22807
  }
22717
- write(sessionId, data, ttl) {
22808
+ write(sessionId, data, ttl = 0) {
22718
22809
  const json = JSON.stringify(data);
22719
22810
  if (ttl > 0) {
22720
22811
  this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
@@ -22729,6 +22820,29 @@ var init_valkeyHandler = __esm({
22729
22820
  }
22730
22821
  });
22731
22822
 
22823
+ // src/sessionHandlers/valkeyHandler.ts
22824
+ var ValkeySessionHandler;
22825
+ var init_valkeyHandler = __esm({
22826
+ "src/sessionHandlers/valkeyHandler.ts"() {
22827
+ "use strict";
22828
+ init_respSessionHandler();
22829
+ ValkeySessionHandler = class extends RespSessionHandler {
22830
+ constructor(config) {
22831
+ super(
22832
+ {
22833
+ host: config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1",
22834
+ port: config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379),
22835
+ password: config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "",
22836
+ prefix: config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:",
22837
+ db: config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0)
22838
+ },
22839
+ "Valkey"
22840
+ );
22841
+ }
22842
+ };
22843
+ }
22844
+ });
22845
+
22732
22846
  // src/sessionHandlers/memcachedHandler.ts
22733
22847
  import { createHash as createHash4 } from "node:crypto";
22734
22848
  var MAX_KEY_BYTES, MAX_RELATIVE_EXPTIME, MemcachedSessionHandler;
@@ -23744,7 +23858,7 @@ __export(session_exports, {
23744
23858
  sessionCookieName: () => sessionCookieName,
23745
23859
  sessionStrictMode: () => sessionStrictMode
23746
23860
  });
23747
- import { createHash as createHash5, randomBytes as randomBytes5 } from "node:crypto";
23861
+ import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
23748
23862
  import { existsSync as existsSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync5, readdirSync as readdirSync6 } from "node:fs";
23749
23863
  import { join as join14 } from "node:path";
23750
23864
  function isValidSessionId(sessionId) {
@@ -23762,6 +23876,20 @@ function resolveBackend(name) {
23762
23876
  }
23763
23877
  return normalised;
23764
23878
  }
23879
+ function fingerprint(value) {
23880
+ try {
23881
+ return JSON.stringify(value);
23882
+ } catch {
23883
+ return void 0;
23884
+ }
23885
+ }
23886
+ function fingerprints(data) {
23887
+ const prints = /* @__PURE__ */ new Map();
23888
+ for (const [key, value] of Object.entries(data)) {
23889
+ prints.set(key, fingerprint(value));
23890
+ }
23891
+ return prints;
23892
+ }
23765
23893
  function isSecureScheme(forwardedProto, socketEncrypted) {
23766
23894
  const forwarded = (forwardedProto ?? "").trim();
23767
23895
  if (forwarded !== "") {
@@ -23794,7 +23922,7 @@ var init_session = __esm({
23794
23922
  "use strict";
23795
23923
  init_logger();
23796
23924
  init_dotenv();
23797
- init_respClient();
23925
+ init_respSessionHandler();
23798
23926
  init_valkeyHandler();
23799
23927
  init_memcachedHandler();
23800
23928
  init_mongoHandler();
@@ -23920,57 +24048,19 @@ var init_session = __esm({
23920
24048
  }
23921
24049
  }
23922
24050
  };
23923
- RedisSessionHandler = class {
23924
- host;
23925
- port;
23926
- password;
23927
- prefix;
23928
- db;
24051
+ RedisSessionHandler = class extends RespSessionHandler {
23929
24052
  constructor(config) {
23930
- this.host = config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1";
23931
- this.port = config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379);
23932
- this.password = config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "";
23933
- this.prefix = config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:";
23934
- this.db = config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0);
23935
- }
23936
- /**
23937
- * Execute a Redis command synchronously against the live server.
23938
- *
23939
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
23940
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
23941
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
23942
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
23943
- */
23944
- execSync(args) {
23945
- return respCommandSync(
23946
- { host: this.host, port: this.port, password: this.password, db: this.db },
23947
- args,
24053
+ super(
24054
+ {
24055
+ host: config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1",
24056
+ port: config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379),
24057
+ password: config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "",
24058
+ prefix: config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:",
24059
+ db: config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0)
24060
+ },
23948
24061
  "Redis"
23949
24062
  );
23950
24063
  }
23951
- key(sessionId) {
23952
- return `${this.prefix}${sessionId}`;
23953
- }
23954
- read(sessionId) {
23955
- const raw = this.execSync(["GET", this.key(sessionId)]);
23956
- if (!raw) return null;
23957
- try {
23958
- return JSON.parse(raw);
23959
- } catch {
23960
- return null;
23961
- }
23962
- }
23963
- write(sessionId, data, ttl = 0) {
23964
- const json = JSON.stringify(data);
23965
- if (ttl > 0) {
23966
- this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
23967
- } else {
23968
- this.execSync(["SET", this.key(sessionId), json]);
23969
- }
23970
- }
23971
- destroy(sessionId) {
23972
- this.execSync(["DEL", this.key(sessionId)]);
23973
- }
23974
24064
  };
23975
24065
  FLASH_PREFIX = "_flash_";
23976
24066
  Session = class {
@@ -23997,6 +24087,29 @@ var init_session = __esm({
23997
24087
  * Lets `start()` tell "no such session" from "the store is unreachable".
23998
24088
  */
23999
24089
  lastReadFailed = false;
24090
+ /**
24091
+ * What this request last saw of the stored record, one fingerprint per key:
24092
+ * taken by start() and again by every save. save() compares the live data
24093
+ * against it to find the keys THIS request changed.
24094
+ */
24095
+ loaded = /* @__PURE__ */ new Map();
24096
+ /**
24097
+ * True when this request is working on a record the store holds: start()
24098
+ * adopted one, or a save has written one since. A save never re-creates such
24099
+ * a record once another request has removed it.
24100
+ */
24101
+ stored = false;
24102
+ /**
24103
+ * True when clear() ran since the last save: the save then replaces the
24104
+ * stored record instead of merging into it.
24105
+ */
24106
+ cleared = false;
24107
+ /**
24108
+ * True once this request found its session ended by another request (the
24109
+ * record it loaded is gone). It stays ended for the rest of the request:
24110
+ * regenerate() mints nothing either.
24111
+ */
24112
+ ended = false;
24000
24113
  constructor(backend, config) {
24001
24114
  const backendType = resolveBackend(
24002
24115
  backend ?? config?.backend ?? process.env.TINA4_SESSION_BACKEND ?? "file"
@@ -24141,6 +24254,10 @@ var init_session = __esm({
24141
24254
  const now2 = Math.floor(Date.now() / 1e3);
24142
24255
  this.data = { _created: now2, _accessed: now2 };
24143
24256
  this.dirty = false;
24257
+ this.stored = false;
24258
+ this.loaded = /* @__PURE__ */ new Map();
24259
+ this.cleared = false;
24260
+ this.ended = false;
24144
24261
  return sessionId;
24145
24262
  }
24146
24263
  if (loaded) {
@@ -24150,14 +24267,22 @@ var init_session = __esm({
24150
24267
  this.data._accessed = now2;
24151
24268
  this.dirty = false;
24152
24269
  this.safeWrite(this.sessionId, this.data, this.ttl);
24270
+ this.stored = true;
24271
+ this.loaded = fingerprints(this.data);
24272
+ this.cleared = false;
24273
+ this.ended = false;
24153
24274
  return sessionId;
24154
24275
  }
24155
24276
  }
24156
- this.sessionId = randomBytes5(16).toString("hex");
24277
+ this.sessionId = randomBytes6(16).toString("hex");
24157
24278
  const now = Math.floor(Date.now() / 1e3);
24158
24279
  this.data = { _created: now, _accessed: now };
24159
24280
  this.dirty = false;
24160
24281
  this.safeWrite(this.sessionId, this.data, this.ttl);
24282
+ this.stored = false;
24283
+ this.loaded = /* @__PURE__ */ new Map();
24284
+ this.cleared = false;
24285
+ this.ended = false;
24161
24286
  return this.sessionId;
24162
24287
  }
24163
24288
  /**
@@ -24198,9 +24323,7 @@ var init_session = __esm({
24198
24323
  if (this.sessionId) {
24199
24324
  this.safeDestroy(this.sessionId);
24200
24325
  }
24201
- this.sessionId = null;
24202
- this.data = null;
24203
- this.dirty = false;
24326
+ this.forget();
24204
24327
  }
24205
24328
  /**
24206
24329
  * Get all session data (excluding internal keys).
@@ -24223,6 +24346,7 @@ var init_session = __esm({
24223
24346
  const now = Math.floor(Date.now() / 1e3);
24224
24347
  this.data = { _created: this.data._created, _accessed: now };
24225
24348
  this.dirty = true;
24349
+ this.cleared = true;
24226
24350
  this.save();
24227
24351
  }
24228
24352
  /**
@@ -24239,17 +24363,36 @@ var init_session = __esm({
24239
24363
  * session fixation — the pre-auth ID is destroyed and the data is carried
24240
24364
  * onto a fresh, unguessable ID. A backend destroy/write failure is logged
24241
24365
  * (never silent) but does not throw under the default policy.
24366
+ *
24367
+ * What is carried is the session as it is stored now with this request's
24368
+ * own changes applied, the same merge save() does. If another request ended
24369
+ * the session after this one loaded it (a logout, or a regenerate of its
24370
+ * own), it stays ended: nothing is carried, no id is minted and null is
24371
+ * returned, so no cookie goes out to replace the one that request sent.
24242
24372
  */
24243
24373
  regenerate() {
24374
+ if (this.ended) return null;
24375
+ if (this.sessionId && this.stored && this.data) {
24376
+ const current = this.safeRead(this.sessionId);
24377
+ if (!this.lastReadFailed) {
24378
+ if (!current) {
24379
+ this.forget();
24380
+ this.ended = true;
24381
+ return null;
24382
+ }
24383
+ if (!this.cleared) this.data = this.merged(current);
24384
+ }
24385
+ }
24244
24386
  const oldId = this.sessionId;
24245
24387
  const oldData = this.data;
24246
24388
  if (oldId) {
24247
24389
  this.safeDestroy(oldId);
24248
24390
  }
24249
- this.sessionId = randomBytes5(16).toString("hex");
24391
+ this.sessionId = randomBytes6(16).toString("hex");
24250
24392
  this.data = oldData ?? { _created: Math.floor(Date.now() / 1e3), _accessed: Math.floor(Date.now() / 1e3) };
24251
24393
  this.data._accessed = Math.floor(Date.now() / 1e3);
24252
24394
  this.dirty = true;
24395
+ this.stored = false;
24253
24396
  this.save();
24254
24397
  return this.sessionId;
24255
24398
  }
@@ -24325,17 +24468,84 @@ var init_session = __esm({
24325
24468
  *
24326
24469
  * Returns true on a successful persist, false if the backend was unreachable
24327
24470
  * (logged). The dirty flag is cleared only on success so a later save()
24328
- * retries once the backend recovers. A nothing-to-persist call returns true.
24471
+ * retries once the backend recovers. A call with no session returns true.
24472
+ *
24473
+ * Other requests on the same session run while this one does, so the save
24474
+ * writes only THIS request's changes, onto the record as it is stored now.
24475
+ * Writing back the whole snapshot start() loaded undid whatever another
24476
+ * request did in between: a logout (destroy, clear, regenerate) or a
24477
+ * privilege change made with set(). A record removed since this request
24478
+ * loaded it is never re-created: the session has ended, for this request
24479
+ * too. A request that has not loaded a stored record (a new session, the new
24480
+ * id regenerate() mints) writes its data whole, as before.
24481
+ *
24482
+ * A session the store already holds is re-written even when this request
24483
+ * changed nothing: that write re-stamps the backend deadline to
24484
+ * now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
24485
+ * rather than that long after its last CHANGE (ADR-0087 — a session's expiry
24486
+ * slides on activity). The re-write is the re-read, merged record, so sliding
24487
+ * the deadline is concurrent-safe: it never clobbers another request's change,
24488
+ * never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
24489
+ * never-expires deadline (ADR-0027). A request with no stored session and no
24490
+ * change writes nothing — there is no deadline to move.
24329
24491
  */
24330
24492
  save() {
24331
24493
  if (!this.sessionId || !this.data) return true;
24332
- if (!this.dirty) return true;
24333
- if (this.safeWrite(this.sessionId, this.data, this.ttl)) {
24494
+ if (!this.dirty && !this.stored) return true;
24495
+ let record = this.data;
24496
+ if (this.stored) {
24497
+ const current = this.safeRead(this.sessionId);
24498
+ if (this.lastReadFailed) return false;
24499
+ if (!current) {
24500
+ this.forget();
24501
+ this.ended = true;
24502
+ return true;
24503
+ }
24504
+ if (!this.cleared) record = this.merged(current);
24505
+ }
24506
+ if (this.safeWrite(this.sessionId, record, this.ttl)) {
24334
24507
  this.dirty = false;
24508
+ this.stored = true;
24509
+ this.cleared = false;
24510
+ this.loaded = fingerprints(this.data);
24335
24511
  return true;
24336
24512
  }
24337
24513
  return false;
24338
24514
  }
24515
+ /**
24516
+ * The stored record with this request's changes applied: every key whose
24517
+ * value differs from what this request loaded or last saved, and the
24518
+ * removal of every key it deleted. Keys it did not touch keep whatever is
24519
+ * stored now.
24520
+ */
24521
+ merged(current) {
24522
+ const data = this.data;
24523
+ const record = { ...current };
24524
+ for (const [key, value] of Object.entries(data)) {
24525
+ const print = fingerprint(value);
24526
+ if (print === void 0 || !this.loaded.has(key) || this.loaded.get(key) !== print) {
24527
+ record[key] = value;
24528
+ }
24529
+ }
24530
+ for (const key of this.loaded.keys()) {
24531
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
24532
+ delete record[key];
24533
+ }
24534
+ }
24535
+ return record;
24536
+ }
24537
+ /**
24538
+ * End the session for this request without touching the store: no id and
24539
+ * no data, so nothing more is written and no cookie goes out for it.
24540
+ */
24541
+ forget() {
24542
+ this.sessionId = null;
24543
+ this.data = null;
24544
+ this.dirty = false;
24545
+ this.stored = false;
24546
+ this.cleared = false;
24547
+ this.loaded = /* @__PURE__ */ new Map();
24548
+ }
24339
24549
  };
24340
24550
  }
24341
24551
  });
@@ -24380,14 +24590,6 @@ function headStripIntercept(rawReq, rawRes) {
24380
24590
  function isCompressibleContentType(contentType) {
24381
24591
  return COMPRESSIBLE_PREFIXES.some((prefix) => contentType.includes(prefix));
24382
24592
  }
24383
- function etagMatchesInm(ifNoneMatch, etag) {
24384
- const strip = (tag) => tag.trim().replace(/^W\//, "");
24385
- const target = strip(etag);
24386
- return ifNoneMatch.split(",").some((candidate) => {
24387
- const trimmed = candidate.trim();
24388
- return trimmed === "*" || strip(trimmed) === target;
24389
- });
24390
- }
24391
24593
  function maybeCompressResponse(body, rawReq, rawRes) {
24392
24594
  const acceptEncoding = String(rawReq.headers["accept-encoding"] ?? "");
24393
24595
  const contentTypeHeader = rawRes.getHeader("content-type");
@@ -24403,7 +24605,7 @@ function maybeCompressResponse(body, rawReq, rawRes) {
24403
24605
  }
24404
24606
  function responseIsNotModified(rawReq, rawRes, etag) {
24405
24607
  const ifNoneMatch = String(rawReq.headers["if-none-match"] ?? "");
24406
- if (ifNoneMatch) return etagMatchesInm(ifNoneMatch, etag);
24608
+ if (ifNoneMatch) return etagMatches(ifNoneMatch, etag);
24407
24609
  const lastModifiedHeader = rawRes.getHeader("last-modified");
24408
24610
  if (typeof lastModifiedHeader !== "string") return false;
24409
24611
  const ifModifiedSince = String(rawReq.headers["if-modified-since"] ?? "");
@@ -24531,6 +24733,7 @@ var init_dispatchPipeline = __esm({
24531
24733
  "src/dispatchPipeline.ts"() {
24532
24734
  "use strict";
24533
24735
  init_logger();
24736
+ init_etag();
24534
24737
  COMPRESSIBLE_PREFIXES = [
24535
24738
  "text/",
24536
24739
  "application/json",
@@ -24626,18 +24829,11 @@ function isNotModified(req2, etag, stat) {
24626
24829
  }
24627
24830
  return false;
24628
24831
  }
24629
- function etagMatches(ifNoneMatch, etag) {
24630
- const strip = (tag) => tag.trim().replace(/^W\//, "");
24631
- const target = strip(etag);
24632
- return ifNoneMatch.split(",").some((candidate) => {
24633
- const trimmed = candidate.trim();
24634
- return trimmed === "*" || strip(trimmed) === target;
24635
- });
24636
- }
24637
24832
  var COMPRESSIBLE_PREFIXES2, MIME_TYPES;
24638
24833
  var init_static = __esm({
24639
24834
  "src/static.ts"() {
24640
24835
  "use strict";
24836
+ init_etag();
24641
24837
  COMPRESSIBLE_PREFIXES2 = [
24642
24838
  "text/",
24643
24839
  "application/json",
@@ -24718,11 +24914,11 @@ function formatSourceBlock(filename, lineno) {
24718
24914
  const lines = readSourceLines(filename, lineno);
24719
24915
  if (lines.length === 0) return "";
24720
24916
  const rows = lines.map(([num, text, isError]) => {
24721
- const bg = isError ? `background:${ERROR_LINE_BG};` : "";
24917
+ const rowClass = isError ? "eo-line eo-line-err" : "eo-line";
24722
24918
  const marker = isError ? "&#x25b6;" : " ";
24723
- 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>`;
24919
+ return `<div class="${rowClass}"><span class="eo-ln">${num}</span><span class="eo-marker">${marker}</span><span class="eo-code">${esc(text)}</span></div>`;
24724
24920
  }).join("\n");
24725
- 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>`;
24921
+ return `<div class="eo-source">` + rows + `</div>`;
24726
24922
  }
24727
24923
  function formatFrame(frame, capturedAt = 0) {
24728
24924
  const source = frame.file && frame.line > 0 ? formatSourceBlock(frame.file, frame.line) : "";
@@ -24734,23 +24930,23 @@ function formatFrame(frame, capturedAt = 0) {
24734
24930
  if (mtime > capturedAt + 0.5) {
24735
24931
  const d = new Date(mtime * 1e3);
24736
24932
  const mtimeIso = `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
24737
- 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>`;
24933
+ staleBadge = ` <span class="eo-stale">FILE MODIFIED @ ${mtimeIso} UTC &mdash; source may not match what failed</span>`;
24738
24934
  }
24739
24935
  } catch {
24740
24936
  }
24741
24937
  }
24742
- 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>`;
24938
+ 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>`;
24743
24939
  }
24744
24940
  function collapsible(title, content, openByDefault = false) {
24745
24941
  const open = openByDefault ? " open" : "";
24746
- 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>`;
24942
+ return `<details class="eo-details"${open}><summary class="eo-summary">${esc(title)}</summary><div class="eo-details-body">${content}</div></details>`;
24747
24943
  }
24748
24944
  function table(pairs) {
24749
- if (pairs.length === 0) return `<span style="color:${SUBTEXT};">None</span>`;
24945
+ if (pairs.length === 0) return `<span class="eo-none">None</span>`;
24750
24946
  const rows = pairs.map(
24751
- ([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>`
24947
+ ([key, val]) => `<tr><td class="eo-key">${esc(key)}</td><td class="eo-val">${esc(val)}</td></tr>`
24752
24948
  ).join("");
24753
- return `<table style="border-collapse:collapse;width:100%;">${rows}</table>`;
24949
+ return `<table class="eo-table">${rows}</table>`;
24754
24950
  }
24755
24951
  function renderErrorOverlay(error2, request) {
24756
24952
  const capturedAt = Date.now() / 1e3;
@@ -24763,7 +24959,7 @@ function renderErrorOverlay(error2, request) {
24763
24959
  }
24764
24960
  const hidden = frames.length - Math.min(frames.length, MAX_FRAMES);
24765
24961
  if (hidden > 0) {
24766
- framesHtml += `<div style="color:${SUBTEXT};padding:8px 0;font-size:13px;">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
24962
+ framesHtml += `<div class="eo-hidden-frames">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
24767
24963
  }
24768
24964
  const requestPairs = [];
24769
24965
  if (request != null) {
@@ -24800,37 +24996,72 @@ function renderErrorOverlay(error2, request) {
24800
24996
  ];
24801
24997
  const envSection = collapsible("Environment", table(envPairs));
24802
24998
  const stackSection = collapsible("Stack Trace", framesHtml, true);
24999
+ const nonce = currentCspNonce();
24803
25000
  return `<!DOCTYPE html>
24804
25001
  <html lang="en">
24805
25002
  <head>
24806
25003
  <meta charset="utf-8">
24807
25004
  <meta name="viewport" content="width=device-width,initial-scale=1">
24808
25005
  <title>Tina4 Error \u2014 ${esc(excType)}</title>
24809
- <style>
24810
- *{margin:0;padding:0;box-sizing:border-box;}
24811
- body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
25006
+ <style nonce="${nonce}">
25007
+ ${overlayStylesheet()}
24812
25008
  </style>
24813
25009
  </head>
24814
25010
  <body>
24815
- <div style="max-width:960px;margin:0 auto;">
24816
- <div style="margin-bottom:24px;">
24817
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
24818
- <span style="background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
24819
- <span style="color:${SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
25011
+ <div class="eo-wrap">
25012
+ <div class="eo-header">
25013
+ <div class="eo-badge-row">
25014
+ <span class="eo-badge">Error</span>
25015
+ <span class="eo-sub">Tina4 Debug Overlay</span>
24820
25016
  </div>
24821
- <h1 style="color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;">${esc(excType)}</h1>
24822
- <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>
25017
+ <h1 class="eo-type">${esc(excType)}</h1>
25018
+ <p class="eo-msg">${esc(excMsg)}</p>
24823
25019
  </div>
24824
25020
  ${stackSection}
24825
25021
  ${requestSection}
24826
25022
  ${envSection}
24827
- <div style="margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;">
25023
+ <div class="eo-footer">
24828
25024
  Tina4 Debug Overlay &mdash; This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
24829
25025
  </div>
24830
25026
  </div>
24831
25027
  </body>
24832
25028
  </html>`;
24833
25029
  }
25030
+ function overlayStylesheet() {
25031
+ return `
25032
+ *{margin:0;padding:0;box-sizing:border-box;}
25033
+ body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
25034
+ .eo-wrap{max-width:960px;margin:0 auto;}
25035
+ .eo-header{margin-bottom:24px;}
25036
+ .eo-badge-row{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
25037
+ .eo-badge{background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;}
25038
+ .eo-sub{color:${SUBTEXT};font-size:14px;}
25039
+ .eo-type{color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;}
25040
+ .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};}
25041
+ .eo-footer{margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;}
25042
+ .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;}
25043
+ .eo-line{display:flex;padding:1px 0;}
25044
+ .eo-line-err{background:${ERROR_LINE_BG};}
25045
+ .eo-ln{color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;}
25046
+ .eo-marker{color:${RED};width:1.2em;user-select:none;}
25047
+ .eo-code{color:${TEXT};white-space:pre-wrap;tab-size:4;}
25048
+ .eo-frame{margin-bottom:16px;}
25049
+ .eo-frame-head{margin-bottom:4px;}
25050
+ .eo-file{color:${BLUE};}
25051
+ .eo-sep{color:${SUBTEXT};}
25052
+ .eo-lineno{color:${YELLOW};}
25053
+ .eo-fn{color:${GREEN};}
25054
+ .eo-stale{background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;}
25055
+ .eo-details{margin-top:16px;}
25056
+ .eo-summary{cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;}
25057
+ .eo-details-body{padding:8px 0;}
25058
+ .eo-table{border-collapse:collapse;width:100%;}
25059
+ .eo-key{color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;}
25060
+ .eo-val{color:${TEXT};padding:4px 0;word-break:break-all;}
25061
+ .eo-none{color:${SUBTEXT};}
25062
+ .eo-hidden-frames{color:${SUBTEXT};padding:8px 0;font-size:13px;}
25063
+ `;
25064
+ }
24834
25065
  function isDebugMode() {
24835
25066
  return isTruthy(process.env.TINA4_DEBUG);
24836
25067
  }
@@ -24839,6 +25070,7 @@ var init_errorOverlay = __esm({
24839
25070
  "src/errorOverlay.ts"() {
24840
25071
  "use strict";
24841
25072
  init_dotenv();
25073
+ init_csp();
24842
25074
  MAX_FRAMES = 50;
24843
25075
  SENSITIVE_KEY_RE = /password|passwd|secret|token|authorization|cookie|key/i;
24844
25076
  REDACTED2 = "[redacted]";
@@ -25214,6 +25446,25 @@ Generated by DevMailbox.seed() for development purposes.`,
25214
25446
  }
25215
25447
  });
25216
25448
 
25449
+ // src/upstreamResponse.ts
25450
+ async function relayUpstreamResponse(res, upstream) {
25451
+ const raw = await upstream.text();
25452
+ const status2 = upstream.status || 200;
25453
+ try {
25454
+ res.json(JSON.parse(raw), status2);
25455
+ } catch {
25456
+ res.raw.writeHead(status2, {
25457
+ "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
25458
+ });
25459
+ res.raw.end(raw);
25460
+ }
25461
+ }
25462
+ var init_upstreamResponse = __esm({
25463
+ "src/upstreamResponse.ts"() {
25464
+ "use strict";
25465
+ }
25466
+ });
25467
+
25217
25468
  // src/metrics.ts
25218
25469
  import * as fs3 from "node:fs";
25219
25470
  import * as path2 from "node:path";
@@ -25429,6 +25680,7 @@ var init_feedback = __esm({
25429
25680
  "use strict";
25430
25681
  init_auth();
25431
25682
  init_devAdmin();
25683
+ init_upstreamResponse();
25432
25684
  RATE_LIMIT_WINDOW_SEC = 3600;
25433
25685
  RATE_LIMIT_MAX = 5;
25434
25686
  _rateLimitHits = /* @__PURE__ */ new Map();
@@ -25475,16 +25727,7 @@ var init_feedback = __esm({
25475
25727
  return;
25476
25728
  }
25477
25729
  clearTimeout(timer);
25478
- const raw = await upstream.text();
25479
- const status2 = upstream.status || 200;
25480
- try {
25481
- res.json(JSON.parse(raw), status2);
25482
- } catch {
25483
- res.raw.writeHead(status2, {
25484
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
25485
- });
25486
- res.raw.end(raw);
25487
- }
25730
+ await relayUpstreamResponse(res, upstream);
25488
25731
  };
25489
25732
  __feedbackDirname = dirname7(fileURLToPath2(import.meta.url));
25490
25733
  WIDGET_BUNDLE_PATH = resolve9(__feedbackDirname, "__feedback", "widget.js");
@@ -25534,6 +25777,38 @@ var init_version = __esm({
25534
25777
  }
25535
25778
  });
25536
25779
 
25780
+ // ../cli/src/util.ts
25781
+ function parseFlags(args, booleanFlags) {
25782
+ const flags = {};
25783
+ const positional = [];
25784
+ let i = 0;
25785
+ while (i < args.length) {
25786
+ const arg = args[i];
25787
+ if (arg.startsWith("--")) {
25788
+ const key = arg.slice(2);
25789
+ if (booleanFlags?.has(key)) {
25790
+ flags[key] = true;
25791
+ i += 1;
25792
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
25793
+ flags[key] = args[i + 1];
25794
+ i += 2;
25795
+ } else {
25796
+ flags[key] = true;
25797
+ i += 1;
25798
+ }
25799
+ } else {
25800
+ positional.push(arg);
25801
+ i += 1;
25802
+ }
25803
+ }
25804
+ return { flags, positional };
25805
+ }
25806
+ var init_util = __esm({
25807
+ "../cli/src/util.ts"() {
25808
+ "use strict";
25809
+ }
25810
+ });
25811
+
25537
25812
  // ../cli/src/commands/generate.ts
25538
25813
  var generate_exports = {};
25539
25814
  __export(generate_exports, {
@@ -25798,28 +26073,7 @@ function parseCliArgs(args) {
25798
26073
  "json",
25799
26074
  "dry-run"
25800
26075
  ]);
25801
- const flags = {};
25802
- const positional = [];
25803
- let i = 0;
25804
- while (i < args.length) {
25805
- if (args[i].startsWith("--")) {
25806
- const key = args[i].slice(2);
25807
- if (booleanFlags.has(key)) {
25808
- flags[key] = true;
25809
- i += 1;
25810
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
25811
- flags[key] = args[i + 1];
25812
- i += 2;
25813
- } else {
25814
- flags[key] = true;
25815
- i += 1;
25816
- }
25817
- } else {
25818
- positional.push(args[i]);
25819
- i += 1;
25820
- }
25821
- }
25822
- return { flags, positional };
26076
+ return parseFlags(args, booleanFlags);
25823
26077
  }
25824
26078
  function parseEvery(every) {
25825
26079
  if (!every || every === true) return 60;
@@ -26667,7 +26921,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26667
26921
  `{% extends "base.twig" %}
26668
26922
  {% block title %}Login{% endblock %}
26669
26923
  {% block content %}
26670
- <div class="container mt-4" style="max-width:400px">
26924
+ {# No inline style attribute: a nonce lets this <style> block run under the
26925
+ strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
26926
+ that returns this response's nonce (ADR-0088). #}
26927
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
26928
+ <div class="container mt-4 auth-form">
26671
26929
  <h1>Login</h1>
26672
26930
  <form method="post" action="/api/auth/login">
26673
26931
  {{ form_token() }}
@@ -26691,7 +26949,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26691
26949
  `{% extends "base.twig" %}
26692
26950
  {% block title %}Register{% endblock %}
26693
26951
  {% block content %}
26694
- <div class="container mt-4" style="max-width:400px">
26952
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
26953
+ <div class="container mt-4 auth-form">
26695
26954
  <h1>Register</h1>
26696
26955
  <form method="post" action="/api/auth/register">
26697
26956
  {{ form_token() }}
@@ -27380,6 +27639,7 @@ var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __res
27380
27639
  var init_generate = __esm({
27381
27640
  "../cli/src/commands/generate.ts"() {
27382
27641
  "use strict";
27642
+ init_util();
27383
27643
  FIELD_TYPE_MAP = {
27384
27644
  string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
27385
27645
  str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
@@ -27627,7 +27887,7 @@ import * as path3 from "node:path";
27627
27887
  import * as os from "node:os";
27628
27888
  import { spawnSync as spawnSync2 } from "node:child_process";
27629
27889
  import { createRequire as createRequire8 } from "node:module";
27630
- import { randomBytes as randomBytes6 } from "node:crypto";
27890
+ import { randomBytes as randomBytes7 } from "node:crypto";
27631
27891
  function reqSibling(pkg) {
27632
27892
  return req(`../../${pkg}/src/index.ts`);
27633
27893
  }
@@ -28949,7 +29209,7 @@ var init_mcp = __esm({
28949
29209
  // ── Session lifecycle + protocol negotiation ──────────────────
28950
29210
  /** Mint a new session id and remember it. Called on `initialize`. */
28951
29211
  openSession() {
28952
- const sid = randomBytes6(16).toString("hex");
29212
+ const sid = randomBytes7(16).toString("hex");
28953
29213
  this._sessions.set(sid, Date.now());
28954
29214
  return sid;
28955
29215
  }
@@ -30998,6 +31258,7 @@ __export(websocket_exports, {
30998
31258
  CLOSE_NORMAL: () => CLOSE_NORMAL,
30999
31259
  CLOSE_POLICY_VIOLATION: () => CLOSE_POLICY_VIOLATION,
31000
31260
  CLOSE_PROTOCOL_ERROR: () => CLOSE_PROTOCOL_ERROR,
31261
+ MessageAssembler: () => MessageAssembler,
31001
31262
  OP_BINARY: () => OP_BINARY,
31002
31263
  OP_CLOSE: () => OP_CLOSE,
31003
31264
  OP_CONTINUATION: () => OP_CONTINUATION,
@@ -31255,21 +31516,24 @@ function serveWebSocketRoute(req2, socket, head) {
31255
31516
  state.closed = true;
31256
31517
  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));
31257
31518
  };
31519
+ const assembler = new MessageAssembler();
31258
31520
  socket.on("data", (chunk) => {
31259
31521
  buffer = Buffer.concat([buffer, chunk]);
31260
31522
  while (buffer.length > 0) {
31261
31523
  const frame = parseFrame(buffer);
31262
31524
  if (!frame) break;
31263
31525
  buffer = buffer.subarray(frame.bytesConsumed);
31264
- switch (frame.opcode) {
31526
+ const whole = assembler.push(frame);
31527
+ if (!whole) continue;
31528
+ switch (whole.opcode) {
31265
31529
  case OP_TEXT: {
31266
- const text = frame.payload.toString("utf-8");
31530
+ const text = whole.payload.toString("utf-8");
31267
31531
  void Promise.resolve().then(() => conn._onMessage ? conn._onMessage(text) : handler(conn, "message", text)).catch((e) => Log.error(`WebSocket message handler error: ${e.message}`));
31268
31532
  break;
31269
31533
  }
31270
31534
  case OP_PING:
31271
31535
  try {
31272
- socket.write(buildFrame(OP_PONG, frame.payload));
31536
+ socket.write(buildFrame(OP_PONG, whole.payload));
31273
31537
  } catch {
31274
31538
  }
31275
31539
  break;
@@ -31288,7 +31552,7 @@ function serveWebSocketRoute(req2, socket, head) {
31288
31552
  socket.on("error", fireClose);
31289
31553
  return true;
31290
31554
  }
31291
- 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;
31555
+ 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;
31292
31556
  var init_websocket = __esm({
31293
31557
  "src/websocket.ts"() {
31294
31558
  "use strict";
@@ -31307,6 +31571,30 @@ var init_websocket = __esm({
31307
31571
  CLOSE_GOING_AWAY = 1001;
31308
31572
  CLOSE_PROTOCOL_ERROR = 1002;
31309
31573
  CLOSE_POLICY_VIOLATION = 1008;
31574
+ MessageAssembler = class {
31575
+ parts = [];
31576
+ opcode = 0;
31577
+ push(frame) {
31578
+ if (frame.opcode >= 8) return { opcode: frame.opcode, payload: frame.payload };
31579
+ if (frame.opcode === OP_CONTINUATION) {
31580
+ if (!this.opcode) return null;
31581
+ this.parts.push(frame.payload);
31582
+ if (!frame.fin) return null;
31583
+ const whole = { opcode: this.opcode, payload: Buffer.concat(this.parts) };
31584
+ this.parts = [];
31585
+ this.opcode = 0;
31586
+ return whole;
31587
+ }
31588
+ if (frame.fin) {
31589
+ this.parts = [];
31590
+ this.opcode = 0;
31591
+ return { opcode: frame.opcode, payload: frame.payload };
31592
+ }
31593
+ this.opcode = frame.opcode;
31594
+ this.parts = [frame.payload];
31595
+ return null;
31596
+ }
31597
+ };
31310
31598
  WebSocketServer = class {
31311
31599
  port;
31312
31600
  server = null;
@@ -31749,6 +32037,15 @@ var init_websocket = __esm({
31749
32037
  this.emit("error", err, client);
31750
32038
  });
31751
32039
  }
32040
+ assemblers = /* @__PURE__ */ new WeakMap();
32041
+ assemblerFor(client) {
32042
+ let a = this.assemblers.get(client);
32043
+ if (!a) {
32044
+ a = new MessageAssembler();
32045
+ this.assemblers.set(client, a);
32046
+ }
32047
+ return a;
32048
+ }
31752
32049
  processBuffer(client, buffer, setBuffer) {
31753
32050
  let remaining = buffer;
31754
32051
  while (remaining.length > 0) {
@@ -31756,15 +32053,17 @@ var init_websocket = __esm({
31756
32053
  if (!frame) break;
31757
32054
  remaining = remaining.subarray(frame.bytesConsumed);
31758
32055
  client.lastActivity = Date.now();
31759
- switch (frame.opcode) {
32056
+ const whole = this.assemblerFor(client).push(frame);
32057
+ if (!whole) continue;
32058
+ switch (whole.opcode) {
31760
32059
  case OP_TEXT:
31761
- this.emit("message", client, frame.payload.toString("utf-8"));
32060
+ this.emit("message", client, whole.payload.toString("utf-8"));
31762
32061
  break;
31763
32062
  case OP_BINARY:
31764
- this.emit("message", client, frame.payload);
32063
+ this.emit("message", client, whole.payload);
31765
32064
  break;
31766
32065
  case OP_PING: {
31767
- const pongFrame = buildFrame(OP_PONG, frame.payload);
32066
+ const pongFrame = buildFrame(OP_PONG, whole.payload);
31768
32067
  try {
31769
32068
  client.socket.write(pongFrame);
31770
32069
  } catch {
@@ -36142,16 +36441,7 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
36142
36441
  return;
36143
36442
  }
36144
36443
  clearTimeout(timer);
36145
- const raw = await upstream.text();
36146
- const status2 = upstream.status || 200;
36147
- try {
36148
- res.json(JSON.parse(raw), status2);
36149
- } catch {
36150
- res.raw.writeHead(status2, {
36151
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
36152
- });
36153
- res.raw.end(raw);
36154
- }
36444
+ await relayUpstreamResponse(res, upstream);
36155
36445
  }
36156
36446
  function resolveDevEnvVar(key) {
36157
36447
  const live = process.env[key];
@@ -36593,6 +36883,7 @@ var init_devAdmin = __esm({
36593
36883
  "src/devAdmin.ts"() {
36594
36884
  "use strict";
36595
36885
  init_devMailbox();
36886
+ init_upstreamResponse();
36596
36887
  init_dotenv();
36597
36888
  init_metrics();
36598
36889
  init_feedback();
@@ -36694,8 +36985,8 @@ var init_devAdmin = __esm({
36694
36985
  * Duplicate errors (same message) increment count and update last_seen.
36695
36986
  */
36696
36987
  static capture(errorType, message, traceback = "", file = "", line = 0) {
36697
- const fingerprint = `${errorType}|${message}|${file}|${line}`;
36698
- const existing = this.errors.find((e) => e.fingerprint === fingerprint);
36988
+ const fingerprint2 = `${errorType}|${message}|${file}|${line}`;
36989
+ const existing = this.errors.find((e) => e.fingerprint === fingerprint2);
36699
36990
  const now = (/* @__PURE__ */ new Date()).toISOString();
36700
36991
  if (existing) {
36701
36992
  existing.count = (existing.count || 1) + 1;
@@ -36708,7 +36999,7 @@ var init_devAdmin = __esm({
36708
36999
  message,
36709
37000
  stack: traceback || void 0,
36710
37001
  resolved: false,
36711
- ...{ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
37002
+ ...{ fingerprint: fingerprint2, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
36712
37003
  });
36713
37004
  if (this.errors.length > this.maxErrors) {
36714
37005
  this.errors = this.errors.slice(-this.maxErrors);
@@ -39387,7 +39678,7 @@ function swaggerEnabled() {
39387
39678
  }
39388
39679
  function createSwaggerRoutes(getSpec) {
39389
39680
  const serveUi = async (_req, res) => {
39390
- res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
39681
+ res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
39391
39682
  };
39392
39683
  return [
39393
39684
  {
@@ -39431,13 +39722,13 @@ var SWAGGER_UI_HTML;
39431
39722
  var init_ui = __esm({
39432
39723
  "../swagger/src/ui.ts"() {
39433
39724
  "use strict";
39434
- SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
39725
+ SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
39435
39726
  <html lang="en">
39436
39727
  <head>
39437
39728
  <meta charset="UTF-8">
39438
39729
  <title>Tina4 API Documentation</title>
39439
39730
  <link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
39440
- <style>
39731
+ <style nonce="${nonce}">
39441
39732
  body { margin: 0; background: #fafafa; }
39442
39733
  .topbar { display: none !important; }
39443
39734
  </style>
@@ -39445,7 +39736,7 @@ var init_ui = __esm({
39445
39736
  <body>
39446
39737
  <div id="swagger-ui"></div>
39447
39738
  <script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
39448
- <script>
39739
+ <script nonce="${nonce}">
39449
39740
  SwaggerUIBundle({
39450
39741
  url: "${specUrl}",
39451
39742
  dom_id: '#swagger-ui',
@@ -39485,7 +39776,7 @@ __export(sso_exports, {
39485
39776
  SsoError: () => SsoError,
39486
39777
  liveSessionIdentity: () => liveSessionIdentity
39487
39778
  });
39488
- import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39779
+ import { createHash as createHash9, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39489
39780
  function expiresAt(tokens) {
39490
39781
  const lifetime = Number(tokens.expires_in ?? 0);
39491
39782
  return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
@@ -39613,9 +39904,9 @@ var init_sso = __esm({
39613
39904
  async login(requestOrSession, returnTo = "/") {
39614
39905
  const session = this.session(requestOrSession);
39615
39906
  if (!session) throw new SsoError("SSO login requires a Tina4 Session");
39616
- const state = randomBytes7(32).toString("base64url");
39617
- const nonce = randomBytes7(32).toString("base64url");
39618
- const verifier = randomBytes7(64).toString("base64url");
39907
+ const state = randomBytes8(32).toString("base64url");
39908
+ const nonce = randomBytes8(32).toString("base64url");
39909
+ const verifier = randomBytes8(64).toString("base64url");
39619
39910
  const challenge = createHash9("sha256").update(verifier).digest("base64url");
39620
39911
  session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
39621
39912
  const metadata = await this.discover();
@@ -39843,7 +40134,7 @@ var init_docsAutoDiscovery = __esm({
39843
40134
  });
39844
40135
 
39845
40136
  // src/server.ts
39846
- import { randomBytes as randomBytes8 } from "node:crypto";
40137
+ import { randomBytes as randomBytes9 } from "node:crypto";
39847
40138
  import { resolve as resolve19, dirname as dirname13, join as join30, relative as relative10 } from "node:path";
39848
40139
  import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync18 } from "node:fs";
39849
40140
  import { isatty } from "node:tty";
@@ -40023,7 +40314,8 @@ function isDevMode() {
40023
40314
  }
40024
40315
  async function renderErrorPage(code, data, templatesDir) {
40025
40316
  try {
40026
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40317
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40318
+ setCspNonceProvider2(currentCspNonce);
40027
40319
  const templateFile = `errors/${code}.twig`;
40028
40320
  const getCachedFrond = (dir) => {
40029
40321
  let instance = frondCache.get(dir);
@@ -40139,6 +40431,7 @@ function resolveTemplate(pathname, templatesDir) {
40139
40431
  }
40140
40432
  function renderLandingPage(port = 7148) {
40141
40433
  const version = TINA4_VERSION;
40434
+ const nonce = currentCspNonce();
40142
40435
  const galleryItems = [
40143
40436
  { 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" },
40144
40437
  { id: "orm", icon: "&#128451;", name: "ORM", desc: "Product model with CRUD endpoints", accent: "accent-green", tryUrl: "/api/gallery/products" },
@@ -40153,7 +40446,7 @@ function renderLandingPage(port = 7148) {
40153
40446
  const isDeployed = deployed[item.id] === true;
40154
40447
  const tryBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-try" target="_blank">Try It</a>` : "";
40155
40448
  const viewBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-view" target="_blank">View</a>` : "";
40156
- const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" onclick="deployGallery('${item.id}')">Deploy</button>`;
40449
+ const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" data-deploy="${item.id}">Deploy</button>`;
40157
40450
  return `<div class="gallery-card">
40158
40451
  <div class="accent ${item.accent}"></div>
40159
40452
  <div class="icon">${item.icon}</div>
@@ -40168,7 +40461,7 @@ function renderLandingPage(port = 7148) {
40168
40461
  <meta charset="utf-8">
40169
40462
  <meta name="viewport" content="width=device-width, initial-scale=1">
40170
40463
  <title>Tina4NodeJs</title>
40171
- <style>
40464
+ <style nonce="${nonce}">
40172
40465
  *{margin:0;padding:0;box-sizing:border-box}
40173
40466
  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}
40174
40467
  .bg-watermark{position:fixed;bottom:-5%;right:-5%;width:45%;opacity:0.04;pointer-events:none;z-index:0}
@@ -40207,6 +40500,11 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
40207
40500
  .gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
40208
40501
  @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)}}
40209
40502
  .star-wiggle{display:inline-block;transform-origin:center}
40503
+ .gallery-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
40504
+ .tok-comment{color:#64748b}
40505
+ .tok-kw{color:#c084fc}
40506
+ .tok-str{color:#4ade80}
40507
+ .tok-fn{color:#38bdf8}
40210
40508
  </style>
40211
40509
  </head>
40212
40510
  <body>
@@ -40232,23 +40530,23 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
40232
40530
  <div class="section">
40233
40531
  <div class="card">
40234
40532
  <h2>Getting Started</h2>
40235
- <pre class="code-block"><code><span style="color:#64748b">// app.ts</span>
40236
- <span style="color:#c084fc">import</span> { startServer, Router } <span style="color:#c084fc">from</span> <span style="color:#4ade80">"tina4-nodejs"</span>;
40533
+ <pre class="code-block"><code><span class="tok-comment">// app.ts</span>
40534
+ <span class="tok-kw">import</span> { startServer, Router } <span class="tok-kw">from</span> <span class="tok-str">"tina4-nodejs"</span>;
40237
40535
 
40238
- 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; {
40239
- <span style="color:#c084fc">return</span> res.json({ message: <span style="color:#4ade80">"Hello World!"</span> });
40536
+ 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; {
40537
+ <span class="tok-kw">return</span> res.json({ message: <span class="tok-str">"Hello World!"</span> });
40240
40538
  });
40241
40539
 
40242
- startServer({ port: 7148 }); <span style="color:#64748b">// starts on port 7148</span></code></pre>
40540
+ startServer({ port: 7148 }); <span class="tok-comment">// starts on port 7148</span></code></pre>
40243
40541
  </div>
40244
40542
  </div>
40245
40543
  <div class="gallery">
40246
40544
  <h2 id="gallery">What You Can Build</h2>
40247
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;">
40545
+ <div class="gallery-cards">
40248
40546
  ${galleryCards}
40249
40547
  </div>
40250
40548
  </div>
40251
- <script>
40549
+ <script nonce="${nonce}">
40252
40550
  function deployGallery(name) {
40253
40551
  if (!confirm('Deploy the "' + name + '" gallery example into your project? This will copy files into src/.')) return;
40254
40552
  fetch('/__dev/api/gallery/deploy', {
@@ -40283,6 +40581,12 @@ function deployGallery(name) {
40283
40581
  }
40284
40582
  setTimeout(doWiggle,3000);
40285
40583
  })();
40584
+ // CSP-clean wiring: no inline onclick handlers \u2014 bind every Deploy button from
40585
+ // its data-deploy attribute (a nonce covers script elements, not event-handler
40586
+ // attributes).
40587
+ document.querySelectorAll('.gbtn-deploy[data-deploy]').forEach(function(btn){
40588
+ btn.addEventListener('click', function(){ deployGallery(btn.dataset.deploy); });
40589
+ });
40286
40590
  </script>
40287
40591
  </body>
40288
40592
  </html>`;
@@ -40499,7 +40803,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
40499
40803
  }
40500
40804
  }
40501
40805
  }
40502
- const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
40806
+ const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
40503
40807
  if (wantsJson(req2)) {
40504
40808
  const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
40505
40809
  res.raw.writeHead(500, { "Content-Type": "application/json" });
@@ -40570,7 +40874,7 @@ function serveStaticAsset(ctx) {
40570
40874
  return false;
40571
40875
  }
40572
40876
  async function serveNotFound(ctx) {
40573
- const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
40877
+ const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
40574
40878
  if (wantsJson(ctx.req)) {
40575
40879
  const body = negotiatedErrorBody(404, "Not Found", requestId);
40576
40880
  ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
@@ -40593,7 +40897,8 @@ async function buildDispatchContext(router, base) {
40593
40897
  const templatesDir = resolve19(root, "src/templates");
40594
40898
  let frondEngine = null;
40595
40899
  try {
40596
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40900
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40901
+ setCspNonceProvider2(currentCspNonce);
40597
40902
  frondEngine = new Frond2(templatesDir);
40598
40903
  } catch {
40599
40904
  }
@@ -40703,9 +41008,13 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
40703
41008
  }
40704
41009
  }
40705
41010
  async function runDispatch(ctx, rawReq, rawRes) {
40706
- const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
41011
+ const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes9(4).toString("hex");
40707
41012
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
40708
- return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
41013
+ const requestNonce = generateNonce();
41014
+ return Log.runWithRequestId(
41015
+ requestId,
41016
+ () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
41017
+ );
40709
41018
  }
40710
41019
  function loopbackBindHosts(host) {
40711
41020
  const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
@@ -40832,7 +41141,8 @@ async function startServer(config) {
40832
41141
  let frondEngine = null;
40833
41142
  setDefaultTemplatesDir(templatesDir);
40834
41143
  try {
40835
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
41144
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
41145
+ setCspNonceProvider2(currentCspNonce);
40836
41146
  frondEngine = new Frond2(templatesDir);
40837
41147
  router.addRoute({
40838
41148
  method: "GET",
@@ -40973,10 +41283,12 @@ async function startServer(config) {
40973
41283
  }
40974
41284
  });
40975
41285
  aiServer.on("error", (err) => {
40976
- if (err.code === "EADDRINUSE") {
41286
+ if (err?.code === "EADDRINUSE") {
40977
41287
  Log.warning(`Test port ${testPort} in use \u2014 skipping`);
40978
- aiServer = null;
41288
+ } else {
41289
+ Log.error(`Test port ${testPort} bind failed (${err?.code ?? err}) \u2014 skipping`);
40979
41290
  }
41291
+ aiServer = null;
40980
41292
  });
40981
41293
  aiServer.listen(testPort, host);
40982
41294
  }
@@ -41191,6 +41503,7 @@ var init_server = __esm({
41191
41503
  init_static();
41192
41504
  init_dotenv();
41193
41505
  init_errorOverlay();
41506
+ init_csp();
41194
41507
  init_health();
41195
41508
  init_rateLimiter();
41196
41509
  init_logger();
@@ -41440,7 +41753,7 @@ var init_mqttMessage = __esm({
41440
41753
  // src/mqtt.ts
41441
41754
  import net2 from "node:net";
41442
41755
  import tls from "node:tls";
41443
- import { randomBytes as randomBytes9 } from "node:crypto";
41756
+ import { randomBytes as randomBytes10 } from "node:crypto";
41444
41757
  import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
41445
41758
  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;
41446
41759
  var init_mqtt = __esm({
@@ -41528,7 +41841,7 @@ var init_mqtt = __esm({
41528
41841
  this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
41529
41842
  this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
41530
41843
  let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
41531
- if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
41844
+ if (cid === null || cid === "") cid = "tina4-" + randomBytes10(8).toString("hex");
41532
41845
  this.clientId = cid;
41533
41846
  this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
41534
41847
  this.cleanSession = options.cleanSession ?? true;
@@ -42642,7 +42955,7 @@ var init_ssrf = __esm({
42642
42955
  import http from "node:http";
42643
42956
  import https from "node:https";
42644
42957
  import { URL as URL2 } from "node:url";
42645
- import { randomBytes as randomBytes10 } from "node:crypto";
42958
+ import { randomBytes as randomBytes11 } from "node:crypto";
42646
42959
  import { promises as fsp, createWriteStream } from "node:fs";
42647
42960
  import { basename as basename6 } from "node:path";
42648
42961
  import { pipeline } from "node:stream/promises";
@@ -43034,7 +43347,7 @@ var init_api = __esm({
43034
43347
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
43035
43348
  }
43036
43349
  const partContentType = guessContentType(uploadName);
43037
- const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
43350
+ const boundary = "----Tina4Boundary" + randomBytes11(16).toString("hex");
43038
43351
  const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
43039
43352
  const contentType = `multipart/form-data; boundary=${boundary}`;
43040
43353
  return this.execute("POST", this.buildUrl(path10), bodyBuffer, contentType, headers);
@@ -45998,7 +46311,7 @@ import {
45998
46311
  createHmac as createHmac4,
45999
46312
  createPrivateKey,
46000
46313
  createSign as createSign2,
46001
- randomBytes as randomBytes11
46314
+ randomBytes as randomBytes12
46002
46315
  } from "node:crypto";
46003
46316
  function encodeBase64Url(value) {
46004
46317
  return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
@@ -46078,7 +46391,7 @@ function encryptPayload(payload, subscription) {
46078
46391
  ]);
46079
46392
  const prkKey = hmac(authSecret, sharedSecret);
46080
46393
  const ikm = hkdfExpand(prkKey, keyInfo, 32);
46081
- const salt = randomBytes11(16);
46394
+ const salt = randomBytes12(16);
46082
46395
  const contentPrk = hmac(salt, ikm);
46083
46396
  const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
46084
46397
  const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
@@ -48835,6 +49148,7 @@ __export(index_exports, {
48835
49148
  cacheStats: () => cacheStats,
48836
49149
  checkPassword: () => checkPassword,
48837
49150
  clearCache: () => clearCache,
49151
+ clearCurrentNonce: () => clearCurrentNonce,
48838
49152
  computeAcceptKey: () => computeAcceptKey,
48839
49153
  container: () => container,
48840
49154
  cors: () => cors,
@@ -48846,6 +49160,8 @@ __export(index_exports, {
48846
49160
  createMessenger: () => createMessenger,
48847
49161
  createRequest: () => createRequest,
48848
49162
  createResponse: () => createResponse,
49163
+ cspNonce: () => cspNonce,
49164
+ currentCspNonce: () => currentCspNonce,
48849
49165
  decodeRequest: () => decodeRequest,
48850
49166
  defaultContext: () => defaultContext,
48851
49167
  defaultRouter: () => defaultRouter,
@@ -48871,6 +49187,7 @@ __export(index_exports, {
48871
49187
  feedbackWhitelist: () => feedbackWhitelist,
48872
49188
  fts5Supported: () => fts5Supported,
48873
49189
  generateContext: () => generateContext,
49190
+ generateNonce: () => generateNonce,
48874
49191
  generateVapidKeys: () => generateVapidKeys,
48875
49192
  get: () => get,
48876
49193
  getDefaultDevServer: () => getDefaultDevServer,
@@ -48893,6 +49210,7 @@ __export(index_exports, {
48893
49210
  httpReason: () => httpReason,
48894
49211
  inContainer: () => inContainer,
48895
49212
  injectFeedbackWidget: () => injectFeedbackWidget,
49213
+ injectNonceIntoCsp: () => injectNonceIntoCsp,
48896
49214
  installAll: () => installAll,
48897
49215
  installSelected: () => installSelected,
48898
49216
  isBannerSuppressed: () => isBannerSuppressed,
@@ -48947,6 +49265,7 @@ __export(index_exports, {
48947
49265
  resetTrustedProxyCache: () => resetTrustedProxyCache,
48948
49266
  resolveAlgorithm: () => resolveAlgorithm,
48949
49267
  resolveClientIp: () => resolveClientIp,
49268
+ resolveCspHeader: () => resolveCspHeader,
48950
49269
  resolveFrameworkVersion: () => resolveFrameworkVersion,
48951
49270
  resolvePortAndHost: () => resolvePortAndHost,
48952
49271
  resolveStringMiddleware: () => resolveStringMiddleware,
@@ -48954,12 +49273,14 @@ __export(index_exports, {
48954
49273
  responseCache: () => responseCache,
48955
49274
  runAll: () => runAll,
48956
49275
  runRouteMiddlewares: () => runRouteMiddlewares,
49276
+ runWithCspNonce: () => runWithCspNonce,
48957
49277
  runtimeDir: () => runtimeDir,
48958
49278
  saveUpload: () => saveUpload,
48959
49279
  schemaFromParams: () => schemaFromParams,
48960
49280
  selectablePids: () => selectablePids,
48961
49281
  serveWebSocketRoute: () => serveWebSocketRoute,
48962
49282
  sessionCookieName: () => sessionCookieName,
49283
+ setCurrentNonce: () => setCurrentNonce,
48963
49284
  setDefaultTemplatesDir: () => setDefaultTemplatesDir,
48964
49285
  setFrond: () => setFrond,
48965
49286
  showMenu: () => showMenu,
@@ -49025,6 +49346,7 @@ var init_index = __esm({
49025
49346
  init_wsdl();
49026
49347
  init_htmlElement();
49027
49348
  init_errorOverlay();
49349
+ init_csp();
49028
49350
  init_ai();
49029
49351
  init_sso();
49030
49352
  init_push();
@@ -49215,6 +49537,7 @@ export {
49215
49537
  cacheStats,
49216
49538
  checkPassword,
49217
49539
  clearCache,
49540
+ clearCurrentNonce,
49218
49541
  computeAcceptKey,
49219
49542
  container,
49220
49543
  cors,
@@ -49226,6 +49549,8 @@ export {
49226
49549
  createMessenger,
49227
49550
  createRequest,
49228
49551
  createResponse,
49552
+ cspNonce,
49553
+ currentCspNonce,
49229
49554
  decodeRequest,
49230
49555
  defaultContext,
49231
49556
  defaultRouter,
@@ -49251,6 +49576,7 @@ export {
49251
49576
  feedbackWhitelist,
49252
49577
  fts5Supported,
49253
49578
  generateContext,
49579
+ generateNonce,
49254
49580
  generateVapidKeys,
49255
49581
  get,
49256
49582
  getDefaultDevServer,
@@ -49273,6 +49599,7 @@ export {
49273
49599
  httpReason,
49274
49600
  inContainer,
49275
49601
  injectFeedbackWidget,
49602
+ injectNonceIntoCsp,
49276
49603
  installAll,
49277
49604
  installSelected,
49278
49605
  isBannerSuppressed,
@@ -49327,6 +49654,7 @@ export {
49327
49654
  resetTrustedProxyCache,
49328
49655
  resolveAlgorithm,
49329
49656
  resolveClientIp,
49657
+ resolveCspHeader,
49330
49658
  resolveFrameworkVersion,
49331
49659
  resolvePortAndHost,
49332
49660
  resolveStringMiddleware,
@@ -49334,12 +49662,14 @@ export {
49334
49662
  responseCache,
49335
49663
  runAll,
49336
49664
  runRouteMiddlewares,
49665
+ runWithCspNonce,
49337
49666
  runtimeDir,
49338
49667
  saveUpload,
49339
49668
  schemaFromParams,
49340
49669
  selectablePids,
49341
49670
  serveWebSocketRoute,
49342
49671
  sessionCookieName,
49672
+ setCurrentNonce,
49343
49673
  setDefaultTemplatesDir,
49344
49674
  setFrond,
49345
49675
  showMenu,