tina4-nodejs 3.10.20 → 3.10.23

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 CHANGED
@@ -1,10 +1,10 @@
1
- # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.21)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.10.23)
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.21 — a convention-over-configuration structural paradigm. **Not a framework.** The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.10.23 — 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.20",
3
+ "version": "3.10.23",
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 _generateFormToken(descriptor: string = ""): SafeString {
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
- const token = `${h}.${p}.${sig}`;
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 {