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.
- package/CLAUDE.md +60 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20620 -18995
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20459 -18815
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +28 -12
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +31 -13
- package/packages/frond/src/engine.ts +39 -7
- package/packages/orm/dist/index.js +10879 -9258
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +10 -0
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- 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.
|
|
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.
|
|
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?.
|
|
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
|
-
|
|
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,
|
|
445
|
-
if (!expr.includes(
|
|
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 =
|
|
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(
|
|
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,
|
|
1105
|
-
split: (v,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2821
|
+
throw err;
|
|
2790
2822
|
}
|
|
2791
2823
|
|
|
2792
2824
|
const incContext = { ...context };
|