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.
- package/CLAUDE.md +60 -25
- package/package.json +1 -2
- package/packages/cli/dist/bin.js +20620 -18995
- package/packages/cli/src/bin.ts +28 -71
- package/packages/cli/src/commands/migrate.ts +36 -75
- package/packages/cli/src/commands/migrateRollback.ts +10 -1
- package/packages/cli/src/commands/test.ts +92 -21
- package/packages/core/dist/index.js +20459 -18815
- package/packages/core/public/js/tina4-dev-admin.min.js +23 -19
- package/packages/core/src/ai.ts +28 -12
- package/packages/core/src/api.ts +13 -5
- package/packages/core/src/background.ts +9 -3
- package/packages/core/src/devAdmin.ts +135 -20
- package/packages/core/src/dispatchPipeline.ts +185 -1
- package/packages/core/src/docs.ts +33 -5
- package/packages/core/src/env.ts +1 -1
- package/packages/core/src/errorOverlay.ts +39 -48
- package/packages/core/src/fakeData.ts +15 -0
- package/packages/core/src/index.ts +17 -6
- package/packages/core/src/logger.ts +892 -572
- package/packages/core/src/mcp.ts +9 -1
- package/packages/core/src/messenger.ts +31 -4
- package/packages/core/src/middleware.ts +169 -43
- package/packages/core/src/portTakeover.ts +232 -0
- package/packages/core/src/request.ts +57 -8
- package/packages/core/src/response.ts +67 -0
- package/packages/core/src/router.ts +35 -7
- package/packages/core/src/server.ts +450 -190
- package/packages/core/src/static.ts +81 -12
- package/packages/core/src/testClient.ts +126 -137
- package/packages/core/src/testing.ts +16 -12
- package/packages/core/src/types.ts +21 -9
- package/packages/core/src/version.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketBackplane.ts +2 -2
- package/packages/frond/dist/index.js +31 -13
- package/packages/frond/src/engine.ts +39 -7
- package/packages/orm/dist/index.js +10879 -9258
- package/packages/orm/src/adapters/firebird.ts +200 -27
- package/packages/orm/src/adapters/mongodb.ts +160 -10
- package/packages/orm/src/adapters/mssql.ts +38 -11
- package/packages/orm/src/adapters/mysql.ts +24 -1
- package/packages/orm/src/adapters/odbc.ts +127 -29
- package/packages/orm/src/adapters/postgres.ts +18 -0
- package/packages/orm/src/adapters/sqlite.ts +93 -14
- package/packages/orm/src/autoCrud.ts +72 -8
- package/packages/orm/src/baseModel.ts +323 -71
- package/packages/orm/src/cachedDatabase.ts +48 -1
- package/packages/orm/src/database.ts +162 -59
- package/packages/orm/src/fakeData.ts +6 -2
- package/packages/orm/src/index.ts +4 -1
- package/packages/orm/src/migration.ts +95 -52
- package/packages/orm/src/query.ts +16 -4
- package/packages/orm/src/seeder.ts +43 -25
- package/packages/orm/src/sqlTranslator.ts +104 -19
- package/packages/orm/src/types.ts +97 -21
- package/packages/orm/src/validation.ts +5 -1
- package/packages/swagger/dist/index.js +3 -2
- package/packages/swagger/src/generator.ts +19 -4
- package/packages/swagger/src/ui.ts +6 -4
- package/types/cli/src/bin.d.ts +0 -22
- package/types/core/src/api.d.ts +11 -4
- package/types/core/src/background.d.ts +5 -2
- package/types/core/src/devAdmin.d.ts +35 -0
- package/types/core/src/dispatchPipeline.d.ts +41 -1
- package/types/core/src/errorOverlay.d.ts +13 -13
- package/types/core/src/index.d.ts +9 -6
- package/types/core/src/logger.d.ts +111 -185
- package/types/core/src/middleware.d.ts +40 -5
- package/types/core/src/portTakeover.d.ts +50 -0
- package/types/core/src/request.d.ts +15 -0
- package/types/core/src/response.d.ts +29 -0
- package/types/core/src/server.d.ts +92 -0
- package/types/core/src/testClient.d.ts +29 -3
- package/types/core/src/testing.d.ts +16 -12
- package/types/core/src/types.d.ts +21 -9
- package/types/core/src/version.d.ts +11 -0
- package/types/core/src/websocketBackplane.d.ts +1 -1
- package/types/frond/src/engine.d.ts +10 -0
- package/types/orm/src/adapters/firebird.d.ts +61 -2
- package/types/orm/src/adapters/mongodb.d.ts +20 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +11 -0
- package/types/orm/src/adapters/odbc.d.ts +35 -4
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlite.d.ts +23 -4
- package/types/orm/src/baseModel.d.ts +45 -25
- package/types/orm/src/cachedDatabase.d.ts +27 -1
- package/types/orm/src/database.d.ts +56 -6
- package/types/orm/src/index.d.ts +3 -2
- package/types/orm/src/migration.d.ts +23 -5
- package/types/orm/src/query.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +15 -2
- package/types/orm/src/sqlTranslator.d.ts +17 -4
- package/types/orm/src/types.d.ts +75 -16
- 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
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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://
|
|
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 (
|
|
369
|
-
* ALWAYS excluded; then TINA4_SWAGGER_INCLUDE (allow-list) / _EXCLUDE
|
|
370
|
-
* Mirrors
|
|
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
|
|
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).
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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://
|
|
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>
|
package/types/cli/src/bin.d.ts
CHANGED
|
@@ -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;
|
package/types/core/src/api.d.ts
CHANGED
|
@@ -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 +
|
|
215
|
-
* body to a Buffer. Shared by every verb,
|
|
216
|
-
*
|
|
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: () =>
|
|
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
|
|
5
|
-
*
|
|
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,
|
|
8
|
+
* import { renderErrorOverlay, isDebugMode } from "./errorOverlay.js";
|
|
8
9
|
*
|
|
9
10
|
* try {
|
|
10
11
|
* await handler(req, res);
|
|
11
12
|
* } catch (err) {
|
|
12
|
-
*
|
|
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
|
-
*
|
|
19
|
-
*
|
|
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,
|
|
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,
|
|
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";
|