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
@@ -1221,8 +1221,8 @@ function hashPassword(password, salt, iterations = 26e4) {
1221
1221
  }
1222
1222
  function checkPassword(password, hash) {
1223
1223
  try {
1224
- const delimiter5 = hash.includes("$") ? "$" : ":";
1225
- const parts = hash.split(delimiter5);
1224
+ const delimiter4 = hash.includes("$") ? "$" : ":";
1225
+ const parts = hash.split(delimiter4);
1226
1226
  if (parts.length !== 4 || parts[0] !== "pbkdf2_sha256") return false;
1227
1227
  const iterations = parseInt(parts[1], 10);
1228
1228
  const salt = parts[2];
@@ -8980,7 +8980,7 @@ async function removeMigrationRecord(name) {
8980
8980
  [name]
8981
8981
  );
8982
8982
  }
8983
- async function rollback(migrationsDir, delimiter5) {
8983
+ async function rollback(migrationsDir, delimiter4) {
8984
8984
  if (migrationsDir instanceof Map) {
8985
8985
  const downFunctions = migrationsDir;
8986
8986
  const migrations2 = await getLastBatchMigrations();
@@ -8999,7 +8999,7 @@ async function rollback(migrationsDir, delimiter5) {
8999
8999
  return rolledBack2;
9000
9000
  }
9001
9001
  const dir = resolve4(migrationsDir ?? "migrations");
9002
- const delim = delimiter5 ?? ";";
9002
+ const delim = delimiter4 ?? ";";
9003
9003
  const db = getAdapter();
9004
9004
  const migrations = await getLastBatchMigrations();
9005
9005
  const rolledBack = [];
@@ -9115,13 +9115,13 @@ function consumeSqlDelimiter(state) {
9115
9115
  }
9116
9116
  return true;
9117
9117
  }
9118
- function splitStatements(sql, delimiter5 = ";") {
9118
+ function splitStatements(sql, delimiter4 = ";") {
9119
9119
  const state = {
9120
9120
  sql: normalizeQuotes(sql),
9121
9121
  statements: [],
9122
9122
  current: "",
9123
- delimiter: delimiter5,
9124
- dlen: delimiter5.length,
9123
+ delimiter: delimiter4,
9124
+ dlen: delimiter4.length,
9125
9125
  i: 0,
9126
9126
  inDollarBlock: false,
9127
9127
  inSlashBlock: false
@@ -9160,7 +9160,7 @@ function warnUnprefixedMigrations(files) {
9160
9160
  async function migrate(adapter, options) {
9161
9161
  const db = adapter ?? getAdapter();
9162
9162
  const dir = resolve4(options?.migrationsDir ?? "migrations");
9163
- const delimiter5 = options?.delimiter ?? ";";
9163
+ const delimiter4 = options?.delimiter ?? ";";
9164
9164
  const result = { applied: [], skipped: [], failed: [] };
9165
9165
  if (!existsSync5(dir)) {
9166
9166
  return result;
@@ -9203,7 +9203,7 @@ async function migrate(adapter, options) {
9203
9203
  result.skipped.push(file);
9204
9204
  continue;
9205
9205
  }
9206
- const statements = splitStatements(sqlContent, delimiter5);
9206
+ const statements = splitStatements(sqlContent, delimiter4);
9207
9207
  try {
9208
9208
  await adapterStartTransaction(db);
9209
9209
  for (const stmt of statements) {
@@ -13696,6 +13696,36 @@ var init_shapes = __esm({
13696
13696
  }
13697
13697
  });
13698
13698
 
13699
+ // ../orm/src/graph/adapters/rowMapping.ts
13700
+ function errorMessage(exc) {
13701
+ if (exc instanceof Error) return exc.message;
13702
+ return String(exc);
13703
+ }
13704
+ function nodeFromRow(row) {
13705
+ if (row === null || row === void 0) return null;
13706
+ return new GraphNode(
13707
+ String(row.id),
13708
+ row.labels ?? [],
13709
+ row.props ?? {}
13710
+ );
13711
+ }
13712
+ function edgeFromRow(row) {
13713
+ if (row === null || row === void 0) return null;
13714
+ return new GraphEdge(
13715
+ String(row.id),
13716
+ String(row.type),
13717
+ String(row.f),
13718
+ String(row.t),
13719
+ row.props ?? {}
13720
+ );
13721
+ }
13722
+ var init_rowMapping = __esm({
13723
+ "../orm/src/graph/adapters/rowMapping.ts"() {
13724
+ "use strict";
13725
+ init_shapes();
13726
+ }
13727
+ });
13728
+
13699
13729
  // ../orm/src/graph/connectTimeout.ts
13700
13730
  function resolveGraphConnectTimeout() {
13701
13731
  const raw = (process.env[GRAPH_CONNECT_TIMEOUT_VARIABLE] ?? "").trim();
@@ -13735,15 +13765,12 @@ function propClause(properties) {
13735
13765
  for (const key of keys) params[`p_${key}`] = props[key];
13736
13766
  return { clause: `{${pairs}}`, params };
13737
13767
  }
13738
- function errorMessage(exc) {
13739
- if (exc instanceof Error) return exc.message;
13740
- return String(exc);
13741
- }
13742
13768
  var DRIVER_PACKAGE, driver, UltipaClient, UltipaConnectError, UNBOUNDED_CONNECT_SECONDS, UltipaGraphAdapter;
13743
13769
  var init_ultipa = __esm({
13744
13770
  async "../orm/src/graph/adapters/ultipa.ts"() {
13745
13771
  "use strict";
13746
13772
  init_shapes();
13773
+ init_rowMapping();
13747
13774
  init_errors();
13748
13775
  init_connectTimeout2();
13749
13776
  DRIVER_PACKAGE = "tina4-ultipa";
@@ -13801,12 +13828,7 @@ var init_ultipa = __esm({
13801
13828
  }
13802
13829
  // -- portable node/edge/traverse core (GQL) ----------------------------
13803
13830
  nodeFromRow(row) {
13804
- if (row === null || row === void 0) return null;
13805
- return new GraphNode(
13806
- String(row.id),
13807
- row.labels ?? [],
13808
- row.props ?? {}
13809
- );
13831
+ return nodeFromRow(row);
13810
13832
  }
13811
13833
  async addNode(label, properties = null) {
13812
13834
  const { clause, params } = propClause(properties);
@@ -13820,15 +13842,7 @@ var init_ultipa = __esm({
13820
13842
  params.to_id = toId;
13821
13843
  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`;
13822
13844
  const rows = (await this.run(gql, params, false)).dicts();
13823
- if (rows.length === 0) return null;
13824
- const row = rows[0];
13825
- return new GraphEdge(
13826
- String(row.id),
13827
- String(row.type),
13828
- String(row.f),
13829
- String(row.t),
13830
- row.props ?? {}
13831
- );
13845
+ return edgeFromRow(rows[0]);
13832
13846
  }
13833
13847
  async getNode(nodeId) {
13834
13848
  const gql = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -13893,10 +13907,6 @@ var bolt_exports = {};
13893
13907
  __export(bolt_exports, {
13894
13908
  BoltGraphAdapter: () => BoltGraphAdapter
13895
13909
  });
13896
- function errorMessage2(exc) {
13897
- if (exc instanceof Error) return exc.message;
13898
- return String(exc);
13899
- }
13900
13910
  function boltId(id) {
13901
13911
  const value = Number(id);
13902
13912
  return Number.isNaN(value) ? -1 : value;
@@ -13906,6 +13916,7 @@ var init_bolt = __esm({
13906
13916
  async "../orm/src/graph/adapters/bolt.ts"() {
13907
13917
  "use strict";
13908
13918
  init_shapes();
13919
+ init_rowMapping();
13909
13920
  init_errors();
13910
13921
  init_connectTimeout2();
13911
13922
  DRIVER_PACKAGE2 = "neo4j-driver";
@@ -13941,7 +13952,7 @@ var init_bolt = __esm({
13941
13952
  const result = await session.run(cypher, params ?? {});
13942
13953
  return result.records.map((record) => record.toObject());
13943
13954
  } catch (exc) {
13944
- this.lastError = errorMessage2(exc);
13955
+ this.lastError = errorMessage(exc);
13945
13956
  const code = exc?.code ?? "";
13946
13957
  const message = this.lastError.toLowerCase();
13947
13958
  if (code === "ServiceUnavailable" || code === neo4j.error?.SERVICE_UNAVAILABLE || message.includes("timed out") || message.includes("timeout")) {
@@ -13965,12 +13976,7 @@ var init_bolt = __esm({
13965
13976
  }
13966
13977
  // -- portable node/edge/traverse core (Cypher) -------------------------
13967
13978
  nodeFromRow(row) {
13968
- if (row === null || row === void 0) return null;
13969
- return new GraphNode(
13970
- String(row.id),
13971
- row.labels ?? [],
13972
- row.props ?? {}
13973
- );
13979
+ return nodeFromRow(row);
13974
13980
  }
13975
13981
  async addNode(label, properties = null) {
13976
13982
  const cypher = `CREATE (n:\`${label}\` $props) RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -13984,15 +13990,7 @@ var init_bolt = __esm({
13984
13990
  to_id: boltId(toId),
13985
13991
  props: properties ?? {}
13986
13992
  });
13987
- if (rows.length === 0) return null;
13988
- const row = rows[0];
13989
- return new GraphEdge(
13990
- String(row.id),
13991
- String(row.type),
13992
- String(row.f),
13993
- String(row.t),
13994
- row.props ?? {}
13995
- );
13993
+ return edgeFromRow(rows[0]);
13996
13994
  }
13997
13995
  async getNode(nodeId) {
13998
13996
  const cypher = `MATCH (n) WHERE id(n) = $id RETURN id(n) AS id, labels(n) AS labels, properties(n) AS props`;
@@ -14058,7 +14056,7 @@ function cleanProps(doc) {
14058
14056
  }
14059
14057
  return props;
14060
14058
  }
14061
- function errorMessage3(exc) {
14059
+ function errorMessage2(exc) {
14062
14060
  if (exc instanceof Error) return exc.message;
14063
14061
  return String(exc);
14064
14062
  }
@@ -14097,14 +14095,14 @@ var init_arango = __esm({
14097
14095
  this.db = new ArangoDatabase(config);
14098
14096
  }
14099
14097
  connectOrError(exc) {
14100
- const text = errorMessage3(exc).toLowerCase();
14098
+ const text = errorMessage2(exc).toLowerCase();
14101
14099
  if (text.includes("timed out") || text.includes("timeout") || text.includes("connection") || text.includes("econnrefused") || text.includes("etimedout") || text.includes("max retries")) {
14102
14100
  return new GraphConnectTimeout(
14103
14101
  `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.`,
14104
14102
  exc
14105
14103
  );
14106
14104
  }
14107
- return new GraphError(errorMessage3(exc), exc);
14105
+ return new GraphError(errorMessage2(exc), exc);
14108
14106
  }
14109
14107
  async ensureCollections() {
14110
14108
  if (this.ensured) return;
@@ -14115,7 +14113,7 @@ var init_arango = __esm({
14115
14113
  if (!await edges.exists()) await this.db.createEdgeCollection(EDGE_COLLECTION);
14116
14114
  this.ensured = true;
14117
14115
  } catch (exc) {
14118
- this.lastError = errorMessage3(exc);
14116
+ this.lastError = errorMessage2(exc);
14119
14117
  throw this.connectOrError(exc);
14120
14118
  }
14121
14119
  }
@@ -14125,7 +14123,7 @@ var init_arango = __esm({
14125
14123
  const cursor = await this.db.query({ query, bindVars: bind2 ?? {} });
14126
14124
  return await cursor.all();
14127
14125
  } catch (exc) {
14128
- this.lastError = errorMessage3(exc);
14126
+ this.lastError = errorMessage2(exc);
14129
14127
  throw this.connectOrError(exc);
14130
14128
  }
14131
14129
  }
@@ -17433,6 +17431,73 @@ var init_trustedProxy = __esm({
17433
17431
  }
17434
17432
  });
17435
17433
 
17434
+ // ../core/src/csp.ts
17435
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
17436
+ import { randomBytes as randomBytes4 } from "node:crypto";
17437
+ function generateNonce() {
17438
+ return randomBytes4(16).toString("base64");
17439
+ }
17440
+ function runWithCspNonce(nonce, fn) {
17441
+ return nonceStore.run({ nonce }, fn);
17442
+ }
17443
+ function setCurrentNonce(nonce) {
17444
+ const store = nonceStore.getStore();
17445
+ if (store) store.nonce = nonce;
17446
+ else nonceFallback = nonce;
17447
+ }
17448
+ function clearCurrentNonce() {
17449
+ setCurrentNonce(void 0);
17450
+ }
17451
+ function currentCspNonce() {
17452
+ const store = nonceStore.getStore();
17453
+ let value = store ? store.nonce : nonceFallback;
17454
+ if (!value) {
17455
+ value = generateNonce();
17456
+ if (store) store.nonce = value;
17457
+ else nonceFallback = value;
17458
+ }
17459
+ return value;
17460
+ }
17461
+ function cspNonce() {
17462
+ return currentCspNonce();
17463
+ }
17464
+ function injectNonceIntoCsp(csp, nonce) {
17465
+ const token = `'nonce-${nonce}'`;
17466
+ const directives = [];
17467
+ for (const rawPart of csp.split(";")) {
17468
+ const part = rawPart.trim();
17469
+ if (!part) continue;
17470
+ const spaceAt = part.search(/\s/);
17471
+ const name = (spaceAt === -1 ? part : part.slice(0, spaceAt)).toLowerCase();
17472
+ const value = spaceAt === -1 ? "" : part.slice(spaceAt + 1).trim();
17473
+ directives.push([name, value]);
17474
+ }
17475
+ const defaultEntry = directives.find(([name]) => name === "default-src");
17476
+ const defaultValue = defaultEntry && defaultEntry[1] ? defaultEntry[1] : "'self'";
17477
+ for (const directive of ["style-src", "script-src"]) {
17478
+ const existing = directives.find(([name]) => name === directive);
17479
+ if (existing) {
17480
+ if (!existing[1].split(/\s+/).includes(token)) {
17481
+ existing[1] = `${existing[1]} ${token}`.trim();
17482
+ }
17483
+ } else {
17484
+ directives.push([directive, `${defaultValue} ${token}`.trim()]);
17485
+ }
17486
+ }
17487
+ return directives.map(([name, value]) => value ? `${name} ${value}` : name).join("; ");
17488
+ }
17489
+ function resolveCspHeader(nonce) {
17490
+ const csp = process.env.TINA4_CSP || "default-src 'self'";
17491
+ return injectNonceIntoCsp(csp, nonce);
17492
+ }
17493
+ var nonceStore, nonceFallback;
17494
+ var init_csp = __esm({
17495
+ "../core/src/csp.ts"() {
17496
+ "use strict";
17497
+ nonceStore = new AsyncLocalStorage3();
17498
+ }
17499
+ });
17500
+
17436
17501
  // ../frond/src/engine.ts
17437
17502
  var engine_exports = {};
17438
17503
  __export(engine_exports, {
@@ -17442,9 +17507,10 @@ __export(engine_exports, {
17442
17507
  expressionFormCache: () => expressionFormCache,
17443
17508
  filterChainCache: () => filterChainCache,
17444
17509
  pathParseCache: () => pathParseCache,
17510
+ setCspNonceProvider: () => setCspNonceProvider,
17445
17511
  setFormTokenSessionId: () => setFormTokenSessionId
17446
17512
  });
17447
- import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes4 } from "node:crypto";
17513
+ import { createHash as createHash3, createHmac as createHmac3, randomBytes as randomBytes5 } from "node:crypto";
17448
17514
  import { readFileSync as readFileSync6, existsSync as existsSync8, statSync as statSync6, realpathSync } from "node:fs";
17449
17515
  import { join as join11, resolve as resolve7, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17450
17516
  function jsonText(value) {
@@ -18511,6 +18577,9 @@ function _b64url(data) {
18511
18577
  function setFormTokenSessionId(sessionId) {
18512
18578
  _formTokenSessionId = sessionId || "";
18513
18579
  }
18580
+ function setCspNonceProvider(fn) {
18581
+ _cspNonceProvider = fn;
18582
+ }
18514
18583
  function _buildFormTokenJwt(descriptor = "") {
18515
18584
  const secret = process.env.TINA4_SECRET ?? "";
18516
18585
  const secretBytes = Buffer.byteLength(secret, "utf8");
@@ -18522,7 +18591,7 @@ function _buildFormTokenJwt(descriptor = "") {
18522
18591
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
18523
18592
  const header = { alg: "HS256", typ: "JWT" };
18524
18593
  const now = Math.floor(Date.now() / 1e3);
18525
- const payload = { type: "form", nonce: randomBytes4(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
18594
+ const payload = { type: "form", nonce: randomBytes5(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
18526
18595
  if (descriptor) {
18527
18596
  if (descriptor.includes("|")) {
18528
18597
  const [ctx, ref] = descriptor.split("|", 2);
@@ -18549,7 +18618,7 @@ function _generateFormToken(descriptor = "") {
18549
18618
  function _generateFormTokenValue(descriptor = "") {
18550
18619
  return new SafeString(_buildFormTokenJwt(descriptor));
18551
18620
  }
18552
- 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;
18621
+ 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;
18553
18622
  var init_engine = __esm({
18554
18623
  "../frond/src/engine.ts"() {
18555
18624
  "use strict";
@@ -18903,6 +18972,7 @@ var init_engine = __esm({
18903
18972
  js_escape: (v) => new SafeString(jsEscape(v))
18904
18973
  };
18905
18974
  _formTokenSessionId = "";
18975
+ _cspNonceProvider = () => "";
18906
18976
  Frond = class _Frond {
18907
18977
  // ── Class-level registries ──────────────────────────────────
18908
18978
  // Persist globals, filters, and tests across hot-reloads and at
@@ -19056,6 +19126,7 @@ var init_engine = __esm({
19056
19126
  this.globals.formTokenValue = (descriptor) => _generateFormTokenValue(descriptor || "");
19057
19127
  this.globals.form_token_value = (descriptor) => _generateFormTokenValue(descriptor || "");
19058
19128
  this.globals.dump = (value) => renderDump(value);
19129
+ this.globals.csp_nonce = () => _cspNonceProvider();
19059
19130
  for (const [k, v] of _Frond.classFilters) this.filters[k] = v;
19060
19131
  for (const [k, v] of _Frond.classGlobals) this.globals[k] = v;
19061
19132
  for (const [k, v] of _Frond.classTests) this.tests[k] = v;
@@ -20319,7 +20390,8 @@ async function getFrond() {
20319
20390
  const dir = _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
20320
20391
  let engine = _frondCache.get(dir);
20321
20392
  if (!engine) {
20322
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20393
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20394
+ setCspNonceProvider2(currentCspNonce);
20323
20395
  engine = new Frond2(dir);
20324
20396
  _frondCache.set(dir, engine);
20325
20397
  }
@@ -20330,7 +20402,8 @@ async function getFrameworkFrond() {
20330
20402
  const frameworkDir = nodePath.resolve(nodePath.dirname(import.meta.url.replace("file://", "")), "..", "templates");
20331
20403
  if (!_frameworkFrond && fs2.existsSync(frameworkDir)) {
20332
20404
  try {
20333
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20405
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20406
+ setCspNonceProvider2(currentCspNonce);
20334
20407
  _frameworkFrond = new Frond2(frameworkDir);
20335
20408
  } catch {
20336
20409
  return null;
@@ -20430,6 +20503,7 @@ function createResponse(res) {
20430
20503
  return response;
20431
20504
  };
20432
20505
  response.raw = res;
20506
+ response.cspNonce = currentCspNonce();
20433
20507
  response.json = function(data, status2) {
20434
20508
  if (res.headersSent) return response;
20435
20509
  if (status2 !== void 0) res.statusCode = status2;
@@ -20575,7 +20649,8 @@ function createResponse(res) {
20575
20649
  };
20576
20650
  response.render = async function(templateName, data, status2, templateDir) {
20577
20651
  try {
20578
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20652
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
20653
+ setCspNonceProvider2(currentCspNonce);
20579
20654
  const dir = templateDir ?? _defaultTemplatesDir ?? nodePath.resolve(process.cwd(), "src/templates");
20580
20655
  let engine = _frondCache.get(dir);
20581
20656
  if (!engine) {
@@ -20693,6 +20768,7 @@ var HEADER_TOKEN, _frondCache, _defaultTemplatesDir, _globalFrond, _frameworkFro
20693
20768
  var init_response = __esm({
20694
20769
  "../core/src/response.ts"() {
20695
20770
  "use strict";
20771
+ init_csp();
20696
20772
  HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
20697
20773
  _frondCache = /* @__PURE__ */ new Map();
20698
20774
  _defaultTemplatesDir = null;
@@ -20904,6 +20980,7 @@ var init_middleware = __esm({
20904
20980
  init_dotenv();
20905
20981
  init_router();
20906
20982
  init_trustedProxy();
20983
+ init_csp();
20907
20984
  init_response();
20908
20985
  MiddlewareChain = class {
20909
20986
  middlewares = [];
@@ -21336,12 +21413,18 @@ var init_middleware = __esm({
21336
21413
  * overrides, WITHOUT Strict-Transport-Security, which depends on the
21337
21414
  * request's scheme. Also what a transport rejection carries (ADR-0068
21338
21415
  * section 4), written before the scheme is known.
21416
+ *
21417
+ * Content-Security-Policy always carries a per-response nonce in style-src
21418
+ * AND script-src (ADR-0088), read from the request async-context (minted on
21419
+ * first access), so the framework's own inline content runs under the strict
21420
+ * default without 'unsafe-inline'. The nonce is random per response, so a
21421
+ * caller comparing this header must assert its STRUCTURE, never a byte string.
21339
21422
  */
21340
21423
  static canonicalHeaders() {
21341
21424
  return {
21342
21425
  "X-Frame-Options": process.env.TINA4_FRAME_OPTIONS ?? "SAMEORIGIN",
21343
21426
  "X-Content-Type-Options": "nosniff",
21344
- "Content-Security-Policy": process.env.TINA4_CSP ?? "default-src 'self'",
21427
+ "Content-Security-Policy": resolveCspHeader(currentCspNonce()),
21345
21428
  "Referrer-Policy": process.env.TINA4_REFERRER_POLICY ?? "strict-origin-when-cross-origin",
21346
21429
  "X-XSS-Protection": "0",
21347
21430
  "Permissions-Policy": process.env.TINA4_PERMISSIONS_POLICY ?? "camera=(), microphone=(), geolocation=()"
@@ -21383,7 +21466,7 @@ var init_middleware = __esm({
21383
21466
  static warnCspDefaultOnce() {
21384
21467
  if (_SecurityHeadersMiddleware.cspDefaultWarned) return;
21385
21468
  _SecurityHeadersMiddleware.cspDefaultWarned = true;
21386
- 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'".`;
21469
+ 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'".`;
21387
21470
  try {
21388
21471
  Log.warning(message);
21389
21472
  } catch {
@@ -21621,14 +21704,14 @@ function extractBoundary(contentType) {
21621
21704
  function parseMultipart(body, boundary) {
21622
21705
  const fields = {};
21623
21706
  const files = {};
21624
- const delimiter5 = Buffer.from(`--${boundary}`);
21707
+ const delimiter4 = Buffer.from(`--${boundary}`);
21625
21708
  const closeDelimiter = Buffer.from(`--${boundary}--`);
21626
21709
  const crlf = Buffer.from("\r\n");
21627
21710
  const doubleCrlf = Buffer.from("\r\n\r\n");
21628
21711
  let offset = 0;
21629
- const firstIdx = bufferIndexOf(body, delimiter5, offset);
21712
+ const firstIdx = bufferIndexOf(body, delimiter4, offset);
21630
21713
  if (firstIdx === -1) return { fields, files };
21631
- offset = firstIdx + delimiter5.length;
21714
+ offset = firstIdx + delimiter4.length;
21632
21715
  if (body[offset] === 13 && body[offset + 1] === 10) {
21633
21716
  offset += 2;
21634
21717
  }
@@ -21637,7 +21720,7 @@ function parseMultipart(body, boundary) {
21637
21720
  if (headersEnd === -1) break;
21638
21721
  const headersStr = body.subarray(offset, headersEnd).toString("utf-8");
21639
21722
  offset = headersEnd + doubleCrlf.length;
21640
- const nextDelimIdx = bufferIndexOf(body, delimiter5, offset);
21723
+ const nextDelimIdx = bufferIndexOf(body, delimiter4, offset);
21641
21724
  if (nextDelimIdx === -1) break;
21642
21725
  const contentEnd = nextDelimIdx - crlf.length;
21643
21726
  const content = body.subarray(offset, contentEnd);
@@ -21660,7 +21743,7 @@ function parseMultipart(body, boundary) {
21660
21743
  } else if (disposition.name) {
21661
21744
  fields[disposition.name] = content.toString("utf-8");
21662
21745
  }
21663
- offset = nextDelimIdx + delimiter5.length;
21746
+ offset = nextDelimIdx + delimiter4.length;
21664
21747
  if (body[offset] === 45 && body[offset + 1] === 45) {
21665
21748
  break;
21666
21749
  }
@@ -22408,6 +22491,21 @@ var init_portTakeover = __esm({
22408
22491
  }
22409
22492
  });
22410
22493
 
22494
+ // ../core/src/etag.ts
22495
+ function etagMatches(ifNoneMatch, etag) {
22496
+ const strip = (tag) => tag.trim().replace(/^W\//, "");
22497
+ const target = strip(etag);
22498
+ return ifNoneMatch.split(",").some((candidate) => {
22499
+ const trimmed = candidate.trim();
22500
+ return trimmed === "*" || strip(trimmed) === target;
22501
+ });
22502
+ }
22503
+ var init_etag = __esm({
22504
+ "../core/src/etag.ts"() {
22505
+ "use strict";
22506
+ }
22507
+ });
22508
+
22411
22509
  // ../core/src/sessionHandlers/syncBridge.ts
22412
22510
  import { Worker } from "node:worker_threads";
22413
22511
  function getBridge(key, workerSource, workerData) {
@@ -22689,42 +22787,35 @@ var init_respClient = __esm({
22689
22787
  }
22690
22788
  });
22691
22789
 
22692
- // ../core/src/sessionHandlers/valkeyHandler.ts
22693
- var ValkeySessionHandler;
22694
- var init_valkeyHandler = __esm({
22695
- "../core/src/sessionHandlers/valkeyHandler.ts"() {
22790
+ // ../core/src/sessionHandlers/respSessionHandler.ts
22791
+ var RespSessionHandler;
22792
+ var init_respSessionHandler = __esm({
22793
+ "../core/src/sessionHandlers/respSessionHandler.ts"() {
22696
22794
  "use strict";
22697
22795
  init_respClient();
22698
- ValkeySessionHandler = class {
22699
- host;
22700
- port;
22701
- password;
22702
- prefix;
22703
- db;
22704
- constructor(config) {
22705
- this.host = config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1";
22706
- this.port = config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379);
22707
- this.password = config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "";
22708
- this.prefix = config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:";
22709
- this.db = config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0);
22796
+ RespSessionHandler = class {
22797
+ constructor(conn, label) {
22798
+ this.conn = conn;
22799
+ this.label = label;
22710
22800
  }
22801
+ conn;
22802
+ label;
22711
22803
  /**
22712
- * Execute a RESP command synchronously against the live Valkey server.
22804
+ * Execute a RESP command synchronously against the live server.
22713
22805
  *
22714
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
22715
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
22716
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
22717
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
22806
+ * Delegates to the shared {@link respCommandSync} transport: a genuine key
22807
+ * miss yields `""`, and a transport/connection FAILURE THROWS so the caller
22808
+ * can distinguish "not found" from "backend failed". Backend-failure parity.
22718
22809
  */
22719
22810
  execSync(args) {
22720
22811
  return respCommandSync(
22721
- { host: this.host, port: this.port, password: this.password, db: this.db },
22812
+ { host: this.conn.host, port: this.conn.port, password: this.conn.password, db: this.conn.db },
22722
22813
  args,
22723
- "Valkey"
22814
+ this.label
22724
22815
  );
22725
22816
  }
22726
22817
  key(sessionId) {
22727
- return `${this.prefix}${sessionId}`;
22818
+ return `${this.conn.prefix}${sessionId}`;
22728
22819
  }
22729
22820
  read(sessionId) {
22730
22821
  const raw = this.execSync(["GET", this.key(sessionId)]);
@@ -22735,7 +22826,7 @@ var init_valkeyHandler = __esm({
22735
22826
  return null;
22736
22827
  }
22737
22828
  }
22738
- write(sessionId, data, ttl) {
22829
+ write(sessionId, data, ttl = 0) {
22739
22830
  const json = JSON.stringify(data);
22740
22831
  if (ttl > 0) {
22741
22832
  this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
@@ -22750,6 +22841,29 @@ var init_valkeyHandler = __esm({
22750
22841
  }
22751
22842
  });
22752
22843
 
22844
+ // ../core/src/sessionHandlers/valkeyHandler.ts
22845
+ var ValkeySessionHandler;
22846
+ var init_valkeyHandler = __esm({
22847
+ "../core/src/sessionHandlers/valkeyHandler.ts"() {
22848
+ "use strict";
22849
+ init_respSessionHandler();
22850
+ ValkeySessionHandler = class extends RespSessionHandler {
22851
+ constructor(config) {
22852
+ super(
22853
+ {
22854
+ host: config?.host ?? process.env.TINA4_SESSION_VALKEY_HOST ?? "127.0.0.1",
22855
+ port: config?.port ?? (process.env.TINA4_SESSION_VALKEY_PORT ? parseInt(process.env.TINA4_SESSION_VALKEY_PORT, 10) : 6379),
22856
+ password: config?.password ?? process.env.TINA4_SESSION_VALKEY_PASSWORD ?? "",
22857
+ prefix: config?.prefix ?? process.env.TINA4_SESSION_VALKEY_PREFIX ?? "tina4:session:",
22858
+ db: config?.db ?? (process.env.TINA4_SESSION_VALKEY_DB ? parseInt(process.env.TINA4_SESSION_VALKEY_DB, 10) : 0)
22859
+ },
22860
+ "Valkey"
22861
+ );
22862
+ }
22863
+ };
22864
+ }
22865
+ });
22866
+
22753
22867
  // ../core/src/sessionHandlers/memcachedHandler.ts
22754
22868
  import { createHash as createHash4 } from "node:crypto";
22755
22869
  var MAX_KEY_BYTES, MAX_RELATIVE_EXPTIME, MemcachedSessionHandler;
@@ -23765,7 +23879,7 @@ __export(session_exports, {
23765
23879
  sessionCookieName: () => sessionCookieName,
23766
23880
  sessionStrictMode: () => sessionStrictMode
23767
23881
  });
23768
- import { createHash as createHash5, randomBytes as randomBytes5 } from "node:crypto";
23882
+ import { createHash as createHash5, randomBytes as randomBytes6 } from "node:crypto";
23769
23883
  import { existsSync as existsSync11, mkdirSync as mkdirSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, readdirSync as readdirSync6 } from "node:fs";
23770
23884
  import { join as join15 } from "node:path";
23771
23885
  function isValidSessionId(sessionId) {
@@ -23783,6 +23897,20 @@ function resolveBackend(name) {
23783
23897
  }
23784
23898
  return normalised;
23785
23899
  }
23900
+ function fingerprint(value) {
23901
+ try {
23902
+ return JSON.stringify(value);
23903
+ } catch {
23904
+ return void 0;
23905
+ }
23906
+ }
23907
+ function fingerprints(data) {
23908
+ const prints = /* @__PURE__ */ new Map();
23909
+ for (const [key, value] of Object.entries(data)) {
23910
+ prints.set(key, fingerprint(value));
23911
+ }
23912
+ return prints;
23913
+ }
23786
23914
  function isSecureScheme(forwardedProto, socketEncrypted) {
23787
23915
  const forwarded = (forwardedProto ?? "").trim();
23788
23916
  if (forwarded !== "") {
@@ -23815,7 +23943,7 @@ var init_session = __esm({
23815
23943
  "use strict";
23816
23944
  init_logger();
23817
23945
  init_dotenv();
23818
- init_respClient();
23946
+ init_respSessionHandler();
23819
23947
  init_valkeyHandler();
23820
23948
  init_memcachedHandler();
23821
23949
  init_mongoHandler();
@@ -23941,57 +24069,19 @@ var init_session = __esm({
23941
24069
  }
23942
24070
  }
23943
24071
  };
23944
- RedisSessionHandler = class {
23945
- host;
23946
- port;
23947
- password;
23948
- prefix;
23949
- db;
24072
+ RedisSessionHandler = class extends RespSessionHandler {
23950
24073
  constructor(config) {
23951
- this.host = config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1";
23952
- this.port = config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379);
23953
- this.password = config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "";
23954
- this.prefix = config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:";
23955
- this.db = config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0);
23956
- }
23957
- /**
23958
- * Execute a Redis command synchronously against the live server.
23959
- *
23960
- * Delegates to the shared {@link respCommandSync} transport: a genuine key miss
23961
- * yields `""`, and a transport/connection FAILURE (server unreachable, rejected
23962
- * AUTH, timeout) THROWS so the Session boundary can distinguish "not found"
23963
- * (silent) from "backend failed" (log-loud + degrade). Backend-failure parity.
23964
- */
23965
- execSync(args) {
23966
- return respCommandSync(
23967
- { host: this.host, port: this.port, password: this.password, db: this.db },
23968
- args,
24074
+ super(
24075
+ {
24076
+ host: config?.redisHost ?? process.env.TINA4_SESSION_REDIS_HOST ?? "127.0.0.1",
24077
+ port: config?.redisPort ?? (process.env.TINA4_SESSION_REDIS_PORT ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10) : 6379),
24078
+ password: config?.redisPassword ?? process.env.TINA4_SESSION_REDIS_PASSWORD ?? "",
24079
+ prefix: config?.redisPrefix ?? process.env.TINA4_SESSION_REDIS_PREFIX ?? "tina4:session:",
24080
+ db: config?.redisDb ?? (process.env.TINA4_SESSION_REDIS_DB ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10) : 0)
24081
+ },
23969
24082
  "Redis"
23970
24083
  );
23971
24084
  }
23972
- key(sessionId) {
23973
- return `${this.prefix}${sessionId}`;
23974
- }
23975
- read(sessionId) {
23976
- const raw = this.execSync(["GET", this.key(sessionId)]);
23977
- if (!raw) return null;
23978
- try {
23979
- return JSON.parse(raw);
23980
- } catch {
23981
- return null;
23982
- }
23983
- }
23984
- write(sessionId, data, ttl = 0) {
23985
- const json = JSON.stringify(data);
23986
- if (ttl > 0) {
23987
- this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
23988
- } else {
23989
- this.execSync(["SET", this.key(sessionId), json]);
23990
- }
23991
- }
23992
- destroy(sessionId) {
23993
- this.execSync(["DEL", this.key(sessionId)]);
23994
- }
23995
24085
  };
23996
24086
  FLASH_PREFIX = "_flash_";
23997
24087
  Session = class {
@@ -24018,6 +24108,29 @@ var init_session = __esm({
24018
24108
  * Lets `start()` tell "no such session" from "the store is unreachable".
24019
24109
  */
24020
24110
  lastReadFailed = false;
24111
+ /**
24112
+ * What this request last saw of the stored record, one fingerprint per key:
24113
+ * taken by start() and again by every save. save() compares the live data
24114
+ * against it to find the keys THIS request changed.
24115
+ */
24116
+ loaded = /* @__PURE__ */ new Map();
24117
+ /**
24118
+ * True when this request is working on a record the store holds: start()
24119
+ * adopted one, or a save has written one since. A save never re-creates such
24120
+ * a record once another request has removed it.
24121
+ */
24122
+ stored = false;
24123
+ /**
24124
+ * True when clear() ran since the last save: the save then replaces the
24125
+ * stored record instead of merging into it.
24126
+ */
24127
+ cleared = false;
24128
+ /**
24129
+ * True once this request found its session ended by another request (the
24130
+ * record it loaded is gone). It stays ended for the rest of the request:
24131
+ * regenerate() mints nothing either.
24132
+ */
24133
+ ended = false;
24021
24134
  constructor(backend, config) {
24022
24135
  const backendType = resolveBackend(
24023
24136
  backend ?? config?.backend ?? process.env.TINA4_SESSION_BACKEND ?? "file"
@@ -24162,6 +24275,10 @@ var init_session = __esm({
24162
24275
  const now2 = Math.floor(Date.now() / 1e3);
24163
24276
  this.data = { _created: now2, _accessed: now2 };
24164
24277
  this.dirty = false;
24278
+ this.stored = false;
24279
+ this.loaded = /* @__PURE__ */ new Map();
24280
+ this.cleared = false;
24281
+ this.ended = false;
24165
24282
  return sessionId;
24166
24283
  }
24167
24284
  if (loaded) {
@@ -24171,14 +24288,22 @@ var init_session = __esm({
24171
24288
  this.data._accessed = now2;
24172
24289
  this.dirty = false;
24173
24290
  this.safeWrite(this.sessionId, this.data, this.ttl);
24291
+ this.stored = true;
24292
+ this.loaded = fingerprints(this.data);
24293
+ this.cleared = false;
24294
+ this.ended = false;
24174
24295
  return sessionId;
24175
24296
  }
24176
24297
  }
24177
- this.sessionId = randomBytes5(16).toString("hex");
24298
+ this.sessionId = randomBytes6(16).toString("hex");
24178
24299
  const now = Math.floor(Date.now() / 1e3);
24179
24300
  this.data = { _created: now, _accessed: now };
24180
24301
  this.dirty = false;
24181
24302
  this.safeWrite(this.sessionId, this.data, this.ttl);
24303
+ this.stored = false;
24304
+ this.loaded = /* @__PURE__ */ new Map();
24305
+ this.cleared = false;
24306
+ this.ended = false;
24182
24307
  return this.sessionId;
24183
24308
  }
24184
24309
  /**
@@ -24219,9 +24344,7 @@ var init_session = __esm({
24219
24344
  if (this.sessionId) {
24220
24345
  this.safeDestroy(this.sessionId);
24221
24346
  }
24222
- this.sessionId = null;
24223
- this.data = null;
24224
- this.dirty = false;
24347
+ this.forget();
24225
24348
  }
24226
24349
  /**
24227
24350
  * Get all session data (excluding internal keys).
@@ -24244,6 +24367,7 @@ var init_session = __esm({
24244
24367
  const now = Math.floor(Date.now() / 1e3);
24245
24368
  this.data = { _created: this.data._created, _accessed: now };
24246
24369
  this.dirty = true;
24370
+ this.cleared = true;
24247
24371
  this.save();
24248
24372
  }
24249
24373
  /**
@@ -24260,17 +24384,36 @@ var init_session = __esm({
24260
24384
  * session fixation — the pre-auth ID is destroyed and the data is carried
24261
24385
  * onto a fresh, unguessable ID. A backend destroy/write failure is logged
24262
24386
  * (never silent) but does not throw under the default policy.
24387
+ *
24388
+ * What is carried is the session as it is stored now with this request's
24389
+ * own changes applied, the same merge save() does. If another request ended
24390
+ * the session after this one loaded it (a logout, or a regenerate of its
24391
+ * own), it stays ended: nothing is carried, no id is minted and null is
24392
+ * returned, so no cookie goes out to replace the one that request sent.
24263
24393
  */
24264
24394
  regenerate() {
24395
+ if (this.ended) return null;
24396
+ if (this.sessionId && this.stored && this.data) {
24397
+ const current = this.safeRead(this.sessionId);
24398
+ if (!this.lastReadFailed) {
24399
+ if (!current) {
24400
+ this.forget();
24401
+ this.ended = true;
24402
+ return null;
24403
+ }
24404
+ if (!this.cleared) this.data = this.merged(current);
24405
+ }
24406
+ }
24265
24407
  const oldId = this.sessionId;
24266
24408
  const oldData = this.data;
24267
24409
  if (oldId) {
24268
24410
  this.safeDestroy(oldId);
24269
24411
  }
24270
- this.sessionId = randomBytes5(16).toString("hex");
24412
+ this.sessionId = randomBytes6(16).toString("hex");
24271
24413
  this.data = oldData ?? { _created: Math.floor(Date.now() / 1e3), _accessed: Math.floor(Date.now() / 1e3) };
24272
24414
  this.data._accessed = Math.floor(Date.now() / 1e3);
24273
24415
  this.dirty = true;
24416
+ this.stored = false;
24274
24417
  this.save();
24275
24418
  return this.sessionId;
24276
24419
  }
@@ -24346,17 +24489,84 @@ var init_session = __esm({
24346
24489
  *
24347
24490
  * Returns true on a successful persist, false if the backend was unreachable
24348
24491
  * (logged). The dirty flag is cleared only on success so a later save()
24349
- * retries once the backend recovers. A nothing-to-persist call returns true.
24492
+ * retries once the backend recovers. A call with no session returns true.
24493
+ *
24494
+ * Other requests on the same session run while this one does, so the save
24495
+ * writes only THIS request's changes, onto the record as it is stored now.
24496
+ * Writing back the whole snapshot start() loaded undid whatever another
24497
+ * request did in between: a logout (destroy, clear, regenerate) or a
24498
+ * privilege change made with set(). A record removed since this request
24499
+ * loaded it is never re-created: the session has ended, for this request
24500
+ * too. A request that has not loaded a stored record (a new session, the new
24501
+ * id regenerate() mints) writes its data whole, as before.
24502
+ *
24503
+ * A session the store already holds is re-written even when this request
24504
+ * changed nothing: that write re-stamps the backend deadline to
24505
+ * now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
24506
+ * rather than that long after its last CHANGE (ADR-0087 — a session's expiry
24507
+ * slides on activity). The re-write is the re-read, merged record, so sliding
24508
+ * the deadline is concurrent-safe: it never clobbers another request's change,
24509
+ * never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
24510
+ * never-expires deadline (ADR-0027). A request with no stored session and no
24511
+ * change writes nothing — there is no deadline to move.
24350
24512
  */
24351
24513
  save() {
24352
24514
  if (!this.sessionId || !this.data) return true;
24353
- if (!this.dirty) return true;
24354
- if (this.safeWrite(this.sessionId, this.data, this.ttl)) {
24515
+ if (!this.dirty && !this.stored) return true;
24516
+ let record = this.data;
24517
+ if (this.stored) {
24518
+ const current = this.safeRead(this.sessionId);
24519
+ if (this.lastReadFailed) return false;
24520
+ if (!current) {
24521
+ this.forget();
24522
+ this.ended = true;
24523
+ return true;
24524
+ }
24525
+ if (!this.cleared) record = this.merged(current);
24526
+ }
24527
+ if (this.safeWrite(this.sessionId, record, this.ttl)) {
24355
24528
  this.dirty = false;
24529
+ this.stored = true;
24530
+ this.cleared = false;
24531
+ this.loaded = fingerprints(this.data);
24356
24532
  return true;
24357
24533
  }
24358
24534
  return false;
24359
24535
  }
24536
+ /**
24537
+ * The stored record with this request's changes applied: every key whose
24538
+ * value differs from what this request loaded or last saved, and the
24539
+ * removal of every key it deleted. Keys it did not touch keep whatever is
24540
+ * stored now.
24541
+ */
24542
+ merged(current) {
24543
+ const data = this.data;
24544
+ const record = { ...current };
24545
+ for (const [key, value] of Object.entries(data)) {
24546
+ const print = fingerprint(value);
24547
+ if (print === void 0 || !this.loaded.has(key) || this.loaded.get(key) !== print) {
24548
+ record[key] = value;
24549
+ }
24550
+ }
24551
+ for (const key of this.loaded.keys()) {
24552
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
24553
+ delete record[key];
24554
+ }
24555
+ }
24556
+ return record;
24557
+ }
24558
+ /**
24559
+ * End the session for this request without touching the store: no id and
24560
+ * no data, so nothing more is written and no cookie goes out for it.
24561
+ */
24562
+ forget() {
24563
+ this.sessionId = null;
24564
+ this.data = null;
24565
+ this.dirty = false;
24566
+ this.stored = false;
24567
+ this.cleared = false;
24568
+ this.loaded = /* @__PURE__ */ new Map();
24569
+ }
24360
24570
  };
24361
24571
  }
24362
24572
  });
@@ -24401,14 +24611,6 @@ function headStripIntercept(rawReq, rawRes) {
24401
24611
  function isCompressibleContentType(contentType) {
24402
24612
  return COMPRESSIBLE_PREFIXES.some((prefix) => contentType.includes(prefix));
24403
24613
  }
24404
- function etagMatchesInm(ifNoneMatch, etag) {
24405
- const strip = (tag) => tag.trim().replace(/^W\//, "");
24406
- const target = strip(etag);
24407
- return ifNoneMatch.split(",").some((candidate) => {
24408
- const trimmed = candidate.trim();
24409
- return trimmed === "*" || strip(trimmed) === target;
24410
- });
24411
- }
24412
24614
  function maybeCompressResponse(body, rawReq, rawRes) {
24413
24615
  const acceptEncoding = String(rawReq.headers["accept-encoding"] ?? "");
24414
24616
  const contentTypeHeader = rawRes.getHeader("content-type");
@@ -24424,7 +24626,7 @@ function maybeCompressResponse(body, rawReq, rawRes) {
24424
24626
  }
24425
24627
  function responseIsNotModified(rawReq, rawRes, etag) {
24426
24628
  const ifNoneMatch = String(rawReq.headers["if-none-match"] ?? "");
24427
- if (ifNoneMatch) return etagMatchesInm(ifNoneMatch, etag);
24629
+ if (ifNoneMatch) return etagMatches(ifNoneMatch, etag);
24428
24630
  const lastModifiedHeader = rawRes.getHeader("last-modified");
24429
24631
  if (typeof lastModifiedHeader !== "string") return false;
24430
24632
  const ifModifiedSince = String(rawReq.headers["if-modified-since"] ?? "");
@@ -24552,6 +24754,7 @@ var init_dispatchPipeline = __esm({
24552
24754
  "../core/src/dispatchPipeline.ts"() {
24553
24755
  "use strict";
24554
24756
  init_logger();
24757
+ init_etag();
24555
24758
  COMPRESSIBLE_PREFIXES = [
24556
24759
  "text/",
24557
24760
  "application/json",
@@ -24647,18 +24850,11 @@ function isNotModified(req2, etag, stat) {
24647
24850
  }
24648
24851
  return false;
24649
24852
  }
24650
- function etagMatches(ifNoneMatch, etag) {
24651
- const strip = (tag) => tag.trim().replace(/^W\//, "");
24652
- const target = strip(etag);
24653
- return ifNoneMatch.split(",").some((candidate) => {
24654
- const trimmed = candidate.trim();
24655
- return trimmed === "*" || strip(trimmed) === target;
24656
- });
24657
- }
24658
24853
  var COMPRESSIBLE_PREFIXES2, MIME_TYPES;
24659
24854
  var init_static = __esm({
24660
24855
  "../core/src/static.ts"() {
24661
24856
  "use strict";
24857
+ init_etag();
24662
24858
  COMPRESSIBLE_PREFIXES2 = [
24663
24859
  "text/",
24664
24860
  "application/json",
@@ -24739,11 +24935,11 @@ function formatSourceBlock(filename, lineno) {
24739
24935
  const lines = readSourceLines(filename, lineno);
24740
24936
  if (lines.length === 0) return "";
24741
24937
  const rows = lines.map(([num, text, isError]) => {
24742
- const bg = isError ? `background:${ERROR_LINE_BG};` : "";
24938
+ const rowClass = isError ? "eo-line eo-line-err" : "eo-line";
24743
24939
  const marker = isError ? "&#x25b6;" : " ";
24744
- 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>`;
24940
+ return `<div class="${rowClass}"><span class="eo-ln">${num}</span><span class="eo-marker">${marker}</span><span class="eo-code">${esc(text)}</span></div>`;
24745
24941
  }).join("\n");
24746
- 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>`;
24942
+ return `<div class="eo-source">` + rows + `</div>`;
24747
24943
  }
24748
24944
  function formatFrame(frame, capturedAt = 0) {
24749
24945
  const source = frame.file && frame.line > 0 ? formatSourceBlock(frame.file, frame.line) : "";
@@ -24755,23 +24951,23 @@ function formatFrame(frame, capturedAt = 0) {
24755
24951
  if (mtime > capturedAt + 0.5) {
24756
24952
  const d = new Date(mtime * 1e3);
24757
24953
  const mtimeIso = `${String(d.getUTCHours()).padStart(2, "0")}:${String(d.getUTCMinutes()).padStart(2, "0")}:${String(d.getUTCSeconds()).padStart(2, "0")}`;
24758
- 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>`;
24954
+ staleBadge = ` <span class="eo-stale">FILE MODIFIED @ ${mtimeIso} UTC &mdash; source may not match what failed</span>`;
24759
24955
  }
24760
24956
  } catch {
24761
24957
  }
24762
24958
  }
24763
- 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>`;
24959
+ 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>`;
24764
24960
  }
24765
24961
  function collapsible(title, content, openByDefault = false) {
24766
24962
  const open = openByDefault ? " open" : "";
24767
- 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>`;
24963
+ return `<details class="eo-details"${open}><summary class="eo-summary">${esc(title)}</summary><div class="eo-details-body">${content}</div></details>`;
24768
24964
  }
24769
24965
  function table(pairs) {
24770
- if (pairs.length === 0) return `<span style="color:${SUBTEXT};">None</span>`;
24966
+ if (pairs.length === 0) return `<span class="eo-none">None</span>`;
24771
24967
  const rows = pairs.map(
24772
- ([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>`
24968
+ ([key, val]) => `<tr><td class="eo-key">${esc(key)}</td><td class="eo-val">${esc(val)}</td></tr>`
24773
24969
  ).join("");
24774
- return `<table style="border-collapse:collapse;width:100%;">${rows}</table>`;
24970
+ return `<table class="eo-table">${rows}</table>`;
24775
24971
  }
24776
24972
  function renderErrorOverlay(error2, request) {
24777
24973
  const capturedAt = Date.now() / 1e3;
@@ -24784,7 +24980,7 @@ function renderErrorOverlay(error2, request) {
24784
24980
  }
24785
24981
  const hidden = frames.length - Math.min(frames.length, MAX_FRAMES);
24786
24982
  if (hidden > 0) {
24787
- framesHtml += `<div style="color:${SUBTEXT};padding:8px 0;font-size:13px;">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
24983
+ framesHtml += `<div class="eo-hidden-frames">&#8230; ${hidden} more stack frames hidden (truncated at ${MAX_FRAMES})</div>`;
24788
24984
  }
24789
24985
  const requestPairs = [];
24790
24986
  if (request != null) {
@@ -24821,37 +25017,72 @@ function renderErrorOverlay(error2, request) {
24821
25017
  ];
24822
25018
  const envSection = collapsible("Environment", table(envPairs));
24823
25019
  const stackSection = collapsible("Stack Trace", framesHtml, true);
25020
+ const nonce = currentCspNonce();
24824
25021
  return `<!DOCTYPE html>
24825
25022
  <html lang="en">
24826
25023
  <head>
24827
25024
  <meta charset="utf-8">
24828
25025
  <meta name="viewport" content="width=device-width,initial-scale=1">
24829
25026
  <title>Tina4 Error \u2014 ${esc(excType)}</title>
24830
- <style>
24831
- *{margin:0;padding:0;box-sizing:border-box;}
24832
- body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
25027
+ <style nonce="${nonce}">
25028
+ ${overlayStylesheet()}
24833
25029
  </style>
24834
25030
  </head>
24835
25031
  <body>
24836
- <div style="max-width:960px;margin:0 auto;">
24837
- <div style="margin-bottom:24px;">
24838
- <div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
24839
- <span style="background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;">Error</span>
24840
- <span style="color:${SUBTEXT};font-size:14px;">Tina4 Debug Overlay</span>
25032
+ <div class="eo-wrap">
25033
+ <div class="eo-header">
25034
+ <div class="eo-badge-row">
25035
+ <span class="eo-badge">Error</span>
25036
+ <span class="eo-sub">Tina4 Debug Overlay</span>
24841
25037
  </div>
24842
- <h1 style="color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;">${esc(excType)}</h1>
24843
- <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>
25038
+ <h1 class="eo-type">${esc(excType)}</h1>
25039
+ <p class="eo-msg">${esc(excMsg)}</p>
24844
25040
  </div>
24845
25041
  ${stackSection}
24846
25042
  ${requestSection}
24847
25043
  ${envSection}
24848
- <div style="margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;">
25044
+ <div class="eo-footer">
24849
25045
  Tina4 Debug Overlay &mdash; This page is only shown in debug mode. Set TINA4_DEBUG=false in production.
24850
25046
  </div>
24851
25047
  </div>
24852
25048
  </body>
24853
25049
  </html>`;
24854
25050
  }
25051
+ function overlayStylesheet() {
25052
+ return `
25053
+ *{margin:0;padding:0;box-sizing:border-box;}
25054
+ body{background:${BG};color:${TEXT};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px;line-height:1.5;}
25055
+ .eo-wrap{max-width:960px;margin:0 auto;}
25056
+ .eo-header{margin-bottom:24px;}
25057
+ .eo-badge-row{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
25058
+ .eo-badge{background:${RED};color:${BG};padding:4px 12px;border-radius:4px;font-weight:700;font-size:13px;text-transform:uppercase;}
25059
+ .eo-sub{color:${SUBTEXT};font-size:14px;}
25060
+ .eo-type{color:${RED};font-size:28px;font-weight:700;margin-bottom:8px;}
25061
+ .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};}
25062
+ .eo-footer{margin-top:32px;padding-top:16px;border-top:1px solid ${OVERLAY};color:${SUBTEXT};font-size:12px;}
25063
+ .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;}
25064
+ .eo-line{display:flex;padding:1px 0;}
25065
+ .eo-line-err{background:${ERROR_LINE_BG};}
25066
+ .eo-ln{color:${YELLOW};min-width:3.5em;text-align:right;padding-right:1em;user-select:none;}
25067
+ .eo-marker{color:${RED};width:1.2em;user-select:none;}
25068
+ .eo-code{color:${TEXT};white-space:pre-wrap;tab-size:4;}
25069
+ .eo-frame{margin-bottom:16px;}
25070
+ .eo-frame-head{margin-bottom:4px;}
25071
+ .eo-file{color:${BLUE};}
25072
+ .eo-sep{color:${SUBTEXT};}
25073
+ .eo-lineno{color:${YELLOW};}
25074
+ .eo-fn{color:${GREEN};}
25075
+ .eo-stale{background:${PEACH};color:${BG};padding:1px 8px;border-radius:3px;font-size:11px;font-weight:700;margin-left:6px;}
25076
+ .eo-details{margin-top:16px;}
25077
+ .eo-summary{cursor:pointer;color:${LAVENDER};font-weight:600;font-size:15px;padding:8px 0;user-select:none;}
25078
+ .eo-details-body{padding:8px 0;}
25079
+ .eo-table{border-collapse:collapse;width:100%;}
25080
+ .eo-key{color:${PEACH};padding:4px 16px 4px 0;vertical-align:top;white-space:nowrap;}
25081
+ .eo-val{color:${TEXT};padding:4px 0;word-break:break-all;}
25082
+ .eo-none{color:${SUBTEXT};}
25083
+ .eo-hidden-frames{color:${SUBTEXT};padding:8px 0;font-size:13px;}
25084
+ `;
25085
+ }
24855
25086
  function isDebugMode() {
24856
25087
  return isTruthy(process.env.TINA4_DEBUG);
24857
25088
  }
@@ -24860,6 +25091,7 @@ var init_errorOverlay = __esm({
24860
25091
  "../core/src/errorOverlay.ts"() {
24861
25092
  "use strict";
24862
25093
  init_dotenv();
25094
+ init_csp();
24863
25095
  MAX_FRAMES = 50;
24864
25096
  SENSITIVE_KEY_RE = /password|passwd|secret|token|authorization|cookie|key/i;
24865
25097
  REDACTED2 = "[redacted]";
@@ -25235,6 +25467,25 @@ Generated by DevMailbox.seed() for development purposes.`,
25235
25467
  }
25236
25468
  });
25237
25469
 
25470
+ // ../core/src/upstreamResponse.ts
25471
+ async function relayUpstreamResponse(res, upstream) {
25472
+ const raw = await upstream.text();
25473
+ const status2 = upstream.status || 200;
25474
+ try {
25475
+ res.json(JSON.parse(raw), status2);
25476
+ } catch {
25477
+ res.raw.writeHead(status2, {
25478
+ "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
25479
+ });
25480
+ res.raw.end(raw);
25481
+ }
25482
+ }
25483
+ var init_upstreamResponse = __esm({
25484
+ "../core/src/upstreamResponse.ts"() {
25485
+ "use strict";
25486
+ }
25487
+ });
25488
+
25238
25489
  // ../core/src/metrics.ts
25239
25490
  import * as fs3 from "node:fs";
25240
25491
  import * as path2 from "node:path";
@@ -25450,6 +25701,7 @@ var init_feedback = __esm({
25450
25701
  "use strict";
25451
25702
  init_auth();
25452
25703
  init_devAdmin();
25704
+ init_upstreamResponse();
25453
25705
  RATE_LIMIT_WINDOW_SEC = 3600;
25454
25706
  RATE_LIMIT_MAX = 5;
25455
25707
  _rateLimitHits = /* @__PURE__ */ new Map();
@@ -25496,16 +25748,7 @@ var init_feedback = __esm({
25496
25748
  return;
25497
25749
  }
25498
25750
  clearTimeout(timer);
25499
- const raw = await upstream.text();
25500
- const status2 = upstream.status || 200;
25501
- try {
25502
- res.json(JSON.parse(raw), status2);
25503
- } catch {
25504
- res.raw.writeHead(status2, {
25505
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
25506
- });
25507
- res.raw.end(raw);
25508
- }
25751
+ await relayUpstreamResponse(res, upstream);
25509
25752
  };
25510
25753
  __feedbackDirname = dirname8(fileURLToPath3(import.meta.url));
25511
25754
  WIDGET_BUNDLE_PATH = resolve10(__feedbackDirname, "__feedback", "widget.js");
@@ -25555,6 +25798,61 @@ var init_version = __esm({
25555
25798
  }
25556
25799
  });
25557
25800
 
25801
+ // src/util.ts
25802
+ import { delimiter as delimiter2, join as join21 } from "node:path";
25803
+ import { statSync as statSync11 } from "node:fs";
25804
+ function parseFlags(args, booleanFlags) {
25805
+ const flags = {};
25806
+ const positional = [];
25807
+ let i = 0;
25808
+ while (i < args.length) {
25809
+ const arg = args[i];
25810
+ if (arg.startsWith("--")) {
25811
+ const key = arg.slice(2);
25812
+ if (booleanFlags?.has(key)) {
25813
+ flags[key] = true;
25814
+ i += 1;
25815
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
25816
+ flags[key] = args[i + 1];
25817
+ i += 2;
25818
+ } else {
25819
+ flags[key] = true;
25820
+ i += 1;
25821
+ }
25822
+ } else {
25823
+ positional.push(arg);
25824
+ i += 1;
25825
+ }
25826
+ }
25827
+ return { flags, positional };
25828
+ }
25829
+ function findOnPath(names) {
25830
+ for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
25831
+ if (!dir) continue;
25832
+ for (const name of names) {
25833
+ const candidate = join21(dir, name);
25834
+ try {
25835
+ if (statSync11(candidate).isFile()) return candidate;
25836
+ } catch {
25837
+ }
25838
+ }
25839
+ }
25840
+ return null;
25841
+ }
25842
+ async function initDatabaseOrExit(initDatabase2) {
25843
+ try {
25844
+ await initDatabase2();
25845
+ } catch (err) {
25846
+ console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
25847
+ process.exit(1);
25848
+ }
25849
+ }
25850
+ var init_util = __esm({
25851
+ "src/util.ts"() {
25852
+ "use strict";
25853
+ }
25854
+ });
25855
+
25558
25856
  // src/commands/generate.ts
25559
25857
  var generate_exports = {};
25560
25858
  __export(generate_exports, {
@@ -25579,7 +25877,7 @@ __export(generate_exports, {
25579
25877
  toTableName: () => toTableName
25580
25878
  });
25581
25879
  import { existsSync as existsSync16, mkdirSync as mkdirSync13, writeFileSync as writeFileSync10 } from "node:fs";
25582
- import { join as join21, relative as relative2, resolve as resolve11, sep as sep5 } from "node:path";
25880
+ import { join as join22, relative as relative2, resolve as resolve11, sep as sep5 } from "node:path";
25583
25881
  function ensureDir(dir) {
25584
25882
  if (__resolution.dryRun) return;
25585
25883
  if (!existsSync16(dir)) {
@@ -25819,28 +26117,7 @@ function parseCliArgs(args) {
25819
26117
  "json",
25820
26118
  "dry-run"
25821
26119
  ]);
25822
- const flags = {};
25823
- const positional = [];
25824
- let i = 0;
25825
- while (i < args.length) {
25826
- if (args[i].startsWith("--")) {
25827
- const key = args[i].slice(2);
25828
- if (booleanFlags.has(key)) {
25829
- flags[key] = true;
25830
- i += 1;
25831
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
25832
- flags[key] = args[i + 1];
25833
- i += 2;
25834
- } else {
25835
- flags[key] = true;
25836
- i += 1;
25837
- }
25838
- } else {
25839
- positional.push(args[i]);
25840
- i += 1;
25841
- }
25842
- }
25843
- return { flags, positional };
26120
+ return parseFlags(args, booleanFlags);
25844
26121
  }
25845
26122
  function parseEvery(every) {
25846
26123
  if (!every || every === true) return 60;
@@ -25939,7 +26216,7 @@ function generateModel(name, flags, emitTest = true) {
25939
26216
  const table2 = resolveTable(name, flags, { announce: true });
25940
26217
  const dir = resolve11("src/models");
25941
26218
  ensureDir(dir);
25942
- const path10 = join21(dir, `${name}.ts`);
26219
+ const path10 = join22(dir, `${name}.ts`);
25943
26220
  setResolutionField("class_name", name);
25944
26221
  setResolutionField("table_name", table2);
25945
26222
  setResolutionField("file_path", `src/models/${name}.ts`);
@@ -25979,7 +26256,7 @@ function generateRoute(name, flags, emitTest = true) {
25979
26256
  const model = flags.model;
25980
26257
  const isPublic = Boolean(flags.public);
25981
26258
  const base = resolve11("src/routes/api", routePath);
25982
- const idDir = join21(base, "[id]");
26259
+ const idDir = join22(base, "[id]");
25983
26260
  ensureDir(base);
25984
26261
  ensureDir(idDir);
25985
26262
  pushRoute(`/api/${routePath}`);
@@ -25995,7 +26272,7 @@ function generateRoute(name, flags, emitTest = true) {
25995
26272
  const writeDoc = isPublic ? "Public (--public): no token required." : "Secure by default: requires a Bearer token (use --public to open).";
25996
26273
  if (model) {
25997
26274
  writeFileSafe(
25998
- join21(base, "get.ts"),
26275
+ join22(base, "get.ts"),
25999
26276
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26000
26277
  ${modelImportBase}
26001
26278
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -26012,7 +26289,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26012
26289
  );
26013
26290
  } else {
26014
26291
  writeFileSafe(
26015
- join21(base, "get.ts"),
26292
+ join22(base, "get.ts"),
26016
26293
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26017
26294
 
26018
26295
  export const meta = { summary: "List all ${routePath}", tags: ["${routePath}"] };
@@ -26031,7 +26308,7 @@ ${aiFill(`list_${routePath}`, {
26031
26308
  }
26032
26309
  if (model) {
26033
26310
  writeFileSafe(
26034
- join21(base, "post.ts"),
26311
+ join22(base, "post.ts"),
26035
26312
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26036
26313
  ${modelImportBase}${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
26037
26314
 
@@ -26054,7 +26331,7 @@ ${extend(
26054
26331
  );
26055
26332
  } else {
26056
26333
  writeFileSafe(
26057
- join21(base, "post.ts"),
26334
+ join22(base, "post.ts"),
26058
26335
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26059
26336
 
26060
26337
  ${secureOptOut(isPublic)}export const meta = { summary: "Create a new ${singular}", tags: ["${routePath}"] };
@@ -26075,7 +26352,7 @@ ${aiFill(`create_${singular}`, {
26075
26352
  }
26076
26353
  if (model) {
26077
26354
  writeFileSafe(
26078
- join21(idDir, "get.ts"),
26355
+ join22(idDir, "get.ts"),
26079
26356
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26080
26357
  ${modelImportId}
26081
26358
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -26093,7 +26370,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26093
26370
  );
26094
26371
  } else {
26095
26372
  writeFileSafe(
26096
- join21(idDir, "get.ts"),
26373
+ join22(idDir, "get.ts"),
26097
26374
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26098
26375
 
26099
26376
  export const meta = { summary: "Get a ${singular} by ID", tags: ["${routePath}"] };
@@ -26112,7 +26389,7 @@ ${aiFill(`get_${singular}`, {
26112
26389
  }
26113
26390
  if (model) {
26114
26391
  writeFileSafe(
26115
- join21(idDir, "put.ts"),
26392
+ join22(idDir, "put.ts"),
26116
26393
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26117
26394
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
26118
26395
 
@@ -26141,7 +26418,7 @@ ${extend(
26141
26418
  );
26142
26419
  } else {
26143
26420
  writeFileSafe(
26144
- join21(idDir, "put.ts"),
26421
+ join22(idDir, "put.ts"),
26145
26422
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26146
26423
 
26147
26424
  ${secureOptOut(isPublic)}export const meta = { summary: "Update a ${singular} by ID", tags: ["${routePath}"] };
@@ -26162,7 +26439,7 @@ ${aiFill(`update_${singular}`, {
26162
26439
  }
26163
26440
  if (model) {
26164
26441
  writeFileSafe(
26165
- join21(idDir, "delete.ts"),
26442
+ join22(idDir, "delete.ts"),
26166
26443
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26167
26444
  ${modelImportId}${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
26168
26445
 
@@ -26181,7 +26458,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26181
26458
  );
26182
26459
  } else {
26183
26460
  writeFileSafe(
26184
- join21(idDir, "delete.ts"),
26461
+ join22(idDir, "delete.ts"),
26185
26462
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26186
26463
 
26187
26464
  ${secureOptOut(isPublic)}export const meta = { summary: "Delete a ${singular} by ID", tags: ["${routePath}"] };
@@ -26243,7 +26520,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
26243
26520
  const fields = fieldsOverride || parseFields(flags.fields || "");
26244
26521
  const isCreate = name.startsWith("create_") || fieldsOverride !== void 0;
26245
26522
  const fileName = `${ts}_${name}.sql`;
26246
- const path10 = join21(dir, fileName);
26523
+ const path10 = join22(dir, fileName);
26247
26524
  setResolutionField("migration_path", `migrations/${fileName}`);
26248
26525
  if (__resolution.target === "migration") {
26249
26526
  setResolutionField("file_path", `migrations/${fileName}`);
@@ -26282,7 +26559,7 @@ ${upSql}
26282
26559
  ${downSql}
26283
26560
  `;
26284
26561
  writeFileSafe(path10, content);
26285
- const downPath = join21(dir, `${ts}_${name}.down.sql`);
26562
+ const downPath = join22(dir, `${ts}_${name}.down.sql`);
26286
26563
  const downContent = `-- Rollback: ${name}
26287
26564
  -- Created: ${now}
26288
26565
 
@@ -26295,7 +26572,7 @@ function generateMiddleware(name, _flags) {
26295
26572
  const snake = toSnake(name);
26296
26573
  const dir = resolve11("src/middleware");
26297
26574
  ensureDir(dir);
26298
- const path10 = join21(dir, `${snake}.ts`);
26575
+ const path10 = join22(dir, `${snake}.ts`);
26299
26576
  if (__resolution.target === "middleware") {
26300
26577
  setResolutionField("class_name", name);
26301
26578
  setResolutionField("file_path", `src/middleware/${snake}.ts`);
@@ -26342,7 +26619,7 @@ function generateTest(name, flags) {
26342
26619
  const model = flags.model;
26343
26620
  const dir = resolve11("tests");
26344
26621
  ensureDir(dir);
26345
- const path10 = join21(dir, `${snake}.test.ts`);
26622
+ const path10 = join22(dir, `${snake}.test.ts`);
26346
26623
  if (model && flags["secure-writes"]) {
26347
26624
  const isPublic = Boolean(flags.public);
26348
26625
  const posture = isPublic ? "open (--public)" : "gated";
@@ -26480,7 +26757,7 @@ function generateForm(name, flags) {
26480
26757
  };
26481
26758
  const dir = resolve11("src/templates/forms");
26482
26759
  ensureDir(dir);
26483
- const path10 = join21(dir, `${table2}.twig`);
26760
+ const path10 = join22(dir, `${table2}.twig`);
26484
26761
  let fieldHtml = "";
26485
26762
  for (const [fname, ftype] of fields) {
26486
26763
  const itype = inputTypes[ftype] || "text";
@@ -26533,7 +26810,7 @@ function generateView(name, flags) {
26533
26810
  const cols = fields.map(([f]) => f);
26534
26811
  const dir = resolve11("src/templates/pages");
26535
26812
  ensureDir(dir);
26536
- const listPath = join21(dir, `${routeName}.twig`);
26813
+ const listPath = join22(dir, `${routeName}.twig`);
26537
26814
  const th = cols.map((c) => ` <th>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}</th>`).join("\n");
26538
26815
  const td = cols.map((c) => ` <td>{{ item.${c} }}</td>`).join("\n");
26539
26816
  const listContent = `{% extends "base.twig" %}
@@ -26570,7 +26847,7 @@ ${td}
26570
26847
  {% endblock %}
26571
26848
  `;
26572
26849
  writeFileSafe(listPath, listContent);
26573
- const detailPath = join21(dir, `${table2}.twig`);
26850
+ const detailPath = join22(dir, `${table2}.twig`);
26574
26851
  const detailFields = cols.map((c) => ` <div class="mb-3"><strong>${c.replace(/_/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase())}:</strong> {{ item.${c} }}</div>`).join("\n");
26575
26852
  const detailContent = `{% extends "base.twig" %}
26576
26853
  {% block title %}${name} Detail{% endblock %}
@@ -26600,7 +26877,7 @@ function generateAuth(_flags) {
26600
26877
  ensureDir(loginDir);
26601
26878
  ensureDir(meDir);
26602
26879
  writeFileSafe(
26603
- join21(registerDir, "post.ts"),
26880
+ join22(registerDir, "post.ts"),
26604
26881
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26605
26882
  import { hashPassword } from "tina4-nodejs";
26606
26883
  import User from "../../../../models/User.js";
@@ -26632,7 +26909,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26632
26909
  `
26633
26910
  );
26634
26911
  writeFileSafe(
26635
- join21(loginDir, "post.ts"),
26912
+ join22(loginDir, "post.ts"),
26636
26913
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26637
26914
  import { checkPassword, getToken } from "tina4-nodejs";
26638
26915
  import User from "../../../../models/User.js";
@@ -26665,7 +26942,7 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26665
26942
  `
26666
26943
  );
26667
26944
  writeFileSafe(
26668
- join21(meDir, "get.ts"),
26945
+ join22(meDir, "get.ts"),
26669
26946
  `import type { Tina4Request, Tina4Response } from "tina4-nodejs";
26670
26947
  import { authenticateRequest } from "tina4-nodejs";
26671
26948
 
@@ -26684,11 +26961,15 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26684
26961
  const formsDir = resolve11("src/templates/forms");
26685
26962
  ensureDir(formsDir);
26686
26963
  writeFileSafe(
26687
- join21(formsDir, "login.twig"),
26964
+ join22(formsDir, "login.twig"),
26688
26965
  `{% extends "base.twig" %}
26689
26966
  {% block title %}Login{% endblock %}
26690
26967
  {% block content %}
26691
- <div class="container mt-4" style="max-width:400px">
26968
+ {# No inline style attribute: a nonce lets this <style> block run under the
26969
+ strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
26970
+ that returns this response's nonce (ADR-0088). #}
26971
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
26972
+ <div class="container mt-4 auth-form">
26692
26973
  <h1>Login</h1>
26693
26974
  <form method="post" action="/api/auth/login">
26694
26975
  {{ form_token() }}
@@ -26708,11 +26989,12 @@ export default async function (req: Tina4Request, res: Tina4Response) {
26708
26989
  `
26709
26990
  );
26710
26991
  writeFileSafe(
26711
- join21(formsDir, "register.twig"),
26992
+ join22(formsDir, "register.twig"),
26712
26993
  `{% extends "base.twig" %}
26713
26994
  {% block title %}Register{% endblock %}
26714
26995
  {% block content %}
26715
- <div class="container mt-4" style="max-width:400px">
26996
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
26997
+ <div class="container mt-4 auth-form">
26716
26998
  <h1>Register</h1>
26717
26999
  <form method="post" action="/api/auth/register">
26718
27000
  {{ form_token() }}
@@ -26746,7 +27028,7 @@ function generateService(name, flags) {
26746
27028
  const cron = flags.cron;
26747
27029
  const dir = resolve11("src/services");
26748
27030
  ensureDir(dir);
26749
- const path10 = join21(dir, `${snake}.ts`);
27031
+ const path10 = join22(dir, `${snake}.ts`);
26750
27032
  let scheduleField;
26751
27033
  let note;
26752
27034
  if (cron && cron !== true) {
@@ -26798,7 +27080,7 @@ function generateQueue(name, _flags) {
26798
27080
  const pascal = toPascal(topic) || "Topic";
26799
27081
  const dir = resolve11("src/services");
26800
27082
  ensureDir(dir);
26801
- const path10 = join21(dir, `${slug}_consumer.ts`);
27083
+ const path10 = join22(dir, `${slug}_consumer.ts`);
26802
27084
  const body = aiFill(`handle${pascal}`, {
26803
27085
  intent: `process ONE ${topic} job payload`,
26804
27086
  given: "payload -> the produced job data (job.payload)",
@@ -26859,7 +27141,7 @@ export default {
26859
27141
  function generateValidator(name, _flags) {
26860
27142
  const dir = resolve11("src/validators");
26861
27143
  ensureDir(dir);
26862
- const path10 = join21(dir, `${toSnake(name)}.ts`);
27144
+ const path10 = join22(dir, `${toSnake(name)}.ts`);
26863
27145
  const rules = extend(
26864
27146
  "add / adjust the validation rules for this payload",
26865
27147
  `e.g. .email("email").minLength("name", 2).integer("age"); ground: tina4_context("validate request body with Validator", "nodejs")`
@@ -26887,7 +27169,7 @@ function generateSeeder(name, flags) {
26887
27169
  const table2 = resolveTable(name, flags, { announce: false });
26888
27170
  const dir = resolve11("src/seeds");
26889
27171
  ensureDir(dir);
26890
- const path10 = join21(dir, `${table2}_seeder.ts`);
27172
+ const path10 = join22(dir, `${table2}_seeder.ts`);
26891
27173
  const overrides = extend(
26892
27174
  "override fields that need a specific shape (seedOrm auto-fills the rest)",
26893
27175
  `e.g. return { email: (f) => f.email(), status: "active" }; ground: tina4_context("seed ORM model with FakeData", "nodejs")`
@@ -26932,7 +27214,7 @@ function generateWebsocket(name, _flags) {
26932
27214
  const handlerName = `${toCamel(toPascal(base))}Ws`;
26933
27215
  const dir = resolve11("src/routes");
26934
27216
  ensureDir(dir);
26935
- const path10 = join21(dir, `ws_${base}.ts`);
27217
+ const path10 = join22(dir, `ws_${base}.ts`);
26936
27218
  const body = aiFill(handlerName, {
26937
27219
  intent: `handle an inbound "message" frame on ${wsPath}`,
26938
27220
  given: "data -> the message payload (string); connection -> WebSocketConnection",
@@ -26983,7 +27265,7 @@ function generateListener(name, _flags) {
26983
27265
  const handlerName = `on${toPascal(slug)}`;
26984
27266
  const dir = resolve11("src/listeners");
26985
27267
  ensureDir(dir);
26986
- const path10 = join21(dir, `${slug}.ts`);
27268
+ const path10 = join22(dir, `${slug}.ts`);
26987
27269
  const body = aiFill(handlerName, {
26988
27270
  intent: `react to the '${event}' event`,
26989
27271
  given: `args -> whatever Events.emit("${event}", ...args) passed`,
@@ -27015,7 +27297,7 @@ Events.on("${event}", ${handlerName});
27015
27297
  function writeTest(testName, content) {
27016
27298
  const dir = resolve11("tests");
27017
27299
  ensureDir(dir);
27018
- writeFileSafe(join21(dir, `${testName}.test.ts`), content);
27300
+ writeFileSafe(join22(dir, `${testName}.test.ts`), content);
27019
27301
  }
27020
27302
  function standaloneTest(doc, body) {
27021
27303
  return `${doc}
@@ -27401,6 +27683,7 @@ var FIELD_TYPE_MAP, SQL_RESERVED_TABLE_NAMES, RESOLUTION_ENVELOPE_VERSION, __res
27401
27683
  var init_generate = __esm({
27402
27684
  "src/commands/generate.ts"() {
27403
27685
  "use strict";
27686
+ init_util();
27404
27687
  FIELD_TYPE_MAP = {
27405
27688
  string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
27406
27689
  str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
@@ -27648,7 +27931,7 @@ import * as path3 from "node:path";
27648
27931
  import * as os from "node:os";
27649
27932
  import { spawnSync as spawnSync2 } from "node:child_process";
27650
27933
  import { createRequire as createRequire8 } from "node:module";
27651
- import { randomBytes as randomBytes6 } from "node:crypto";
27934
+ import { randomBytes as randomBytes7 } from "node:crypto";
27652
27935
  function reqSibling(pkg) {
27653
27936
  return req(`../../${pkg}/src/index.ts`);
27654
27937
  }
@@ -28970,7 +29253,7 @@ var init_mcp = __esm({
28970
29253
  // ── Session lifecycle + protocol negotiation ──────────────────
28971
29254
  /** Mint a new session id and remember it. Called on `initialize`. */
28972
29255
  openSession() {
28973
- const sid = randomBytes6(16).toString("hex");
29256
+ const sid = randomBytes7(16).toString("hex");
28974
29257
  this._sessions.set(sid, Date.now());
28975
29258
  return sid;
28976
29259
  }
@@ -30459,7 +30742,7 @@ __export(context_exports, {
30459
30742
  });
30460
30743
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
30461
30744
  import { existsSync as existsSync18, mkdirSync as mkdirSync15, readFileSync as readFileSync15, readdirSync as readdirSync10, realpathSync as realpathSync5 } from "node:fs";
30462
- import { basename as basename5, dirname as dirname11, extname as extname5, isAbsolute as isAbsolute6, join as join23, relative as relative4, resolve as resolve13 } from "node:path";
30745
+ import { basename as basename5, dirname as dirname11, extname as extname5, isAbsolute as isAbsolute6, join as join24, relative as relative4, resolve as resolve13 } from "node:path";
30463
30746
  function fts5Supported() {
30464
30747
  try {
30465
30748
  const conn = new DatabaseSync4(":memory:");
@@ -30482,13 +30765,13 @@ function realResolve(abs) {
30482
30765
  } catch {
30483
30766
  }
30484
30767
  try {
30485
- return join23(realpathSync5(dirname11(abs)), basename5(abs));
30768
+ return join24(realpathSync5(dirname11(abs)), basename5(abs));
30486
30769
  } catch {
30487
30770
  return abs;
30488
30771
  }
30489
30772
  }
30490
30773
  function dbKey(db) {
30491
- return resolve13(db ? String(db) : join23(process.cwd(), ".tina4", "context.db"));
30774
+ return resolve13(db ? String(db) : join24(process.cwd(), ".tina4", "context.db"));
30492
30775
  }
30493
30776
  function defaultContext(root, db) {
30494
30777
  const key = dbKey(db);
@@ -30661,11 +30944,11 @@ var init_context = __esm({
30661
30944
  files.sort();
30662
30945
  for (const fn of files) {
30663
30946
  if (!_Context.eligible(fn)) continue;
30664
- const full = join23(dir, fn);
30947
+ const full = join24(dir, fn);
30665
30948
  const rel = relative4(rootAbs, full);
30666
30949
  total += this.indexPath(full, rel);
30667
30950
  }
30668
- for (const d of subdirs) walk2(join23(dir, d));
30951
+ for (const d of subdirs) walk2(join24(dir, d));
30669
30952
  };
30670
30953
  walk2(rootAbs);
30671
30954
  return total;
@@ -30681,7 +30964,7 @@ var init_context = __esm({
30681
30964
  reindexFile(changedPath) {
30682
30965
  if (!this.available || this.root === null) return -1;
30683
30966
  const raw = String(changedPath);
30684
- const abs = isAbsolute6(raw) ? raw : join23(process.cwd(), raw);
30967
+ const abs = isAbsolute6(raw) ? raw : join24(process.cwd(), raw);
30685
30968
  const resolved = realResolve(resolve13(abs));
30686
30969
  const rel = relative4(this.root, resolved);
30687
30970
  if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) {
@@ -31019,6 +31302,7 @@ __export(websocket_exports, {
31019
31302
  CLOSE_NORMAL: () => CLOSE_NORMAL,
31020
31303
  CLOSE_POLICY_VIOLATION: () => CLOSE_POLICY_VIOLATION,
31021
31304
  CLOSE_PROTOCOL_ERROR: () => CLOSE_PROTOCOL_ERROR,
31305
+ MessageAssembler: () => MessageAssembler,
31022
31306
  OP_BINARY: () => OP_BINARY,
31023
31307
  OP_CLOSE: () => OP_CLOSE,
31024
31308
  OP_CONTINUATION: () => OP_CONTINUATION,
@@ -31276,21 +31560,24 @@ function serveWebSocketRoute(req2, socket, head) {
31276
31560
  state.closed = true;
31277
31561
  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));
31278
31562
  };
31563
+ const assembler = new MessageAssembler();
31279
31564
  socket.on("data", (chunk) => {
31280
31565
  buffer = Buffer.concat([buffer, chunk]);
31281
31566
  while (buffer.length > 0) {
31282
31567
  const frame = parseFrame(buffer);
31283
31568
  if (!frame) break;
31284
31569
  buffer = buffer.subarray(frame.bytesConsumed);
31285
- switch (frame.opcode) {
31570
+ const whole = assembler.push(frame);
31571
+ if (!whole) continue;
31572
+ switch (whole.opcode) {
31286
31573
  case OP_TEXT: {
31287
- const text = frame.payload.toString("utf-8");
31574
+ const text = whole.payload.toString("utf-8");
31288
31575
  void Promise.resolve().then(() => conn._onMessage ? conn._onMessage(text) : handler(conn, "message", text)).catch((e) => Log.error(`WebSocket message handler error: ${e.message}`));
31289
31576
  break;
31290
31577
  }
31291
31578
  case OP_PING:
31292
31579
  try {
31293
- socket.write(buildFrame(OP_PONG, frame.payload));
31580
+ socket.write(buildFrame(OP_PONG, whole.payload));
31294
31581
  } catch {
31295
31582
  }
31296
31583
  break;
@@ -31309,7 +31596,7 @@ function serveWebSocketRoute(req2, socket, head) {
31309
31596
  socket.on("error", fireClose);
31310
31597
  return true;
31311
31598
  }
31312
- 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;
31599
+ 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;
31313
31600
  var init_websocket = __esm({
31314
31601
  "../core/src/websocket.ts"() {
31315
31602
  "use strict";
@@ -31328,6 +31615,30 @@ var init_websocket = __esm({
31328
31615
  CLOSE_GOING_AWAY = 1001;
31329
31616
  CLOSE_PROTOCOL_ERROR = 1002;
31330
31617
  CLOSE_POLICY_VIOLATION = 1008;
31618
+ MessageAssembler = class {
31619
+ parts = [];
31620
+ opcode = 0;
31621
+ push(frame) {
31622
+ if (frame.opcode >= 8) return { opcode: frame.opcode, payload: frame.payload };
31623
+ if (frame.opcode === OP_CONTINUATION) {
31624
+ if (!this.opcode) return null;
31625
+ this.parts.push(frame.payload);
31626
+ if (!frame.fin) return null;
31627
+ const whole = { opcode: this.opcode, payload: Buffer.concat(this.parts) };
31628
+ this.parts = [];
31629
+ this.opcode = 0;
31630
+ return whole;
31631
+ }
31632
+ if (frame.fin) {
31633
+ this.parts = [];
31634
+ this.opcode = 0;
31635
+ return { opcode: frame.opcode, payload: frame.payload };
31636
+ }
31637
+ this.opcode = frame.opcode;
31638
+ this.parts = [frame.payload];
31639
+ return null;
31640
+ }
31641
+ };
31331
31642
  WebSocketServer = class {
31332
31643
  port;
31333
31644
  server = null;
@@ -31770,6 +32081,15 @@ var init_websocket = __esm({
31770
32081
  this.emit("error", err, client);
31771
32082
  });
31772
32083
  }
32084
+ assemblers = /* @__PURE__ */ new WeakMap();
32085
+ assemblerFor(client) {
32086
+ let a = this.assemblers.get(client);
32087
+ if (!a) {
32088
+ a = new MessageAssembler();
32089
+ this.assemblers.set(client, a);
32090
+ }
32091
+ return a;
32092
+ }
31773
32093
  processBuffer(client, buffer, setBuffer) {
31774
32094
  let remaining = buffer;
31775
32095
  while (remaining.length > 0) {
@@ -31777,15 +32097,17 @@ var init_websocket = __esm({
31777
32097
  if (!frame) break;
31778
32098
  remaining = remaining.subarray(frame.bytesConsumed);
31779
32099
  client.lastActivity = Date.now();
31780
- switch (frame.opcode) {
32100
+ const whole = this.assemblerFor(client).push(frame);
32101
+ if (!whole) continue;
32102
+ switch (whole.opcode) {
31781
32103
  case OP_TEXT:
31782
- this.emit("message", client, frame.payload.toString("utf-8"));
32104
+ this.emit("message", client, whole.payload.toString("utf-8"));
31783
32105
  break;
31784
32106
  case OP_BINARY:
31785
- this.emit("message", client, frame.payload);
32107
+ this.emit("message", client, whole.payload);
31786
32108
  break;
31787
32109
  case OP_PING: {
31788
- const pongFrame = buildFrame(OP_PONG, frame.payload);
32110
+ const pongFrame = buildFrame(OP_PONG, whole.payload);
31789
32111
  try {
31790
32112
  client.socket.write(pongFrame);
31791
32113
  } catch {
@@ -32704,7 +33026,7 @@ var init_job = __esm({
32704
33026
 
32705
33027
  // ../core/src/queueBackends/liteBackend.ts
32706
33028
  import { mkdirSync as mkdirSync16, readdirSync as readdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync7, existsSync as existsSync19 } from "node:fs";
32707
- import { join as join24 } from "node:path";
33029
+ import { join as join25 } from "node:path";
32708
33030
  import { randomUUID as randomUUID6 } from "node:crypto";
32709
33031
  var LiteBackend;
32710
33032
  var init_liteBackend = __esm({
@@ -32728,22 +33050,22 @@ var init_liteBackend = __esm({
32728
33050
  this.visibilityTimeout = visibilityTimeout;
32729
33051
  }
32730
33052
  ensureDir(queue) {
32731
- const dir = join24(this.basePath, queue);
33053
+ const dir = join25(this.basePath, queue);
32732
33054
  mkdirSync16(dir, { recursive: true });
32733
33055
  return dir;
32734
33056
  }
32735
33057
  ensureFailedDir(queue) {
32736
- const dir = join24(this.basePath, queue, "failed");
33058
+ const dir = join25(this.basePath, queue, "failed");
32737
33059
  mkdirSync16(dir, { recursive: true });
32738
33060
  return dir;
32739
33061
  }
32740
33062
  ensureReservedDir(queue) {
32741
- const dir = join24(this.basePath, queue, "reserved");
33063
+ const dir = join25(this.basePath, queue, "reserved");
32742
33064
  mkdirSync16(dir, { recursive: true });
32743
33065
  return dir;
32744
33066
  }
32745
33067
  reservedPath(queue, jobId) {
32746
- return join24(this.ensureReservedDir(queue), `${jobId}.queue-data`);
33068
+ return join25(this.ensureReservedDir(queue), `${jobId}.queue-data`);
32747
33069
  }
32748
33070
  nowIso() {
32749
33071
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -32781,7 +33103,7 @@ var init_liteBackend = __esm({
32781
33103
  error: void 0
32782
33104
  };
32783
33105
  const prefix = this.nextPrefix();
32784
- writeFileSync12(join24(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
33106
+ writeFileSync12(join25(dir, `${prefix}_${id}.queue-data`), JSON.stringify(job, null, 2));
32785
33107
  return id;
32786
33108
  }
32787
33109
  /**
@@ -32800,7 +33122,7 @@ var init_liteBackend = __esm({
32800
33122
  }
32801
33123
  const candidates = [];
32802
33124
  for (const filename of filenames) {
32803
- const filePath = join24(dir, filename);
33125
+ const filePath = join25(dir, filename);
32804
33126
  let job;
32805
33127
  try {
32806
33128
  job = JSON.parse(readFileSync16(filePath, "utf-8"));
@@ -32864,7 +33186,7 @@ var init_liteBackend = __esm({
32864
33186
  return;
32865
33187
  }
32866
33188
  for (const filename of filenames) {
32867
- const filePath = join24(reservedDir, filename);
33189
+ const filePath = join25(reservedDir, filename);
32868
33190
  let record;
32869
33191
  try {
32870
33192
  record = JSON.parse(readFileSync16(filePath, "utf-8"));
@@ -32902,7 +33224,7 @@ var init_liteBackend = __esm({
32902
33224
  this.reclaimExpired(queue, bridge.getMaxRetries(), this.nowIso());
32903
33225
  const now = this.nowIso();
32904
33226
  for (const [filename, job] of this.availableCandidates(queue, now)) {
32905
- const filePath = join24(dir, filename);
33227
+ const filePath = join25(dir, filename);
32906
33228
  job.topic = queue;
32907
33229
  job.priority = job.priority ?? 0;
32908
33230
  this.writeReserved(queue, job);
@@ -32927,7 +33249,7 @@ var init_liteBackend = __esm({
32927
33249
  const results = [];
32928
33250
  for (const [filename, job] of this.availableCandidates(queue, now)) {
32929
33251
  if (results.length >= count) break;
32930
- const filePath = join24(dir, filename);
33252
+ const filePath = join25(dir, filename);
32931
33253
  job.topic = queue;
32932
33254
  job.priority = job.priority ?? 0;
32933
33255
  this.writeReserved(queue, job);
@@ -32980,7 +33302,7 @@ var init_liteBackend = __esm({
32980
33302
  let count = 0;
32981
33303
  for (const file of files) {
32982
33304
  try {
32983
- const job = JSON.parse(readFileSync16(join24(scanDir, file), "utf-8"));
33305
+ const job = JSON.parse(readFileSync16(join25(scanDir, file), "utf-8"));
32984
33306
  if (job.status === status2) count++;
32985
33307
  } catch {
32986
33308
  }
@@ -32993,28 +33315,28 @@ var init_liteBackend = __esm({
32993
33315
  try {
32994
33316
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
32995
33317
  for (const file of files) {
32996
- unlinkSync7(join24(dir, file));
33318
+ unlinkSync7(join25(dir, file));
32997
33319
  count++;
32998
33320
  }
32999
33321
  } catch {
33000
33322
  }
33001
- const failedDir = join24(dir, "failed");
33323
+ const failedDir = join25(dir, "failed");
33002
33324
  try {
33003
33325
  if (existsSync19(failedDir)) {
33004
33326
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
33005
33327
  for (const file of files) {
33006
- unlinkSync7(join24(failedDir, file));
33328
+ unlinkSync7(join25(failedDir, file));
33007
33329
  count++;
33008
33330
  }
33009
33331
  }
33010
33332
  } catch {
33011
33333
  }
33012
- const reservedDir = join24(dir, "reserved");
33334
+ const reservedDir = join25(dir, "reserved");
33013
33335
  try {
33014
33336
  if (existsSync19(reservedDir)) {
33015
33337
  const files = readdirSync11(reservedDir).filter((f) => f.endsWith(".queue-data"));
33016
33338
  for (const file of files) {
33017
- unlinkSync7(join24(reservedDir, file));
33339
+ unlinkSync7(join25(reservedDir, file));
33018
33340
  count++;
33019
33341
  }
33020
33342
  }
@@ -33037,7 +33359,7 @@ var init_liteBackend = __esm({
33037
33359
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data")).sort();
33038
33360
  for (const file of files) {
33039
33361
  try {
33040
- const job = JSON.parse(readFileSync16(join24(dir, file), "utf-8"));
33362
+ const job = JSON.parse(readFileSync16(join25(dir, file), "utf-8"));
33041
33363
  const attempts = job.attempts || 0;
33042
33364
  if (attempts > 0 && attempts < maxRetries) {
33043
33365
  results.push(job);
@@ -33060,8 +33382,8 @@ var init_liteBackend = __esm({
33060
33382
  try {
33061
33383
  const queues = readdirSync11(this.basePath);
33062
33384
  for (const q of queues) {
33063
- const failedDir = join24(this.basePath, q, "failed");
33064
- const filePath = join24(failedDir, `${jobId}.queue-data`);
33385
+ const failedDir = join25(this.basePath, q, "failed");
33386
+ const filePath = join25(failedDir, `${jobId}.queue-data`);
33065
33387
  if (existsSync19(filePath)) {
33066
33388
  const job = JSON.parse(readFileSync16(filePath, "utf-8"));
33067
33389
  job.status = "pending";
@@ -33070,8 +33392,8 @@ var init_liteBackend = __esm({
33070
33392
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
33071
33393
  job.delayUntil = delaySeconds ? new Date(Date.now() + delaySeconds * 1e3).toISOString() : null;
33072
33394
  const prefix = this.nextPrefix();
33073
- const queueDir = join24(this.basePath, q);
33074
- writeFileSync12(join24(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
33395
+ const queueDir = join25(this.basePath, q);
33396
+ writeFileSync12(join25(queueDir, `${prefix}_${jobId}.queue-data`), JSON.stringify(job, null, 2));
33075
33397
  unlinkSync7(filePath);
33076
33398
  return true;
33077
33399
  }
@@ -33087,7 +33409,7 @@ var init_liteBackend = __esm({
33087
33409
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
33088
33410
  for (const file of files) {
33089
33411
  try {
33090
- const job = JSON.parse(readFileSync16(join24(failedDir, file), "utf-8"));
33412
+ const job = JSON.parse(readFileSync16(join25(failedDir, file), "utf-8"));
33091
33413
  if ((job.attempts || 0) >= maxRetries) {
33092
33414
  job.status = "dead";
33093
33415
  results.push(job);
@@ -33108,7 +33430,7 @@ var init_liteBackend = __esm({
33108
33430
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
33109
33431
  for (const file of files) {
33110
33432
  try {
33111
- unlinkSync7(join24(failedDir, file));
33433
+ unlinkSync7(join25(failedDir, file));
33112
33434
  count++;
33113
33435
  } catch {
33114
33436
  }
@@ -33121,9 +33443,9 @@ var init_liteBackend = __esm({
33121
33443
  const files = readdirSync11(dir).filter((f) => f.endsWith(".queue-data"));
33122
33444
  for (const file of files) {
33123
33445
  try {
33124
- const job = JSON.parse(readFileSync16(join24(dir, file), "utf-8"));
33446
+ const job = JSON.parse(readFileSync16(join25(dir, file), "utf-8"));
33125
33447
  if (job.status === status2) {
33126
- unlinkSync7(join24(dir, file));
33448
+ unlinkSync7(join25(dir, file));
33127
33449
  count++;
33128
33450
  }
33129
33451
  } catch {
@@ -33147,7 +33469,7 @@ var init_liteBackend = __esm({
33147
33469
  const files = readdirSync11(failedDir).filter((f) => f.endsWith(".queue-data"));
33148
33470
  for (const file of files) {
33149
33471
  try {
33150
- const filePath = join24(failedDir, file);
33472
+ const filePath = join25(failedDir, file);
33151
33473
  const job = JSON.parse(readFileSync16(filePath, "utf-8"));
33152
33474
  if ((job.attempts || 0) >= maxRetries) {
33153
33475
  continue;
@@ -33157,7 +33479,7 @@ var init_liteBackend = __esm({
33157
33479
  job.createdAt = (/* @__PURE__ */ new Date()).toISOString();
33158
33480
  job.delayUntil = null;
33159
33481
  const prefix = this.nextPrefix();
33160
- writeFileSync12(join24(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
33482
+ writeFileSync12(join25(queueDir, `${prefix}_${job.id}.queue-data`), JSON.stringify(job, null, 2));
33161
33483
  unlinkSync7(filePath);
33162
33484
  count++;
33163
33485
  } catch {
@@ -33177,7 +33499,7 @@ var init_liteBackend = __esm({
33177
33499
  }
33178
33500
  for (const file of files) {
33179
33501
  if (!file.includes(id)) continue;
33180
- const filePath = join24(dir, file);
33502
+ const filePath = join25(dir, file);
33181
33503
  let job;
33182
33504
  try {
33183
33505
  job = JSON.parse(readFileSync16(filePath, "utf-8"));
@@ -33225,7 +33547,7 @@ var init_liteBackend = __esm({
33225
33547
  error: error2
33226
33548
  };
33227
33549
  const prefix = this.nextPrefix();
33228
- writeFileSync12(join24(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
33550
+ writeFileSync12(join25(dir, `${prefix}_${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
33229
33551
  }
33230
33552
  /**
33231
33553
  * Move the job to the dead-letter (failed/) directory. Terminal until a
@@ -33245,7 +33567,7 @@ var init_liteBackend = __esm({
33245
33567
  error: error2,
33246
33568
  failedAt: (/* @__PURE__ */ new Date()).toISOString()
33247
33569
  };
33248
- writeFileSync12(join24(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
33570
+ writeFileSync12(join25(failedDir, `${job.id}.queue-data`), JSON.stringify(jobData, null, 2));
33249
33571
  }
33250
33572
  /**
33251
33573
  * Record a failed attempt.
@@ -33295,7 +33617,7 @@ var init_liteBackend = __esm({
33295
33617
  retryJob(queue, job, delaySeconds) {
33296
33618
  this.clearReservation(queue, job.id);
33297
33619
  try {
33298
- unlinkSync7(join24(this.ensureFailedDir(queue), `${job.id}.queue-data`));
33620
+ unlinkSync7(join25(this.ensureFailedDir(queue), `${job.id}.queue-data`));
33299
33621
  } catch {
33300
33622
  }
33301
33623
  job.attempts = (job.attempts || 0) + 1;
@@ -35915,8 +36237,8 @@ ${end}
35915
36237
 
35916
36238
  // ../core/src/devAdmin.ts
35917
36239
  import { cpus as osCpus } from "node:os";
35918
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync23, readdirSync as readdirSync15, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, statSync as statSync17, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
35919
- import { join as join28, dirname as dirname13, resolve as resolve17, relative as relative9, sep as sep8, basename as basename6 } from "node:path";
36240
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync23, readdirSync as readdirSync15, mkdirSync as mkdirSync19, copyFileSync as copyFileSync2, statSync as statSync18, openSync as openSync4, closeSync as closeSync4, fstatSync as fstatSync2, fchmodSync as fchmodSync2, ftruncateSync, constants as constants3, realpathSync as realpathSync6, lstatSync } from "node:fs";
36241
+ import { join as join29, dirname as dirname13, resolve as resolve17, relative as relative9, sep as sep8, basename as basename6 } from "node:path";
35920
36242
  import { fileURLToPath as fileURLToPath6 } from "node:url";
35921
36243
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
35922
36244
  function escapeHtml(value) {
@@ -36060,7 +36382,7 @@ function readQueueDir(dir, topic, status2) {
36060
36382
  for (const filename of readdirSync15(dir).sort()) {
36061
36383
  if (!filename.endsWith(".queue-data")) continue;
36062
36384
  try {
36063
- jobs.push(mapQueueJob(JSON.parse(readFileSync20(join28(dir, filename), "utf-8")), topic, status2));
36385
+ jobs.push(mapQueueJob(JSON.parse(readFileSync20(join29(dir, filename), "utf-8")), topic, status2));
36064
36386
  } catch {
36065
36387
  }
36066
36388
  }
@@ -36163,21 +36485,12 @@ async function proxyToSupervisor(req2, res, downstreamPath) {
36163
36485
  return;
36164
36486
  }
36165
36487
  clearTimeout(timer);
36166
- const raw = await upstream.text();
36167
- const status2 = upstream.status || 200;
36168
- try {
36169
- res.json(JSON.parse(raw), status2);
36170
- } catch {
36171
- res.raw.writeHead(status2, {
36172
- "Content-Type": upstream.headers.get("content-type") ?? "text/plain; charset=utf-8"
36173
- });
36174
- res.raw.end(raw);
36175
- }
36488
+ await relayUpstreamResponse(res, upstream);
36176
36489
  }
36177
36490
  function resolveDevEnvVar(key) {
36178
36491
  const live = process.env[key];
36179
36492
  if (live !== void 0 && live !== "") return live;
36180
- const envPath = join28(process.cwd(), ".env");
36493
+ const envPath = join29(process.cwd(), ".env");
36181
36494
  if (!existsSync23(envPath)) return "";
36182
36495
  for (const line of readFileSync20(envPath, "utf-8").split("\n")) {
36183
36496
  const t = line.trim();
@@ -36188,7 +36501,7 @@ function resolveDevEnvVar(key) {
36188
36501
  return "";
36189
36502
  }
36190
36503
  function upsertDevEnvVar(key, value) {
36191
- const envPath = join28(process.cwd(), ".env");
36504
+ const envPath = join29(process.cwd(), ".env");
36192
36505
  const lines = existsSync23(envPath) ? readFileSync20(envPath, "utf-8").split("\n") : [];
36193
36506
  let found = false;
36194
36507
  const out = [];
@@ -36219,7 +36532,7 @@ function formatUptime(seconds) {
36219
36532
  return parts.join(" ");
36220
36533
  }
36221
36534
  function parseEnvFile() {
36222
- const envPath = join28(process.cwd(), ".env");
36535
+ const envPath = join29(process.cwd(), ".env");
36223
36536
  const result = {};
36224
36537
  if (!existsSync23(envPath)) return result;
36225
36538
  const lines = readFileSync20(envPath, "utf-8").split("\n");
@@ -36235,8 +36548,8 @@ function walkDirRecursive(dir) {
36235
36548
  const results = [];
36236
36549
  if (!existsSync23(dir)) return results;
36237
36550
  for (const entry of readdirSync15(dir)) {
36238
- const full = join28(dir, entry);
36239
- if (statSync17(full).isDirectory()) {
36551
+ const full = join29(dir, entry);
36552
+ if (statSync18(full).isDirectory()) {
36240
36553
  results.push(...walkDirRecursive(full));
36241
36554
  } else {
36242
36555
  results.push(full);
@@ -36253,7 +36566,7 @@ function handleGalleryDeploy(router) {
36253
36566
  return;
36254
36567
  }
36255
36568
  const galleryDir = resolve17(__devAdminDirname, "..", "gallery");
36256
- const gallerySrc = join28(galleryDir, name, "src");
36569
+ const gallerySrc = join29(galleryDir, name, "src");
36257
36570
  if (!existsSync23(gallerySrc)) {
36258
36571
  res.json({ error: `Gallery item '${name}' not found` }, 404);
36259
36572
  return;
@@ -36263,7 +36576,7 @@ function handleGalleryDeploy(router) {
36263
36576
  const allFiles = walkDirRecursive(gallerySrc);
36264
36577
  for (const srcFile of allFiles) {
36265
36578
  const rel = relative9(gallerySrc, srcFile);
36266
- const dest = join28(projectSrc, rel);
36579
+ const dest = join29(projectSrc, rel);
36267
36580
  mkdirSync19(dirname13(dest), { recursive: true });
36268
36581
  copyFileSync2(srcFile, dest);
36269
36582
  copied.push(rel);
@@ -36299,7 +36612,7 @@ function realPathOf(target) {
36299
36612
  tail.unshift(basename6(existing));
36300
36613
  existing = dirname13(existing);
36301
36614
  }
36302
- return join28(realpathSync6(existing), ...tail);
36615
+ return join29(realpathSync6(existing), ...tail);
36303
36616
  }
36304
36617
  function safeJoin(projectRoot3, rel, extraRoots = []) {
36305
36618
  try {
@@ -36614,6 +36927,7 @@ var init_devAdmin = __esm({
36614
36927
  "../core/src/devAdmin.ts"() {
36615
36928
  "use strict";
36616
36929
  init_devMailbox();
36930
+ init_upstreamResponse();
36617
36931
  init_dotenv();
36618
36932
  init_metrics();
36619
36933
  init_feedback();
@@ -36715,8 +37029,8 @@ var init_devAdmin = __esm({
36715
37029
  * Duplicate errors (same message) increment count and update last_seen.
36716
37030
  */
36717
37031
  static capture(errorType, message, traceback = "", file = "", line = 0) {
36718
- const fingerprint = `${errorType}|${message}|${file}|${line}`;
36719
- const existing = this.errors.find((e) => e.fingerprint === fingerprint);
37032
+ const fingerprint2 = `${errorType}|${message}|${file}|${line}`;
37033
+ const existing = this.errors.find((e) => e.fingerprint === fingerprint2);
36720
37034
  const now = (/* @__PURE__ */ new Date()).toISOString();
36721
37035
  if (existing) {
36722
37036
  existing.count = (existing.count || 1) + 1;
@@ -36729,7 +37043,7 @@ var init_devAdmin = __esm({
36729
37043
  message,
36730
37044
  stack: traceback || void 0,
36731
37045
  resolved: false,
36732
- ...{ fingerprint, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
37046
+ ...{ fingerprint: fingerprint2, error_type: errorType, file, line, count: 1, first_seen: now, last_seen: now }
36733
37047
  });
36734
37048
  if (this.errors.length > this.maxErrors) {
36735
37049
  this.errors = this.errors.slice(-this.maxErrors);
@@ -36981,7 +37295,7 @@ var init_devAdmin = __esm({
36981
37295
  const p = (url.searchParams.get("path") || "").toString();
36982
37296
  const scanRoot = metricsScanRoot();
36983
37297
  let target = p ? safeJoin(process.cwd(), p, [scanRoot]) : "";
36984
- if (target === null && scanRoot) target = safeJoin(process.cwd(), join28(scanRoot, p), [scanRoot]);
37298
+ if (target === null && scanRoot) target = safeJoin(process.cwd(), join29(scanRoot, p), [scanRoot]);
36985
37299
  if (target === null) {
36986
37300
  res.status(403).json({ error: "Path outside project" });
36987
37301
  return;
@@ -37296,16 +37610,16 @@ var init_devAdmin = __esm({
37296
37610
  failed: queue.size("failed"),
37297
37611
  reserved: queue.size("reserved")
37298
37612
  };
37299
- const topicDir = join28(queueBasePath2(), topic);
37613
+ const topicDir = join29(queueBasePath2(), topic);
37300
37614
  const jobs = [];
37301
37615
  if (!statusFilter || statusFilter === "pending") {
37302
37616
  jobs.push(...readQueueDir(topicDir, topic, "pending"));
37303
37617
  }
37304
37618
  if (!statusFilter || statusFilter === "reserved") {
37305
- jobs.push(...readQueueDir(join28(topicDir, "reserved"), topic, "reserved"));
37619
+ jobs.push(...readQueueDir(join29(topicDir, "reserved"), topic, "reserved"));
37306
37620
  }
37307
37621
  if (!statusFilter || statusFilter === "failed" || statusFilter === "dead") {
37308
- jobs.push(...readQueueDir(join28(topicDir, "failed"), topic, "dead_letter"));
37622
+ jobs.push(...readQueueDir(join29(topicDir, "failed"), topic, "dead_letter"));
37309
37623
  }
37310
37624
  res.json({ stats, jobs });
37311
37625
  } catch (e) {
@@ -37324,7 +37638,7 @@ var init_devAdmin = __esm({
37324
37638
  if (existsSync23(queueDir)) {
37325
37639
  topics = readdirSync15(queueDir).filter((d) => {
37326
37640
  try {
37327
- return statSync17(join28(queueDir, d)).isDirectory();
37641
+ return statSync18(join29(queueDir, d)).isDirectory();
37328
37642
  } catch {
37329
37643
  return false;
37330
37644
  }
@@ -37782,7 +38096,7 @@ var init_devAdmin = __esm({
37782
38096
  return;
37783
38097
  }
37784
38098
  try {
37785
- const envPath = join28(process.cwd(), ".env");
38099
+ const envPath = join29(process.cwd(), ".env");
37786
38100
  const fd = openSync4(envPath, constants3.O_RDWR | constants3.O_APPEND | constants3.O_CREAT | (constants3.O_NOFOLLOW ?? 0) | (constants3.O_NONBLOCK ?? 0), 384);
37787
38101
  try {
37788
38102
  const info = fstatSync2(fd);
@@ -37834,13 +38148,13 @@ var init_devAdmin = __esm({
37834
38148
  if (existsSync23(galleryDir)) {
37835
38149
  const entries = readdirSync15(galleryDir).sort();
37836
38150
  for (const entry of entries) {
37837
- const entryPath = join28(galleryDir, entry);
37838
- const metaFile = join28(entryPath, "meta.json");
37839
- if (statSync17(entryPath).isDirectory() && existsSync23(metaFile)) {
38151
+ const entryPath = join29(galleryDir, entry);
38152
+ const metaFile = join29(entryPath, "meta.json");
38153
+ if (statSync18(entryPath).isDirectory() && existsSync23(metaFile)) {
37840
38154
  try {
37841
38155
  const meta = JSON.parse(readFileSync20(metaFile, "utf-8"));
37842
38156
  meta.id = entry;
37843
- const srcDir = join28(entryPath, "src");
38157
+ const srcDir = join29(entryPath, "src");
37844
38158
  if (existsSync23(srcDir)) {
37845
38159
  const allFiles = walkDirRecursive(srcDir);
37846
38160
  meta.files = allFiles.map((f) => relative9(srcDir, f));
@@ -37848,7 +38162,7 @@ var init_devAdmin = __esm({
37848
38162
  const projectSrc = resolve17(process.cwd(), "src");
37849
38163
  if (existsSync23(srcDir) && meta.files) {
37850
38164
  meta.deployed = meta.files.every(
37851
- (f) => existsSync23(join28(projectSrc, f))
38165
+ (f) => existsSync23(join29(projectSrc, f))
37852
38166
  );
37853
38167
  } else {
37854
38168
  meta.deployed = false;
@@ -37925,7 +38239,7 @@ var init_devAdmin = __esm({
37925
38239
  const root = resolve17(process.cwd());
37926
38240
  const target = safeJoin(root, rel);
37927
38241
  const { branch, gitRoot, status: gitStatus } = await devGitInfo(root);
37928
- if (!target || !existsSync23(target) || !statSync17(target).isDirectory()) {
38242
+ if (!target || !existsSync23(target) || !statSync18(target).isDirectory()) {
37929
38243
  res.json({ path: rel, branch, entries: [], error: "not a directory" });
37930
38244
  return;
37931
38245
  }
@@ -37938,13 +38252,13 @@ var init_devAdmin = __esm({
37938
38252
  const entries = [];
37939
38253
  for (const name of readdirSync15(target).sort()) {
37940
38254
  if (devFilesHidden(name)) continue;
37941
- const full = join28(target, name);
38255
+ const full = join29(target, name);
37942
38256
  const entryRel = relative9(root, full).replace(/\\/g, "/");
37943
38257
  if (isSecretPath(entryRel)) continue;
37944
38258
  let isDir = false;
37945
38259
  let size = null;
37946
38260
  try {
37947
- const st = statSync17(full);
38261
+ const st = statSync18(full);
37948
38262
  isDir = st.isDirectory();
37949
38263
  if (!isDir) size = st.size;
37950
38264
  } catch {
@@ -38489,13 +38803,13 @@ var init_devAdmin = __esm({
38489
38803
  };
38490
38804
  handleDevAdminJs = async (_req, res) => {
38491
38805
  const { readFileSync: readFileSync30, existsSync: existsSync37 } = await import("node:fs");
38492
- const { dirname: dirname18, join: join40, resolve: resolve29 } = await import("node:path");
38806
+ const { dirname: dirname18, join: join41, resolve: resolve29 } = await import("node:path");
38493
38807
  const { fileURLToPath: fileURLToPath10 } = await import("node:url");
38494
38808
  const dir = dirname18(fileURLToPath10(import.meta.url));
38495
38809
  const candidates = [
38496
- join40(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
38810
+ join41(dir, "..", "public", "js", "tina4-dev-admin.min.js"),
38497
38811
  // src/../public/js/
38498
- join40(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
38812
+ join41(dir, "..", "..", "public", "js", "tina4-dev-admin.min.js"),
38499
38813
  // deeper nesting
38500
38814
  resolve29(process.cwd(), "node_modules", "tina4-nodejs", "packages", "core", "public", "js", "tina4-dev-admin.min.js"),
38501
38815
  resolve29(process.cwd(), "public", "js", "tina4-dev-admin.min.js")
@@ -38528,7 +38842,7 @@ var init_devAdmin = __esm({
38528
38842
 
38529
38843
  // ../core/src/i18n.ts
38530
38844
  import { readFileSync as readFileSync21, readdirSync as readdirSync16, existsSync as existsSync24 } from "node:fs";
38531
- import { join as join29, resolve as resolve18 } from "node:path";
38845
+ import { join as join30, resolve as resolve18 } from "node:path";
38532
38846
  var I18n;
38533
38847
  var init_i18n = __esm({
38534
38848
  "../core/src/i18n.ts"() {
@@ -38621,7 +38935,7 @@ var init_i18n = __esm({
38621
38935
  if (this._translations.has(locale)) {
38622
38936
  return;
38623
38937
  }
38624
- const filePath = join29(this._localeDir, `${locale}.json`);
38938
+ const filePath = join30(this._localeDir, `${locale}.json`);
38625
38939
  if (existsSync24(filePath)) {
38626
38940
  try {
38627
38941
  const raw = readFileSync21(filePath, "utf-8");
@@ -38634,7 +38948,7 @@ var init_i18n = __esm({
38634
38948
  }
38635
38949
  }
38636
38950
  for (const ext of [".yml", ".yaml"]) {
38637
- const yamlPath = join29(this._localeDir, `${locale}${ext}`);
38951
+ const yamlPath = join30(this._localeDir, `${locale}${ext}`);
38638
38952
  if (existsSync24(yamlPath)) {
38639
38953
  try {
38640
38954
  const raw = readFileSync21(yamlPath, "utf-8");
@@ -39408,7 +39722,7 @@ function swaggerEnabled() {
39408
39722
  }
39409
39723
  function createSwaggerRoutes(getSpec) {
39410
39724
  const serveUi = async (_req, res) => {
39411
- res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
39725
+ res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
39412
39726
  };
39413
39727
  return [
39414
39728
  {
@@ -39452,13 +39766,13 @@ var SWAGGER_UI_HTML;
39452
39766
  var init_ui = __esm({
39453
39767
  "../swagger/src/ui.ts"() {
39454
39768
  "use strict";
39455
- SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
39769
+ SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
39456
39770
  <html lang="en">
39457
39771
  <head>
39458
39772
  <meta charset="UTF-8">
39459
39773
  <title>Tina4 API Documentation</title>
39460
39774
  <link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
39461
- <style>
39775
+ <style nonce="${nonce}">
39462
39776
  body { margin: 0; background: #fafafa; }
39463
39777
  .topbar { display: none !important; }
39464
39778
  </style>
@@ -39466,7 +39780,7 @@ var init_ui = __esm({
39466
39780
  <body>
39467
39781
  <div id="swagger-ui"></div>
39468
39782
  <script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
39469
- <script>
39783
+ <script nonce="${nonce}">
39470
39784
  SwaggerUIBundle({
39471
39785
  url: "${specUrl}",
39472
39786
  dom_id: '#swagger-ui',
@@ -39506,7 +39820,7 @@ __export(sso_exports, {
39506
39820
  SsoError: () => SsoError,
39507
39821
  liveSessionIdentity: () => liveSessionIdentity
39508
39822
  });
39509
- import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39823
+ import { createHash as createHash9, randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
39510
39824
  function expiresAt(tokens) {
39511
39825
  const lifetime = Number(tokens.expires_in ?? 0);
39512
39826
  return lifetime > 0 ? Math.floor(Date.now() / 1e3) + lifetime : 0;
@@ -39634,9 +39948,9 @@ var init_sso = __esm({
39634
39948
  async login(requestOrSession, returnTo = "/") {
39635
39949
  const session = this.session(requestOrSession);
39636
39950
  if (!session) throw new SsoError("SSO login requires a Tina4 Session");
39637
- const state = randomBytes7(32).toString("base64url");
39638
- const nonce = randomBytes7(32).toString("base64url");
39639
- const verifier = randomBytes7(64).toString("base64url");
39951
+ const state = randomBytes8(32).toString("base64url");
39952
+ const nonce = randomBytes8(32).toString("base64url");
39953
+ const verifier = randomBytes8(64).toString("base64url");
39640
39954
  const challenge = createHash9("sha256").update(verifier).digest("base64url");
39641
39955
  session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
39642
39956
  const metadata = await this.discover();
@@ -39864,9 +40178,9 @@ var init_docsAutoDiscovery = __esm({
39864
40178
  });
39865
40179
 
39866
40180
  // ../core/src/server.ts
39867
- import { randomBytes as randomBytes8 } from "node:crypto";
39868
- import { resolve as resolve20, dirname as dirname14, join as join31, relative as relative10 } from "node:path";
39869
- import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync18 } from "node:fs";
40181
+ import { randomBytes as randomBytes9 } from "node:crypto";
40182
+ import { resolve as resolve20, dirname as dirname14, join as join32, relative as relative10 } from "node:path";
40183
+ import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync19 } from "node:fs";
39870
40184
  import { isatty } from "node:tty";
39871
40185
  import { fileURLToPath as fileURLToPath7 } from "node:url";
39872
40186
  import { execFileSync as execFileSync3, exec } from "node:child_process";
@@ -40044,7 +40358,8 @@ function isDevMode() {
40044
40358
  }
40045
40359
  async function renderErrorPage(code, data, templatesDir) {
40046
40360
  try {
40047
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40361
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40362
+ setCspNonceProvider2(currentCspNonce);
40048
40363
  const templateFile = `errors/${code}.twig`;
40049
40364
  const getCachedFrond = (dir) => {
40050
40365
  let instance = frondCache.get(dir);
@@ -40054,11 +40369,11 @@ async function renderErrorPage(code, data, templatesDir) {
40054
40369
  }
40055
40370
  return instance;
40056
40371
  };
40057
- const userTemplatePath = join31(templatesDir, templateFile);
40372
+ const userTemplatePath = join32(templatesDir, templateFile);
40058
40373
  if (existsSync26(userTemplatePath)) {
40059
40374
  return getCachedFrond(templatesDir).render(templateFile, data);
40060
40375
  }
40061
- const builtinTemplatePath = join31(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
40376
+ const builtinTemplatePath = join32(BUILTIN_ERROR_TEMPLATES_DIR, templateFile);
40062
40377
  if (existsSync26(builtinTemplatePath)) {
40063
40378
  return getCachedFrond(BUILTIN_ERROR_TEMPLATES_DIR).render(templateFile, data);
40064
40379
  }
@@ -40078,8 +40393,8 @@ function walkGalleryFiles(dir) {
40078
40393
  const results = [];
40079
40394
  if (!existsSync26(dir)) return results;
40080
40395
  for (const f of readdirSync17(dir)) {
40081
- const full = join31(dir, f);
40082
- if (statSync18(full).isDirectory()) results.push(...walkGalleryFiles(full));
40396
+ const full = join32(dir, f);
40397
+ if (statSync19(full).isDirectory()) results.push(...walkGalleryFiles(full));
40083
40398
  else results.push(full);
40084
40399
  }
40085
40400
  return results;
@@ -40091,14 +40406,14 @@ function getGalleryDeployedState() {
40091
40406
  try {
40092
40407
  const entries = readdirSync17(galleryDir).sort();
40093
40408
  for (const entry of entries) {
40094
- const entryPath = join31(galleryDir, entry);
40095
- const metaFile = join31(entryPath, "meta.json");
40096
- if (statSync18(entryPath).isDirectory() && existsSync26(metaFile)) {
40097
- const srcDir = join31(entryPath, "src");
40409
+ const entryPath = join32(galleryDir, entry);
40410
+ const metaFile = join32(entryPath, "meta.json");
40411
+ if (statSync19(entryPath).isDirectory() && existsSync26(metaFile)) {
40412
+ const srcDir = join32(entryPath, "src");
40098
40413
  if (existsSync26(srcDir)) {
40099
40414
  const files = walkGalleryFiles(srcDir);
40100
40415
  const projectSrc = resolve20(process.cwd(), "src");
40101
- state[entry] = files.every((f) => existsSync26(join31(projectSrc, relative10(srcDir, f))));
40416
+ state[entry] = files.every((f) => existsSync26(join32(projectSrc, relative10(srcDir, f))));
40102
40417
  } else {
40103
40418
  state[entry] = false;
40104
40419
  }
@@ -40160,6 +40475,7 @@ function resolveTemplate(pathname, templatesDir) {
40160
40475
  }
40161
40476
  function renderLandingPage(port = 7148) {
40162
40477
  const version = TINA4_VERSION;
40478
+ const nonce = currentCspNonce();
40163
40479
  const galleryItems = [
40164
40480
  { 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" },
40165
40481
  { id: "orm", icon: "&#128451;", name: "ORM", desc: "Product model with CRUD endpoints", accent: "accent-green", tryUrl: "/api/gallery/products" },
@@ -40174,7 +40490,7 @@ function renderLandingPage(port = 7148) {
40174
40490
  const isDeployed = deployed[item.id] === true;
40175
40491
  const tryBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-try" target="_blank">Try It</a>` : "";
40176
40492
  const viewBtn = isDeployed ? `<a href="${item.tryUrl}" class="gbtn gbtn-view" target="_blank">View</a>` : "";
40177
- const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" onclick="deployGallery('${item.id}')">Deploy</button>`;
40493
+ const deployBtn = isDeployed ? `<span class="gbtn gbtn-deployed">Deployed</span>` : `<button class="gbtn gbtn-deploy" data-deploy="${item.id}">Deploy</button>`;
40178
40494
  return `<div class="gallery-card">
40179
40495
  <div class="accent ${item.accent}"></div>
40180
40496
  <div class="icon">${item.icon}</div>
@@ -40189,7 +40505,7 @@ function renderLandingPage(port = 7148) {
40189
40505
  <meta charset="utf-8">
40190
40506
  <meta name="viewport" content="width=device-width, initial-scale=1">
40191
40507
  <title>Tina4NodeJs</title>
40192
- <style>
40508
+ <style nonce="${nonce}">
40193
40509
  *{margin:0;padding:0;box-sizing:border-box}
40194
40510
  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}
40195
40511
  .bg-watermark{position:fixed;bottom:-5%;right:-5%;width:45%;opacity:0.04;pointer-events:none;z-index:0}
@@ -40228,6 +40544,11 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
40228
40544
  .gbtn-deployed{background:transparent;border:1px solid #22c55e;color:#22c55e;cursor:default;font-size:0.7rem}
40229
40545
  @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)}}
40230
40546
  .star-wiggle{display:inline-block;transform-origin:center}
40547
+ .gallery-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem}
40548
+ .tok-comment{color:#64748b}
40549
+ .tok-kw{color:#c084fc}
40550
+ .tok-str{color:#4ade80}
40551
+ .tok-fn{color:#38bdf8}
40231
40552
  </style>
40232
40553
  </head>
40233
40554
  <body>
@@ -40253,23 +40574,23 @@ h1{font-size:3rem;font-weight:700;margin-bottom:0.25rem;letter-spacing:-1px}
40253
40574
  <div class="section">
40254
40575
  <div class="card">
40255
40576
  <h2>Getting Started</h2>
40256
- <pre class="code-block"><code><span style="color:#64748b">// app.ts</span>
40257
- <span style="color:#c084fc">import</span> { startServer, Router } <span style="color:#c084fc">from</span> <span style="color:#4ade80">"tina4-nodejs"</span>;
40577
+ <pre class="code-block"><code><span class="tok-comment">// app.ts</span>
40578
+ <span class="tok-kw">import</span> { startServer, Router } <span class="tok-kw">from</span> <span class="tok-str">"tina4-nodejs"</span>;
40258
40579
 
40259
- 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; {
40260
- <span style="color:#c084fc">return</span> res.json({ message: <span style="color:#4ade80">"Hello World!"</span> });
40580
+ 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; {
40581
+ <span class="tok-kw">return</span> res.json({ message: <span class="tok-str">"Hello World!"</span> });
40261
40582
  });
40262
40583
 
40263
- startServer({ port: 7148 }); <span style="color:#64748b">// starts on port 7148</span></code></pre>
40584
+ startServer({ port: 7148 }); <span class="tok-comment">// starts on port 7148</span></code></pre>
40264
40585
  </div>
40265
40586
  </div>
40266
40587
  <div class="gallery">
40267
40588
  <h2 id="gallery">What You Can Build</h2>
40268
- <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;">
40589
+ <div class="gallery-cards">
40269
40590
  ${galleryCards}
40270
40591
  </div>
40271
40592
  </div>
40272
- <script>
40593
+ <script nonce="${nonce}">
40273
40594
  function deployGallery(name) {
40274
40595
  if (!confirm('Deploy the "' + name + '" gallery example into your project? This will copy files into src/.')) return;
40275
40596
  fetch('/__dev/api/gallery/deploy', {
@@ -40304,6 +40625,12 @@ function deployGallery(name) {
40304
40625
  }
40305
40626
  setTimeout(doWiggle,3000);
40306
40627
  })();
40628
+ // CSP-clean wiring: no inline onclick handlers \u2014 bind every Deploy button from
40629
+ // its data-deploy attribute (a nonce covers script elements, not event-handler
40630
+ // attributes).
40631
+ document.querySelectorAll('.gbtn-deploy[data-deploy]').forEach(function(btn){
40632
+ btn.addEventListener('click', function(){ deployGallery(btn.dataset.deploy); });
40633
+ });
40307
40634
  </script>
40308
40635
  </body>
40309
40636
  </html>`;
@@ -40520,7 +40847,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
40520
40847
  }
40521
40848
  }
40522
40849
  }
40523
- const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
40850
+ const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
40524
40851
  if (wantsJson(req2)) {
40525
40852
  const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
40526
40853
  res.raw.writeHead(500, { "Content-Type": "application/json" });
@@ -40591,7 +40918,7 @@ function serveStaticAsset(ctx) {
40591
40918
  return false;
40592
40919
  }
40593
40920
  async function serveNotFound(ctx) {
40594
- const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
40921
+ const requestId = Log.getRequestId() ?? randomBytes9(4).toString("hex");
40595
40922
  if (wantsJson(ctx.req)) {
40596
40923
  const body = negotiatedErrorBody(404, "Not Found", requestId);
40597
40924
  ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
@@ -40614,7 +40941,8 @@ async function buildDispatchContext(router, base) {
40614
40941
  const templatesDir = resolve20(root, "src/templates");
40615
40942
  let frondEngine = null;
40616
40943
  try {
40617
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40944
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
40945
+ setCspNonceProvider2(currentCspNonce);
40618
40946
  frondEngine = new Frond2(templatesDir);
40619
40947
  } catch {
40620
40948
  }
@@ -40724,9 +41052,13 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
40724
41052
  }
40725
41053
  }
40726
41054
  async function runDispatch(ctx, rawReq, rawRes) {
40727
- const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
41055
+ const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes9(4).toString("hex");
40728
41056
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
40729
- return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
41057
+ const requestNonce = generateNonce();
41058
+ return Log.runWithRequestId(
41059
+ requestId,
41060
+ () => runWithCspNonce(requestNonce, () => dispatchInner(ctx, rawReq, rawRes, requestId))
41061
+ );
40730
41062
  }
40731
41063
  function loopbackBindHosts(host) {
40732
41064
  const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
@@ -40853,7 +41185,8 @@ async function startServer(config) {
40853
41185
  let frondEngine = null;
40854
41186
  setDefaultTemplatesDir(templatesDir);
40855
41187
  try {
40856
- const { Frond: Frond2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
41188
+ const { Frond: Frond2, setCspNonceProvider: setCspNonceProvider2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
41189
+ setCspNonceProvider2(currentCspNonce);
40857
41190
  frondEngine = new Frond2(templatesDir);
40858
41191
  router.addRoute({
40859
41192
  method: "GET",
@@ -40994,10 +41327,12 @@ async function startServer(config) {
40994
41327
  }
40995
41328
  });
40996
41329
  aiServer.on("error", (err) => {
40997
- if (err.code === "EADDRINUSE") {
41330
+ if (err?.code === "EADDRINUSE") {
40998
41331
  Log.warning(`Test port ${testPort} in use \u2014 skipping`);
40999
- aiServer = null;
41332
+ } else {
41333
+ Log.error(`Test port ${testPort} bind failed (${err?.code ?? err}) \u2014 skipping`);
41000
41334
  }
41335
+ aiServer = null;
41001
41336
  });
41002
41337
  aiServer.listen(testPort, host);
41003
41338
  }
@@ -41212,6 +41547,7 @@ var init_server = __esm({
41212
41547
  init_static();
41213
41548
  init_dotenv();
41214
41549
  init_errorOverlay();
41550
+ init_csp();
41215
41551
  init_health();
41216
41552
  init_rateLimiter();
41217
41553
  init_logger();
@@ -41461,7 +41797,7 @@ var init_mqttMessage = __esm({
41461
41797
  // ../core/src/mqtt.ts
41462
41798
  import net2 from "node:net";
41463
41799
  import tls from "node:tls";
41464
- import { randomBytes as randomBytes9 } from "node:crypto";
41800
+ import { randomBytes as randomBytes10 } from "node:crypto";
41465
41801
  import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
41466
41802
  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;
41467
41803
  var init_mqtt = __esm({
@@ -41549,7 +41885,7 @@ var init_mqtt = __esm({
41549
41885
  this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
41550
41886
  this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
41551
41887
  let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
41552
- if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
41888
+ if (cid === null || cid === "") cid = "tina4-" + randomBytes10(8).toString("hex");
41553
41889
  this.clientId = cid;
41554
41890
  this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
41555
41891
  this.cleanSession = options.cleanSession ?? true;
@@ -42147,8 +42483,8 @@ var init_mqtt = __esm({
42147
42483
  });
42148
42484
 
42149
42485
  // ../core/src/service.ts
42150
- import { readdirSync as readdirSync18, statSync as statSync19, watchFile, unwatchFile } from "node:fs";
42151
- import { join as join32, extname as extname8 } from "node:path";
42486
+ import { readdirSync as readdirSync18, statSync as statSync20, watchFile, unwatchFile } from "node:fs";
42487
+ import { join as join33, extname as extname8 } from "node:path";
42152
42488
  import { pathToFileURL } from "node:url";
42153
42489
  function matchCronField(field, value) {
42154
42490
  if (field === "*") return true;
@@ -42322,8 +42658,8 @@ var init_service = __esm({
42322
42658
  for (const entry of entries) {
42323
42659
  const ext = extname8(entry);
42324
42660
  if (ext !== ".ts" && ext !== ".js") continue;
42325
- const fullPath = join32(dir, entry);
42326
- const stat = statSync19(fullPath);
42661
+ const fullPath = join33(dir, entry);
42662
+ const stat = statSync20(fullPath);
42327
42663
  if (!stat.isFile()) continue;
42328
42664
  try {
42329
42665
  const fileUrl = pathToFileURL(fullPath).href;
@@ -42449,7 +42785,7 @@ var init_service = __esm({
42449
42785
  for (const entry of entries) {
42450
42786
  const ext = extname8(entry);
42451
42787
  if (ext !== ".ts" && ext !== ".js") continue;
42452
- const fullPath = join32(dir, entry);
42788
+ const fullPath = join33(dir, entry);
42453
42789
  if (watchedFiles.has(fullPath)) continue;
42454
42790
  watchedFiles.add(fullPath);
42455
42791
  watchFile(fullPath, { interval: 1e3 }, async () => {
@@ -42663,7 +42999,7 @@ var init_ssrf = __esm({
42663
42999
  import http from "node:http";
42664
43000
  import https from "node:https";
42665
43001
  import { URL as URL2 } from "node:url";
42666
- import { randomBytes as randomBytes10 } from "node:crypto";
43002
+ import { randomBytes as randomBytes11 } from "node:crypto";
42667
43003
  import { promises as fsp, createWriteStream } from "node:fs";
42668
43004
  import { basename as basename7 } from "node:path";
42669
43005
  import { pipeline } from "node:stream/promises";
@@ -42709,23 +43045,23 @@ function guessContentType(filename) {
42709
43045
  }
42710
43046
  function buildMultipartBody(boundary, fieldName, filename, fileContent, contentType, extraFields) {
42711
43047
  const crlf = "\r\n";
42712
- const delimiter5 = `--${boundary}`;
43048
+ const delimiter4 = `--${boundary}`;
42713
43049
  const parts = [];
42714
43050
  if (extraFields) {
42715
43051
  for (const [key, value] of Object.entries(extraFields)) {
42716
- parts.push(Buffer.from(delimiter5 + crlf, "utf-8"));
43052
+ parts.push(Buffer.from(delimiter4 + crlf, "utf-8"));
42717
43053
  parts.push(Buffer.from(`Content-Disposition: form-data; name="${key}"` + crlf + crlf, "utf-8"));
42718
43054
  parts.push(Buffer.from(String(value) + crlf, "utf-8"));
42719
43055
  }
42720
43056
  }
42721
- parts.push(Buffer.from(delimiter5 + crlf, "utf-8"));
43057
+ parts.push(Buffer.from(delimiter4 + crlf, "utf-8"));
42722
43058
  parts.push(
42723
43059
  Buffer.from(`Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"` + crlf, "utf-8")
42724
43060
  );
42725
43061
  parts.push(Buffer.from(`Content-Type: ${contentType}` + crlf + crlf, "utf-8"));
42726
43062
  parts.push(fileContent);
42727
43063
  parts.push(Buffer.from(crlf, "utf-8"));
42728
- parts.push(Buffer.from(delimiter5 + "--" + crlf, "utf-8"));
43064
+ parts.push(Buffer.from(delimiter4 + "--" + crlf, "utf-8"));
42729
43065
  return Buffer.concat(parts);
42730
43066
  }
42731
43067
  async function* parseLineStream(chunks) {
@@ -43055,7 +43391,7 @@ var init_api = __esm({
43055
43391
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
43056
43392
  }
43057
43393
  const partContentType = guessContentType(uploadName);
43058
- const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
43394
+ const boundary = "----Tina4Boundary" + randomBytes11(16).toString("hex");
43059
43395
  const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
43060
43396
  const contentType = `multipart/form-data; boundary=${boundary}`;
43061
43397
  return this.execute("POST", this.buildUrl(path10), bodyBuffer, contentType, headers);
@@ -45203,7 +45539,7 @@ __export(ai_exports, {
45203
45539
  });
45204
45540
  import { existsSync as existsSync28, mkdirSync as mkdirSync22, writeFileSync as writeFileSync19, readFileSync as readFileSync26 } from "node:fs";
45205
45541
  import { homedir } from "node:os";
45206
- import { join as join33, resolve as resolve21, relative as relative11, dirname as dirname15 } from "node:path";
45542
+ import { join as join34, resolve as resolve21, relative as relative11, dirname as dirname15 } from "node:path";
45207
45543
  import { fileURLToPath as fileURLToPath8 } from "node:url";
45208
45544
  import { execSync as execSync2, execFileSync as execFileSync4 } from "node:child_process";
45209
45545
  import { createInterface } from "node:readline";
@@ -45274,8 +45610,8 @@ function downloadSkillsSync(jobs) {
45274
45610
  function installSkills(root = ".", targets) {
45275
45611
  const ref = skillsRef();
45276
45612
  const dests = targets ?? [
45277
- join33(resolve21(root), ".claude", "skills"),
45278
- join33(homedir(), ".claude", "skills")
45613
+ join34(resolve21(root), ".claude", "skills"),
45614
+ join34(homedir(), ".claude", "skills")
45279
45615
  ];
45280
45616
  const jobs = [];
45281
45617
  const index = /* @__PURE__ */ new Map();
@@ -45293,9 +45629,9 @@ function installSkills(root = ".", targets) {
45293
45629
  const base = `https://raw.githubusercontent.com/tina4stack/${spec.repo}/${ref}/.claude/skills/${skill}`;
45294
45630
  skillMdUrl[skill] = `${base}/SKILL.md`;
45295
45631
  for (const dest of dests) {
45296
- add(`${base}/SKILL.md`, join33(dest, skill, "SKILL.md"));
45632
+ add(`${base}/SKILL.md`, join34(dest, skill, "SKILL.md"));
45297
45633
  for (const r of spec.references) {
45298
- add(`${base}/references/${r}`, join33(dest, skill, "references", r));
45634
+ add(`${base}/references/${r}`, join34(dest, skill, "references", r));
45299
45635
  }
45300
45636
  }
45301
45637
  }
@@ -45307,7 +45643,7 @@ function installSkills(root = ".", targets) {
45307
45643
  return installed;
45308
45644
  }
45309
45645
  function isInstalled(root, tool) {
45310
- return existsSync28(join33(resolve21(root), tool.contextFile));
45646
+ return existsSync28(join34(resolve21(root), tool.contextFile));
45311
45647
  }
45312
45648
  function showMenu(root = ".") {
45313
45649
  const r = resolve21(root);
@@ -45451,9 +45787,9 @@ function writeOrMerge(contextPath, contextFile, frameworkGuide) {
45451
45787
  }
45452
45788
  function installForTool(root, tool, context) {
45453
45789
  const created = [];
45454
- const contextPath = join33(root, tool.contextFile);
45790
+ const contextPath = join34(root, tool.contextFile);
45455
45791
  if (tool.configDir) {
45456
- mkdirSync22(join33(root, tool.configDir), { recursive: true });
45792
+ mkdirSync22(join34(root, tool.configDir), { recursive: true });
45457
45793
  }
45458
45794
  const parentDir = dirname15(contextPath);
45459
45795
  mkdirSync22(parentDir, { recursive: true });
@@ -45491,7 +45827,7 @@ function installTina4Ai() {
45491
45827
  function installClaudeSkills(root) {
45492
45828
  const created = [];
45493
45829
  for (const skill of installSkills(root)) {
45494
- created.push(join33(".claude", "skills", skill));
45830
+ created.push(join34(".claude", "skills", skill));
45495
45831
  console.log(` ${GREEN2}\u2713${RESET2} Installed .claude/skills/${skill} (project + global)`);
45496
45832
  }
45497
45833
  return created;
@@ -45834,7 +46170,7 @@ function generateClaudeCodeContext() {
45834
46170
  try {
45835
46171
  const thisDir = dirname15(fileURLToPath8(import.meta.url));
45836
46172
  const repoRoot = resolve21(thisDir, "..", "..", "..");
45837
- const claudeMdPath = join33(repoRoot, "CLAUDE.md");
46173
+ const claudeMdPath = join34(repoRoot, "CLAUDE.md");
45838
46174
  if (existsSync28(claudeMdPath)) {
45839
46175
  return readFileSync26(claudeMdPath, "utf-8");
45840
46176
  }
@@ -46037,7 +46373,7 @@ import {
46037
46373
  createHmac as createHmac4,
46038
46374
  createPrivateKey,
46039
46375
  createSign as createSign2,
46040
- randomBytes as randomBytes11
46376
+ randomBytes as randomBytes12
46041
46377
  } from "node:crypto";
46042
46378
  function encodeBase64Url(value) {
46043
46379
  return Buffer.from(value).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
@@ -46117,7 +46453,7 @@ function encryptPayload(payload, subscription) {
46117
46453
  ]);
46118
46454
  const prkKey = hmac(authSecret, sharedSecret);
46119
46455
  const ikm = hkdfExpand(prkKey, keyInfo, 32);
46120
- const salt = randomBytes11(16);
46456
+ const salt = randomBytes12(16);
46121
46457
  const contentPrk = hmac(salt, ikm);
46122
46458
  const cek = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: aes128gcm\0", "ascii"), 16);
46123
46459
  const nonce = hkdfExpand(contentPrk, Buffer.from("Content-Encoding: nonce\0", "ascii"), 12);
@@ -48874,6 +49210,7 @@ __export(src_exports3, {
48874
49210
  cacheStats: () => cacheStats,
48875
49211
  checkPassword: () => checkPassword,
48876
49212
  clearCache: () => clearCache,
49213
+ clearCurrentNonce: () => clearCurrentNonce,
48877
49214
  computeAcceptKey: () => computeAcceptKey,
48878
49215
  container: () => container,
48879
49216
  cors: () => cors,
@@ -48885,6 +49222,8 @@ __export(src_exports3, {
48885
49222
  createMessenger: () => createMessenger,
48886
49223
  createRequest: () => createRequest,
48887
49224
  createResponse: () => createResponse,
49225
+ cspNonce: () => cspNonce,
49226
+ currentCspNonce: () => currentCspNonce,
48888
49227
  decodeRequest: () => decodeRequest,
48889
49228
  defaultContext: () => defaultContext,
48890
49229
  defaultRouter: () => defaultRouter,
@@ -48910,6 +49249,7 @@ __export(src_exports3, {
48910
49249
  feedbackWhitelist: () => feedbackWhitelist,
48911
49250
  fts5Supported: () => fts5Supported,
48912
49251
  generateContext: () => generateContext,
49252
+ generateNonce: () => generateNonce,
48913
49253
  generateVapidKeys: () => generateVapidKeys,
48914
49254
  get: () => get,
48915
49255
  getDefaultDevServer: () => getDefaultDevServer,
@@ -48932,6 +49272,7 @@ __export(src_exports3, {
48932
49272
  httpReason: () => httpReason,
48933
49273
  inContainer: () => inContainer,
48934
49274
  injectFeedbackWidget: () => injectFeedbackWidget,
49275
+ injectNonceIntoCsp: () => injectNonceIntoCsp,
48935
49276
  installAll: () => installAll,
48936
49277
  installSelected: () => installSelected,
48937
49278
  isBannerSuppressed: () => isBannerSuppressed,
@@ -48986,6 +49327,7 @@ __export(src_exports3, {
48986
49327
  resetTrustedProxyCache: () => resetTrustedProxyCache,
48987
49328
  resolveAlgorithm: () => resolveAlgorithm,
48988
49329
  resolveClientIp: () => resolveClientIp,
49330
+ resolveCspHeader: () => resolveCspHeader,
48989
49331
  resolveFrameworkVersion: () => resolveFrameworkVersion,
48990
49332
  resolvePortAndHost: () => resolvePortAndHost,
48991
49333
  resolveStringMiddleware: () => resolveStringMiddleware,
@@ -48993,12 +49335,14 @@ __export(src_exports3, {
48993
49335
  responseCache: () => responseCache,
48994
49336
  runAll: () => runAll,
48995
49337
  runRouteMiddlewares: () => runRouteMiddlewares,
49338
+ runWithCspNonce: () => runWithCspNonce,
48996
49339
  runtimeDir: () => runtimeDir,
48997
49340
  saveUpload: () => saveUpload,
48998
49341
  schemaFromParams: () => schemaFromParams,
48999
49342
  selectablePids: () => selectablePids,
49000
49343
  serveWebSocketRoute: () => serveWebSocketRoute,
49001
49344
  sessionCookieName: () => sessionCookieName,
49345
+ setCurrentNonce: () => setCurrentNonce,
49002
49346
  setDefaultTemplatesDir: () => setDefaultTemplatesDir,
49003
49347
  setFrond: () => setFrond,
49004
49348
  showMenu: () => showMenu,
@@ -49065,6 +49409,7 @@ var init_src3 = __esm({
49065
49409
  init_wsdl();
49066
49410
  init_htmlElement();
49067
49411
  init_errorOverlay();
49412
+ init_csp();
49068
49413
  init_ai();
49069
49414
  init_sso();
49070
49415
  init_push();
@@ -49273,6 +49618,15 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
49273
49618
  <p>The Intelligent Native Application 4ramework</p>
49274
49619
  </body>
49275
49620
  </html>
49621
+ `
49622
+ );
49623
+ writeFileSync(
49624
+ join(targetDir, "public/style.css"),
49625
+ `body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
49626
+ h1 { font-size: 2.5em; margin-bottom: 0.2em; }
49627
+ p { color: #666; font-size: 1.1em; }
49628
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
49629
+ a { color: #2563eb; }
49276
49630
  `
49277
49631
  );
49278
49632
  writeFileSync(
@@ -49281,13 +49635,7 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
49281
49635
  <html>
49282
49636
  <head>
49283
49637
  <title>Tina4</title>
49284
- <style>
49285
- body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
49286
- h1 { font-size: 2.5em; margin-bottom: 0.2em; }
49287
- p { color: #666; font-size: 1.1em; }
49288
- code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
49289
- a { color: #2563eb; }
49290
- </style>
49638
+ <link rel="stylesheet" href="/style.css">
49291
49639
  </head>
49292
49640
  <body>
49293
49641
  <h1>tina4</h1>
@@ -49358,6 +49706,7 @@ async function serveProject(options) {
49358
49706
 
49359
49707
  // src/commands/migrate.ts
49360
49708
  init_dotenv();
49709
+ init_util();
49361
49710
  import { existsSync as existsSync30 } from "node:fs";
49362
49711
  import { resolve as resolve23 } from "node:path";
49363
49712
  async function runMigrations(migrationDir) {
@@ -49377,12 +49726,7 @@ async function runMigrations(migrationDir) {
49377
49726
  console.error(" Error: @tina4/orm is required to run migrations.");
49378
49727
  process.exit(1);
49379
49728
  }
49380
- try {
49381
- await initDatabase2();
49382
- } catch (err) {
49383
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
49384
- process.exit(1);
49385
- }
49729
+ await initDatabaseOrExit(initDatabase2);
49386
49730
  const result = await migrate2(void 0, { migrationsDir: dir });
49387
49731
  if (result.applied.length === 0 && result.failed.length === 0) {
49388
49732
  console.log(" Nothing to migrate \u2014 all migrations already applied.");
@@ -49421,6 +49765,7 @@ async function createMigration2(args = []) {
49421
49765
 
49422
49766
  // src/commands/migrateStatus.ts
49423
49767
  init_dotenv();
49768
+ init_util();
49424
49769
  import { resolve as resolve24 } from "node:path";
49425
49770
  async function migrateStatus(migrationDir) {
49426
49771
  loadEnv();
@@ -49437,12 +49782,7 @@ async function migrateStatus(migrationDir) {
49437
49782
  console.error(" Error: @tina4/orm is required to check migration status.");
49438
49783
  process.exit(1);
49439
49784
  }
49440
- try {
49441
- await initDatabase2();
49442
- } catch (err) {
49443
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
49444
- process.exit(1);
49445
- }
49785
+ await initDatabaseOrExit(initDatabase2);
49446
49786
  ensureMigrationTable2();
49447
49787
  const result = await statusFn(void 0, { migrationsDir: dir });
49448
49788
  console.log("");
@@ -49469,6 +49809,7 @@ async function migrateStatus(migrationDir) {
49469
49809
 
49470
49810
  // src/commands/migrateRollback.ts
49471
49811
  init_dotenv();
49812
+ init_util();
49472
49813
  import { resolve as resolve25 } from "node:path";
49473
49814
  async function migrateRollback(migrationDir) {
49474
49815
  loadEnv();
@@ -49487,12 +49828,7 @@ async function migrateRollback(migrationDir) {
49487
49828
  console.error(" Error: @tina4/orm is required to rollback migrations.");
49488
49829
  process.exit(1);
49489
49830
  }
49490
- try {
49491
- await initDatabase2();
49492
- } catch (err) {
49493
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
49494
- process.exit(1);
49495
- }
49831
+ await initDatabaseOrExit(initDatabase2);
49496
49832
  await ensureMigrationTable2();
49497
49833
  const lastBatch = await getLastBatchMigrations2();
49498
49834
  if (lastBatch.length === 0) {
@@ -49562,8 +49898,8 @@ async function listRoutes() {
49562
49898
  }
49563
49899
 
49564
49900
  // src/commands/test.ts
49565
- import { existsSync as existsSync32, readdirSync as readdirSync19, readFileSync as readFileSync27, statSync as statSync20 } from "node:fs";
49566
- import { resolve as resolve27, join as join34 } from "node:path";
49901
+ import { existsSync as existsSync32, readdirSync as readdirSync19, readFileSync as readFileSync27, statSync as statSync21 } from "node:fs";
49902
+ import { resolve as resolve27, join as join35 } from "node:path";
49567
49903
  import { pathToFileURL as pathToFileURL2 } from "node:url";
49568
49904
  import { execSync as execSync3 } from "node:child_process";
49569
49905
  function walkSource(dir) {
@@ -49575,10 +49911,10 @@ function walkSource(dir) {
49575
49911
  return out;
49576
49912
  }
49577
49913
  for (const name of entries) {
49578
- const full = join34(dir, name);
49914
+ const full = join35(dir, name);
49579
49915
  let st;
49580
49916
  try {
49581
- st = statSync20(full);
49917
+ st = statSync21(full);
49582
49918
  } catch {
49583
49919
  continue;
49584
49920
  }
@@ -49646,7 +49982,7 @@ async function runTests(testPath) {
49646
49982
  break;
49647
49983
  }
49648
49984
  try {
49649
- const files = readdirSync19(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join34(fullPath, f));
49985
+ const files = readdirSync19(fullPath).filter((f) => f.endsWith(".ts")).map((f) => join35(fullPath, f));
49650
49986
  testFiles.push(...files);
49651
49987
  } catch {
49652
49988
  }
@@ -49675,9 +50011,10 @@ async function runTests(testPath) {
49675
50011
  }
49676
50012
 
49677
50013
  // src/commands/lint.ts
49678
- import { existsSync as existsSync33, readdirSync as readdirSync20, readFileSync as readFileSync28, statSync as statSync21, writeFileSync as writeFileSync20 } from "node:fs";
50014
+ init_util();
50015
+ import { existsSync as existsSync33, readdirSync as readdirSync20, readFileSync as readFileSync28, statSync as statSync22, writeFileSync as writeFileSync20 } from "node:fs";
49679
50016
  import { createRequire as createRequire9 } from "node:module";
49680
- import { delimiter as delimiter2, dirname as dirname16, join as join35, relative as relative12 } from "node:path";
50017
+ import { dirname as dirname16, join as join36, relative as relative12 } from "node:path";
49681
50018
  import { spawnSync as spawnSync3 } from "node:child_process";
49682
50019
  var LINT_EXTENSIONS = [".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"];
49683
50020
  var JS_EXTENSIONS = [".js", ".mjs", ".cjs"];
@@ -49695,10 +50032,10 @@ function walkSource2(dir, out) {
49695
50032
  }
49696
50033
  for (const name of entries) {
49697
50034
  if (SKIP_DIRS3.has(name)) continue;
49698
- const full = join35(dir, name);
50035
+ const full = join36(dir, name);
49699
50036
  let st;
49700
50037
  try {
49701
- st = statSync21(full);
50038
+ st = statSync22(full);
49702
50039
  } catch {
49703
50040
  continue;
49704
50041
  }
@@ -49711,15 +50048,15 @@ function walkSource2(dir, out) {
49711
50048
  }
49712
50049
  function collectAppFiles(cwd) {
49713
50050
  const files = [];
49714
- const srcDir = join35(cwd, "src");
50051
+ const srcDir = join36(cwd, "src");
49715
50052
  try {
49716
- if (statSync21(srcDir).isDirectory()) walkSource2(srcDir, files);
50053
+ if (statSync22(srcDir).isDirectory()) walkSource2(srcDir, files);
49717
50054
  } catch {
49718
50055
  }
49719
50056
  for (const entry of ["app.ts", "app.mts", "app.js", "app.mjs"]) {
49720
- const p = join35(cwd, entry);
50057
+ const p = join36(cwd, entry);
49721
50058
  try {
49722
- if (statSync21(p).isFile()) files.push(p);
50059
+ if (statSync22(p).isFile()) files.push(p);
49723
50060
  } catch {
49724
50061
  }
49725
50062
  }
@@ -49740,15 +50077,15 @@ export default [js.configs.recommended, ...tseslint.configs.recommended];
49740
50077
  `;
49741
50078
  function findEslintConfig(cwd) {
49742
50079
  for (const name of ESLINT_FLAT_CONFIGS) {
49743
- const p = join35(cwd, name);
50080
+ const p = join36(cwd, name);
49744
50081
  if (existsSync33(p)) return p;
49745
50082
  }
49746
50083
  return null;
49747
50084
  }
49748
50085
  function resolvePackageBin(cwd, pkg, binRelative) {
49749
- const direct = join35(cwd, "node_modules", pkg, binRelative);
50086
+ const direct = join36(cwd, "node_modules", pkg, binRelative);
49750
50087
  if (existsSync33(direct)) return direct;
49751
- const require3 = createRequire9(join35(cwd, "package.json"));
50088
+ const require3 = createRequire9(join36(cwd, "package.json"));
49752
50089
  let entry;
49753
50090
  try {
49754
50091
  entry = require3.resolve(pkg);
@@ -49757,11 +50094,11 @@ function resolvePackageBin(cwd, pkg, binRelative) {
49757
50094
  }
49758
50095
  let dir = dirname16(entry);
49759
50096
  for (let i = 0; i < 8; i++) {
49760
- const pkgJson = join35(dir, "package.json");
50097
+ const pkgJson = join36(dir, "package.json");
49761
50098
  if (existsSync33(pkgJson)) {
49762
50099
  try {
49763
50100
  if (JSON.parse(readFileSync28(pkgJson, "utf-8")).name === pkg) {
49764
- const bin = join35(dir, binRelative);
50101
+ const bin = join36(dir, binRelative);
49765
50102
  return existsSync33(bin) ? bin : null;
49766
50103
  }
49767
50104
  } catch {
@@ -49777,22 +50114,12 @@ function resolveEslintBin(cwd) {
49777
50114
  return resolvePackageBin(cwd, "eslint", "bin/eslint.js");
49778
50115
  }
49779
50116
  function hasPackage(cwd, pkg) {
49780
- return existsSync33(join35(cwd, "node_modules", ...pkg.split("/"), "package.json"));
50117
+ return existsSync33(join36(cwd, "node_modules", ...pkg.split("/"), "package.json"));
49781
50118
  }
49782
50119
  function resolveNpm() {
49783
50120
  const windows = process.platform === "win32";
49784
50121
  const names = windows ? ["npm.cmd", "npm.exe", "npm"] : ["npm"];
49785
- for (const dir of (process.env.PATH ?? "").split(delimiter2)) {
49786
- if (!dir) continue;
49787
- for (const name of names) {
49788
- const candidate = join35(dir, name);
49789
- try {
49790
- if (statSync21(candidate).isFile()) return candidate;
49791
- } catch {
49792
- }
49793
- }
49794
- }
49795
- return null;
50122
+ return findOnPath(names);
49796
50123
  }
49797
50124
  function bootstrapEslint(cwd, noInstall) {
49798
50125
  let bin = resolveEslintBin(cwd);
@@ -49810,7 +50137,7 @@ function bootstrapEslint(cwd, noInstall) {
49810
50137
  else console.log(" \xB7 could not install eslint \u2014 using the zero-dependency baseline.");
49811
50138
  }
49812
50139
  if (bin && !config && hasPackage(cwd, "@eslint/js") && hasPackage(cwd, "typescript-eslint")) {
49813
- const scaffold = join35(cwd, ESLINT_SCAFFOLD_FILE);
50140
+ const scaffold = join36(cwd, ESLINT_SCAFFOLD_FILE);
49814
50141
  try {
49815
50142
  writeFileSync20(scaffold, ESLINT_SCAFFOLD, "utf-8");
49816
50143
  config = scaffold;
@@ -49879,7 +50206,7 @@ function runLint(args) {
49879
50206
  if (fix) {
49880
50207
  console.log(" \xB7 --fix needs eslint \u2014 the baseline check has no autofix.");
49881
50208
  }
49882
- if (existsSync33(join35(cwd, "tsconfig.json")) && runTypescript(cwd, files)) return;
50209
+ if (existsSync33(join36(cwd, "tsconfig.json")) && runTypescript(cwd, files)) return;
49883
50210
  runJavaScript(cwd, files);
49884
50211
  }
49885
50212
 
@@ -49888,7 +50215,7 @@ init_generate();
49888
50215
 
49889
50216
  // src/commands/seed.ts
49890
50217
  import { existsSync as existsSync34, readdirSync as readdirSync21 } from "node:fs";
49891
- import { resolve as resolve28, join as join36 } from "node:path";
50218
+ import { resolve as resolve28, join as join37 } from "node:path";
49892
50219
  import { execSync as execSync4 } from "node:child_process";
49893
50220
  async function runSeeds(seedPath) {
49894
50221
  const cwd = process.cwd();
@@ -49915,7 +50242,7 @@ async function runSeeds(seedPath) {
49915
50242
  }
49916
50243
  let seedFiles;
49917
50244
  try {
49918
- seedFiles = readdirSync21(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join36(seedDir, f));
50245
+ seedFiles = readdirSync21(seedDir).filter((f) => f.endsWith(".ts")).sort().map((f) => join37(seedDir, f));
49919
50246
  } catch {
49920
50247
  console.log(" Could not read src/seeds/ directory.");
49921
50248
  return;
@@ -49946,39 +50273,16 @@ async function runSeeds(seedPath) {
49946
50273
  // src/commands/queue.ts
49947
50274
  init_dotenv();
49948
50275
  init_queue();
49949
- import { readdirSync as readdirSync22, statSync as statSync22 } from "node:fs";
49950
- import { extname as extname9, join as join37 } from "node:path";
50276
+ init_util();
50277
+ import { readdirSync as readdirSync22, statSync as statSync23 } from "node:fs";
50278
+ import { extname as extname9, join as join38 } from "node:path";
49951
50279
  import { pathToFileURL as pathToFileURL3 } from "node:url";
49952
50280
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["once", "json"]);
49953
- function parseFlags(args) {
49954
- const flags = {};
49955
- const positional = [];
49956
- let i = 0;
49957
- while (i < args.length) {
49958
- const arg = args[i];
49959
- if (arg.startsWith("--")) {
49960
- const key = arg.slice(2);
49961
- if (BOOLEAN_FLAGS.has(key)) {
49962
- flags[key] = true;
49963
- i += 1;
49964
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
49965
- flags[key] = args[i + 1];
49966
- i += 2;
49967
- } else {
49968
- flags[key] = true;
49969
- i += 1;
49970
- }
49971
- } else {
49972
- positional.push(arg);
49973
- i += 1;
49974
- }
49975
- }
49976
- return { flags, positional };
49977
- }
50281
+ var parseFlags2 = (args) => parseFlags(args, BOOLEAN_FLAGS);
49978
50282
  async function resolveQueueHandler(servicesDir, topic) {
49979
50283
  let entries;
49980
50284
  try {
49981
- if (!statSync22(servicesDir).isDirectory()) return null;
50285
+ if (!statSync23(servicesDir).isDirectory()) return null;
49982
50286
  entries = readdirSync22(servicesDir);
49983
50287
  } catch {
49984
50288
  return null;
@@ -49987,9 +50291,9 @@ async function resolveQueueHandler(servicesDir, topic) {
49987
50291
  if (entry.startsWith("_")) continue;
49988
50292
  const ext = extname9(entry);
49989
50293
  if (ext !== ".ts" && ext !== ".js") continue;
49990
- const fullPath = join37(servicesDir, entry);
50294
+ const fullPath = join38(servicesDir, entry);
49991
50295
  try {
49992
- if (!statSync22(fullPath).isFile()) continue;
50296
+ if (!statSync23(fullPath).isFile()) continue;
49993
50297
  const mod = await import(pathToFileURL3(fullPath).href);
49994
50298
  const config = mod.default ?? mod;
49995
50299
  if (config && typeof config === "object" && config.topic === topic && typeof config.handle === "function") {
@@ -50006,7 +50310,7 @@ function firstJob(yielded) {
50006
50310
  }
50007
50311
  async function queueWork(args) {
50008
50312
  loadEnv();
50009
- const { flags, positional } = parseFlags(args);
50313
+ const { flags, positional } = parseFlags2(args);
50010
50314
  const topic = positional[0] ?? "default";
50011
50315
  const once2 = Boolean(flags.once);
50012
50316
  let pollSeconds;
@@ -50058,7 +50362,7 @@ async function queueWork(args) {
50058
50362
  }
50059
50363
  async function queueStats(args) {
50060
50364
  loadEnv();
50061
- const { flags, positional } = parseFlags(args);
50365
+ const { flags, positional } = parseFlags2(args);
50062
50366
  const topic = positional[0] ?? "default";
50063
50367
  const queue = new Queue({ topic });
50064
50368
  const stats = {
@@ -50089,7 +50393,7 @@ async function queueStats(args) {
50089
50393
  }
50090
50394
  async function queueRetry(args) {
50091
50395
  loadEnv();
50092
- const { positional } = parseFlags(args);
50396
+ const { positional } = parseFlags2(args);
50093
50397
  const topic = positional[0] ?? "default";
50094
50398
  const queue = new Queue({ topic });
50095
50399
  const dead = queue.deadLetters(0);
@@ -50105,7 +50409,7 @@ async function queueRetry(args) {
50105
50409
  }
50106
50410
  async function queueClear(args) {
50107
50411
  loadEnv();
50108
- const { positional } = parseFlags(args);
50412
+ const { positional } = parseFlags2(args);
50109
50413
  const status2 = positional[0] ?? "completed";
50110
50414
  const topic = positional[1] ?? "default";
50111
50415
  const queue = new Queue({ topic });
@@ -50139,28 +50443,12 @@ async function queueCommand(args = []) {
50139
50443
  }
50140
50444
 
50141
50445
  // src/commands/build.ts
50142
- import { accessSync as accessSync2, constants as constants4, existsSync as existsSync35, statSync as statSync23 } from "node:fs";
50143
- import { basename as basename9, delimiter as delimiter3, join as join38 } from "node:path";
50446
+ init_util();
50447
+ import { accessSync as accessSync2, constants as constants4, existsSync as existsSync35, statSync as statSync24 } from "node:fs";
50448
+ import { basename as basename9, delimiter as delimiter3, join as join39 } from "node:path";
50144
50449
  import { spawnSync as spawnSync4 } from "node:child_process";
50145
- function parseFlags2(args) {
50146
- const flags = {};
50147
- let i = 0;
50148
- while (i < args.length) {
50149
- const arg = args[i];
50150
- if (arg.startsWith("--")) {
50151
- const key = arg.slice(2);
50152
- if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
50153
- flags[key] = args[i + 1];
50154
- i += 2;
50155
- } else {
50156
- flags[key] = true;
50157
- i += 1;
50158
- }
50159
- } else {
50160
- i += 1;
50161
- }
50162
- }
50163
- return flags;
50450
+ function parseFlags3(args) {
50451
+ return parseFlags(args).flags;
50164
50452
  }
50165
50453
  function whichDocker() {
50166
50454
  const pathValue = process.env.PATH || process.env.Path || "";
@@ -50169,10 +50457,10 @@ function whichDocker() {
50169
50457
  for (const dir of pathValue.split(delimiter3)) {
50170
50458
  if (!dir) continue;
50171
50459
  for (const ext of exts) {
50172
- const candidate = join38(dir, `docker${ext}`);
50460
+ const candidate = join39(dir, `docker${ext}`);
50173
50461
  try {
50174
50462
  accessSync2(candidate, constants4.X_OK);
50175
- if (statSync23(candidate).isFile()) return candidate;
50463
+ if (statSync24(candidate).isFile()) return candidate;
50176
50464
  } catch {
50177
50465
  }
50178
50466
  }
@@ -50180,14 +50468,14 @@ function whichDocker() {
50180
50468
  return null;
50181
50469
  }
50182
50470
  function buildImage(args) {
50183
- const flags = parseFlags2(args);
50471
+ const flags = parseFlags3(args);
50184
50472
  let tag = typeof flags.tag === "string" ? flags.tag : "";
50185
50473
  if (!tag) {
50186
50474
  const dirName = basename9(process.cwd()).toLowerCase();
50187
50475
  tag = `${dirName || "tina4app"}:latest`;
50188
50476
  }
50189
50477
  const dockerfile = typeof flags.file === "string" && flags.file ? flags.file : "Dockerfile";
50190
- if (!existsSync35(dockerfile) || !statSync23(dockerfile).isFile()) {
50478
+ if (!existsSync35(dockerfile) || !statSync24(dockerfile).isFile()) {
50191
50479
  console.log(` \u2717 No ${dockerfile} found.`);
50192
50480
  console.log(" A Tina4 app deploys as a container. Scaffold a Dockerfile first:");
50193
50481
  console.log(" tina4 deploy docker (or: tina4nodejs init)");
@@ -50214,14 +50502,15 @@ function buildImage(args) {
50214
50502
  }
50215
50503
 
50216
50504
  // src/bin.ts
50505
+ init_util();
50217
50506
  import { spawnSync as spawnSync5 } from "node:child_process";
50218
- import { existsSync as existsSync36, readFileSync as readFileSync29, statSync as statSync24 } from "node:fs";
50219
- import { delimiter as delimiter4, dirname as dirname17, join as join39 } from "node:path";
50507
+ import { existsSync as existsSync36, readFileSync as readFileSync29 } from "node:fs";
50508
+ import { dirname as dirname17, join as join40 } from "node:path";
50220
50509
  import { fileURLToPath as fileURLToPath9, pathToFileURL as pathToFileURL4 } from "node:url";
50221
50510
  function readCliVersion() {
50222
50511
  let dir = dirname17(fileURLToPath9(import.meta.url));
50223
50512
  for (let i = 0; i < 6; i++) {
50224
- const pkgPath = join39(dir, "package.json");
50513
+ const pkgPath = join40(dir, "package.json");
50225
50514
  if (existsSync36(pkgPath)) {
50226
50515
  try {
50227
50516
  const pkg = JSON.parse(readFileSync29(pkgPath, "utf-8"));
@@ -50526,17 +50815,7 @@ var CLIENT_INSTALL_HINT = " Install it: curl -fsSL https://tina4.com/install.s
50526
50815
  function findClient() {
50527
50816
  const windows = process.platform === "win32";
50528
50817
  const names = windows ? [`${CLIENT_BINARY}.exe`, `${CLIENT_BINARY}.cmd`, `${CLIENT_BINARY}.bat`] : [CLIENT_BINARY];
50529
- for (const dir of (process.env.PATH ?? "").split(delimiter4)) {
50530
- if (!dir) continue;
50531
- for (const name of names) {
50532
- const candidate = join39(dir, name);
50533
- try {
50534
- if (statSync24(candidate).isFile()) return candidate;
50535
- } catch {
50536
- }
50537
- }
50538
- }
50539
- return null;
50818
+ return findOnPath(names);
50540
50819
  }
50541
50820
  function delegateToClient(command, args) {
50542
50821
  if (process.env[DELEGATION_GUARD_ENV] === command) {