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,200 +1,126 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured logger for Tina4.
|
|
2
|
+
* Structured logger for Tina4. Conformant to the shared cross-framework
|
|
3
|
+
* contract at plan/v3/fixtures/logger_contract.json (feature 2), decided in
|
|
4
|
+
* plan/v3/features/002-structured-logger.md and ADR-0041, with the
|
|
5
|
+
* 2026-08-10 override of decisions 8 and 20 (separate console/file levels;
|
|
6
|
+
* single-file, in-process lock only).
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* flipped BOTH sinks to JSON here, and "production" meant four different things
|
|
7
|
-
* across the four frameworks (Node: !TINA4_DEBUG; Ruby: TINA4_ENV/RACK_ENV/
|
|
8
|
-
* RUBY_ENV == "production"; Python: only configure(production=True); PHP: no
|
|
9
|
-
* switch at all, JSON always) — same machine, same .env, four log formats. That
|
|
10
|
-
* implicit switch is deleted; an object passed as the message is still
|
|
11
|
-
* JSON-encoded INLINE inside the text line, which is the only JSON a default
|
|
12
|
-
* install emits.
|
|
8
|
+
* Rewritten 2026-08-13 alongside the shared conformance pass. Node's OWN
|
|
9
|
+
* adaptations, each real and each documented at the point it applies:
|
|
13
10
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* TINA4_LOG_LEVEL — minimum console level: DEBUG | INFO | WARNING | ERROR | CRITICAL (default: "INFO")
|
|
30
|
-
* TINA4_LOG_STRICT — truthy: a log-write failure THROWS instead of being swallowed (default: off)
|
|
31
|
-
*
|
|
32
|
-
* Every one of these is read LAZILY, on each log() call — a script, worker, CLI
|
|
33
|
-
* tool or test that never boots a server still gets the operator's configuration.
|
|
34
|
-
*
|
|
35
|
-
* Rotation is stdlib roll-your-own:
|
|
36
|
-
* - On each write, statSync the file. If size >= TINA4_LOG_ROTATE_SIZE, rotate.
|
|
37
|
-
* - app.log.{N-1} → app.log.{N}, …, app.log → app.log.1 via fs.renameSync.
|
|
38
|
-
* - Files beyond _KEEP are dropped via fs.unlinkSync.
|
|
39
|
-
* - _SIZE=0 disables rotation entirely.
|
|
11
|
+
* - Request id: AsyncLocalStorage, not a manual clear-in-finally. A
|
|
12
|
+
* request's id is scoped to the ALS `run()` call around its dispatch, so
|
|
13
|
+
* it cannot leak into a sibling request and needs no "clear" step for
|
|
14
|
+
* correctness (see runWithRequestId below).
|
|
15
|
+
* - Fork (Decision 12's fork-discard requirement): child_process.fork()
|
|
16
|
+
* spawns a genuinely NEW process re-executing the module from scratch —
|
|
17
|
+
* unlike POSIX fork() in Python/PHP/Ruby, it shares no memory with the
|
|
18
|
+
* parent, so there is nothing to inherit and nothing to discard. A child
|
|
19
|
+
* process starts with `activeSnapshot === null` by construction.
|
|
20
|
+
* - The in-process lock (Decision 20): synchronous fs calls on the event
|
|
21
|
+
* loop's single thread cannot interleave (log() never awaits), so the
|
|
22
|
+
* common case needs no lock at all. Where a real lock is exercised (a
|
|
23
|
+
* worker_threads writer, or the LOG-E05 conformance case), it is backed
|
|
24
|
+
* by a real SharedArrayBuffer + Atomics — Node's actual cross-thread
|
|
25
|
+
* synchronous primitive — not a simulation.
|
|
40
26
|
*/
|
|
27
|
+
export declare class LogConfigurationError extends Error {
|
|
28
|
+
setting?: string;
|
|
29
|
+
value?: unknown;
|
|
30
|
+
accepted?: string[];
|
|
31
|
+
sink?: string;
|
|
32
|
+
operation?: string;
|
|
33
|
+
constructor(message: string, opts?: {
|
|
34
|
+
setting?: string;
|
|
35
|
+
value?: unknown;
|
|
36
|
+
accepted?: string[];
|
|
37
|
+
sink?: string;
|
|
38
|
+
operation?: string;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export declare class LogArgumentError extends Error {
|
|
42
|
+
argument?: string;
|
|
43
|
+
accepted?: string[];
|
|
44
|
+
constructor(message: string, opts?: {
|
|
45
|
+
argument?: string;
|
|
46
|
+
accepted?: string[];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export declare class LogWriteError extends Error {
|
|
50
|
+
sink?: string;
|
|
51
|
+
operation?: string;
|
|
52
|
+
constructor(message: string, opts?: {
|
|
53
|
+
sink?: string;
|
|
54
|
+
operation?: string;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export type LogLevel = "ALL" | "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL" | "NONE";
|
|
58
|
+
interface ConfigureOptions {
|
|
59
|
+
logDir?: string;
|
|
60
|
+
logFile?: string;
|
|
61
|
+
level?: string;
|
|
62
|
+
fileLevel?: string;
|
|
63
|
+
format?: string;
|
|
64
|
+
output?: string;
|
|
65
|
+
rotateSize?: number;
|
|
66
|
+
rotateKeep?: number;
|
|
67
|
+
strict?: boolean;
|
|
68
|
+
caller?: boolean;
|
|
69
|
+
}
|
|
41
70
|
export declare class Log {
|
|
42
|
-
private static requestId;
|
|
43
71
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
72
|
+
* Configure the logger. Two-phase and transactional: the WHOLE candidate
|
|
73
|
+
* configuration is resolved and validated first (LOG-C07 — a bad setting
|
|
74
|
+
* never touches the filesystem and never disturbs the prior snapshot),
|
|
75
|
+
* then its sinks are opened (LOG-E01 — an inaccessible sink fails
|
|
76
|
+
* configuration, still without replacing the prior snapshot), and only on
|
|
77
|
+
* full success does it become the active configuration.
|
|
46
78
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* argument instead of what the operator set. Reading configuration must not
|
|
52
|
-
* write it. Keeping the explicit values in their own slot means resolution
|
|
53
|
-
* is explicit > env > default with the environment left intact and still
|
|
54
|
-
* readable.
|
|
55
|
-
*/
|
|
56
|
-
private static explicitLogDir;
|
|
57
|
-
private static explicitLogFile;
|
|
58
|
-
/**
|
|
59
|
-
* Re-read all log-related env vars. Called on every log() so tests that
|
|
60
|
-
* mutate process.env between calls see the new values without having to
|
|
61
|
-
* call configure() each time.
|
|
62
|
-
*/
|
|
63
|
-
private static readEnv;
|
|
64
|
-
/**
|
|
65
|
-
* The single console-threshold predicate: does a message at `level` clear
|
|
66
|
-
* the configured minimum console level? This is the ONE place level
|
|
67
|
-
* comparison lives — both the live log() gate and the public isEnabled()
|
|
68
|
-
* predicate call it, so they can never disagree about what actually prints.
|
|
69
|
-
*/
|
|
70
|
-
private static passesThreshold;
|
|
71
|
-
/**
|
|
72
|
-
* Return true if a message at `level` would pass the configured minimum
|
|
73
|
-
* console level (TINA4_LOG_LEVEL) — the same threshold that gates stdout.
|
|
79
|
+
* Log.configure() -> logs/tina4.log + logs/error.log
|
|
80
|
+
* Log.configure({ logDir: "/var/log/myapp" }) -> /var/log/myapp/tina4.log + error.log
|
|
81
|
+
* Log.configure({ logFile: "/var/log/myapp/app.log" }) -> that exact file, no error.log sibling
|
|
82
|
+
* Log.configure({ level: "debug", format: "text", output: "both", strict: true, caller: true })
|
|
74
83
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
84
|
+
* A plain string is also accepted as shorthand for `{ logDir: string }` —
|
|
85
|
+
* or `{ logFile: string }` when it looks like a file (has an extension and
|
|
86
|
+
* is not an existing directory), via the same targetIsFile heuristic
|
|
87
|
+
* configure() itself uses to split a bare TINA4_LOG_DIR that names a file.
|
|
79
88
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* }
|
|
83
|
-
*
|
|
84
|
-
* `level` is case-insensitive. "critical" is the highest severity (priority
|
|
85
|
-
* 4 > error 3) and flows through the ordinary threshold check like every
|
|
86
|
-
* other level — there is no toggle. It reuses the same passesThreshold()
|
|
87
|
-
* check the logger itself uses, so it never drifts from what print does.
|
|
88
|
-
*/
|
|
89
|
-
static isEnabled(level: string): boolean;
|
|
90
|
-
/**
|
|
91
|
-
* Set the current request ID for log correlation.
|
|
89
|
+
* Log.configure("/var/log/myapp") -> same as { logDir: "/var/log/myapp" }
|
|
90
|
+
* Log.configure("/var/log/myapp/app.log") -> same as { logFile: "/var/log/myapp/app.log" }
|
|
92
91
|
*/
|
|
92
|
+
static configure(options?: string | ConfigureOptions): void;
|
|
93
|
+
/** Forget the active configuration; the next use resolves fresh from the
|
|
94
|
+
* environment. Idempotent. */
|
|
95
|
+
static reset(): void;
|
|
96
|
+
/** A defensive copy of the effective, stable configuration (LOG-C10). */
|
|
97
|
+
static configuration(): Record<string, unknown>;
|
|
98
|
+
/** Run `fn` with `id` established as the request-scoped correlation id.
|
|
99
|
+
* Every log line inside `fn` — across every await — carries `id`, and two
|
|
100
|
+
* concurrent requests each keep their own; the id needs no explicit clear
|
|
101
|
+
* because it is scoped to this call, not to shared mutable state. */
|
|
102
|
+
static runWithRequestId<T>(id: string | undefined, fn: () => T): T;
|
|
93
103
|
static setRequestId(id: string | undefined): void;
|
|
94
|
-
/**
|
|
95
|
-
* Get the current request ID.
|
|
96
|
-
*/
|
|
97
104
|
static getRequestId(): string | undefined;
|
|
105
|
+
/** Explicitly end a request's correlation scope (LOG-Q01/A01 public
|
|
106
|
+
* surface). Equivalent to setRequestId(undefined) but named for parity
|
|
107
|
+
* with the other three frameworks' clear_request_id. */
|
|
108
|
+
static clearRequestId(): void;
|
|
109
|
+
static sanitizeRequestId(value: string | string[] | undefined | null): string | undefined;
|
|
98
110
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* configure() -> ./logs/tina4.log + ./logs/error.log
|
|
105
|
-
* configure("/var/log/myapp") -> /var/log/myapp/tina4.log + error.log
|
|
106
|
-
* configure("/var/log/myapp/app.log") -> that exact file (no error.log sibling)
|
|
107
|
-
* configure({ logDir, logFile }) -> the explicit object form still works
|
|
108
|
-
*
|
|
109
|
-
* A plain string used to be accepted and silently ignored, because only the
|
|
110
|
-
* object form was read - so the call that works in the other three
|
|
111
|
-
* frameworks produced no log file here and said nothing (feature 2 of the
|
|
112
|
-
* audit, D4). Both forms now work.
|
|
113
|
-
*/
|
|
114
|
-
static configure(options?: string | {
|
|
115
|
-
logDir?: string;
|
|
116
|
-
logFile?: string;
|
|
117
|
-
}): void;
|
|
118
|
-
/**
|
|
119
|
-
* Forget what configure() was told, so resolution falls back to the
|
|
120
|
-
* environment and then the built-in defaults. Parity with PHP's Log::reset().
|
|
121
|
-
*
|
|
122
|
-
* This exists because the explicit values are now HELD here rather than
|
|
123
|
-
* written back into process.env, and that makes them STICKY for the life of
|
|
124
|
-
* the process -- which is right for an application (configure() at boot is
|
|
125
|
-
* the operator's instruction and a later stray env write should not silently
|
|
126
|
-
* re-point the logs) and wrong for a long-lived test process that wants to
|
|
127
|
-
* drive the logger purely from the environment afterwards.
|
|
128
|
-
*
|
|
129
|
-
* I removed this method once for having no callers. That was correct about
|
|
130
|
-
* the grep and wrong about the code: the full suite is the caller. Before the
|
|
131
|
-
* explicit slots existed, configure() ASSIGNED to process.env, so a later
|
|
132
|
-
* direct assignment simply overwrote it and env-driven cases kept working by
|
|
133
|
-
* accident. test/logger.test.ts depends on exactly that -- it configures a
|
|
134
|
-
* file early, then runs the whole default-output block off the environment
|
|
135
|
-
* (see its own note: "these cases must NOT route through Log.configure").
|
|
136
|
-
* Three of those cases failed on the lab until this came back.
|
|
137
|
-
*/
|
|
138
|
-
static reset(): void;
|
|
139
|
-
/**
|
|
140
|
-
* TINA4_LOG_APPEND — append (default) or overwrite on startup.
|
|
141
|
-
*
|
|
142
|
-
* APPEND IS THE DEFAULT: a log you can lose by restarting the process is not
|
|
143
|
-
* a log. Set it false for one file per run (a short CLI, a test fixture, a
|
|
144
|
-
* container shipping logs elsewhere); the files are truncated once here at
|
|
145
|
-
* configure time, never per line.
|
|
146
|
-
*/
|
|
147
|
-
private static applyAppendMode;
|
|
148
|
-
/** Log an informational message. */
|
|
149
|
-
static info(message: unknown, data?: unknown): void;
|
|
150
|
-
/** Log a debug message. */
|
|
151
|
-
static debug(message: unknown, data?: unknown): void;
|
|
152
|
-
/** Log a warning message. */
|
|
153
|
-
static warning(message: unknown, data?: unknown): void;
|
|
154
|
-
/** Backwards-compat alias for warning(). */
|
|
155
|
-
static warn(message: unknown, data?: unknown): void;
|
|
156
|
-
/** Log an error message. */
|
|
157
|
-
static error(message: unknown, data?: unknown): void;
|
|
158
|
-
/**
|
|
159
|
-
* Log a critical message. CRITICAL is the highest severity (priority 4 >
|
|
160
|
-
* error 3) and ALWAYS emits like every other level — subject only to the
|
|
161
|
-
* console threshold, which it always passes at normal levels — and is always
|
|
162
|
-
* persisted to the log file (Node tees every level to a single tina4.log;
|
|
163
|
-
* critical 4 >= warning 2 so it would be in error.log on a split-file model).
|
|
164
|
-
* Matches Python master parity — there is no enable toggle.
|
|
165
|
-
*/
|
|
166
|
-
static critical(message: unknown, data?: unknown): void;
|
|
167
|
-
/** Check if running in production mode (TINA4_DEBUG is not truthy). */
|
|
168
|
-
private static isProduction;
|
|
169
|
-
/** Get current ISO timestamp */
|
|
170
|
-
private static timestamp;
|
|
171
|
-
/** Ensure the log directory exists */
|
|
172
|
-
private static ensureLogDir;
|
|
173
|
-
/**
|
|
174
|
-
* Roll-your-own rotation, stdlib only.
|
|
175
|
-
*
|
|
176
|
-
* Sequence on each write:
|
|
177
|
-
* 1. statSync the current file. If size < rotateSize, return.
|
|
178
|
-
* 2. Drop any file beyond keep via unlinkSync (cap the historical count).
|
|
179
|
-
* 3. Atomic shift: app.log.{N-1} → app.log.{N}, …, app.log.1 → app.log.2.
|
|
180
|
-
* 4. Rename current app.log → app.log.1.
|
|
181
|
-
* 5. Truncate via writeFileSync(path, "") so subsequent appends start fresh.
|
|
182
|
-
*
|
|
183
|
-
* Sync calls per write are fine — the worst case is contention on a single
|
|
184
|
-
* file, and the OS atomically serialises rename/unlink anyway.
|
|
185
|
-
*
|
|
186
|
-
* `rotateSize` of 0 disables rotation entirely.
|
|
187
|
-
*/
|
|
188
|
-
private static rotateIfNeeded;
|
|
189
|
-
/**
|
|
190
|
-
* Write a line to the log file, stripping ANSI codes.
|
|
191
|
-
*
|
|
192
|
-
* A failure is swallowed by default — logging must never crash the app. With
|
|
193
|
-
* TINA4_LOG_STRICT truthy it is RE-THROWN instead: an app that believes it is
|
|
194
|
-
* writing an audit trail into a read-only directory, and is not, is worse off
|
|
195
|
-
* than one that dies at the first line. Same contract in all four frameworks.
|
|
111
|
+
* True when `level` passes the queried sink's threshold and that sink is
|
|
112
|
+
* active. `sink` is undefined/"console"/"stdout" (console — the historical
|
|
113
|
+
* meaning) or "file" (Decision 8: the file sink has its own independent
|
|
114
|
+
* TINA4_LOG_FILE_LEVEL threshold).
|
|
196
115
|
*/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
116
|
+
static isEnabled(level: string, sink?: "console" | "stdout" | "file"): boolean;
|
|
117
|
+
static debug(message: unknown, context?: unknown): void;
|
|
118
|
+
static info(message: unknown, context?: unknown): void;
|
|
119
|
+
static warning(message: unknown, context?: unknown): void;
|
|
120
|
+
static error(message: unknown, context?: unknown): void;
|
|
121
|
+
/** Highest severity. Always emits, subject only to the configured
|
|
122
|
+
* threshold — there is no separate enable toggle. */
|
|
123
|
+
static critical(message: unknown, context?: unknown): void;
|
|
124
|
+
private static emit;
|
|
200
125
|
}
|
|
126
|
+
export {};
|
|
@@ -303,23 +303,42 @@ export declare class RequestLogger {
|
|
|
303
303
|
*/
|
|
304
304
|
export declare class SecurityHeadersMiddleware {
|
|
305
305
|
static beforeSecurity(req: Tina4Request, res: Tina4Response): [Tina4Request, Tina4Response];
|
|
306
|
+
/**
|
|
307
|
+
* True when the client request is HTTPS. Proxy-aware and byte-parity with
|
|
308
|
+
* Python (request.is_secure_scheme), PHP (Request::isSecureScheme) and Ruby
|
|
309
|
+
* (Request.secure_scheme?): a TLS-terminating proxy forwards plain HTTP with
|
|
310
|
+
* `x-forwarded-proto`, whose FIRST hop is the client-facing scheme; falling
|
|
311
|
+
* back to the native TLS socket when no such header is present. Its name is
|
|
312
|
+
* not before- or after-prefixed, so hook discovery never calls it as a hook.
|
|
313
|
+
*/
|
|
314
|
+
private static isSecureRequest;
|
|
306
315
|
}
|
|
307
316
|
/**
|
|
308
317
|
* Class-based CSRF middleware using the before/after convention.
|
|
309
318
|
* Validates form tokens on state-changing requests (POST, PUT, PATCH, DELETE).
|
|
310
319
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
320
|
+
* OFF by default — a default app has NO CSRF gate because the middleware is
|
|
321
|
+
* NOT attached. Set TINA4_CSRF=true (or 1/yes/on) and the framework
|
|
322
|
+
* auto-attaches it at boot (see attachCsrfFromEnv); or register it explicitly
|
|
323
|
+
* via Router.use(CsrfMiddleware). Once attached, TINA4_CSRF=false (or 0/no) is
|
|
324
|
+
* the kill switch that disables enforcement again.
|
|
313
325
|
*
|
|
314
|
-
* Behaviour:
|
|
326
|
+
* Behaviour (identical to the Python master, feature 37):
|
|
315
327
|
* - Skips GET, HEAD, OPTIONS requests.
|
|
316
328
|
* - Skips routes marked .noAuth().
|
|
329
|
+
* - Fails CLOSED: with TINA4_SECRET unset the signing secret resolves to
|
|
330
|
+
* blank (there is NO built-in default), and a blank HMAC key is publicly
|
|
331
|
+
* reproducible — so no token can be trusted and every write is rejected
|
|
332
|
+
* (403). This is the SEC-01 / CSRF-DEC-01 no-default-secret guarantee.
|
|
317
333
|
* - Skips requests with a valid Authorization: Bearer header (API clients).
|
|
318
334
|
* - Checks request body formToken then X-Form-Token header.
|
|
319
335
|
* - Rejects if token found in query string formToken (log warning, 403).
|
|
320
|
-
* - Validates token with validToken using SECRET
|
|
336
|
+
* - Validates token with validToken using the resolved SECRET, and enforces
|
|
337
|
+
* that the token's `type` claim is "form" — a non-form JWT presented in the
|
|
338
|
+
* formToken slot is rejected (CSRF-DEC-02).
|
|
321
339
|
* - If token payload has session_id, verifies it matches request session.
|
|
322
|
-
* -
|
|
340
|
+
* - Every rejection is 403 with the CSRF_INVALID envelope
|
|
341
|
+
* { error: true, code: "CSRF_INVALID", message, status: 403 }.
|
|
323
342
|
*
|
|
324
343
|
* Usage:
|
|
325
344
|
* Router.use(CsrfMiddleware);
|
|
@@ -327,4 +346,20 @@ export declare class SecurityHeadersMiddleware {
|
|
|
327
346
|
export declare class CsrfMiddleware {
|
|
328
347
|
static beforeCsrf(req: Tina4Request, res: Tina4Response): [Tina4Request, Tina4Response];
|
|
329
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Auto-attach CsrfMiddleware when TINA4_CSRF is enabled in the environment.
|
|
351
|
+
*
|
|
352
|
+
* CSRF is OFF by default: with TINA4_CSRF unset the middleware is never
|
|
353
|
+
* attached, so a default app has no CSRF gate. Setting TINA4_CSRF to a truthy
|
|
354
|
+
* value (true/1/yes/on, case-insensitive, trimmed) attaches it globally at boot
|
|
355
|
+
* so every state-changing route is gated — the env flag is the switch, no code
|
|
356
|
+
* change needed. Idempotent (MiddlewareRunner.use de-dupes). Returns true when
|
|
357
|
+
* the middleware is now attached.
|
|
358
|
+
*
|
|
359
|
+
* The framework calls this once during startServer (after route discovery,
|
|
360
|
+
* before listen); a false/0/no value still lets an explicit Router.use opt-in
|
|
361
|
+
* be disabled at runtime by the kill switch in beforeCsrf. Mirrors Python's
|
|
362
|
+
* attach_csrf_from_env.
|
|
363
|
+
*/
|
|
364
|
+
export declare function attachCsrfFromEnv(): boolean;
|
|
330
365
|
export declare function requestLogger(): Middleware;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const TAKEOVER_NOTHING = "nothing";
|
|
2
|
+
export declare const TAKEOVER_KILLED = "killed";
|
|
3
|
+
export declare const TAKEOVER_REFUSED_FOREIGN = "refused_foreign";
|
|
4
|
+
export declare const TAKEOVER_REFUSED_OPTOUT = "refused_optout";
|
|
5
|
+
export declare const TAKEOVER_REFUSED_PROD = "refused_prod";
|
|
6
|
+
export declare const TAKEOVER_SKIPPED_CONTAINER = "skipped_container";
|
|
7
|
+
/** Statuses that mean a holder was left running on purpose. */
|
|
8
|
+
export declare const TAKEOVER_REFUSALS: string[];
|
|
9
|
+
export interface TakeoverResult {
|
|
10
|
+
status: string;
|
|
11
|
+
port: number;
|
|
12
|
+
killed: number[];
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
/** Dev mode = TINA4_DEBUG truthy. Takeover runs only in dev. */
|
|
16
|
+
export declare function isDev(): boolean;
|
|
17
|
+
/** True when takeover is disabled via TINA4_NO_TAKEOVER. */
|
|
18
|
+
export declare function noTakeoverOptedOut(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* True when this process is running inside a container. Reclaiming a port makes
|
|
21
|
+
* sense on a dev machine; inside a container the server IS the container, so
|
|
22
|
+
* there is no stale sibling to reclaim from.
|
|
23
|
+
*/
|
|
24
|
+
export declare function inContainer(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The PIDs from `lsof -ti` output that are safe to signal.
|
|
27
|
+
*
|
|
28
|
+
* Pure so the safety rule can be tested directly. A non-numeric field parses to
|
|
29
|
+
* NaN/0, and signalling PID 0 hits EVERY process in the caller's own process
|
|
30
|
+
* group -- the server kills itself. Accept only all-digit tokens; never PID 0
|
|
31
|
+
* (our group), PID 1 (init), ourselves, or our own process group. This is the
|
|
32
|
+
* PID-SAFETY gate only; whether a survivor is a Tina4 server is the SEPARATE
|
|
33
|
+
* identity check in takeOverPort().
|
|
34
|
+
*/
|
|
35
|
+
export declare function selectablePids(lsofOutput: string, me: number, myGroup?: number): number[];
|
|
36
|
+
export declare function runtimeDir(baseDir?: string): string;
|
|
37
|
+
export declare function pidfilePath(port: number, baseDir?: string): string;
|
|
38
|
+
/** Record THIS process as the Tina4 dev server on `port` (best-effort). */
|
|
39
|
+
export declare function writePidfile(port: number, baseDir?: string, pid?: number): void;
|
|
40
|
+
/** The PID a Tina4 dev server recorded for `port`, or null if none/garbage. */
|
|
41
|
+
export declare function readPidfile(port: number, baseDir?: string): number | null;
|
|
42
|
+
/** Drop the PID file for `port` (clean shutdown, or after reclaiming it). */
|
|
43
|
+
export declare function removePidfile(port: number, baseDir?: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Reclaim `port` ONLY from an identity-confirmed Tina4 dev server. The single
|
|
46
|
+
* guarded path for both the CLI (`tina4 serve`) and the runtime bind-failure
|
|
47
|
+
* fallback. `dev`/`noTakeover` are passed in so this stays pure and directly
|
|
48
|
+
* testable; callers resolve them from isDev() / noTakeoverOptedOut().
|
|
49
|
+
*/
|
|
50
|
+
export declare function takeOverPort(port: number, dev: boolean, noTakeover: boolean, baseDir?: string, grace?: number): TakeoverResult;
|
|
@@ -23,3 +23,18 @@ export declare function parseMultipart(body: Buffer, boundary: string): {
|
|
|
23
23
|
fields: Record<string, string>;
|
|
24
24
|
files: Record<string, UploadedFile | UploadedFile[]>;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Persist an uploaded file's content inside `targetDir` under a SAFE name.
|
|
28
|
+
*
|
|
29
|
+
* The client-supplied filename is untrusted. Directory components are stripped
|
|
30
|
+
* (so `../../evil` or `/etc/passwd` becomes `evil` / `passwd`), a NUL byte or an
|
|
31
|
+
* unusable name (`''`/`.`/`..`) is refused, and the resolved path is confined to
|
|
32
|
+
* `targetDir` (realpath containment) so an upload can never write outside it.
|
|
33
|
+
*
|
|
34
|
+
* @param file an uploaded-file descriptor (`req.files[name]`) carrying `content`.
|
|
35
|
+
* @param targetDir the directory to write into (created if missing).
|
|
36
|
+
* @param filename an explicit name to use instead of the client filename.
|
|
37
|
+
* @returns the absolute path written.
|
|
38
|
+
* @throws when the derived name is unsafe or would escape `targetDir`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function saveUpload(file: UploadedFile, targetDir: string, filename?: string): string;
|
|
@@ -26,3 +26,32 @@ export declare function createResponse(res: ServerResponse): Tina4Response;
|
|
|
26
26
|
* return response(errorResponse("VALIDATION_FAILED", "Email is required", 400), 400);
|
|
27
27
|
*/
|
|
28
28
|
export declare function errorResponse(code: string, message: string, status?: number): Record<string, unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Content negotiation for an error response (feature 42, ERR-DEC-02): does an
|
|
31
|
+
* Accept header prefer application/json over text/html?
|
|
32
|
+
*
|
|
33
|
+
* `Accept: application/json` (an API client) prefers JSON; a browser Accept
|
|
34
|
+
* (`text/html`, `*\/*`, or no header at all) prefers HTML. A mixed Accept
|
|
35
|
+
* header - a real browser's
|
|
36
|
+
* `text/html,application/xhtml+xml,application/xml;q=0.9,*\/*;q=0.8` - is
|
|
37
|
+
* resolved by q-value: whichever of the two media types this function cares
|
|
38
|
+
* about is weighted higher wins; a tie or neither present defaults to HTML,
|
|
39
|
+
* the historical/back-compatible behaviour for an unspecified client. This is
|
|
40
|
+
* the ONE shared decision reused by the 403/404/500 error paths (server.ts's
|
|
41
|
+
* serveNotFound/renderDispatchError, middleware.ts's interpretHookResult), so
|
|
42
|
+
* a JSON API client sees the SAME negotiated shape everywhere
|
|
43
|
+
* (ERR-403-SPLIT) - ported with the same algorithm to Python/PHP/Ruby.
|
|
44
|
+
*/
|
|
45
|
+
export declare function acceptPrefersJson(accept: string | undefined | null): boolean;
|
|
46
|
+
/** True when the request's Accept header prefers JSON - see acceptPrefersJson(). */
|
|
47
|
+
export declare function wantsJson(req: {
|
|
48
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
49
|
+
}): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The ONE JSON error envelope for a negotiated 403/404/500 (ERR-DEC-02).
|
|
52
|
+
*
|
|
53
|
+
* Reuses errorResponse() (error: true, code, message, status) already shared
|
|
54
|
+
* by app-level response.error() calls, plus request_id for correlation
|
|
55
|
+
* (feature 43, ERR-404-REQUESTID) - the SAME shape Python/PHP/Ruby build.
|
|
56
|
+
*/
|
|
57
|
+
export declare function negotiatedErrorBody(code: number, message: string, requestId: string): Record<string, unknown>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { Tina4Config } from "./types.js";
|
|
3
3
|
import { Router } from "./router.js";
|
|
4
|
+
import { MiddlewareChain } from "./middleware.js";
|
|
4
5
|
/** How long a graceful shutdown waits for in-flight requests, in seconds. */
|
|
5
6
|
export declare const DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
6
7
|
/**
|
|
@@ -67,6 +68,26 @@ export declare function autoMigrateOnStartup(migrationDir?: string, base?: strin
|
|
|
67
68
|
* that genuinely need both names set during a transition window.
|
|
68
69
|
*/
|
|
69
70
|
export declare function _checkLegacyEnvVars(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Kill whatever process is listening on *port*.
|
|
73
|
+
* Uses lsof on macOS/Linux and netstat + taskkill on Windows.
|
|
74
|
+
* Throws if the port cannot be freed.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Reclaim `port` from a stale Tina4 dev server via the shared, guarded path.
|
|
78
|
+
*
|
|
79
|
+
* This is the runtime bind-failure fallback. It used to SIGTERM whatever held
|
|
80
|
+
* the port with NONE of the CLI's guards -- no identity check, no container
|
|
81
|
+
* guard, no PID-safety filter -- so a foreign holder (another dev server, a
|
|
82
|
+
* database) was killed on any bind failure. It now routes through the SAME
|
|
83
|
+
* identity-checked helper the CLI uses (TAKEOVER-DEC-02), so only a
|
|
84
|
+
* PID-file-confirmed Tina4 dev server is ever signalled.
|
|
85
|
+
*
|
|
86
|
+
* Throws when the port is held by a non-Tina4 process (or takeover is opted out
|
|
87
|
+
* / disabled outside dev), so the bind fails loudly with a clear message instead
|
|
88
|
+
* of killing an innocent process.
|
|
89
|
+
*/
|
|
90
|
+
export declare function killPort(port: number): void;
|
|
70
91
|
/**
|
|
71
92
|
* Resolve port and host with priority: explicit config > ENV var > default.
|
|
72
93
|
* Exported for testability.
|
|
@@ -120,6 +141,17 @@ export declare function resetTemplateCache(): void;
|
|
|
120
141
|
* The whole feature can be turned off with `TINA4_TEMPLATE_ROUTING=off`.
|
|
121
142
|
*/
|
|
122
143
|
export declare function resolveTemplate(pathname: string, templatesDir: string): string | null;
|
|
144
|
+
/**
|
|
145
|
+
* The DispatchContext the currently-running server (if any) built at
|
|
146
|
+
* startServer() time — the FULL context (real ORM/Swagger/DevAdmin/CSRF
|
|
147
|
+
* wiring included), not the lighter one buildDispatchContext() makes on its
|
|
148
|
+
* own. `null` when startServer() has not run yet in this process.
|
|
149
|
+
*
|
|
150
|
+
* TestClient prefers this over a freshly-built context so a no-argument
|
|
151
|
+
* `new TestClient()` gets maximum fidelity to whatever server is actually
|
|
152
|
+
* live — mirroring Ruby's `RackApp.current` (feature 131, TC-DEC-01).
|
|
153
|
+
*/
|
|
154
|
+
export declare function getLiveDispatchContext(): DispatchContext | null;
|
|
123
155
|
export declare function start(config?: Tina4Config): Promise<{
|
|
124
156
|
close: () => void;
|
|
125
157
|
router: Router;
|
|
@@ -135,6 +167,66 @@ export declare function stop(): void;
|
|
|
135
167
|
* Useful for testing and embedding.
|
|
136
168
|
*/
|
|
137
169
|
export declare function handle(rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Everything one request's dispatch needs beyond req/res: the resolved
|
|
172
|
+
* router, middleware chain, filesystem roots and template engine a boot
|
|
173
|
+
* resolved once.
|
|
174
|
+
*
|
|
175
|
+
* Passed explicitly — never closed over — so the SAME dispatch function can
|
|
176
|
+
* be driven from TWO places: the live socket server (startServer() builds
|
|
177
|
+
* the full context, wired to real ORM/Swagger/DevAdmin/CSRF registrations)
|
|
178
|
+
* and the in-process TestClient (buildDispatchContext(), below, when no live
|
|
179
|
+
* server is running in this process, or when the caller wants an isolated
|
|
180
|
+
* router). Both run the identical runDispatch()/dispatchInner() — no stage
|
|
181
|
+
* is ever skipped for one caller and not the other (feature 131, TC-DEC-01
|
|
182
|
+
* — this was a closure trapped inside startServer() until now, which is
|
|
183
|
+
* exactly why TestClient could not call it and grew its own competing
|
|
184
|
+
* dispatch instead).
|
|
185
|
+
*/
|
|
186
|
+
export interface DispatchContext {
|
|
187
|
+
router: Router;
|
|
188
|
+
middleware: MiddlewareChain;
|
|
189
|
+
port: number;
|
|
190
|
+
staticDir: string;
|
|
191
|
+
srcPublicDir: string;
|
|
192
|
+
templatesDir: string;
|
|
193
|
+
frondEngine: {
|
|
194
|
+
render(file: string, data: Record<string, unknown>): string;
|
|
195
|
+
} | null;
|
|
196
|
+
swaggerAssetsEnabled: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build a standalone DispatchContext bound to `router`, without booting a
|
|
200
|
+
* real server — no port bind, no route discovery, no ORM/Swagger/DevAdmin/
|
|
201
|
+
* CSRF wiring (those are startServer()'s job, and they mutate global/process
|
|
202
|
+
* state a lightweight caller should not trigger just by dispatching one
|
|
203
|
+
* request).
|
|
204
|
+
*
|
|
205
|
+
* For the in-process TestClient (and any other embedder) to reach the REAL
|
|
206
|
+
* pipeline when no `startServer()` has run yet in this process, or when the
|
|
207
|
+
* caller wants an ISOLATED router independent of whatever live server might
|
|
208
|
+
* be running — parity with the existing `new TestClient(router)`
|
|
209
|
+
* test-isolation contract (see testClientFrontController.test.ts, which
|
|
210
|
+
* builds a fresh Router precisely so it never races with concurrent
|
|
211
|
+
* TestClient suites on the shared defaultRouter).
|
|
212
|
+
*
|
|
213
|
+
* Best-effort, like startServer()'s own setup: Frond and @tina4/swagger are
|
|
214
|
+
* optional dependencies of @tina4/core, so a missing package degrades to
|
|
215
|
+
* `null` / `false` rather than throwing — a template route or a swagger
|
|
216
|
+
* asset request simply falls through the fallback chain the same way it
|
|
217
|
+
* would if those packages were never installed.
|
|
218
|
+
*/
|
|
219
|
+
export declare function buildDispatchContext(router: Router, base?: string): Promise<DispatchContext>;
|
|
220
|
+
/**
|
|
221
|
+
* Dispatch one request through the REAL Tina4 pipeline (stamps the
|
|
222
|
+
* per-request correlation id, then hands off to dispatchInner). This is the
|
|
223
|
+
* exact function startServer() wires to every live socket connection AND to
|
|
224
|
+
* the module-level handle() — an in-process caller (TestClient) that builds
|
|
225
|
+
* its own DispatchContext (buildDispatchContext(), or the live one via
|
|
226
|
+
* getLiveDispatchContext()) runs the IDENTICAL function, so no stage is ever
|
|
227
|
+
* skipped for one caller and not the other (feature 131, TC-DEC-01).
|
|
228
|
+
*/
|
|
229
|
+
export declare function runDispatch(ctx: DispatchContext, rawReq: IncomingMessage, rawRes: ServerResponse): Promise<void>;
|
|
138
230
|
export declare function startServer(config?: Tina4Config): Promise<{
|
|
139
231
|
close: () => void;
|
|
140
232
|
router: Router;
|