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.
Files changed (96) hide show
  1. package/CLAUDE.md +60 -25
  2. package/package.json +1 -2
  3. package/packages/cli/dist/bin.js +20620 -18995
  4. package/packages/cli/src/bin.ts +28 -71
  5. package/packages/cli/src/commands/migrate.ts +36 -75
  6. package/packages/cli/src/commands/migrateRollback.ts +10 -1
  7. package/packages/cli/src/commands/test.ts +92 -21
  8. package/packages/core/dist/index.js +20459 -18815
  9. package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
  10. package/packages/core/src/ai.ts +28 -12
  11. package/packages/core/src/api.ts +13 -5
  12. package/packages/core/src/background.ts +9 -3
  13. package/packages/core/src/devAdmin.ts +135 -20
  14. package/packages/core/src/dispatchPipeline.ts +185 -1
  15. package/packages/core/src/docs.ts +33 -5
  16. package/packages/core/src/env.ts +1 -1
  17. package/packages/core/src/errorOverlay.ts +39 -48
  18. package/packages/core/src/fakeData.ts +15 -0
  19. package/packages/core/src/index.ts +17 -6
  20. package/packages/core/src/logger.ts +892 -572
  21. package/packages/core/src/mcp.ts +9 -1
  22. package/packages/core/src/messenger.ts +31 -4
  23. package/packages/core/src/middleware.ts +169 -43
  24. package/packages/core/src/portTakeover.ts +232 -0
  25. package/packages/core/src/request.ts +57 -8
  26. package/packages/core/src/response.ts +67 -0
  27. package/packages/core/src/router.ts +35 -7
  28. package/packages/core/src/server.ts +450 -190
  29. package/packages/core/src/static.ts +81 -12
  30. package/packages/core/src/testClient.ts +126 -137
  31. package/packages/core/src/testing.ts +16 -12
  32. package/packages/core/src/types.ts +21 -9
  33. package/packages/core/src/version.ts +66 -0
  34. package/packages/core/src/websocket.ts +2 -2
  35. package/packages/core/src/websocketBackplane.ts +2 -2
  36. package/packages/frond/dist/index.js +31 -13
  37. package/packages/frond/src/engine.ts +39 -7
  38. package/packages/orm/dist/index.js +10879 -9258
  39. package/packages/orm/src/adapters/firebird.ts +200 -27
  40. package/packages/orm/src/adapters/mongodb.ts +160 -10
  41. package/packages/orm/src/adapters/mssql.ts +38 -11
  42. package/packages/orm/src/adapters/mysql.ts +24 -1
  43. package/packages/orm/src/adapters/odbc.ts +127 -29
  44. package/packages/orm/src/adapters/postgres.ts +18 -0
  45. package/packages/orm/src/adapters/sqlite.ts +93 -14
  46. package/packages/orm/src/autoCrud.ts +72 -8
  47. package/packages/orm/src/baseModel.ts +323 -71
  48. package/packages/orm/src/cachedDatabase.ts +48 -1
  49. package/packages/orm/src/database.ts +162 -59
  50. package/packages/orm/src/fakeData.ts +6 -2
  51. package/packages/orm/src/index.ts +4 -1
  52. package/packages/orm/src/migration.ts +95 -52
  53. package/packages/orm/src/query.ts +16 -4
  54. package/packages/orm/src/seeder.ts +43 -25
  55. package/packages/orm/src/sqlTranslator.ts +104 -19
  56. package/packages/orm/src/types.ts +97 -21
  57. package/packages/orm/src/validation.ts +5 -1
  58. package/packages/swagger/dist/index.js +3 -2
  59. package/packages/swagger/src/generator.ts +19 -4
  60. package/packages/swagger/src/ui.ts +6 -4
  61. package/types/cli/src/bin.d.ts +0 -22
  62. package/types/core/src/api.d.ts +11 -4
  63. package/types/core/src/background.d.ts +5 -2
  64. package/types/core/src/devAdmin.d.ts +35 -0
  65. package/types/core/src/dispatchPipeline.d.ts +41 -1
  66. package/types/core/src/errorOverlay.d.ts +13 -13
  67. package/types/core/src/index.d.ts +9 -6
  68. package/types/core/src/logger.d.ts +111 -185
  69. package/types/core/src/middleware.d.ts +40 -5
  70. package/types/core/src/portTakeover.d.ts +50 -0
  71. package/types/core/src/request.d.ts +15 -0
  72. package/types/core/src/response.d.ts +29 -0
  73. package/types/core/src/server.d.ts +92 -0
  74. package/types/core/src/testClient.d.ts +29 -3
  75. package/types/core/src/testing.d.ts +16 -12
  76. package/types/core/src/types.d.ts +21 -9
  77. package/types/core/src/version.d.ts +11 -0
  78. package/types/core/src/websocketBackplane.d.ts +1 -1
  79. package/types/frond/src/engine.d.ts +10 -0
  80. package/types/orm/src/adapters/firebird.d.ts +61 -2
  81. package/types/orm/src/adapters/mongodb.d.ts +20 -0
  82. package/types/orm/src/adapters/mssql.d.ts +11 -0
  83. package/types/orm/src/adapters/mysql.d.ts +11 -0
  84. package/types/orm/src/adapters/odbc.d.ts +35 -4
  85. package/types/orm/src/adapters/postgres.d.ts +11 -0
  86. package/types/orm/src/adapters/sqlite.d.ts +23 -4
  87. package/types/orm/src/baseModel.d.ts +45 -25
  88. package/types/orm/src/cachedDatabase.d.ts +27 -1
  89. package/types/orm/src/database.d.ts +56 -6
  90. package/types/orm/src/index.d.ts +3 -2
  91. package/types/orm/src/migration.d.ts +23 -5
  92. package/types/orm/src/query.d.ts +3 -0
  93. package/types/orm/src/seeder.d.ts +15 -2
  94. package/types/orm/src/sqlTranslator.d.ts +17 -4
  95. package/types/orm/src/types.d.ts +75 -16
  96. package/packages/core/src/errorOverlay.test.ts +0 -122
@@ -1,4 +1,4 @@
1
- export type FieldType = "string" | "integer" | "number" | "numeric" | "boolean" | "datetime" | "text" | "json" | "foreignKey";
1
+ export type FieldType = "string" | "integer" | "number" | "numeric" | "decimal" | "boolean" | "datetime" | "text" | "json" | "foreignKey";
2
2
 
3
3
  export interface FieldDefinition {
4
4
  type: FieldType;
@@ -11,6 +11,15 @@ export interface FieldDefinition {
11
11
  min?: number;
12
12
  max?: number;
13
13
  pattern?: string;
14
+ /**
15
+ * For type "decimal": the fixed precision/scale of a real DECIMAL(p, s)
16
+ * column. `number`/`numeric` stay a floating type (the documented money
17
+ * guidance); a `decimal` field keeps the declared scale in the COLUMN, so
18
+ * createTable emits DECIMAL(precision, scale) — identical on
19
+ * PostgreSQL/MySQL/MSSQL/Firebird/SQLite. Default 10 / 2 when omitted.
20
+ */
21
+ precision?: number;
22
+ scale?: number;
14
23
  /** For type "foreignKey": the referenced model name (string) */
15
24
  references?: string;
16
25
  /** For type "foreignKey": override the has-many property name on the referenced model */
@@ -54,6 +63,12 @@ export interface ColumnInfo {
54
63
  nullable?: boolean;
55
64
  default?: unknown;
56
65
  primaryKey?: boolean;
66
+ /**
67
+ * ADR-0044 amendment (Feature 5 Decision 7, 2026-08-10): null for a
68
+ * non-key column; for a composite key this is the 1-based DECLARED
69
+ * PRIMARY KEY (...) order, not table-column order.
70
+ */
71
+ primaryKeyPosition?: number | null;
57
72
  }
58
73
 
59
74
  export interface DatabaseResult {
@@ -65,31 +80,62 @@ export interface DatabaseResult {
65
80
  error?: string;
66
81
  }
67
82
 
83
+ /**
84
+ * ADR-0044 (feature 3, plan/v3/fixtures/adapter_contract.json): the exact
85
+ * fourteen adapter capabilities every DatabaseAdapter implementation must
86
+ * provide (DBA-S01). Kept as data so the conformance suite can check it
87
+ * without a second hand-maintained copy of the list.
88
+ */
89
+ export const REQUIRED_ADAPTER_CAPABILITIES = [
90
+ "connect", "close", "getDatabaseType",
91
+ "execute", "executeMany", "fetch", "fetchOne",
92
+ "startTransaction", "commit", "rollback", "autocommit",
93
+ "getTables", "getColumns", "tableExists",
94
+ ] as const;
95
+
96
+ /**
97
+ * ADR-0044 NOT_REQUIRED_ON_ADAPTER (DBA-S03): engine-neutral composition that
98
+ * the adapter CONTRACT does not require. Node keeps these as REQUIRED
99
+ * TypeScript interface members anyway (unlike Python/PHP/Ruby's stricter
100
+ * runtime reflection) because `database.ts`/`cachedDatabase.ts` call them
101
+ * directly at dozens of sites with no optional-chaining guard, so making them
102
+ * TS-optional ripples into a much larger refactor than this pass covers;
103
+ * this constant records the ADR's INTENT for the conformance suite to check
104
+ * even though the compiler will not enforce their absence.
105
+ */
106
+ export const NOT_REQUIRED_ON_ADAPTER = [
107
+ "query", "insert", "update", "delete", "truncate", "fetchAll",
108
+ "createTable", "addColumn", "lastInsertId", "error", "sqlTranslation",
109
+ ] as const;
110
+
68
111
  export interface DatabaseAdapter {
112
+ /** Connect (ADR-0044 canonical lifecycle name). May be sync or async. */
113
+ connect(): void | Promise<void>;
114
+
115
+ /** Return the canonical, credential-free engine name ("sqlite", "postgres", ...). */
116
+ getDatabaseType(): string;
117
+
69
118
  /** Execute a statement (INSERT, UPDATE, DELETE, DDL). */
70
119
  execute(sql: string, params?: unknown[]): unknown;
71
120
 
72
- /** Execute a single SQL statement with multiple parameter sets (batch). */
73
- executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastId?: number | bigint };
74
-
75
- /** Query rows. */
76
- query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
121
+ /**
122
+ * Execute a single SQL statement with multiple parameter sets as ONE
123
+ * aggregate result (ADR-0044). The shared write shape `DatabaseResult`
124
+ * ({success, affectedRows, lastId?}) is the target; the pre-ADR-0044 async
125
+ * adapters (Postgres/MySQL/MSSQL/Firebird/Mongo) still return their
126
+ * original `{totalAffected, lastId?}` shape internally today — the union
127
+ * covers both while `adapterExecuteMany()` in database.ts normalises
128
+ * whichever shape it receives at the one chokepoint every public batch
129
+ * write flows through.
130
+ */
131
+ executeMany(sql: string, paramsList: unknown[][]): DatabaseResult | { totalAffected: number; lastId?: number | bigint };
77
132
 
78
- /** Fetch rows with optional pagination (limit/skip). */
133
+ /** Fetch rows with optional pagination (limit/skip). Native list, no envelope. */
79
134
  fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[];
80
135
 
81
- /** Fetch a single row or null. */
136
+ /** Fetch a single row or null. No pagination count probe. */
82
137
  fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null;
83
138
 
84
- /** Insert one or more rows into a table, returns result with lastId. */
85
- insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
86
-
87
- /** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
88
- update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
89
-
90
- /** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
91
- delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
92
-
93
139
  /** Start a transaction. */
94
140
  startTransaction(): void;
95
141
 
@@ -99,24 +145,54 @@ export interface DatabaseAdapter {
99
145
  /** Rollback the current transaction. */
100
146
  rollback(): void;
101
147
 
148
+ /**
149
+ * Native boolean, readable and writable. A plain field is the idiomatic JS
150
+ * shape for "readable and writable" — no getter/setter ceremony needed.
151
+ */
152
+ autocommit: boolean;
153
+
154
+ /**
155
+ * ADR-0044 / DBA-P02: whether this adapter's deployment can guarantee an
156
+ * atomic multi-row batch write. Every built-in adapter defaults true (see
157
+ * each adapter's field initializer); a deployment that genuinely cannot (a
158
+ * standalone MongoDB without a replica set is the motivating real case)
159
+ * sets this false so executeMany rejects BEFORE the first write.
160
+ */
161
+ supportsAtomicBatch?: boolean;
162
+
102
163
  /** List all tables in the database. */
103
164
  getTables(): string[];
104
165
 
105
166
  /** List columns with types for a table. */
106
167
  getColumns(table: string): ColumnInfo[];
107
168
 
108
- /** Get the last inserted id (auto-increment integer, or a UUID/string PK). */
109
- lastInsertId(): number | bigint | string | null;
110
-
111
169
  /** Close the connection. */
112
170
  close(): void;
113
171
 
114
172
  /** Check if a table exists. */
115
173
  tableExists(name: string): boolean;
116
174
 
117
- /** Create a table from field definitions. */
175
+ // -- Engine-neutral composition NOT part of the ADR-0044 CONTRACT, but
176
+ // kept as required TS members for now (see NOT_REQUIRED_ON_ADAPTER above).
177
+
178
+ /** Insert one or more rows into a table, returns result with lastId. */
179
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult;
180
+
181
+ /** Update rows in a table matching filter (object or string WHERE), returns affected row count. */
182
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown> | string, params?: unknown[]): DatabaseResult;
183
+
184
+ /** Delete rows from a table matching filter (object, string WHERE, or array of objects). */
185
+ delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[], params?: unknown[]): DatabaseResult;
186
+
187
+ /** Query rows (legacy convenience, superseded by fetch). */
188
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
189
+
190
+ /** Create a table from field definitions (legacy, DDL composition lives above the adapter). */
118
191
  createTable(name: string, columns: Record<string, FieldDefinition>): void;
119
192
 
193
+ /** Get the last inserted id (legacy — prefer DatabaseResult.lastId from execute/executeMany). */
194
+ lastInsertId(): number | bigint | string | null;
195
+
120
196
  /** Get raw column info (legacy, used by migration). */
121
197
  getTableColumns?(name: string): Array<{ name: string; type: string }>;
122
198
 
@@ -50,7 +50,11 @@ export function validate(
50
50
  }
51
51
  const regex = compiledPatterns.get(name);
52
52
  if (regex && !regex.test(value)) {
53
- errors.push({ field: name, message: `does not match required pattern` });
53
+ // Feature 19 (VALID-TWO-MESSAGES): one canonical wording per rule
54
+ // across BOTH validators. The request Validator says "does not match
55
+ // the required format"; the ORM validator must say the same so a
56
+ // client keying on the message matches either surface.
57
+ errors.push({ field: name, message: `does not match the required format` });
54
58
  }
55
59
  }
56
60
  break;
@@ -1,5 +1,6 @@
1
1
  // src/generator.ts
2
2
  var WRITE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
3
+ var INTERNAL_PREFIXES = ["/swagger", "/__dev", "/__feedback", "/ai", "/rag", "/vision", "/embed", "/image"];
3
4
  var registeredSchemes = {};
4
5
  var registeredSchemas = {};
5
6
  function addSecurityScheme(name, definition) {
@@ -239,7 +240,7 @@ function routeRequiresAuth(route, method) {
239
240
  return route.secure === true;
240
241
  }
241
242
  function isIncludedPath(rawPath, include, exclude) {
242
- for (const internal of ["/swagger", "/__dev"]) {
243
+ for (const internal of INTERNAL_PREFIXES) {
243
244
  if (rawPath === internal || rawPath.startsWith(internal + "/")) return false;
244
245
  }
245
246
  if (include.length > 0 && !include.some((p) => rawPath === p || rawPath.startsWith(p))) {
@@ -440,7 +441,7 @@ function uniqueOperationId(method, openApiPath, seen) {
440
441
 
441
442
  // src/ui.ts
442
443
  function swaggerUiCdn() {
443
- return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://unpkg.com/swagger-ui-dist@5").replace(/\/+$/, "");
444
+ return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
444
445
  }
445
446
  var SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
446
447
  <html lang="en">
@@ -20,6 +20,21 @@ interface OpenAPISpec {
20
20
 
21
21
  const WRITE_METHODS = new Set(["post", "put", "patch", "delete"]);
22
22
 
23
+ /**
24
+ * Framework-internal route prefixes that are NEVER part of an application's
25
+ * public API document. SHARED across all four frameworks (SWAG-EXCLUSION-NOT-
26
+ * SHARED, ADR-0004) so the exclusion is one rule everywhere, not three
27
+ * mechanisms: the dev tools (/swagger, /__dev), the feedback widget
28
+ * (/__feedback), and the built-in AI/RAG service probes (/ai, /rag, /vision,
29
+ * /embed, /image). This is the ONE thing standing between `/__feedback/*`
30
+ * (genuinely registered into the router by DevAdmin.register -> feedback.ts)
31
+ * and the public document now that generate() reads the LIVE route table per
32
+ * request (SWAG-NODE-FEEDBACK-LEAK) — before this list carried only
33
+ * /swagger + /__dev, so `/__feedback` was excluded only by BOOT ORDERING
34
+ * (swagger's route snapshot predated DevAdmin.register), not by a rule.
35
+ */
36
+ const INTERNAL_PREFIXES = ["/swagger", "/__dev", "/__feedback", "/ai", "/rag", "/vision", "/embed", "/image"];
37
+
23
38
  // ── Configuration registries (v3.13.42) ───────────────────────────
24
39
  // Process-wide registries for security schemes and reusable component schemas
25
40
  // declared programmatically (addSecurityScheme / addSchema). Kept module-level so
@@ -365,12 +380,12 @@ function routeRequiresAuth(route: RouteDefinition, method: string): boolean {
365
380
  }
366
381
 
367
382
  /**
368
- * Path-filter a raw route pattern. Framework internals (/swagger, /__dev) are
369
- * ALWAYS excluded; then TINA4_SWAGGER_INCLUDE (allow-list) / _EXCLUDE apply.
370
- * Mirrors Python's _included.
383
+ * Path-filter a raw route pattern. Framework internals (INTERNAL_PREFIXES)
384
+ * are ALWAYS excluded; then TINA4_SWAGGER_INCLUDE (allow-list) / _EXCLUDE
385
+ * apply. Mirrors the other three frameworks' _included/included?.
371
386
  */
372
387
  function isIncludedPath(rawPath: string, include: string[], exclude: string[]): boolean {
373
- for (const internal of ["/swagger", "/__dev"]) {
388
+ for (const internal of INTERNAL_PREFIXES) {
374
389
  if (rawPath === internal || rawPath.startsWith(internal + "/")) return false;
375
390
  }
376
391
  if (include.length > 0 && !include.some((p) => rawPath === p || rawPath.startsWith(p))) {
@@ -1,11 +1,13 @@
1
1
  import type { Tina4Request, Tina4Response, RouteDefinition } from "../../core/src/index.js";
2
2
 
3
3
  // The UI assets load from a CDN by default (a documented architecture decision —
4
- // we don't vendor ~1.4MB of swagger-ui-dist, to stay small). Air-gapped
5
- // deployments point TINA4_SWAGGER_UI_CDN at a self-hosted mirror (a base URL
6
- // serving swagger-ui.css + swagger-ui-bundle.js).
4
+ // we don't vendor ~1.4MB of swagger-ui-dist, to stay small). jsdelivr
5
+ // (SWAG-CDN-NO-SRI, ADR-0004) the SAME default as the Python and Ruby
6
+ // masters, so all four frameworks pull the UI bundle from one CDN rather than
7
+ // splitting jsdelivr/unpkg. Air-gapped deployments point TINA4_SWAGGER_UI_CDN
8
+ // at a self-hosted mirror (a base URL serving swagger-ui.css + swagger-ui-bundle.js).
7
9
  function swaggerUiCdn(): string {
8
- return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://unpkg.com/swagger-ui-dist@5").replace(/\/+$/, "");
10
+ return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
9
11
  }
10
12
 
11
13
  const SWAGGER_UI_HTML = (specUrl: string) => `<!DOCTYPE html>
@@ -1,25 +1,3 @@
1
- /**
2
- * Kill any process listening on `port`. Returns true if anything was killed.
3
- *
4
- * Every PID is validated first. `parseInt` on a non-numeric lsof field yields
5
- * 1, and SIGTERM to PID 1 is the container's own init -- which is exactly how a
6
- * production container logged "Killed existing process on port 7148 (PID: 1
7
- * ...)" and then exited 143, killing itself on startup.
8
- */
9
- /**
10
- * The PIDs from `lsof -ti` output that are safe to signal.
11
- *
12
- * Pure so the safety rule can be tested directly. An unvalidated parse is a
13
- * footgun with real teeth: where lsof prints a different shape than -ti
14
- * implies, a non-numeric field becomes 0, and signalling PID 0 hits EVERY
15
- * process in the caller's own process group -- the server kills itself. That
16
- * is what produced "Killed existing process on port 7148 (PID: 1 ...)" in a
17
- * real image, where the container then exited 143.
18
- *
19
- * Accepts only all-digit tokens; never PID 0 (our group), PID 1 (init),
20
- * ourselves, or our own process group.
21
- */
22
- export declare function selectablePids(lsofOutput: string, me: number, myGroup?: number): number[];
23
1
  export interface CommandManifestEntry {
24
2
  name: string;
25
3
  summary: string;
@@ -211,10 +211,17 @@ export declare class Api {
211
211
  download(path: string, destPath: string, params?: Record<string, string>): Promise<DownloadResult>;
212
212
  private buildUrl;
213
213
  /**
214
- * Build the request headers (auth + cookie jar + extras) and serialize the
215
- * body to a Buffer. Shared by every verb, upload, and download so the wire
216
- * shape is identical and the transport seam sees exactly what the network
217
- * path would.
214
+ * Build the request headers (default User-Agent + auth + cookie jar +
215
+ * extras) and serialize the body to a Buffer. Shared by every verb,
216
+ * upload, and download so the wire shape is identical and the transport
217
+ * seam sees exactly what the network path would.
218
+ *
219
+ * VERSION-DEC-03 (feature 130): every outbound request carries a default
220
+ * `Tina4/<version>` User-Agent. `this.headers` is spread AFTER the
221
+ * default, and `extraHeaders` after that, so a caller-supplied
222
+ * `User-Agent` (via the constructor's `headers` option, `addHeaders()`,
223
+ * or a per-call `extraHeaders`) always wins -- this is a default, never a
224
+ * clobber.
218
225
  */
219
226
  private buildRequest;
220
227
  /**
@@ -19,10 +19,13 @@
19
19
  *
20
20
  * @param callback Function to call (sync or async, no arguments).
21
21
  * @param intervalSeconds Seconds between invocations (default: 1).
22
- * @returns A handle whose `stop()` clears just this one task.
22
+ * @returns A handle whose `stop()` clears just this one task and returns whether
23
+ * it removed a live task (true) or was already stopped (false). This is
24
+ * the ONE background surface — a stop-handle plus a count — shared with
25
+ * Python/PHP/Ruby (`handle.stop()` -> bool, `backgroundTaskCount()`).
23
26
  */
24
27
  export declare function background(callback: () => unknown | Promise<unknown>, intervalSeconds?: number): {
25
- stop: () => void;
28
+ stop: () => boolean;
26
29
  };
27
30
  /**
28
31
  * Clear every registered background task. Called by the server's graceful
@@ -9,6 +9,41 @@
9
9
  * - System info (Node.js version, V8, memory, uptime, platform)
10
10
  */
11
11
  import type { Router } from "./router.js";
12
+ import type { Tina4Request } from "./types.js";
13
+ /** Safe HTTP methods that never carry a state change — they skip the write gate. */
14
+ export declare const DEV_SAFE_METHODS: Set<string>;
15
+ /**
16
+ * Fail-closed same-origin check for a dev-admin mutation (DEVADMIN-DEC-01).
17
+ *
18
+ * A drive-by CSRF is a BROWSER cross-origin request, and a modern browser always
19
+ * sends `Sec-Fetch-Site` (and any browser sends `Origin` on a cross-origin POST):
20
+ * - Sec-Fetch-Site present -> trust the browser's classification
21
+ * (cross-site refused; same-origin / same-site / none ok).
22
+ * - else Origin present -> require its host to match the request Host.
23
+ * - else neither header -> not a browser cross-origin request (curl, a test
24
+ * client, a server-side caller); it cannot be a drive-by, so allow here and
25
+ * let the loopback gate still constrain the peer.
26
+ */
27
+ export declare function devSameOriginOk(req: Tina4Request): boolean;
28
+ /**
29
+ * Return `{status, error}` to REFUSE a dev-admin write, or `null` to allow.
30
+ *
31
+ * Two independent fail-closed gates on every /__dev mutation:
32
+ * DEVADMIN-DEC-01 same-origin (all writes, incl. mcp/call) - drive-by CSRF.
33
+ * DEVADMIN-DEC-02 loopback peer (all writes EXCEPT the MCP surface, which
34
+ * carries its own gate) - a network-exposed debug box.
35
+ * Reads the RAW socket peer (never X-Forwarded-For), exactly like the MCP gate.
36
+ */
37
+ export declare function devMutationDenial(req: Tina4Request): {
38
+ status: number;
39
+ error: string;
40
+ } | null;
41
+ /**
42
+ * True when `rel` names secret material the file endpoints must never serve
43
+ * (DEVADMIN-DEC-03): `.env` / `.env.*` (the `.env.example` template is allowed),
44
+ * anything under `.git/` or `secrets/`, and private-key material.
45
+ */
46
+ export declare function isSecretPath(rel: string): boolean;
12
47
  interface LogEntry {
13
48
  id: string;
14
49
  timestamp: string;
@@ -34,7 +34,7 @@ import type { Tina4Request } from "./types.js";
34
34
  * The prologue, in order. Exported as DATA so the pipeline can be asserted and
35
35
  * compared across frameworks without reading an implementation.
36
36
  */
37
- export declare const PROLOGUE_STAGES: readonly ["resetRequestCaches", "headStripIntercept", "sessionAutoStart"];
37
+ export declare const PROLOGUE_STAGES: readonly ["resetRequestCaches", "headStripIntercept", "compressionEtagIntercept", "sessionAutoStart"];
38
38
  /**
39
39
  * After the prologue, before a route is looked up.
40
40
  *
@@ -98,6 +98,46 @@ export declare function resetRequestCaches(): Promise<void>;
98
98
  * @param rawRes Node's server response, whose write/end are replaced in place
99
99
  */
100
100
  export declare function headStripIntercept(rawReq: IncomingMessage, rawRes: ServerResponse): void;
101
+ /**
102
+ * Gzip-compress + attach an ETag, and answer a matching conditional GET with
103
+ * a 304 that PRESERVES whichever validators the 200 would have carried
104
+ * (feature 40, CE-DEC-01/02). Mirrors the Python master's `build_headers()` +
105
+ * `app()` dispatch — the ONE header-builder step every DYNAMIC response
106
+ * funnels through.
107
+ *
108
+ * Node has no single "build the response, then send it" object the way
109
+ * Python/PHP/Ruby do — every response.ts method (json/html/text/xml/send/
110
+ * file/render) calls `res.end()` directly. So this intercepts `write`/`end`
111
+ * on the raw `ServerResponse` and buffers the body until `end()` is finally
112
+ * called, which is the only point a COMPLETE body — and therefore a
113
+ * Content-Length, a gzip candidate, and an ETag — exists to compute.
114
+ *
115
+ * BYPASS: a response that has ALREADY sent its headers by the time
116
+ * write()/end() is first called here (checked via `rawRes.headersSent`) is a
117
+ * streaming response — `response.ts`'s `stream()` calls `res.raw.writeHead()`
118
+ * up front, before any chunk — and is passed straight through unbuffered,
119
+ * exactly like Python's "streaming responses bypass ETag/compression".
120
+ *
121
+ * Installed in the PROLOGUE, right after `headStripIntercept`: since the LAST
122
+ * installed wrapper runs FIRST when `end()` is finally called, and
123
+ * `wrapResponseEnd` (dev-toolbar/feedback injection) installs LATER (in the
124
+ * REQUEST stage), the real execution order at send time is
125
+ * injection -> this -> `headStripIntercept` -> the true Node `res.end()` — so
126
+ * a HEAD response's preserved Content-Length reflects the (possibly
127
+ * compressed) body the equivalent GET would have sent, and the injected
128
+ * bytes are included in the compressed body + ETag hash, matching Python's
129
+ * ordering exactly.
130
+ *
131
+ * A static-file response (`static.ts`) still funnels through this same
132
+ * intercepted `write`/`end` — it pins its own weak size+mtime ETag and (when
133
+ * eligible) compresses itself BEFORE calling `res.raw.end()`, so by the time
134
+ * this runs, its status is already 200-with-ETag-set (this never overwrites
135
+ * it) or already 304 (the `statusCode === 200` guard below leaves it alone).
136
+ *
137
+ * @param rawReq Node's incoming message, read for Accept-Encoding / If-None-Match / If-Modified-Since
138
+ * @param rawRes Node's server response, whose write/end are replaced in place
139
+ */
140
+ export declare function compressionEtagIntercept(rawReq: IncomingMessage, rawRes: ServerResponse): void;
101
141
  /**
102
142
  * Auto-start the session: read the cookie, create the session, then save it and
103
143
  * set the cookie when the response ends.
@@ -1,22 +1,26 @@
1
1
  /**
2
2
  * Tina4 Debug — Rich error overlay for development mode.
3
3
  *
4
- * Renders a professional, syntax-highlighted HTML error page when an unhandled
5
- * exception occurs in a route handler.
4
+ * Renders a rich HTML error page (exception type + message, the full stack with a
5
+ * seven-line source window per frame, request details, environment) when an unhandled
6
+ * exception reaches the server dispatch in development.
6
7
  *
7
- * import { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
8
+ * import { renderErrorOverlay, isDebugMode } from "./errorOverlay.js";
8
9
  *
9
10
  * try {
10
11
  * await handler(req, res);
11
12
  * } catch (err) {
12
- * const html = isDebugMode()
13
- * ? renderErrorOverlay(err as Error, req)
14
- * : renderProductionError();
15
- * res.html(html, 500);
13
+ * if (isDebugMode()) res.html(renderErrorOverlay(err as Error, req), 500);
16
14
  * }
17
15
  *
18
- * Only activate when TINA4_DEBUG is true.
19
- * In production, call renderProductionError() instead.
16
+ * Dev-only: the caller gates this on isDebugMode() (TINA4_DEBUG). The production 500 is
17
+ * NOT rendered here the server dispatch renders errors/500.twig with an empty
18
+ * error_message (CWE-209), so the exception detail stays in the server log only.
19
+ *
20
+ * Sensitive request fields (Authorization / Cookie / Set-Cookie headers and
21
+ * password-like body/param keys) are redacted even in the dev overlay, the frame count
22
+ * is capped, and the caller wraps this render in a guard, so a broken overlay or a
23
+ * recursive stack still yields a bounded, safe 500.
20
24
  */
21
25
  /**
22
26
  * Render a rich HTML error overlay.
@@ -26,10 +30,6 @@
26
30
  * @returns Complete HTML page string.
27
31
  */
28
32
  export declare function renderErrorOverlay(error: Error, request?: any): string;
29
- /**
30
- * Render a safe, generic error page for production.
31
- */
32
- export declare function renderProductionError(statusCode?: number, message?: string, path?: string): string;
33
33
  /**
34
34
  * Check if TINA4_DEBUG is enabled.
35
35
  */
@@ -5,14 +5,14 @@ export { Router, RouteGroup, RouteRef, WsRouteRef, defaultRouter, runRouteMiddle
5
5
  export { get, post, put, patch, del, any, websocket, del as delete } from "./router.js";
6
6
  export type { RouteInfo } from "./router.js";
7
7
  export { discoverRoutes } from "./routeDiscovery.js";
8
- export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware } from "./middleware.js";
8
+ export { MiddlewareChain, MiddlewareRunner, cors, requestLogger, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware, CsrfMiddleware, attachCsrfFromEnv } from "./middleware.js";
9
9
  export type { CorsConfig } from "./middleware.js";
10
- export { createRequest, makeCaseInsensitiveHeaders } from "./request.js";
11
- export { createResponse, errorResponse, setDefaultTemplatesDir, getFrond, setFrond, getFrameworkFrond } from "./response.js";
10
+ export { createRequest, makeCaseInsensitiveHeaders, parseMultipart, saveUpload } from "./request.js";
11
+ export { createResponse, errorResponse, setDefaultTemplatesDir, getFrond, setFrond, getFrameworkFrond, acceptPrefersJson, wantsJson, negotiatedErrorBody, } from "./response.js";
12
12
  export { tryServeStatic } from "./static.js";
13
13
  export { loadEnv, getEnv, requireEnv, hasEnv, allEnv, resetEnv, isTruthy } from "./dotenv.js";
14
14
  export { Env } from "./env.js";
15
- export { Log } from "./logger.js";
15
+ export { Log, LogConfigurationError, LogArgumentError, LogWriteError } from "./logger.js";
16
16
  export { createHealthRoute, createHealthRoutes, healthPath } from "./health.js";
17
17
  export { rateLimiter } from "./rateLimiter.js";
18
18
  export { isTrustedProxy, trustedProxyNetworks, resolveClientIp, resetTrustedProxyCache } from "./trustedProxy.js";
@@ -52,7 +52,7 @@ export { DevMailbox } from "./devMailbox.js";
52
52
  export { WSDLService, WSDLOperation } from "./wsdl.js";
53
53
  export type { WSDLOperationMeta } from "./wsdl.js";
54
54
  export { HtmlElement, htmlElement, addHtmlHelpers, Raw, SafeString } from "./htmlElement.js";
55
- export { renderErrorOverlay, renderProductionError, isDebugMode } from "./errorOverlay.js";
55
+ export { renderErrorOverlay, isDebugMode } from "./errorOverlay.js";
56
56
  export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
57
57
  export type { AiTool } from "./ai.js";
58
58
  export type { ImapMessage, ImapFullMessage, ImapAttachment } from "./messenger.js";
@@ -69,7 +69,7 @@ export { MongoSessionHandler } from "./sessionHandlers/mongoHandler.js";
69
69
  export type { MongoSessionConfig } from "./sessionHandlers/mongoHandler.js";
70
70
  export { ValkeySessionHandler } from "./sessionHandlers/valkeyHandler.js";
71
71
  export type { ValkeySessionConfig } from "./sessionHandlers/valkeyHandler.js";
72
- export { tests, assertEqual, assertRaises, assertTrue, assertFalse, runAll, reset } from "./testing.js";
72
+ export { tests, expectEqual, expectRaises, expectTrue, expectFalse, runAll, reset } from "./testing.js";
73
73
  export { TestClient, TestResponse } from "./testClient.js";
74
74
  export { Tina4Test, AssertionError as Tina4AssertionError } from "./test.js";
75
75
  export type { TestRunResults } from "./test.js";
@@ -88,3 +88,6 @@ export type { FileEntry, FileRoute } from "./projectIndex.js";
88
88
  export { Docs } from "./docs.js";
89
89
  export type { DocsHit, ClassSpec, MethodSpec, IndexEntry, DriftHit } from "./docs.js";
90
90
  export { writeMcpDiscovery } from "./docsAutoDiscovery.js";
91
+ export { takeOverPort, selectablePids, inContainer, isDev, noTakeoverOptedOut, writePidfile, readPidfile, removePidfile, pidfilePath, runtimeDir, TAKEOVER_NOTHING, TAKEOVER_KILLED, TAKEOVER_REFUSED_FOREIGN, TAKEOVER_REFUSED_OPTOUT, TAKEOVER_REFUSED_PROD, TAKEOVER_SKIPPED_CONTAINER, TAKEOVER_REFUSALS, } from "./portTakeover.js";
92
+ export type { TakeoverResult } from "./portTakeover.js";
93
+ export { resolveFrameworkVersion, TINA4_VERSION } from "./version.js";