tina4-nodejs 3.13.3 → 3.13.5

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.13.3)
1
+ # CLAUDE.md — AI Developer Guide for tina4-nodejs (v3.13.5)
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.13.3 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. The developer writes TypeScript; Tina4 is invisible infrastructure.
7
+ Tina4 for Node.js/TypeScript v3.13.5 — The Intelligent Native Application 4ramework. A convention-over-configuration structural paradigm. 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
 
@@ -571,7 +571,7 @@ db.delete(table, filter?, params?): DatabaseWriteResult
571
571
  db.getLastId(): string | number | null
572
572
  db.getError(): string | null
573
573
 
574
- // Transactions — autoCommit defaults to ON unless TINA4_DB_AUTOCOMMIT=false
574
+ // Transactions — autoCommit defaults to OFF; set TINA4_AUTOCOMMIT=true to enable
575
575
  db.startTransaction(): void
576
576
  db.commit(): void
577
577
  db.rollback(): void
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.13.3",
6
+ "version": "3.13.5",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript \u2014 54 built-in features, zero dependencies",
@@ -20,7 +20,7 @@ export type { RouteInfo } from "./router.js";
20
20
  export { discoverRoutes } from "./routeDiscovery.js";
21
21
  export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware } from "./middleware.js";
22
22
  export type { CorsConfig } from "./middleware.js";
23
- export { createRequest } from "./request.js";
23
+ export { createRequest, makeCaseInsensitiveHeaders } from "./request.js";
24
24
  export { createResponse, errorResponse, setDefaultTemplatesDir, getFrond, setFrond, getFrameworkFrond } from "./response.js";
25
25
  export { tryServeStatic } from "./static.js";
26
26
  export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
@@ -1,8 +1,42 @@
1
- import type { IncomingMessage } from "node:http";
1
+ import type { IncomingMessage, IncomingHttpHeaders } from "node:http";
2
2
  import type { Tina4Request, UploadedFile } from "./types.js";
3
3
 
4
+ /**
5
+ * Wrap Node's `IncomingHttpHeaders` in a Proxy so mixed-case lookups
6
+ * (`req.headers["Content-Type"]`) work alongside the canonical lowercase
7
+ * form Node already provides. Parity with PY-10-03 (Python ships a
8
+ * `CaseInsensitiveDict` for the same reason).
9
+ *
10
+ * The raw object is returned as-is by `Object.keys` / iteration — only
11
+ * string property reads/`in` checks are normalised.
12
+ */
13
+ export function makeCaseInsensitiveHeaders(
14
+ raw: IncomingHttpHeaders,
15
+ ): IncomingHttpHeaders {
16
+ return new Proxy(raw, {
17
+ get(target, prop, receiver) {
18
+ if (typeof prop === "string") {
19
+ const lower = prop.toLowerCase();
20
+ if (lower in target) return (target as Record<string, unknown>)[lower];
21
+ return Reflect.get(target, prop, receiver);
22
+ }
23
+ return Reflect.get(target, prop, receiver);
24
+ },
25
+ has(target, prop) {
26
+ if (typeof prop === "string") {
27
+ return prop.toLowerCase() in target || Reflect.has(target, prop);
28
+ }
29
+ return Reflect.has(target, prop);
30
+ },
31
+ }) as IncomingHttpHeaders;
32
+ }
33
+
4
34
  export function createRequest(req: IncomingMessage): Tina4Request {
5
35
  const tReq = req as Tina4Request;
36
+ // Wrap `req.headers` so mixed-case lookups work — Node's underlying object
37
+ // is already lower-cased, this just lets readers use any casing they like.
38
+ (tReq as unknown as { headers: IncomingHttpHeaders }).headers =
39
+ makeCaseInsensitiveHeaders(req.headers);
6
40
 
7
41
  // Resolve scheme + host honouring proxy headers — parity with PHP/Python/Ruby.
8
42
  const xfProto = req.headers["x-forwarded-proto"];
@@ -131,12 +131,13 @@ export class Router {
131
131
  routes.splice(existingIndex, 1);
132
132
  }
133
133
 
134
- // Write methods (POST/PUT/PATCH/DELETE) are secure by default,
135
- // unless custom middleware is registered (developer handles auth themselves)
134
+ // Write methods (POST/PUT/PATCH/DELETE) are secure by default. Middleware is
135
+ // purely additive — registering custom middleware must NOT silently disable the
136
+ // built-in Bearer-token gate (parity with PY-10-02). Use `noAuth()` to open a
137
+ // write route explicitly.
136
138
  const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
137
139
  const isWrite = WRITE_METHODS.has(method);
138
- const hasMiddleware = definition.middlewares && definition.middlewares.length > 0;
139
- const secureDefault = isWrite && !hasMiddleware ? (definition.secure ?? true) : definition.secure;
140
+ const secureDefault = isWrite ? (definition.secure ?? true) : definition.secure;
140
141
 
141
142
  const compiled: CompiledRoute = {
142
143
  pattern: definition.pattern,
@@ -1317,6 +1317,56 @@ function _generateFormTokenValue(descriptor: string = ""): SafeString {
1317
1317
  // ── Frond Engine ───────────────────────────────────────────────
1318
1318
 
1319
1319
  export class Frond {
1320
+ // ── Class-level registries ──────────────────────────────────
1321
+ // Persist globals, filters, and tests across hot-reloads and at
1322
+ // app-startup before any instance exists. When app.ts calls
1323
+ // ``Frond.addFilter("money", fn)`` once, the class remembers it
1324
+ // and every future ``new Frond()`` inherits it automatically.
1325
+ // Mirrors Python's _class_globals / _class_filters / _class_tests.
1326
+ private static classFilters: Map<string, FilterFn> = new Map();
1327
+ private static classGlobals: Map<string, unknown> = new Map();
1328
+ private static classTests: Map<string, TestFn> = new Map();
1329
+
1330
+ /**
1331
+ * Register a custom filter at the class level — available to every
1332
+ * future ``new Frond()`` instance. Callable as ``Frond.addFilter()``
1333
+ * (static) or ``frond.addFilter()`` (instance). See instance method
1334
+ * below for the dual-call semantics.
1335
+ */
1336
+ static addFilter(name: string, fn: FilterFn): void {
1337
+ Frond.classFilters.set(name, fn);
1338
+ }
1339
+
1340
+ /**
1341
+ * Register a global variable available in all templates of every
1342
+ * future instance. Callable as ``Frond.addGlobal()`` (static) or
1343
+ * ``frond.addGlobal()`` (instance).
1344
+ */
1345
+ static addGlobal(name: string, value: unknown): void {
1346
+ Frond.classGlobals.set(name, value);
1347
+ }
1348
+
1349
+ /**
1350
+ * Register a custom test (``{% if x is positive %}``) at the class
1351
+ * level. Callable as ``Frond.addTest()`` (static) or
1352
+ * ``frond.addTest()`` (instance).
1353
+ */
1354
+ static addTest(name: string, fn: TestFn): void {
1355
+ Frond.classTests.set(name, fn);
1356
+ }
1357
+
1358
+ /**
1359
+ * Clear the class-level globals/filters/tests registries.
1360
+ * Useful in test fixtures to prevent leaking state between tests.
1361
+ * Does NOT affect built-in filters or globals — only user-registered
1362
+ * ones via Frond.addFilter / addGlobal / addTest.
1363
+ */
1364
+ static clearRegistry(): void {
1365
+ Frond.classFilters.clear();
1366
+ Frond.classGlobals.clear();
1367
+ Frond.classTests.clear();
1368
+ }
1369
+
1320
1370
  private templateDir: string;
1321
1371
  private filters: Record<string, FilterFn>;
1322
1372
  private globals: Record<string, unknown>;
@@ -1362,6 +1412,14 @@ export class Frond {
1362
1412
  // Available alongside the |dump filter so both styles work:
1363
1413
  // {{ user|dump }} and {{ dump(user) }}
1364
1414
  this.globals.dump = (value: unknown) => renderDump(value);
1415
+
1416
+ // Drain the class-level registry. This is the key to surviving
1417
+ // hot-reloads AND the static-facade pattern: app.ts calls
1418
+ // Frond.addFilter("money", fn) once, the class remembers it,
1419
+ // and every future Frond() instance picks it up automatically.
1420
+ for (const [k, v] of Frond.classFilters) this.filters[k] = v;
1421
+ for (const [k, v] of Frond.classGlobals) this.globals[k] = v;
1422
+ for (const [k, v] of Frond.classTests) this.tests[k] = v;
1365
1423
  }
1366
1424
 
1367
1425
  sandbox(filters?: string[], tags?: string[], vars?: string[]): Frond {
@@ -1380,15 +1438,32 @@ export class Frond {
1380
1438
  return this;
1381
1439
  }
1382
1440
 
1441
+ /**
1442
+ * Register a custom filter. The filter is persisted at class level
1443
+ * so new instances created by hot-reload inherit it automatically;
1444
+ * the live instance's local filter map also receives the addition
1445
+ * immediately. Mirrors Python's _ClassOrInstanceMethod dual-call.
1446
+ */
1383
1447
  addFilter(name: string, fn: FilterFn): void {
1448
+ Frond.classFilters.set(name, fn);
1384
1449
  this.filters[name] = fn;
1385
1450
  }
1386
1451
 
1452
+ /**
1453
+ * Register a global variable available in all templates. Persisted
1454
+ * at class level — see ``addFilter`` for the dual-call semantics.
1455
+ */
1387
1456
  addGlobal(name: string, value: unknown): void {
1457
+ Frond.classGlobals.set(name, value);
1388
1458
  this.globals[name] = value;
1389
1459
  }
1390
1460
 
1461
+ /**
1462
+ * Register a custom test. Persisted at class level — see
1463
+ * ``addFilter`` for the dual-call semantics.
1464
+ */
1391
1465
  addTest(name: string, fn: TestFn): void {
1466
+ Frond.classTests.set(name, fn);
1392
1467
  this.tests[name] = fn;
1393
1468
  }
1394
1469