tina4-nodejs 3.13.97 → 3.13.99

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 (96) hide show
  1. package/CLAUDE.md +60 -25
  2. package/package.json +1 -2
  3. package/packages/cli/dist/bin.js +20620 -18995
  4. package/packages/cli/src/bin.ts +28 -71
  5. package/packages/cli/src/commands/migrate.ts +36 -75
  6. package/packages/cli/src/commands/migrateRollback.ts +10 -1
  7. package/packages/cli/src/commands/test.ts +92 -21
  8. package/packages/core/dist/index.js +20459 -18815
  9. package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
  10. package/packages/core/src/ai.ts +28 -12
  11. package/packages/core/src/api.ts +13 -5
  12. package/packages/core/src/background.ts +9 -3
  13. package/packages/core/src/devAdmin.ts +135 -20
  14. package/packages/core/src/dispatchPipeline.ts +185 -1
  15. package/packages/core/src/docs.ts +33 -5
  16. package/packages/core/src/env.ts +1 -1
  17. package/packages/core/src/errorOverlay.ts +39 -48
  18. package/packages/core/src/fakeData.ts +15 -0
  19. package/packages/core/src/index.ts +17 -6
  20. package/packages/core/src/logger.ts +892 -572
  21. package/packages/core/src/mcp.ts +9 -1
  22. package/packages/core/src/messenger.ts +31 -4
  23. package/packages/core/src/middleware.ts +169 -43
  24. package/packages/core/src/portTakeover.ts +232 -0
  25. package/packages/core/src/request.ts +57 -8
  26. package/packages/core/src/response.ts +67 -0
  27. package/packages/core/src/router.ts +35 -7
  28. package/packages/core/src/server.ts +450 -190
  29. package/packages/core/src/static.ts +81 -12
  30. package/packages/core/src/testClient.ts +126 -137
  31. package/packages/core/src/testing.ts +16 -12
  32. package/packages/core/src/types.ts +21 -9
  33. package/packages/core/src/version.ts +66 -0
  34. package/packages/core/src/websocket.ts +2 -2
  35. package/packages/core/src/websocketBackplane.ts +2 -2
  36. package/packages/frond/dist/index.js +31 -13
  37. package/packages/frond/src/engine.ts +39 -7
  38. package/packages/orm/dist/index.js +10879 -9258
  39. package/packages/orm/src/adapters/firebird.ts +200 -27
  40. package/packages/orm/src/adapters/mongodb.ts +160 -10
  41. package/packages/orm/src/adapters/mssql.ts +38 -11
  42. package/packages/orm/src/adapters/mysql.ts +24 -1
  43. package/packages/orm/src/adapters/odbc.ts +127 -29
  44. package/packages/orm/src/adapters/postgres.ts +18 -0
  45. package/packages/orm/src/adapters/sqlite.ts +93 -14
  46. package/packages/orm/src/autoCrud.ts +72 -8
  47. package/packages/orm/src/baseModel.ts +323 -71
  48. package/packages/orm/src/cachedDatabase.ts +48 -1
  49. package/packages/orm/src/database.ts +162 -59
  50. package/packages/orm/src/fakeData.ts +6 -2
  51. package/packages/orm/src/index.ts +4 -1
  52. package/packages/orm/src/migration.ts +95 -52
  53. package/packages/orm/src/query.ts +16 -4
  54. package/packages/orm/src/seeder.ts +43 -25
  55. package/packages/orm/src/sqlTranslator.ts +104 -19
  56. package/packages/orm/src/types.ts +97 -21
  57. package/packages/orm/src/validation.ts +5 -1
  58. package/packages/swagger/dist/index.js +3 -2
  59. package/packages/swagger/src/generator.ts +19 -4
  60. package/packages/swagger/src/ui.ts +6 -4
  61. package/types/cli/src/bin.d.ts +0 -22
  62. package/types/core/src/api.d.ts +11 -4
  63. package/types/core/src/background.d.ts +5 -2
  64. package/types/core/src/devAdmin.d.ts +35 -0
  65. package/types/core/src/dispatchPipeline.d.ts +41 -1
  66. package/types/core/src/errorOverlay.d.ts +13 -13
  67. package/types/core/src/index.d.ts +9 -6
  68. package/types/core/src/logger.d.ts +111 -185
  69. package/types/core/src/middleware.d.ts +40 -5
  70. package/types/core/src/portTakeover.d.ts +50 -0
  71. package/types/core/src/request.d.ts +15 -0
  72. package/types/core/src/response.d.ts +29 -0
  73. package/types/core/src/server.d.ts +92 -0
  74. package/types/core/src/testClient.d.ts +29 -3
  75. package/types/core/src/testing.d.ts +16 -12
  76. package/types/core/src/types.d.ts +21 -9
  77. package/types/core/src/version.d.ts +11 -0
  78. package/types/core/src/websocketBackplane.d.ts +1 -1
  79. package/types/frond/src/engine.d.ts +10 -0
  80. package/types/orm/src/adapters/firebird.d.ts +61 -2
  81. package/types/orm/src/adapters/mongodb.d.ts +20 -0
  82. package/types/orm/src/adapters/mssql.d.ts +11 -0
  83. package/types/orm/src/adapters/mysql.d.ts +11 -0
  84. package/types/orm/src/adapters/odbc.d.ts +35 -4
  85. package/types/orm/src/adapters/postgres.d.ts +11 -0
  86. package/types/orm/src/adapters/sqlite.d.ts +23 -4
  87. package/types/orm/src/baseModel.d.ts +45 -25
  88. package/types/orm/src/cachedDatabase.d.ts +27 -1
  89. package/types/orm/src/database.d.ts +56 -6
  90. package/types/orm/src/index.d.ts +3 -2
  91. package/types/orm/src/migration.d.ts +23 -5
  92. package/types/orm/src/query.d.ts +3 -0
  93. package/types/orm/src/seeder.d.ts +15 -2
  94. package/types/orm/src/sqlTranslator.d.ts +17 -4
  95. package/types/orm/src/types.d.ts +75 -16
  96. package/packages/core/src/errorOverlay.test.ts +0 -122
@@ -676,13 +676,13 @@ export class WebSocketServer {
676
676
  const backlog = client.socket.writableLength ?? 0;
677
677
  const maxBacklog = parseInt(process.env.TINA4_WS_MAX_BACKLOG ?? "1048576", 10);
678
678
  if (maxBacklog > 0 && backlog > maxBacklog) {
679
- Log.warn(`WebSocket client ${client.id} backlog ${backlog}B exceeds limit, dropping slow client`);
679
+ Log.warning(`WebSocket client ${client.id} backlog ${backlog}B exceeds limit, dropping slow client`);
680
680
  this.pruneClient(client);
681
681
  return false;
682
682
  }
683
683
  return true;
684
684
  } catch (err) {
685
- Log.warn(`WebSocket send to ${client.id} failed, pruning: ${(err as Error).message}`);
685
+ Log.warning(`WebSocket send to ${client.id} failed, pruning: ${(err as Error).message}`);
686
686
  this.pruneClient(client);
687
687
  return false;
688
688
  }
@@ -348,7 +348,7 @@ export class WsBackplaneManager {
348
348
  // publish() is async; we fire-and-forget but still catch a rejection so a
349
349
  // dead bus can't produce an unhandled rejection that crashes the worker.
350
350
  Promise.resolve(this.backplane.publish(this.channel, JSON.stringify(envelope))).catch(
351
- (err) => log?.warn(`WebSocket backplane publish failed: ${(err as Error).message}`),
351
+ (err) => log?.warning(`WebSocket backplane publish failed: ${(err as Error).message}`),
352
352
  );
353
353
  }
354
354
 
@@ -366,7 +366,7 @@ export class WsBackplaneManager {
366
366
  /** Minimal logger shape so the manager doesn't import the logger module. */
367
367
  export interface WsBackplaneLogger {
368
368
  info(message: string): void;
369
- warn(message: string): void;
369
+ warning(message: string): void;
370
370
  error(message: string): void;
371
371
  }
372
372
 
@@ -1,7 +1,7 @@
1
1
  // src/engine.ts
2
2
  import { createHash, createHmac, randomBytes } from "node:crypto";
3
- import { readFileSync, existsSync, statSync } from "node:fs";
4
- import { join, resolve } from "node:path";
3
+ import { readFileSync, existsSync, statSync, realpathSync } from "node:fs";
4
+ import { join, resolve, isAbsolute, sep } from "node:path";
5
5
  var SafeString = class {
6
6
  constructor(value) {
7
7
  this.value = value;
@@ -441,15 +441,15 @@ function findOutsideQuotes(expr, needle) {
441
441
  }
442
442
  return -1;
443
443
  }
444
- function splitOutsideQuotes(expr, sep) {
445
- if (!expr.includes(sep)) return [expr];
444
+ function splitOutsideQuotes(expr, sep2) {
445
+ if (!expr.includes(sep2)) return [expr];
446
446
  const parts = [];
447
447
  let currentStart = 0;
448
448
  let inQuote = null;
449
449
  let depth = 0;
450
450
  let bracketDepth = 0;
451
451
  let i = 0;
452
- const sepLen = sep.length;
452
+ const sepLen = sep2.length;
453
453
  const lastStart = expr.length - sepLen;
454
454
  while (i <= lastStart) {
455
455
  const ch = expr[i];
@@ -470,7 +470,7 @@ function splitOutsideQuotes(expr, sep) {
470
470
  else if (ch === ")") depth--;
471
471
  else if (ch === "[") bracketDepth++;
472
472
  else if (ch === "]") bracketDepth--;
473
- if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep, i)) {
473
+ if (depth === 0 && bracketDepth === 0 && expr.startsWith(sep2, i)) {
474
474
  parts.push(expr.slice(currentStart, i));
475
475
  i += sepLen;
476
476
  currentStart = i;
@@ -1101,8 +1101,8 @@ var BUILTIN_FILTERS = {
1101
1101
  },
1102
1102
  first: (v) => Array.isArray(v) ? v[0] ?? null : null,
1103
1103
  last: (v) => Array.isArray(v) ? v[v.length - 1] ?? null : null,
1104
- join: (v, sep) => Array.isArray(v) ? v.map(String).join(sep !== void 0 ? String(sep) : ", ") : String(v),
1105
- split: (v, sep) => String(v).split(sep !== void 0 ? String(sep) : " "),
1104
+ join: (v, sep2) => Array.isArray(v) ? v.map(String).join(sep2 !== void 0 ? String(sep2) : ", ") : String(v),
1105
+ split: (v, sep2) => String(v).split(sep2 !== void 0 ? String(sep2) : " "),
1106
1106
  replace: (v, from, to) => {
1107
1107
  const s = String(v);
1108
1108
  if (from !== void 0 && typeof from === "object" && from !== null && !Array.isArray(from)) {
@@ -1290,7 +1290,7 @@ function _b64url(data) {
1290
1290
  }
1291
1291
  var _formTokenSessionId = "";
1292
1292
  function _buildFormTokenJwt(descriptor = "") {
1293
- const secret = process.env.TINA4_SECRET || "tina4-default-secret";
1293
+ const secret = process.env.TINA4_SECRET ?? "";
1294
1294
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
1295
1295
  const header = { alg: "HS256", typ: "JWT" };
1296
1296
  const now = Math.floor(Date.now() / 1e3);
@@ -1537,12 +1537,30 @@ var Frond = class _Frond {
1537
1537
  renderDump(value) {
1538
1538
  return renderDump(value).toString();
1539
1539
  }
1540
+ /**
1541
+ * Load a template's source, CONFINED under the templates directory.
1542
+ *
1543
+ * Every path-taking tag ({% include %}, {% extends %}, {% import %},
1544
+ * {% from ... import %}) funnels through this one loader, so this single guard
1545
+ * confines them all (TAG-DEC-01): a name that is absolute, climbs out with a
1546
+ * `..` up-level segment, or resolves through a symlink to a location OUTSIDE
1547
+ * the templates root is REFUSED -- the outside file is never read. Template
1548
+ * -side analogue of the static-asset confinement (feature 41 / ADR-0050).
1549
+ */
1540
1550
  load(name) {
1551
+ if (isAbsolute(name) || name.split(/[\\/]/).includes("..")) {
1552
+ throw new Error(`Template path escapes the templates directory: ${name}`);
1553
+ }
1541
1554
  const filePath = join(this.templateDir, name);
1542
- if (!existsSync(filePath)) {
1555
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
1543
1556
  throw new Error(`Template not found: ${filePath}`);
1544
1557
  }
1545
- return readFileSync(filePath, "utf-8");
1558
+ const root = realpathSync(this.templateDir);
1559
+ const real = realpathSync(filePath);
1560
+ if (real !== root && !real.startsWith(root + sep)) {
1561
+ throw new Error(`Template path escapes the templates directory: ${name}`);
1562
+ }
1563
+ return readFileSync(real, "utf-8");
1546
1564
  }
1547
1565
  /** Execute pre-tokenized template against context. */
1548
1566
  executeCached(tokens, context) {
@@ -2309,9 +2327,9 @@ var Frond = class _Frond {
2309
2327
  let source;
2310
2328
  try {
2311
2329
  source = this.load(filename);
2312
- } catch {
2330
+ } catch (err) {
2313
2331
  if (ignoreMissing) return "";
2314
- throw new Error(`Template not found: ${join(this.templateDir, filename)}`);
2332
+ throw err;
2315
2333
  }
2316
2334
  const incContext = { ...context };
2317
2335
  if (withExpr) {
@@ -5,8 +5,8 @@
5
5
  * extends/block, include, macro, set, comments, whitespace control, tests.
6
6
  */
7
7
  import { createHash, createHmac, randomBytes } from "node:crypto";
8
- import { readFileSync, existsSync, statSync } from "node:fs";
9
- import { join, resolve } from "node:path";
8
+ import { readFileSync, existsSync, statSync, realpathSync } from "node:fs";
9
+ import { join, resolve, isAbsolute, sep } from "node:path";
10
10
 
11
11
  // ── Types ──────────────────────────────────────────────────────
12
12
 
@@ -1587,7 +1587,14 @@ export function setFormTokenSessionId(sessionId: string): void {
1587
1587
  }
1588
1588
 
1589
1589
  function _buildFormTokenJwt(descriptor: string = ""): string {
1590
- const secret = process.env.TINA4_SECRET || "tina4-default-secret";
1590
+ // Fail-closed, IDENTICAL to the validator (auth.ts validToken:
1591
+ // `secret ?? process.env.TINA4_SECRET ?? ""`). With TINA4_SECRET unset the
1592
+ // signing secret resolves to BLANK — there is NO built-in default. Signing
1593
+ // with the retired public 'tina4-default-secret' made the generator disagree
1594
+ // with the fail-closed validator: a legitimately-rendered token was rejected
1595
+ // AND a ''-forged token was accepted (CSRF-NODE-SECRET-SPLIT, feature 37).
1596
+ // The generator and the validator MUST resolve the same secret.
1597
+ const secret = process.env.TINA4_SECRET ?? "";
1591
1598
  const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
1592
1599
 
1593
1600
  const header = { alg: "HS256", typ: "JWT" };
@@ -1889,12 +1896,34 @@ export class Frond {
1889
1896
  return renderDump(value).toString();
1890
1897
  }
1891
1898
 
1899
+ /**
1900
+ * Load a template's source, CONFINED under the templates directory.
1901
+ *
1902
+ * Every path-taking tag ({% include %}, {% extends %}, {% import %},
1903
+ * {% from ... import %}) funnels through this one loader, so this single guard
1904
+ * confines them all (TAG-DEC-01): a name that is absolute, climbs out with a
1905
+ * `..` up-level segment, or resolves through a symlink to a location OUTSIDE
1906
+ * the templates root is REFUSED -- the outside file is never read. Template
1907
+ * -side analogue of the static-asset confinement (feature 41 / ADR-0050).
1908
+ */
1892
1909
  private load(name: string): string {
1910
+ // Lexical belt: refuse an absolute path or a `..` up-level segment before
1911
+ // touching the filesystem (defense in depth in front of the realpath check).
1912
+ if (isAbsolute(name) || name.split(/[\\/]/).includes("..")) {
1913
+ throw new Error(`Template path escapes the templates directory: ${name}`);
1914
+ }
1893
1915
  const filePath = join(this.templateDir, name);
1894
- if (!existsSync(filePath)) {
1916
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
1895
1917
  throw new Error(`Template not found: ${filePath}`);
1896
1918
  }
1897
- return readFileSync(filePath, "utf-8");
1919
+ // Realpath containment: a symlink INSIDE the templates dir whose target
1920
+ // resolves OUTSIDE it is refused (the lexical belt cannot see a symlink).
1921
+ const root = realpathSync(this.templateDir);
1922
+ const real = realpathSync(filePath);
1923
+ if (real !== root && !real.startsWith(root + sep)) {
1924
+ throw new Error(`Template path escapes the templates directory: ${name}`);
1925
+ }
1926
+ return readFileSync(real, "utf-8");
1898
1927
  }
1899
1928
 
1900
1929
  /** Execute pre-tokenized template against context. */
@@ -2784,9 +2813,12 @@ export class Frond {
2784
2813
  let source: string;
2785
2814
  try {
2786
2815
  source = this.load(filename);
2787
- } catch {
2816
+ } catch (err) {
2817
+ // A genuinely missing optional include is silenced; an escape/other error
2818
+ // is re-thrown with its own message (so a confinement refusal is loud, not
2819
+ // masked as "not found").
2788
2820
  if (ignoreMissing) return "";
2789
- throw new Error(`Template not found: ${join(this.templateDir, filename)}`);
2821
+ throw err;
2790
2822
  }
2791
2823
 
2792
2824
  const incContext = { ...context };