tina4-nodejs 3.13.137 → 3.13.139
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 +33 -3
- package/COMMERCIAL-LICENSE.md +8 -0
- package/LICENSE +373 -0
- package/NOTICE +11 -0
- package/README.md +24 -2
- package/package.json +44 -6
- package/packages/cli/dist/bin.js +19979 -18102
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/build.ts +8 -0
- package/packages/cli/src/commands/generate.ts +8 -0
- package/packages/cli/src/commands/init.ts +8 -0
- package/packages/cli/src/commands/lint.ts +8 -0
- package/packages/cli/src/commands/migrate.ts +8 -0
- package/packages/cli/src/commands/migrateCreate.ts +8 -0
- package/packages/cli/src/commands/migrateRollback.ts +8 -0
- package/packages/cli/src/commands/migrateStatus.ts +8 -0
- package/packages/cli/src/commands/queue.ts +8 -0
- package/packages/cli/src/commands/routes.ts +8 -0
- package/packages/cli/src/commands/seed.ts +8 -0
- package/packages/cli/src/commands/serve.ts +11 -0
- package/packages/cli/src/commands/test.ts +8 -0
- package/packages/core/dist/index.js +19976 -18097
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
- package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
- package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
- package/packages/core/public/bundled-component-provenance.json +48 -0
- package/packages/core/public/js/tina4js.min.js +13 -3
- package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
- package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
- package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
- package/packages/core/src/__feedback/widget.js +8 -0
- package/packages/core/src/_missing.ts +8 -0
- package/packages/core/src/ai.ts +8 -0
- package/packages/core/src/aiClient.ts +8 -0
- package/packages/core/src/api.ts +48 -7
- package/packages/core/src/auth.ts +94 -14
- package/packages/core/src/authGate.ts +35 -20
- package/packages/core/src/background.ts +8 -0
- package/packages/core/src/cache.ts +43 -4
- package/packages/core/src/constants.ts +8 -0
- package/packages/core/src/container.ts +8 -0
- package/packages/core/src/context/chunker.ts +8 -0
- package/packages/core/src/context/index.ts +8 -0
- package/packages/core/src/devAdmin.ts +185 -59
- package/packages/core/src/devMailbox.ts +8 -0
- package/packages/core/src/dispatchPipeline.ts +8 -0
- package/packages/core/src/docs.ts +8 -0
- package/packages/core/src/docsAutoDiscovery.ts +8 -0
- package/packages/core/src/docsParser.ts +8 -0
- package/packages/core/src/docsScanner.ts +44 -0
- package/packages/core/src/docsSignatures.ts +8 -0
- package/packages/core/src/dotenv.ts +8 -0
- package/packages/core/src/env.ts +8 -0
- package/packages/core/src/errorOverlay.ts +8 -0
- package/packages/core/src/events.ts +8 -0
- package/packages/core/src/fakeData.ts +8 -0
- package/packages/core/src/feedback.ts +8 -0
- package/packages/core/src/graphql.ts +108 -5
- package/packages/core/src/health.ts +8 -0
- package/packages/core/src/htmlElement.ts +8 -0
- package/packages/core/src/i18n.ts +8 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/job.ts +16 -1
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +11 -1
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1181 -1097
- package/packages/core/src/mqtt.ts +8 -0
- package/packages/core/src/mqttMessage.ts +8 -0
- package/packages/core/src/optionalPackage.ts +48 -0
- package/packages/core/src/plan.ts +8 -0
- package/packages/core/src/portTakeover.ts +8 -0
- package/packages/core/src/projectIndex.ts +8 -0
- package/packages/core/src/projectIndexExtractors.ts +8 -0
- package/packages/core/src/projectIndexStorage.ts +8 -0
- package/packages/core/src/push.ts +24 -0
- package/packages/core/src/queue.ts +23 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +23 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +61 -14
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
- package/packages/core/src/rateLimiter.ts +8 -0
- package/packages/core/src/request.ts +59 -10
- package/packages/core/src/response.ts +79 -5
- package/packages/core/src/routeDiscovery.ts +8 -0
- package/packages/core/src/router.ts +8 -0
- package/packages/core/src/server.ts +79 -25
- package/packages/core/src/service.ts +8 -0
- package/packages/core/src/session.ts +8 -0
- package/packages/core/src/sessionHandlers/childError.ts +8 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +68 -3
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/respClient.ts +8 -0
- package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
- package/packages/core/src/sso.ts +20 -4
- package/packages/core/src/ssoIdentity.ts +19 -0
- package/packages/core/src/ssrf.ts +221 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +11 -0
- package/packages/core/src/testing.ts +8 -0
- package/packages/core/src/transport.ts +369 -0
- package/packages/core/src/trustedProxy.ts +8 -0
- package/packages/core/src/types.ts +13 -0
- package/packages/core/src/validator.ts +8 -0
- package/packages/core/src/version.ts +8 -0
- package/packages/core/src/websocket.ts +12 -2
- package/packages/core/src/websocketBackplane.ts +49 -32
- package/packages/core/src/websocketConnection.ts +8 -0
- package/packages/core/src/wsdl.ts +197 -115
- package/packages/core/templates/errors/302.twig +8 -0
- package/packages/core/templates/errors/401.twig +8 -0
- package/packages/core/templates/errors/403.twig +8 -0
- package/packages/core/templates/errors/404.twig +8 -0
- package/packages/core/templates/errors/500.twig +8 -0
- package/packages/core/templates/errors/502.twig +8 -0
- package/packages/core/templates/errors/503.twig +8 -0
- package/packages/core/templates/errors/base.twig +8 -0
- package/packages/frond/dist/index.js +157 -17
- package/packages/frond/src/engine.ts +186 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8641 -6765
- package/packages/orm/src/adapters/firebird.ts +50 -3
- package/packages/orm/src/adapters/mongodb.ts +8 -0
- package/packages/orm/src/adapters/mssql.ts +29 -7
- package/packages/orm/src/adapters/mysql.ts +17 -1
- package/packages/orm/src/adapters/odbc.ts +11 -0
- package/packages/orm/src/adapters/pg-types.d.ts +8 -0
- package/packages/orm/src/adapters/postgres.ts +34 -8
- package/packages/orm/src/adapters/sqlDialect.ts +57 -0
- package/packages/orm/src/adapters/sqlite.ts +26 -0
- package/packages/orm/src/autoCrud.ts +78 -36
- package/packages/orm/src/baseModel.ts +61 -29
- package/packages/orm/src/cachedDatabase.ts +67 -1
- package/packages/orm/src/connectTimeout.ts +8 -0
- package/packages/orm/src/database.ts +649 -401
- package/packages/orm/src/databaseResult.ts +8 -0
- package/packages/orm/src/databaseUrl.ts +8 -0
- package/packages/orm/src/docstore.ts +23 -1
- package/packages/orm/src/fakeData.ts +8 -0
- package/packages/orm/src/graph/adapters/arango.ts +8 -0
- package/packages/orm/src/graph/adapters/bolt.ts +8 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
- package/packages/orm/src/graph/connectTimeout.ts +8 -0
- package/packages/orm/src/graph/errors.ts +8 -0
- package/packages/orm/src/graph/graphAdapter.ts +8 -0
- package/packages/orm/src/graph/graphDatabase.ts +8 -0
- package/packages/orm/src/graph/graphUrl.ts +10 -1
- package/packages/orm/src/graph/shapes.ts +8 -0
- package/packages/orm/src/index.ts +147 -139
- package/packages/orm/src/migration.ts +8 -0
- package/packages/orm/src/model.ts +8 -0
- package/packages/orm/src/modelCollection.ts +8 -0
- package/packages/orm/src/point.ts +8 -0
- package/packages/orm/src/query.ts +149 -33
- package/packages/orm/src/queryBuilder.ts +8 -0
- package/packages/orm/src/realtime/index.ts +8 -0
- package/packages/orm/src/realtime/models/attachment.ts +8 -0
- package/packages/orm/src/realtime/models/channel.ts +8 -0
- package/packages/orm/src/realtime/models/channelMember.ts +8 -0
- package/packages/orm/src/realtime/models/message.ts +8 -0
- package/packages/orm/src/realtime/models/workspace.ts +8 -0
- package/packages/orm/src/realtime/realtime.ts +8 -0
- package/packages/orm/src/realtime/storage.ts +43 -12
- package/packages/orm/src/seeder.ts +8 -0
- package/packages/orm/src/seederTable.ts +8 -0
- package/packages/orm/src/seederTypes.ts +8 -0
- package/packages/orm/src/sqlTranslator.ts +185 -8
- package/packages/orm/src/types.ts +15 -0
- package/packages/orm/src/validation.ts +8 -0
- package/packages/swagger/src/generator.ts +8 -0
- package/packages/swagger/src/index.ts +8 -0
- package/packages/swagger/src/ui.ts +8 -0
- package/types/core/src/api.d.ts +6 -0
- package/types/core/src/auth.d.ts +30 -0
- package/types/core/src/background.d.ts +0 -16
- package/types/core/src/cache.d.ts +6 -0
- package/types/core/src/devAdmin.d.ts +18 -12
- package/types/core/src/docs.d.ts +0 -17
- package/types/core/src/errorOverlay.d.ts +0 -24
- package/types/core/src/events.d.ts +0 -12
- package/types/core/src/feedback.d.ts +0 -23
- package/types/core/src/graphql.d.ts +25 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/job.d.ts +1 -0
- package/types/core/src/messenger.d.ts +12 -1
- package/types/core/src/metrics.d.ts +2 -0
- package/types/core/src/middleware.d.ts +45 -1
- package/types/core/src/mqtt.d.ts +0 -33
- package/types/core/src/optionalPackage.d.ts +25 -0
- package/types/core/src/plan.d.ts +0 -10
- package/types/core/src/projectIndex.d.ts +0 -10
- package/types/core/src/push.d.ts +5 -0
- package/types/core/src/queue.d.ts +7 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +10 -1
- package/types/core/src/request.d.ts +12 -0
- package/types/core/src/response.d.ts +13 -0
- package/types/core/src/server.d.ts +19 -0
- package/types/core/src/sessionHandlers/childError.d.ts +0 -15
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
- package/types/core/src/sso.d.ts +1 -0
- package/types/core/src/ssoIdentity.d.ts +10 -0
- package/types/core/src/ssrf.d.ts +19 -0
- package/types/core/src/transport.d.ts +28 -0
- package/types/core/src/trustedProxy.d.ts +0 -16
- package/types/core/src/types.d.ts +5 -0
- package/types/core/src/websocketBackplane.d.ts +1 -0
- package/types/core/src/wsdl.d.ts +8 -22
- package/types/frond/src/engine.d.ts +7 -1
- package/types/orm/src/adapters/firebird.d.ts +12 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +2 -0
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
- package/types/orm/src/adapters/sqlite.d.ts +7 -0
- package/types/orm/src/baseModel.d.ts +5 -0
- package/types/orm/src/cachedDatabase.d.ts +27 -0
- package/types/orm/src/database.d.ts +46 -7
- package/types/orm/src/graph/graphUrl.d.ts +0 -8
- package/types/orm/src/index.d.ts +1 -1
- package/types/orm/src/query.d.ts +48 -2
- package/types/orm/src/realtime/storage.d.ts +2 -1
- package/types/orm/src/sqlTranslator.d.ts +70 -18
- package/types/orm/src/types.d.ts +6 -0
|
@@ -6,6 +6,7 @@ var SafeString = class {
|
|
|
6
6
|
constructor(value) {
|
|
7
7
|
this.value = value;
|
|
8
8
|
}
|
|
9
|
+
value;
|
|
9
10
|
toString() {
|
|
10
11
|
return this.value;
|
|
11
12
|
}
|
|
@@ -365,6 +366,9 @@ function resolveMethodPart(value, part, context) {
|
|
|
365
366
|
if (!methodMatch || typeof value !== "object" || value === null) return { matched: false, value };
|
|
366
367
|
const methodName = methodMatch[1];
|
|
367
368
|
const rawArgs = methodMatch[2] || "";
|
|
369
|
+
if (unsafeKey(methodName) || !Object.prototype.hasOwnProperty.call(value, methodName)) {
|
|
370
|
+
return { matched: false, value };
|
|
371
|
+
}
|
|
368
372
|
const fn = value[methodName];
|
|
369
373
|
if (typeof fn !== "function") return { matched: false, value };
|
|
370
374
|
const args = rawArgs.trim() ? splitArgs(rawArgs).map((a) => evalExpr(a.trim(), context)) : [];
|
|
@@ -384,7 +388,7 @@ function resolvePathKey(part, isBracket, context) {
|
|
|
384
388
|
function resolvePathPart(value, key) {
|
|
385
389
|
if (typeof value !== "object" || value === null) return { found: false, value: null };
|
|
386
390
|
if (Array.isArray(value) && typeof key === "number") return { found: true, value: value[key] };
|
|
387
|
-
if (
|
|
391
|
+
if (unsafeKey(key) || !Object.prototype.hasOwnProperty.call(value, key)) return { found: false, value: null };
|
|
388
392
|
const member = value[key];
|
|
389
393
|
return { found: true, value: typeof member === "function" ? member.call(value) : member };
|
|
390
394
|
}
|
|
@@ -504,7 +508,7 @@ function splitOutsideQuotes(expr, sep2) {
|
|
|
504
508
|
parts.push(expr.slice(currentStart));
|
|
505
509
|
return parts;
|
|
506
510
|
}
|
|
507
|
-
var EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
511
|
+
var EXPR_NOT_MATCHED = /* @__PURE__ */ Symbol("frond-expression-not-matched");
|
|
508
512
|
function parenthesizedInner(expr) {
|
|
509
513
|
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
510
514
|
let depth = 0;
|
|
@@ -634,7 +638,7 @@ function evalDottedFunction(name, rawArgs, context) {
|
|
|
634
638
|
const lastDot = name.lastIndexOf(".");
|
|
635
639
|
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
636
640
|
const member = name.slice(lastDot + 1);
|
|
637
|
-
if (!owner || typeof owner !== "object" ||
|
|
641
|
+
if (!owner || typeof owner !== "object" || unsafeKey(member) || !Object.prototype.hasOwnProperty.call(owner, member)) {
|
|
638
642
|
return EXPR_NOT_MATCHED;
|
|
639
643
|
}
|
|
640
644
|
const method = owner[member];
|
|
@@ -960,6 +964,7 @@ var VarRef = class {
|
|
|
960
964
|
constructor(name) {
|
|
961
965
|
this.name = name;
|
|
962
966
|
}
|
|
967
|
+
name;
|
|
963
968
|
};
|
|
964
969
|
function coerceArg(t) {
|
|
965
970
|
if (/^-?\d+$/.test(t)) return parseInt(t, 10);
|
|
@@ -1063,6 +1068,113 @@ function splitArgs(raw) {
|
|
|
1063
1068
|
function htmlEscape(str) {
|
|
1064
1069
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1065
1070
|
}
|
|
1071
|
+
var JS_SAFE_RE = /[A-Za-z0-9,._]/;
|
|
1072
|
+
function jsEscape(value) {
|
|
1073
|
+
let out = "";
|
|
1074
|
+
for (const ch of String(value)) {
|
|
1075
|
+
if (JS_SAFE_RE.test(ch) && ch.length === 1) {
|
|
1076
|
+
out += ch;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
let code = ch.codePointAt(0);
|
|
1080
|
+
if (code < 128) {
|
|
1081
|
+
out += "\\x" + code.toString(16).toUpperCase().padStart(2, "0");
|
|
1082
|
+
} else if (code <= 65535) {
|
|
1083
|
+
out += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
1084
|
+
} else {
|
|
1085
|
+
code -= 65536;
|
|
1086
|
+
out += "\\u" + (55296 + (code >> 10)).toString(16).toUpperCase().padStart(4, "0");
|
|
1087
|
+
out += "\\u" + (56320 + (code & 1023)).toString(16).toUpperCase().padStart(4, "0");
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return out;
|
|
1091
|
+
}
|
|
1092
|
+
function cssEscape(value) {
|
|
1093
|
+
let out = "";
|
|
1094
|
+
for (const ch of String(value)) {
|
|
1095
|
+
out += /[A-Za-z0-9]/.test(ch) ? ch : "\\" + ch.codePointAt(0).toString(16).toUpperCase().padStart(6, "0") + " ";
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
function htmlAttrEscape(value) {
|
|
1100
|
+
let out = "";
|
|
1101
|
+
for (const ch of String(value)) {
|
|
1102
|
+
out += /[A-Za-z0-9,.\-_]/.test(ch) ? ch : "&#x" + ch.codePointAt(0).toString(16).toUpperCase().padStart(2, "0") + ";";
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
function urlEscape(value) {
|
|
1107
|
+
return encodeURIComponent(String(value)).replace(
|
|
1108
|
+
/[!'()*]/g,
|
|
1109
|
+
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
function escapeStrategy(value, strategy = "html") {
|
|
1113
|
+
const s = String(strategy).trim().replace(/^['"]|['"]$/g, "");
|
|
1114
|
+
switch (s) {
|
|
1115
|
+
case "":
|
|
1116
|
+
case "html":
|
|
1117
|
+
return new SafeString(htmlEscape(String(value)));
|
|
1118
|
+
case "js":
|
|
1119
|
+
return new SafeString(jsEscape(value));
|
|
1120
|
+
case "url":
|
|
1121
|
+
return new SafeString(urlEscape(value));
|
|
1122
|
+
case "css":
|
|
1123
|
+
return new SafeString(cssEscape(value));
|
|
1124
|
+
case "html_attr":
|
|
1125
|
+
case "attr":
|
|
1126
|
+
return new SafeString(htmlAttrEscape(value));
|
|
1127
|
+
default:
|
|
1128
|
+
throw new Error(`Unknown escape strategy: ${strategy}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
var MEDIA_TYPE_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
1132
|
+
function sanitiseMediaType(type) {
|
|
1133
|
+
const t = String(type).trim();
|
|
1134
|
+
return MEDIA_TYPE_RE.test(t) ? t : "application/octet-stream";
|
|
1135
|
+
}
|
|
1136
|
+
var SANDBOX_EXPR_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1137
|
+
"true",
|
|
1138
|
+
"false",
|
|
1139
|
+
"none",
|
|
1140
|
+
"null",
|
|
1141
|
+
"nil",
|
|
1142
|
+
"and",
|
|
1143
|
+
"or",
|
|
1144
|
+
"not",
|
|
1145
|
+
"in",
|
|
1146
|
+
"is",
|
|
1147
|
+
"loop",
|
|
1148
|
+
"if",
|
|
1149
|
+
"else",
|
|
1150
|
+
"empty",
|
|
1151
|
+
"starts",
|
|
1152
|
+
"ends",
|
|
1153
|
+
"with",
|
|
1154
|
+
"matches"
|
|
1155
|
+
]);
|
|
1156
|
+
var SANDBOX_IDENT_RE = /(?<![.\w'"])([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1157
|
+
var SANDBOX_STRING_RE = /'[^']*'|"[^"]*"/g;
|
|
1158
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
1159
|
+
"constructor",
|
|
1160
|
+
"__proto__",
|
|
1161
|
+
"prototype",
|
|
1162
|
+
"__defineGetter__",
|
|
1163
|
+
"__defineSetter__",
|
|
1164
|
+
"__lookupGetter__",
|
|
1165
|
+
"__lookupSetter__",
|
|
1166
|
+
"__defineProperty__",
|
|
1167
|
+
"valueOf"
|
|
1168
|
+
]);
|
|
1169
|
+
function unsafeKey(key) {
|
|
1170
|
+
return UNSAFE_KEYS.has(String(key));
|
|
1171
|
+
}
|
|
1172
|
+
function isEscapableStructured(value) {
|
|
1173
|
+
return value !== null && typeof value === "object" && !(value instanceof SafeString);
|
|
1174
|
+
}
|
|
1175
|
+
function toOutputString(value) {
|
|
1176
|
+
return String(value);
|
|
1177
|
+
}
|
|
1066
1178
|
function dateFilter(value, fmt) {
|
|
1067
1179
|
let dt;
|
|
1068
1180
|
if (value instanceof Date) {
|
|
@@ -1183,8 +1295,8 @@ var BUILTIN_FILTERS = {
|
|
|
1183
1295
|
default: (v, fallback) => v !== null && v !== void 0 && v !== "" ? v : fallback !== void 0 ? fallback : "",
|
|
1184
1296
|
raw: (v) => v,
|
|
1185
1297
|
safe: (v) => v,
|
|
1186
|
-
escape: (v) =>
|
|
1187
|
-
e: (v) =>
|
|
1298
|
+
escape: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
|
|
1299
|
+
e: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
|
|
1188
1300
|
striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
|
|
1189
1301
|
nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
|
|
1190
1302
|
abs: (v) => typeof v === "number" ? Math.abs(v) : v,
|
|
@@ -1273,9 +1385,9 @@ var BUILTIN_FILTERS = {
|
|
|
1273
1385
|
base64decode: (v) => Buffer.from(String(v), "base64").toString("utf-8"),
|
|
1274
1386
|
data_uri: (v) => {
|
|
1275
1387
|
if (v && typeof v === "object" && "content" in v) {
|
|
1276
|
-
const ct = v.type ?? "application/octet-stream";
|
|
1388
|
+
const ct = sanitiseMediaType(String(v.type ?? "application/octet-stream"));
|
|
1277
1389
|
const raw = Buffer.isBuffer(v.content) ? v.content : Buffer.from(String(v.content));
|
|
1278
|
-
return `data:${ct};base64,${raw.toString("base64")}
|
|
1390
|
+
return new SafeString(`data:${ct};base64,${raw.toString("base64")}`);
|
|
1279
1391
|
}
|
|
1280
1392
|
return String(v);
|
|
1281
1393
|
},
|
|
@@ -1295,7 +1407,7 @@ var BUILTIN_FILTERS = {
|
|
|
1295
1407
|
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1296
1408
|
tojson: (v) => jsonSafe(v),
|
|
1297
1409
|
to_json: (v) => jsonSafe(v),
|
|
1298
|
-
js_escape: (v) => new SafeString(
|
|
1410
|
+
js_escape: (v) => new SafeString(jsEscape(v))
|
|
1299
1411
|
};
|
|
1300
1412
|
function _b64url(data) {
|
|
1301
1413
|
return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -1303,6 +1415,12 @@ function _b64url(data) {
|
|
|
1303
1415
|
var _formTokenSessionId = "";
|
|
1304
1416
|
function _buildFormTokenJwt(descriptor = "") {
|
|
1305
1417
|
const secret = process.env.TINA4_SECRET ?? "";
|
|
1418
|
+
const secretBytes = Buffer.byteLength(secret, "utf8");
|
|
1419
|
+
if (secretBytes < 32) {
|
|
1420
|
+
throw new Error(
|
|
1421
|
+
`Auth: TINA4_SECRET ${secretBytes === 0 ? "is not set" : `is ${secretBytes} bytes`}; an HMAC JWT secret must be at least 32 bytes. Generate one with \`openssl rand -hex 32\` and set TINA4_SECRET in your environment or .env.`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1306
1424
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
1307
1425
|
const header = { alg: "HS256", typ: "JWT" };
|
|
1308
1426
|
const now = Math.floor(Date.now() / 1e3);
|
|
@@ -2002,6 +2120,7 @@ var Frond = class _Frond {
|
|
|
2002
2120
|
return this.evalVarInner(expr, context);
|
|
2003
2121
|
}
|
|
2004
2122
|
evalVarRaw(expr, context) {
|
|
2123
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return null;
|
|
2005
2124
|
const [varName, filters] = parseFilterChain(expr);
|
|
2006
2125
|
let value = evalExpr(varName, context);
|
|
2007
2126
|
for (const [fname, rawArgs] of filters) {
|
|
@@ -2044,16 +2163,33 @@ var Frond = class _Frond {
|
|
|
2044
2163
|
const fn = this.filters[fname];
|
|
2045
2164
|
return fn ? fn(value, ...args) : value;
|
|
2046
2165
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2166
|
+
/**
|
|
2167
|
+
* Sandbox gate for a WHOLE expression (F6/ADR-0077): refuse a dunder walk or
|
|
2168
|
+
* a root variable outside the allow-list. Applied to output, if conditions,
|
|
2169
|
+
* set right-hand sides and for iterables so none can smuggle a blocked
|
|
2170
|
+
* variable past the per-output gate.
|
|
2171
|
+
*/
|
|
2172
|
+
sandboxExprOk(expr) {
|
|
2173
|
+
if (!this._sandbox) return true;
|
|
2174
|
+
if (expr.includes("__")) return false;
|
|
2175
|
+
if (this._allowedVars === null) return true;
|
|
2176
|
+
const stripped = expr.replace(SANDBOX_STRING_RE, "");
|
|
2177
|
+
let m;
|
|
2178
|
+
SANDBOX_IDENT_RE.lastIndex = 0;
|
|
2179
|
+
while ((m = SANDBOX_IDENT_RE.exec(stripped)) !== null) {
|
|
2180
|
+
const ident = m[1];
|
|
2181
|
+
if (SANDBOX_EXPR_KEYWORDS.has(ident.toLowerCase())) continue;
|
|
2182
|
+
if (ident in this.filters) continue;
|
|
2183
|
+
if (!this._allowedVars.has(ident)) return false;
|
|
2184
|
+
}
|
|
2185
|
+
return true;
|
|
2051
2186
|
}
|
|
2052
2187
|
resolveConcatenation(expr, context) {
|
|
2053
2188
|
if (findOutsideQuotes(expr, "~") < 0) return { handled: false, value: void 0 };
|
|
2054
2189
|
let value = evalExpr(expr, context);
|
|
2055
2190
|
if (value instanceof SafeString) return { handled: true, value: value.value };
|
|
2056
2191
|
if (this._autoEscape && typeof value === "string") value = htmlEscape(value);
|
|
2192
|
+
else if (this._autoEscape && isEscapableStructured(value)) value = htmlEscape(toOutputString(value));
|
|
2057
2193
|
return { handled: true, value };
|
|
2058
2194
|
}
|
|
2059
2195
|
applyRenderedFilters(value, filters, context) {
|
|
@@ -2064,7 +2200,6 @@ var Frond = class _Frond {
|
|
|
2064
2200
|
if (this.filterPermitted(fname)) safe = true;
|
|
2065
2201
|
continue;
|
|
2066
2202
|
}
|
|
2067
|
-
if ((fname === "escape" || fname === "e") && this.filterPermitted(fname)) safe = true;
|
|
2068
2203
|
if (!this.filterPermitted(fname)) continue;
|
|
2069
2204
|
value = this.applyRenderedFilter(value, fname, args);
|
|
2070
2205
|
}
|
|
@@ -2072,7 +2207,7 @@ var Frond = class _Frond {
|
|
|
2072
2207
|
}
|
|
2073
2208
|
evalVarInner(expr, context) {
|
|
2074
2209
|
const [varName, filters] = parseFilterChain(expr);
|
|
2075
|
-
if (!this.
|
|
2210
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return "";
|
|
2076
2211
|
const concatenated = this.resolveConcatenation(expr, context);
|
|
2077
2212
|
if (concatenated.handled) return concatenated.value;
|
|
2078
2213
|
const applied = this.applyRenderedFilters(evalExpr(varName, context), filters, context);
|
|
@@ -2080,8 +2215,12 @@ var Frond = class _Frond {
|
|
|
2080
2215
|
if (value instanceof SafeString) {
|
|
2081
2216
|
return value.value;
|
|
2082
2217
|
}
|
|
2083
|
-
if (!applied.safe && this._autoEscape
|
|
2084
|
-
value
|
|
2218
|
+
if (!applied.safe && this._autoEscape) {
|
|
2219
|
+
if (typeof value === "string") {
|
|
2220
|
+
value = htmlEscape(value);
|
|
2221
|
+
} else if (isEscapableStructured(value)) {
|
|
2222
|
+
value = new SafeString(htmlEscape(toOutputString(value)));
|
|
2223
|
+
}
|
|
2085
2224
|
}
|
|
2086
2225
|
return value;
|
|
2087
2226
|
}
|
|
@@ -2188,7 +2327,8 @@ var Frond = class _Frond {
|
|
|
2188
2327
|
const var1 = forMatch[1];
|
|
2189
2328
|
const var2 = forMatch[2] || null;
|
|
2190
2329
|
const { bodyTokens, elseTokens, next: i } = this.collectForTokens(tokens, start);
|
|
2191
|
-
const
|
|
2330
|
+
const iterableExpr = forMatch[3].trim();
|
|
2331
|
+
const iterable = this._sandbox && !this.sandboxExprOk(iterableExpr) ? null : evalExpr(iterableExpr, context);
|
|
2192
2332
|
const { items, isDict } = this.forItems(iterable);
|
|
2193
2333
|
if (items.length === 0) return [elseTokens.length ? this.renderTokens([...elseTokens], context) : "", i];
|
|
2194
2334
|
const output = [];
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Frond Engine — Lexer, parser, and runtime.
|
|
3
11
|
* Zero-dependency Twig-like template engine.
|
|
@@ -570,6 +578,11 @@ function resolveMethodPart(value: unknown, part: string, context: Record<string,
|
|
|
570
578
|
if (!methodMatch || typeof value !== "object" || value === null) return { matched: false, value };
|
|
571
579
|
const methodName = methodMatch[1];
|
|
572
580
|
const rawArgs = methodMatch[2] || "";
|
|
581
|
+
// F6/ADR-0077: never traverse a prototype/constructor/dunder key -- that is
|
|
582
|
+
// the Function-constructor and prototype-pollution escape.
|
|
583
|
+
if (unsafeKey(methodName) || !Object.prototype.hasOwnProperty.call(value, methodName)) {
|
|
584
|
+
return { matched: false, value };
|
|
585
|
+
}
|
|
573
586
|
const fn = (value as Record<string, unknown>)[methodName];
|
|
574
587
|
if (typeof fn !== "function") return { matched: false, value };
|
|
575
588
|
const args = rawArgs.trim() ? splitArgs(rawArgs).map(a => evalExpr(a.trim(), context)) : [];
|
|
@@ -592,7 +605,9 @@ function resolvePathKey(part: string, isBracket: boolean, context: Record<string
|
|
|
592
605
|
function resolvePathPart(value: unknown, key: string | number): { found: boolean; value: unknown } {
|
|
593
606
|
if (typeof value !== "object" || value === null) return { found: false, value: null };
|
|
594
607
|
if (Array.isArray(value) && typeof key === "number") return { found: true, value: value[key] };
|
|
595
|
-
|
|
608
|
+
// F6/ADR-0077: only own, non-dunder properties are reachable, so an
|
|
609
|
+
// inherited prototype method (constructor, __proto__, ...) cannot be called.
|
|
610
|
+
if (unsafeKey(key) || !Object.prototype.hasOwnProperty.call(value, key)) return { found: false, value: null };
|
|
596
611
|
const member = (value as Record<string, unknown>)[key as string];
|
|
597
612
|
return { found: true, value: typeof member === "function" ? member.call(value) : member };
|
|
598
613
|
}
|
|
@@ -889,7 +904,10 @@ function evalDottedFunction(name: string, rawArgs: string, context: Record<strin
|
|
|
889
904
|
const lastDot = name.lastIndexOf(".");
|
|
890
905
|
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
891
906
|
const member = name.slice(lastDot + 1);
|
|
892
|
-
|
|
907
|
+
// F6/ADR-0077: never call through a prototype/constructor/dunder key or an
|
|
908
|
+
// inherited method -- that is the Function-constructor and prototype escape.
|
|
909
|
+
if (!owner || typeof owner !== "object" || unsafeKey(member) ||
|
|
910
|
+
!Object.prototype.hasOwnProperty.call(owner, member)) {
|
|
893
911
|
return EXPR_NOT_MATCHED;
|
|
894
912
|
}
|
|
895
913
|
const method = (owner as Record<string, unknown>)[member];
|
|
@@ -1361,6 +1379,115 @@ function htmlEscape(str: string): string {
|
|
|
1361
1379
|
.replace(/'/g, "'");
|
|
1362
1380
|
}
|
|
1363
1381
|
|
|
1382
|
+
// -- Escape strategies (ADR-0077) -------------------------------------------
|
|
1383
|
+
// Shared by js_escape and e(strategy); byte-identical to the Python master.
|
|
1384
|
+
// Twig-compatible. Iterates code points so astral characters emit a surrogate
|
|
1385
|
+
// pair for the js strategy.
|
|
1386
|
+
const JS_SAFE_RE = /[A-Za-z0-9,._]/;
|
|
1387
|
+
function jsEscape(value: unknown): string {
|
|
1388
|
+
let out = "";
|
|
1389
|
+
for (const ch of String(value)) {
|
|
1390
|
+
if (JS_SAFE_RE.test(ch) && ch.length === 1) {
|
|
1391
|
+
out += ch;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
let code = ch.codePointAt(0)!;
|
|
1395
|
+
if (code < 0x80) {
|
|
1396
|
+
out += "\\x" + code.toString(16).toUpperCase().padStart(2, "0");
|
|
1397
|
+
} else if (code <= 0xffff) {
|
|
1398
|
+
out += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
1399
|
+
} else {
|
|
1400
|
+
code -= 0x10000;
|
|
1401
|
+
out += "\\u" + (0xd800 + (code >> 10)).toString(16).toUpperCase().padStart(4, "0");
|
|
1402
|
+
out += "\\u" + (0xdc00 + (code & 0x3ff)).toString(16).toUpperCase().padStart(4, "0");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function cssEscape(value: unknown): string {
|
|
1409
|
+
let out = "";
|
|
1410
|
+
for (const ch of String(value)) {
|
|
1411
|
+
out += /[A-Za-z0-9]/.test(ch)
|
|
1412
|
+
? ch
|
|
1413
|
+
: "\\" + ch.codePointAt(0)!.toString(16).toUpperCase().padStart(6, "0") + " ";
|
|
1414
|
+
}
|
|
1415
|
+
return out;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function htmlAttrEscape(value: unknown): string {
|
|
1419
|
+
let out = "";
|
|
1420
|
+
for (const ch of String(value)) {
|
|
1421
|
+
out += /[A-Za-z0-9,.\-_]/.test(ch)
|
|
1422
|
+
? ch
|
|
1423
|
+
: "&#x" + ch.codePointAt(0)!.toString(16).toUpperCase().padStart(2, "0") + ";";
|
|
1424
|
+
}
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function urlEscape(value: unknown): string {
|
|
1429
|
+
return encodeURIComponent(String(value)).replace(
|
|
1430
|
+
/[!'()*]/g,
|
|
1431
|
+
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Dispatch e(strategy)/escape(strategy). Unknown strategy throws so a typo can
|
|
1436
|
+
// never silently pass the value through unescaped.
|
|
1437
|
+
function escapeStrategy(value: unknown, strategy: string = "html"): SafeString {
|
|
1438
|
+
const s = String(strategy).trim().replace(/^['"]|['"]$/g, "");
|
|
1439
|
+
switch (s) {
|
|
1440
|
+
case "":
|
|
1441
|
+
case "html":
|
|
1442
|
+
return new SafeString(htmlEscape(String(value)));
|
|
1443
|
+
case "js":
|
|
1444
|
+
return new SafeString(jsEscape(value));
|
|
1445
|
+
case "url":
|
|
1446
|
+
return new SafeString(urlEscape(value));
|
|
1447
|
+
case "css":
|
|
1448
|
+
return new SafeString(cssEscape(value));
|
|
1449
|
+
case "html_attr":
|
|
1450
|
+
case "attr":
|
|
1451
|
+
return new SafeString(htmlAttrEscape(value));
|
|
1452
|
+
default:
|
|
1453
|
+
throw new Error(`Unknown escape strategy: ${strategy}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Confine a client-supplied media type to type/subtype (F3/ADR-0077).
|
|
1458
|
+
const MEDIA_TYPE_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
1459
|
+
function sanitiseMediaType(type: string): string {
|
|
1460
|
+
const t = String(type).trim();
|
|
1461
|
+
return MEDIA_TYPE_RE.test(t) ? t : "application/octet-stream";
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Property keys that must never be traversed/invoked through {{ obj.x }} —
|
|
1465
|
+
// prototype pollution and the Function constructor RCE (F6/ADR-0077).
|
|
1466
|
+
const SANDBOX_EXPR_KEYWORDS = new Set([
|
|
1467
|
+
"true","false","none","null","nil","and","or","not","in","is","loop",
|
|
1468
|
+
"if","else","empty","starts","ends","with","matches",
|
|
1469
|
+
]);
|
|
1470
|
+
const SANDBOX_IDENT_RE = /(?<![.\w'"])([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1471
|
+
const SANDBOX_STRING_RE = /'[^']*'|"[^"]*"/g;
|
|
1472
|
+
const UNSAFE_KEYS = new Set([
|
|
1473
|
+
"constructor", "__proto__", "prototype", "__defineGetter__",
|
|
1474
|
+
"__defineSetter__", "__lookupGetter__", "__lookupSetter__",
|
|
1475
|
+
"__defineProperty__", "valueOf",
|
|
1476
|
+
]);
|
|
1477
|
+
function unsafeKey(key: string | number): boolean {
|
|
1478
|
+
return UNSAFE_KEYS.has(String(key));
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// A value whose rendered string form must be auto-escaped: an array/object
|
|
1482
|
+
// (not a SafeString). Strings, numbers, booleans and null are handled by the
|
|
1483
|
+
// caller's own branches. (F4/ADR-0077)
|
|
1484
|
+
function isEscapableStructured(value: unknown): boolean {
|
|
1485
|
+
return value !== null && typeof value === "object" && !(value instanceof SafeString);
|
|
1486
|
+
}
|
|
1487
|
+
function toOutputString(value: unknown): string {
|
|
1488
|
+
return String(value);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1364
1491
|
function dateFilter(value: unknown, fmt: string): string {
|
|
1365
1492
|
let dt: Date;
|
|
1366
1493
|
if (value instanceof Date) {
|
|
@@ -1514,8 +1641,8 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1514
1641
|
default: (v, fallback) => (v !== null && v !== undefined && v !== "") ? v : (fallback !== undefined ? fallback : ""),
|
|
1515
1642
|
raw: (v) => v,
|
|
1516
1643
|
safe: (v) => v,
|
|
1517
|
-
escape: (v) =>
|
|
1518
|
-
e: (v) =>
|
|
1644
|
+
escape: (v, ...a) => escapeStrategy(v, (a[0] as string) ?? "html"),
|
|
1645
|
+
e: (v, ...a) => escapeStrategy(v, (a[0] as string) ?? "html"),
|
|
1519
1646
|
striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
|
|
1520
1647
|
nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
|
|
1521
1648
|
abs: (v) => typeof v === "number" ? Math.abs(v) : v,
|
|
@@ -1604,9 +1731,9 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1604
1731
|
base64decode: (v) => Buffer.from(String(v), "base64").toString("utf-8"),
|
|
1605
1732
|
data_uri: (v) => {
|
|
1606
1733
|
if (v && typeof v === "object" && "content" in v) {
|
|
1607
|
-
const ct = (v as any).type ?? "application/octet-stream";
|
|
1734
|
+
const ct = sanitiseMediaType(String((v as any).type ?? "application/octet-stream"));
|
|
1608
1735
|
const raw = Buffer.isBuffer((v as any).content) ? (v as any).content : Buffer.from(String((v as any).content));
|
|
1609
|
-
return `data:${ct};base64,${raw.toString("base64")}
|
|
1736
|
+
return new SafeString(`data:${ct};base64,${raw.toString("base64")}`);
|
|
1610
1737
|
}
|
|
1611
1738
|
return String(v);
|
|
1612
1739
|
},
|
|
@@ -1626,7 +1753,7 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1626
1753
|
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1627
1754
|
tojson: (v) => jsonSafe(v),
|
|
1628
1755
|
to_json: (v) => jsonSafe(v),
|
|
1629
|
-
js_escape: (v) => new SafeString(
|
|
1756
|
+
js_escape: (v) => new SafeString(jsEscape(v)),
|
|
1630
1757
|
};
|
|
1631
1758
|
|
|
1632
1759
|
// ── Form Token ────────────────────────────────────────────────
|
|
@@ -1667,6 +1794,15 @@ function _buildFormTokenJwt(descriptor: string = ""): string {
|
|
|
1667
1794
|
// AND a ''-forged token was accepted (CSRF-NODE-SECRET-SPLIT, feature 37).
|
|
1668
1795
|
// The generator and the validator MUST resolve the same secret.
|
|
1669
1796
|
const secret = process.env.TINA4_SECRET ?? "";
|
|
1797
|
+
// Same rule as core auth (ADR-0079 s2): never sign with a blank or short key,
|
|
1798
|
+
// which would make every form token reproducible by anyone.
|
|
1799
|
+
const secretBytes = Buffer.byteLength(secret, "utf8");
|
|
1800
|
+
if (secretBytes < 32) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Auth: TINA4_SECRET ${secretBytes === 0 ? "is not set" : `is ${secretBytes} bytes`}; an HMAC JWT secret must be at least 32 bytes. ` +
|
|
1803
|
+
"Generate one with `openssl rand -hex 32` and set TINA4_SECRET in your environment or .env.",
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1670
1806
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
1671
1807
|
|
|
1672
1808
|
const header = { alg: "HS256", typ: "JWT" };
|
|
@@ -2526,6 +2662,9 @@ export class Frond {
|
|
|
2526
2662
|
}
|
|
2527
2663
|
|
|
2528
2664
|
private evalVarRaw(expr: string, context: Record<string, unknown>): unknown {
|
|
2665
|
+
// F6/ADR-0077: gate the whole condition/RHS so a comparison or iterable
|
|
2666
|
+
// cannot leak a blocked variable.
|
|
2667
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return null;
|
|
2529
2668
|
const [varName, filters] = parseFilterChain(expr);
|
|
2530
2669
|
let value = evalExpr(varName, context);
|
|
2531
2670
|
for (const [fname, rawArgs] of filters) {
|
|
@@ -2576,10 +2715,26 @@ export class Frond {
|
|
|
2576
2715
|
return fn ? fn(value, ...args) : value;
|
|
2577
2716
|
}
|
|
2578
2717
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2718
|
+
/**
|
|
2719
|
+
* Sandbox gate for a WHOLE expression (F6/ADR-0077): refuse a dunder walk or
|
|
2720
|
+
* a root variable outside the allow-list. Applied to output, if conditions,
|
|
2721
|
+
* set right-hand sides and for iterables so none can smuggle a blocked
|
|
2722
|
+
* variable past the per-output gate.
|
|
2723
|
+
*/
|
|
2724
|
+
private sandboxExprOk(expr: string): boolean {
|
|
2725
|
+
if (!this._sandbox) return true;
|
|
2726
|
+
if (expr.includes("__")) return false;
|
|
2727
|
+
if (this._allowedVars === null) return true;
|
|
2728
|
+
const stripped = expr.replace(SANDBOX_STRING_RE, "");
|
|
2729
|
+
let m: RegExpExecArray | null;
|
|
2730
|
+
SANDBOX_IDENT_RE.lastIndex = 0;
|
|
2731
|
+
while ((m = SANDBOX_IDENT_RE.exec(stripped)) !== null) {
|
|
2732
|
+
const ident = m[1];
|
|
2733
|
+
if (SANDBOX_EXPR_KEYWORDS.has(ident.toLowerCase())) continue;
|
|
2734
|
+
if (ident in this.filters) continue;
|
|
2735
|
+
if (!this._allowedVars.has(ident)) return false;
|
|
2736
|
+
}
|
|
2737
|
+
return true;
|
|
2583
2738
|
}
|
|
2584
2739
|
|
|
2585
2740
|
private resolveConcatenation(
|
|
@@ -2590,6 +2745,7 @@ export class Frond {
|
|
|
2590
2745
|
let value = evalExpr(expr, context);
|
|
2591
2746
|
if (value instanceof SafeString) return { handled: true, value: value.value };
|
|
2592
2747
|
if (this._autoEscape && typeof value === "string") value = htmlEscape(value);
|
|
2748
|
+
else if (this._autoEscape && isEscapableStructured(value)) value = htmlEscape(toOutputString(value));
|
|
2593
2749
|
return { handled: true, value };
|
|
2594
2750
|
}
|
|
2595
2751
|
|
|
@@ -2605,7 +2761,10 @@ export class Frond {
|
|
|
2605
2761
|
if (this.filterPermitted(fname)) safe = true;
|
|
2606
2762
|
continue;
|
|
2607
2763
|
}
|
|
2608
|
-
|
|
2764
|
+
// F2/ADR-0077: do NOT keep a persistent "safe" flag for escape/e. The
|
|
2765
|
+
// filter returns a SafeString; if a later filter (replace/format/join)
|
|
2766
|
+
// transforms it, the result is a plain string and is re-escaped at
|
|
2767
|
+
// output. Keeping a safe flag here let post-escape filters emit raw.
|
|
2609
2768
|
if (!this.filterPermitted(fname)) continue;
|
|
2610
2769
|
value = this.applyRenderedFilter(value, fname, args);
|
|
2611
2770
|
}
|
|
@@ -2614,7 +2773,7 @@ export class Frond {
|
|
|
2614
2773
|
|
|
2615
2774
|
private evalVarInner(expr: string, context: Record<string, unknown>): unknown {
|
|
2616
2775
|
const [varName, filters] = parseFilterChain(expr);
|
|
2617
|
-
if (!this.
|
|
2776
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return "";
|
|
2618
2777
|
const concatenated = this.resolveConcatenation(expr, context);
|
|
2619
2778
|
if (concatenated.handled) return concatenated.value;
|
|
2620
2779
|
const applied = this.applyRenderedFilters(evalExpr(varName, context), filters, context);
|
|
@@ -2625,9 +2784,16 @@ export class Frond {
|
|
|
2625
2784
|
return value.value;
|
|
2626
2785
|
}
|
|
2627
2786
|
|
|
2628
|
-
// Auto-escape HTML unless marked safe or auto-escape is disabled
|
|
2629
|
-
|
|
2630
|
-
|
|
2787
|
+
// Auto-escape HTML unless marked safe or auto-escape is disabled. A plain
|
|
2788
|
+
// string is escaped directly; an array/object is escaped on its rendered
|
|
2789
|
+
// string form (F4/ADR-0077) -- before this, only string values were
|
|
2790
|
+
// escaped, so {{ items }} holding markup emitted it raw.
|
|
2791
|
+
if (!applied.safe && this._autoEscape) {
|
|
2792
|
+
if (typeof value === "string") {
|
|
2793
|
+
value = htmlEscape(value);
|
|
2794
|
+
} else if (isEscapableStructured(value)) {
|
|
2795
|
+
value = new SafeString(htmlEscape(toOutputString(value)));
|
|
2796
|
+
}
|
|
2631
2797
|
}
|
|
2632
2798
|
|
|
2633
2799
|
return value;
|
|
@@ -2756,7 +2922,10 @@ export class Frond {
|
|
|
2756
2922
|
const { bodyTokens, elseTokens, next: i } = this.collectForTokens(tokens, start);
|
|
2757
2923
|
|
|
2758
2924
|
// Evaluate iterable
|
|
2759
|
-
const
|
|
2925
|
+
const iterableExpr = forMatch[3].trim();
|
|
2926
|
+
const iterable = (this._sandbox && !this.sandboxExprOk(iterableExpr))
|
|
2927
|
+
? null
|
|
2928
|
+
: evalExpr(iterableExpr, context);
|
|
2760
2929
|
const { items, isDict } = this.forItems(iterable);
|
|
2761
2930
|
if (items.length === 0) return [elseTokens.length ? this.renderTokens([...elseTokens], context) : "", i];
|
|
2762
2931
|
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
export { Frond } from "./engine.js";
|
|
2
10
|
export type { FilterFn, TestFn, LiveProvider, LiveRequest, LiveResponse, LiveBroadcaster } from "./engine.js";
|