tina4-nodejs 3.10.21 → 3.10.24
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 +2 -2
- package/package.json +1 -1
- package/packages/frond/src/engine.ts +25 -14
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.
|
|
1
|
+
# CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.24)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
5
5
|
## What This Project Is
|
|
6
6
|
|
|
7
|
-
Tina4 for Node.js/TypeScript v3.10.
|
|
7
|
+
Tina4 for Node.js/TypeScript v3.10.24 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
|
|
8
8
|
|
|
9
9
|
The philosophy: zero ceremony, batteries included, file system as source of truth.
|
|
10
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tina4-nodejs",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
|
|
6
6
|
"keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Supports: variables, filters, if/elseif/else/endif, for/else/endfor,
|
|
5
5
|
* extends/block, include, macro, set, comments, whitespace control, tests.
|
|
6
6
|
*/
|
|
7
|
-
import { createHash, createHmac } from "node:crypto";
|
|
7
|
+
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
8
8
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
9
9
|
import { join, resolve } from "node:path";
|
|
10
10
|
|
|
@@ -1016,6 +1016,8 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1016
1016
|
dump: (v) => JSON.stringify(v),
|
|
1017
1017
|
formToken: (v?: unknown) => _generateFormToken(v != null ? String(v) : ""),
|
|
1018
1018
|
form_token: (v?: unknown) => _generateFormToken(v != null ? String(v) : ""),
|
|
1019
|
+
formTokenValue: (v?: unknown) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1020
|
+
form_token_value: (v?: unknown) => _generateFormTokenValue(v != null ? String(v) : ""),
|
|
1019
1021
|
tojson: (v, indent) => new SafeString(indent !== undefined ? JSON.stringify(v, null, parseInt(String(indent), 10)) : JSON.stringify(v)),
|
|
1020
1022
|
to_json: (v, indent) => new SafeString(indent !== undefined ? JSON.stringify(v, null, parseInt(String(indent), 10)) : JSON.stringify(v)),
|
|
1021
1023
|
js_escape: (v) => new SafeString(String(v).replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")),
|
|
@@ -1050,13 +1052,13 @@ export function setFormTokenSessionId(sessionId: string): void {
|
|
|
1050
1052
|
_formTokenSessionId = sessionId || "";
|
|
1051
1053
|
}
|
|
1052
1054
|
|
|
1053
|
-
function
|
|
1055
|
+
function _buildFormTokenJwt(descriptor: string = ""): string {
|
|
1054
1056
|
const secret = process.env.SECRET || "tina4-default-secret";
|
|
1055
1057
|
const ttlMinutes = parseInt(process.env.TINA4_TOKEN_LIMIT || "60", 10);
|
|
1056
1058
|
|
|
1057
1059
|
const header = { alg: "HS256", typ: "JWT" };
|
|
1058
1060
|
const now = Math.floor(Date.now() / 1000);
|
|
1059
|
-
const payload: Record<string, unknown> = { type: "form", iat: now, exp: now + ttlMinutes * 60 };
|
|
1061
|
+
const payload: Record<string, unknown> = { type: "form", nonce: randomBytes(8).toString("hex"), iat: now, exp: now + ttlMinutes * 60 };
|
|
1060
1062
|
|
|
1061
1063
|
if (descriptor) {
|
|
1062
1064
|
if (descriptor.includes("|")) {
|
|
@@ -1078,11 +1080,22 @@ function _generateFormToken(descriptor: string = ""): SafeString {
|
|
|
1078
1080
|
const sigInput = `${h}.${p}`;
|
|
1079
1081
|
const sig = _b64url(createHmac("sha256", secret).update(sigInput).digest());
|
|
1080
1082
|
|
|
1081
|
-
|
|
1083
|
+
return `${h}.${p}.${sig}`;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function _generateFormToken(descriptor: string = ""): SafeString {
|
|
1087
|
+
const token = _buildFormTokenJwt(descriptor);
|
|
1082
1088
|
const escaped = token.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1083
1089
|
return new SafeString(`<input type="hidden" name="formToken" value="${escaped}">`);
|
|
1084
1090
|
}
|
|
1085
1091
|
|
|
1092
|
+
/**
|
|
1093
|
+
* Generate a JWT form token and return just the raw JWT string (no HTML wrapper).
|
|
1094
|
+
*/
|
|
1095
|
+
function _generateFormTokenValue(descriptor: string = ""): SafeString {
|
|
1096
|
+
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1086
1099
|
// ── Frond Engine ───────────────────────────────────────────────
|
|
1087
1100
|
|
|
1088
1101
|
export class Frond {
|
|
@@ -1118,6 +1131,8 @@ export class Frond {
|
|
|
1118
1131
|
// Built-in global functions
|
|
1119
1132
|
this.globals.formToken = (descriptor?: string) => _generateFormToken(descriptor || "");
|
|
1120
1133
|
this.globals.form_token = (descriptor?: string) => _generateFormToken(descriptor || "");
|
|
1134
|
+
this.globals.formTokenValue = (descriptor?: string) => _generateFormTokenValue(descriptor || "");
|
|
1135
|
+
this.globals.form_token_value = (descriptor?: string) => _generateFormTokenValue(descriptor || "");
|
|
1121
1136
|
}
|
|
1122
1137
|
|
|
1123
1138
|
sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond {
|
|
@@ -1157,20 +1172,16 @@ export class Frond {
|
|
|
1157
1172
|
}
|
|
1158
1173
|
|
|
1159
1174
|
const debugMode = (process.env.TINA4_DEBUG || "").toLowerCase() === "true";
|
|
1160
|
-
const cached = this.compiled.get(template);
|
|
1161
1175
|
|
|
1162
|
-
if (
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
if (cached.mtime === mtime) {
|
|
1167
|
-
return this.executeCached(cached.tokens, context);
|
|
1168
|
-
}
|
|
1169
|
-
} else {
|
|
1170
|
-
// Production: skip mtime check, cache is permanent
|
|
1176
|
+
if (!debugMode) {
|
|
1177
|
+
// Production: use permanent cache (no filesystem checks)
|
|
1178
|
+
const cached = this.compiled.get(template);
|
|
1179
|
+
if (cached) {
|
|
1171
1180
|
return this.executeCached(cached.tokens, context);
|
|
1172
1181
|
}
|
|
1173
1182
|
}
|
|
1183
|
+
// Dev mode: skip cache entirely — always re-read and re-tokenize
|
|
1184
|
+
// so edits to partials and extended base templates are detected
|
|
1174
1185
|
|
|
1175
1186
|
// Cache miss — load, tokenize, cache
|
|
1176
1187
|
const source = readFileSync(filePath, "utf-8");
|