tina4-nodejs 3.13.94 → 3.13.95
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 +157 -28
- package/README.md +1 -1
- package/package.json +2 -1
- package/packages/cli/dist/bin.js +32418 -29638
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +32364 -29501
- package/packages/core/src/ai.ts +7 -1
- package/packages/core/src/auth.ts +191 -39
- package/packages/core/src/background.ts +19 -19
- package/packages/core/src/cache.ts +492 -49
- package/packages/core/src/devAdmin.ts +79 -32
- package/packages/core/src/dispatchPipeline.ts +285 -0
- package/packages/core/src/dotenv.ts +185 -40
- package/packages/core/src/index.ts +5 -4
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +9 -13
- package/packages/core/src/metrics.ts +199 -961
- package/packages/core/src/middleware.ts +390 -123
- package/packages/core/src/queue.ts +188 -32
- package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
- package/packages/core/src/queueBackends/liteBackend.ts +13 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
- package/packages/core/src/rateLimiter.ts +10 -5
- package/packages/core/src/request.ts +6 -9
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +751 -414
- package/packages/core/src/session.ts +244 -27
- package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +293 -208
- package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
- package/packages/core/src/sessionHandlers/respClient.ts +16 -147
- package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
- package/packages/core/src/testClient.ts +18 -5
- package/packages/core/src/trustedProxy.ts +249 -0
- package/packages/core/src/types.ts +29 -5
- package/packages/core/src/websocket.ts +66 -0
- package/packages/orm/dist/index.js +22367 -19504
- package/packages/orm/src/adapters/firebird.ts +183 -56
- package/packages/orm/src/adapters/mongodb.ts +25 -4
- package/packages/orm/src/adapters/mssql.ts +114 -29
- package/packages/orm/src/adapters/mysql.ts +103 -40
- package/packages/orm/src/adapters/odbc.ts +44 -21
- package/packages/orm/src/adapters/postgres.ts +118 -26
- package/packages/orm/src/adapters/sqlDialect.ts +120 -0
- package/packages/orm/src/adapters/sqlite.ts +60 -24
- package/packages/orm/src/baseModel.ts +135 -40
- package/packages/orm/src/cachedDatabase.ts +43 -19
- package/packages/orm/src/connectTimeout.ts +265 -0
- package/packages/orm/src/database.ts +237 -197
- package/packages/orm/src/databaseResult.ts +65 -13
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -3
- package/packages/orm/src/migration.ts +18 -3
- package/packages/orm/src/queryBuilder.ts +38 -4
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +15 -4
- package/types/core/src/ai.d.ts +1 -1
- package/types/core/src/auth.d.ts +28 -5
- package/types/core/src/background.d.ts +3 -3
- package/types/core/src/cache.d.ts +15 -12
- package/types/core/src/dispatchPipeline.d.ts +117 -0
- package/types/core/src/dotenv.d.ts +38 -16
- package/types/core/src/index.d.ts +5 -6
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +2 -2
- package/types/core/src/metrics.d.ts +25 -61
- package/types/core/src/middleware.d.ts +134 -11
- package/types/core/src/queue.d.ts +54 -5
- package/types/core/src/queueBackends/kafkaBackend.d.ts +1 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +24 -2
- package/types/core/src/queueBackends/rabbitmqBackend.d.ts +3 -3
- package/types/core/src/router.d.ts +14 -3
- package/types/core/src/server.d.ts +15 -0
- package/types/core/src/session.d.ts +87 -2
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +60 -5
- package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
- package/types/core/src/sessionHandlers/mongoClient.d.ts +16 -5
- package/types/core/src/sessionHandlers/mongoHandler.d.ts +51 -3
- package/types/core/src/sessionHandlers/respClient.d.ts +2 -2
- package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
- package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
- package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
- package/types/core/src/trustedProxy.d.ts +44 -0
- package/types/core/src/types.d.ts +28 -5
- package/types/core/src/websocket.d.ts +26 -0
- package/types/orm/src/adapters/firebird.d.ts +55 -10
- package/types/orm/src/adapters/mongodb.d.ts +2 -2
- package/types/orm/src/adapters/mssql.d.ts +18 -11
- package/types/orm/src/adapters/mysql.d.ts +11 -10
- package/types/orm/src/adapters/odbc.d.ts +9 -12
- package/types/orm/src/adapters/postgres.d.ts +11 -10
- package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
- package/types/orm/src/adapters/sqlite.d.ts +15 -3
- package/types/orm/src/baseModel.d.ts +45 -9
- package/types/orm/src/cachedDatabase.d.ts +18 -5
- package/types/orm/src/connectTimeout.d.ts +100 -0
- package/types/orm/src/database.d.ts +72 -26
- package/types/orm/src/databaseResult.d.ts +24 -0
- package/types/orm/src/databaseUrl.d.ts +125 -0
- package/types/orm/src/docstore.d.ts +102 -43
- package/types/orm/src/index.d.ts +5 -2
- package/types/orm/src/queryBuilder.d.ts +23 -3
- package/types/orm/src/sqlTranslator.d.ts +126 -2
- package/types/orm/src/types.d.ts +14 -4
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -128,6 +128,66 @@ function stripAnsi(text: string): string {
|
|
|
128
128
|
*
|
|
129
129
|
* Otherwise the directory is `TINA4_LOG_DIR` and filename is `tina4.log`.
|
|
130
130
|
*/
|
|
131
|
+
// The logger must never be surprised by what it is handed, and must never be
|
|
132
|
+
// the reason a request dies. Same numbers in all four frameworks (feature 2).
|
|
133
|
+
const STDOUT_MAX_CHARS = 2000;
|
|
134
|
+
// eslint-disable-next-line no-control-regex
|
|
135
|
+
const CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Turn anything into a single safe line of text.
|
|
139
|
+
*
|
|
140
|
+
* A string passes through. A Buffer is decoded when it is valid UTF-8 and
|
|
141
|
+
* described when it is not: raw bytes at a terminal garble it and can emit
|
|
142
|
+
* escape sequences. Anything else becomes JSON, because an object rendered as
|
|
143
|
+
* text is the whole reason the caller logged it, falling back to String() for a
|
|
144
|
+
* value JSON cannot represent (a circular reference, a BigInt). Without this an
|
|
145
|
+
* object logged as a message reached the output as "[object Object]".
|
|
146
|
+
*/
|
|
147
|
+
function coerceMessage(message: unknown): string {
|
|
148
|
+
let text: string;
|
|
149
|
+
if (typeof message === "string") {
|
|
150
|
+
text = message;
|
|
151
|
+
} else if (message instanceof Uint8Array) {
|
|
152
|
+
const decoded = new TextDecoder("utf-8", { fatal: false }).decode(message);
|
|
153
|
+
text = decoded.includes("�") ? `<binary ${message.byteLength} bytes>` : decoded;
|
|
154
|
+
} else if (message === null || message === undefined) {
|
|
155
|
+
text = "";
|
|
156
|
+
} else if (typeof message === "object") {
|
|
157
|
+
try {
|
|
158
|
+
text = JSON.stringify(message) ?? String(message);
|
|
159
|
+
} catch {
|
|
160
|
+
text = String(message);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
text = String(message);
|
|
164
|
+
}
|
|
165
|
+
return text.replace(CONTROL_CHARS, "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Cap a console line. The file keeps the whole thing; a terminal does not. */
|
|
169
|
+
function truncateForStdout(line: string): string {
|
|
170
|
+
if (line.length <= STDOUT_MAX_CHARS) return line;
|
|
171
|
+
return `${line.slice(0, STDOUT_MAX_CHARS)}... (truncated, ${line.length} chars)`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Is this target a FILE PATH or a DIRECTORY?
|
|
176
|
+
*
|
|
177
|
+
* An existing directory is always a directory, extension or not. Otherwise a
|
|
178
|
+
* basename with an extension (app.log, app.txt) is a file and anything else is
|
|
179
|
+
* a directory to create. That keeps `configure("/var/log/myapp")` a directory
|
|
180
|
+
* and `configure("/var/log/myapp/app.log")` a file without the path needing to
|
|
181
|
+
* exist yet. Identical rule in all four frameworks.
|
|
182
|
+
*/
|
|
183
|
+
function targetIsFile(path: string): boolean {
|
|
184
|
+
try {
|
|
185
|
+
if (existsSync(path) && statSync(path).isDirectory()) return false;
|
|
186
|
+
} catch { /* fall through to the extension test */ }
|
|
187
|
+
const base = path.split(/[\\/]/).pop() ?? "";
|
|
188
|
+
return base.includes(".") && !base.startsWith(".");
|
|
189
|
+
}
|
|
190
|
+
|
|
131
191
|
function resolveLogFilePath(logDir: string, logFile: string): string {
|
|
132
192
|
if (isAbsolute(logFile)) return logFile;
|
|
133
193
|
return join(logDir, logFile);
|
|
@@ -136,23 +196,36 @@ function resolveLogFilePath(logDir: string, logFile: string): string {
|
|
|
136
196
|
/**
|
|
137
197
|
* Structured logger for Tina4.
|
|
138
198
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
199
|
+
* FORMAT IS TEXT BY DEFAULT, and TINA4_LOG_FORMAT=json is the ONLY thing that
|
|
200
|
+
* selects JSON. Nothing else may. Until 3.13.95 an unset TINA4_DEBUG silently
|
|
201
|
+
* flipped BOTH sinks to JSON here, and "production" meant four different things
|
|
202
|
+
* across the four frameworks (Node: !TINA4_DEBUG; Ruby: TINA4_ENV/RACK_ENV/
|
|
203
|
+
* RUBY_ENV == "production"; Python: only configure(production=True); PHP: no
|
|
204
|
+
* switch at all, JSON always) — same machine, same .env, four log formats. That
|
|
205
|
+
* implicit switch is deleted; an object passed as the message is still
|
|
206
|
+
* JSON-encoded INLINE inside the text line, which is the only JSON a default
|
|
207
|
+
* install emits.
|
|
208
|
+
*
|
|
209
|
+
* TINA4_DEBUG still decides COLOUR — a terminal concern, not a format one — so
|
|
210
|
+
* a production pipe gets clean uncoloured bytes and a dev terminal stays
|
|
211
|
+
* readable.
|
|
143
212
|
*
|
|
144
213
|
* Default file-output rule (TINA4_LOG_OUTPUT unset): the log FILE is written
|
|
145
214
|
* only in development. An explicit TINA4_LOG_OUTPUT=file/both, OR an explicit
|
|
146
|
-
* TINA4_LOG_FILE path, always forces a file (explicit wins).
|
|
215
|
+
* TINA4_LOG_FILE path, always forces a file (explicit wins). stdout is ALWAYS on.
|
|
147
216
|
*
|
|
148
217
|
* Env vars:
|
|
149
218
|
* TINA4_LOG_FILE — explicit log file (absolute or relative). Setting it forces a file even in production. Empty = use TINA4_LOG_DIR + tina4.log
|
|
150
219
|
* TINA4_LOG_DIR — directory for log files (default: "logs")
|
|
151
|
-
* TINA4_LOG_FORMAT — "text" | "json" (default: "text")
|
|
220
|
+
* TINA4_LOG_FORMAT — "text" | "json" (default: "text") — the ONLY format switch
|
|
152
221
|
* TINA4_LOG_OUTPUT — "stdout" | "file" | "both" (default: "stdout" → file only in dev)
|
|
153
222
|
* TINA4_LOG_ROTATE_SIZE — bytes; 0 disables rotation (default: 10485760 = 10MB)
|
|
154
223
|
* TINA4_LOG_ROTATE_KEEP — number of historical files to keep (default: 5)
|
|
155
224
|
* TINA4_LOG_LEVEL — minimum console level: DEBUG | INFO | WARNING | ERROR | CRITICAL (default: "INFO")
|
|
225
|
+
* TINA4_LOG_STRICT — truthy: a log-write failure THROWS instead of being swallowed (default: off)
|
|
226
|
+
*
|
|
227
|
+
* Every one of these is read LAZILY, on each log() call — a script, worker, CLI
|
|
228
|
+
* tool or test that never boots a server still gets the operator's configuration.
|
|
156
229
|
*
|
|
157
230
|
* Rotation is stdlib roll-your-own:
|
|
158
231
|
* - On each write, statSync the file. If size >= TINA4_LOG_ROTATE_SIZE, rotate.
|
|
@@ -163,6 +236,22 @@ function resolveLogFilePath(logDir: string, logFile: string): string {
|
|
|
163
236
|
export class Log {
|
|
164
237
|
private static requestId: string | undefined;
|
|
165
238
|
|
|
239
|
+
/**
|
|
240
|
+
* What configure() was explicitly told, held HERE rather than written back
|
|
241
|
+
* into process.env (ADR-0041).
|
|
242
|
+
*
|
|
243
|
+
* configure() used to assign to process.env.TINA4_LOG_DIR / _LOG_FILE. That
|
|
244
|
+
* reached the right answer -- the argument won -- through a mechanism no
|
|
245
|
+
* other framework has: it DESTROYED the operator's value for the rest of the
|
|
246
|
+
* process, and every child process spawned afterwards inherited the
|
|
247
|
+
* argument instead of what the operator set. Reading configuration must not
|
|
248
|
+
* write it. Keeping the explicit values in their own slot means resolution
|
|
249
|
+
* is explicit > env > default with the environment left intact and still
|
|
250
|
+
* readable.
|
|
251
|
+
*/
|
|
252
|
+
private static explicitLogDir: string | undefined;
|
|
253
|
+
private static explicitLogFile: string | undefined;
|
|
254
|
+
|
|
166
255
|
/**
|
|
167
256
|
* Re-read all log-related env vars. Called on every log() so tests that
|
|
168
257
|
* mutate process.env between calls see the new values without having to
|
|
@@ -177,9 +266,14 @@ export class Log {
|
|
|
177
266
|
format: "text" | "json";
|
|
178
267
|
output: "stdout" | "file" | "both";
|
|
179
268
|
fileEnabled: boolean;
|
|
269
|
+
/** True when the operator named ONE file, so no error.log sibling appears. */
|
|
270
|
+
explicitFile: boolean;
|
|
271
|
+
/** TINA4_LOG_STRICT — a failed log write throws instead of being swallowed. */
|
|
272
|
+
strict: boolean;
|
|
180
273
|
} {
|
|
181
|
-
|
|
182
|
-
const
|
|
274
|
+
// explicit argument > environment > default (ADR-0041).
|
|
275
|
+
const logDir = Log.explicitLogDir ?? process.env.TINA4_LOG_DIR ?? DEFAULT_LOG_DIR;
|
|
276
|
+
const explicitFile = (Log.explicitLogFile ?? process.env.TINA4_LOG_FILE ?? "").trim();
|
|
183
277
|
const logFile = explicitFile || DEFAULT_LOG_FILE;
|
|
184
278
|
|
|
185
279
|
const rawSize = process.env.TINA4_LOG_ROTATE_SIZE;
|
|
@@ -225,7 +319,17 @@ export class Log {
|
|
|
225
319
|
fileEnabled = !Log.isProduction();
|
|
226
320
|
}
|
|
227
321
|
|
|
228
|
-
|
|
322
|
+
// TINA4_LOG_STRICT — documented on all four env-var pages, implemented only
|
|
323
|
+
// in Ruby until 3.13.95: a documented no-op in three frameworks. When truthy
|
|
324
|
+
// a log-write failure THROWS instead of being swallowed, so a deploy whose
|
|
325
|
+
// log directory is read-only fails loudly instead of running blind.
|
|
326
|
+
const strict = isTruthy(process.env.TINA4_LOG_STRICT);
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
logDir, logFile, rotateSize, rotateKeep, minLevel, format, output, fileEnabled,
|
|
330
|
+
explicitFile: explicitFile !== "",
|
|
331
|
+
strict,
|
|
332
|
+
};
|
|
229
333
|
}
|
|
230
334
|
|
|
231
335
|
/**
|
|
@@ -277,36 +381,116 @@ export class Log {
|
|
|
277
381
|
}
|
|
278
382
|
|
|
279
383
|
/**
|
|
280
|
-
* Configure
|
|
281
|
-
*
|
|
384
|
+
* Configure where logs are written.
|
|
385
|
+
*
|
|
386
|
+
* Logs land in a `logs/` folder by default. The argument OVERRIDES that, and
|
|
387
|
+
* it accepts a DIRECTORY or a FILE PATH:
|
|
388
|
+
*
|
|
389
|
+
* configure() -> ./logs/tina4.log + ./logs/error.log
|
|
390
|
+
* configure("/var/log/myapp") -> /var/log/myapp/tina4.log + error.log
|
|
391
|
+
* configure("/var/log/myapp/app.log") -> that exact file (no error.log sibling)
|
|
392
|
+
* configure({ logDir, logFile }) -> the explicit object form still works
|
|
393
|
+
*
|
|
394
|
+
* A plain string used to be accepted and silently ignored, because only the
|
|
395
|
+
* object form was read - so the call that works in the other three
|
|
396
|
+
* frameworks produced no log file here and said nothing (feature 2 of the
|
|
397
|
+
* audit, D4). Both forms now work.
|
|
398
|
+
*/
|
|
399
|
+
static configure(options?: string | { logDir?: string; logFile?: string }): void {
|
|
400
|
+
// Record what the caller asked for; never write it back into process.env
|
|
401
|
+
// (ADR-0041 -- reading configuration must not write it).
|
|
402
|
+
if (typeof options === "string") {
|
|
403
|
+
if (targetIsFile(options)) {
|
|
404
|
+
Log.explicitLogDir = dirname(options);
|
|
405
|
+
Log.explicitLogFile = options;
|
|
406
|
+
} else {
|
|
407
|
+
Log.explicitLogDir = options;
|
|
408
|
+
}
|
|
409
|
+
Log.applyAppendMode();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (options) {
|
|
413
|
+
if (options.logDir) Log.explicitLogDir = options.logDir;
|
|
414
|
+
if (options.logFile) Log.explicitLogFile = options.logFile;
|
|
415
|
+
}
|
|
416
|
+
Log.applyAppendMode();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Forget what configure() was told, so resolution falls back to the
|
|
421
|
+
* environment and then the built-in defaults. Parity with PHP's Log::reset().
|
|
422
|
+
*
|
|
423
|
+
* This exists because the explicit values are now HELD here rather than
|
|
424
|
+
* written back into process.env, and that makes them STICKY for the life of
|
|
425
|
+
* the process -- which is right for an application (configure() at boot is
|
|
426
|
+
* the operator's instruction and a later stray env write should not silently
|
|
427
|
+
* re-point the logs) and wrong for a long-lived test process that wants to
|
|
428
|
+
* drive the logger purely from the environment afterwards.
|
|
429
|
+
*
|
|
430
|
+
* I removed this method once for having no callers. That was correct about
|
|
431
|
+
* the grep and wrong about the code: the full suite is the caller. Before the
|
|
432
|
+
* explicit slots existed, configure() ASSIGNED to process.env, so a later
|
|
433
|
+
* direct assignment simply overwrote it and env-driven cases kept working by
|
|
434
|
+
* accident. test/logger.test.ts depends on exactly that -- it configures a
|
|
435
|
+
* file early, then runs the whole default-output block off the environment
|
|
436
|
+
* (see its own note: "these cases must NOT route through Log.configure").
|
|
437
|
+
* Three of those cases failed on the lab until this came back.
|
|
282
438
|
*/
|
|
283
|
-
static
|
|
284
|
-
|
|
285
|
-
|
|
439
|
+
static reset(): void {
|
|
440
|
+
Log.explicitLogDir = undefined;
|
|
441
|
+
Log.explicitLogFile = undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* TINA4_LOG_APPEND — append (default) or overwrite on startup.
|
|
446
|
+
*
|
|
447
|
+
* APPEND IS THE DEFAULT: a log you can lose by restarting the process is not
|
|
448
|
+
* a log. Set it false for one file per run (a short CLI, a test fixture, a
|
|
449
|
+
* container shipping logs elsewhere); the files are truncated once here at
|
|
450
|
+
* configure time, never per line.
|
|
451
|
+
*/
|
|
452
|
+
private static applyAppendMode(): void {
|
|
453
|
+
const raw = process.env.TINA4_LOG_APPEND;
|
|
454
|
+
const append = raw === undefined || isTruthy(raw);
|
|
455
|
+
if (append) return;
|
|
456
|
+
const cfg = Log.readEnv();
|
|
457
|
+
const targets = cfg.explicitFile
|
|
458
|
+
? [resolveLogFilePath(cfg.logDir, cfg.logFile)]
|
|
459
|
+
: [resolveLogFilePath(cfg.logDir, cfg.logFile), join(cfg.logDir, "error.log")];
|
|
460
|
+
for (const path of targets) {
|
|
461
|
+
try {
|
|
462
|
+
if (existsSync(path)) writeFileSync(path, "", "utf-8");
|
|
463
|
+
} catch (err) {
|
|
464
|
+
// Same TINA4_LOG_STRICT contract as writeToFile: this IS a write to the
|
|
465
|
+
// log file, so under strict it must not be swallowed either.
|
|
466
|
+
if (cfg.strict) throw err;
|
|
467
|
+
/* logging must never crash the app */
|
|
468
|
+
}
|
|
469
|
+
}
|
|
286
470
|
}
|
|
287
471
|
|
|
288
472
|
/** Log an informational message. */
|
|
289
|
-
static info(message:
|
|
473
|
+
static info(message: unknown, data?: unknown): void {
|
|
290
474
|
Log.log("INFO", message, data);
|
|
291
475
|
}
|
|
292
476
|
|
|
293
477
|
/** Log a debug message. */
|
|
294
|
-
static debug(message:
|
|
478
|
+
static debug(message: unknown, data?: unknown): void {
|
|
295
479
|
Log.log("DEBUG", message, data);
|
|
296
480
|
}
|
|
297
481
|
|
|
298
482
|
/** Log a warning message. */
|
|
299
|
-
static warning(message:
|
|
483
|
+
static warning(message: unknown, data?: unknown): void {
|
|
300
484
|
Log.log("WARNING", message, data);
|
|
301
485
|
}
|
|
302
486
|
|
|
303
487
|
/** Backwards-compat alias for warning(). */
|
|
304
|
-
static warn(message:
|
|
488
|
+
static warn(message: unknown, data?: unknown): void {
|
|
305
489
|
Log.log("WARNING", message, data);
|
|
306
490
|
}
|
|
307
491
|
|
|
308
492
|
/** Log an error message. */
|
|
309
|
-
static error(message:
|
|
493
|
+
static error(message: unknown, data?: unknown): void {
|
|
310
494
|
Log.log("ERROR", message, data);
|
|
311
495
|
}
|
|
312
496
|
|
|
@@ -318,7 +502,7 @@ export class Log {
|
|
|
318
502
|
* critical 4 >= warning 2 so it would be in error.log on a split-file model).
|
|
319
503
|
* Matches Python master parity — there is no enable toggle.
|
|
320
504
|
*/
|
|
321
|
-
static critical(message:
|
|
505
|
+
static critical(message: unknown, data?: unknown): void {
|
|
322
506
|
Log.log("CRITICAL", message, data);
|
|
323
507
|
}
|
|
324
508
|
|
|
@@ -397,21 +581,40 @@ export class Log {
|
|
|
397
581
|
try { writeFileSync(filePath, "", "utf-8"); } catch { /* ignore */ }
|
|
398
582
|
}
|
|
399
583
|
|
|
400
|
-
/**
|
|
401
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Write a line to the log file, stripping ANSI codes.
|
|
586
|
+
*
|
|
587
|
+
* A failure is swallowed by default — logging must never crash the app. With
|
|
588
|
+
* TINA4_LOG_STRICT truthy it is RE-THROWN instead: an app that believes it is
|
|
589
|
+
* writing an audit trail into a read-only directory, and is not, is worse off
|
|
590
|
+
* than one that dies at the first line. Same contract in all four frameworks.
|
|
591
|
+
*/
|
|
592
|
+
private static writeToFile(
|
|
593
|
+
filePath: string,
|
|
594
|
+
line: string,
|
|
595
|
+
rotateSize: number,
|
|
596
|
+
rotateKeep: number,
|
|
597
|
+
strict: boolean,
|
|
598
|
+
): void {
|
|
402
599
|
try {
|
|
403
600
|
Log.ensureLogDir(filePath);
|
|
404
601
|
Log.rotateIfNeeded(filePath, rotateSize, rotateKeep);
|
|
405
602
|
appendFileSync(filePath, stripAnsi(line) + "\n", "utf-8");
|
|
406
|
-
} catch {
|
|
603
|
+
} catch (err) {
|
|
604
|
+
if (strict) throw err;
|
|
407
605
|
// Silently fail — logging should never crash the app
|
|
408
606
|
}
|
|
409
607
|
}
|
|
410
608
|
|
|
411
609
|
/** Core log method */
|
|
412
|
-
private static log(level: LogLevel,
|
|
610
|
+
private static log(level: LogLevel, rawMessage: unknown, data?: unknown): void {
|
|
413
611
|
const cfg = Log.readEnv();
|
|
414
612
|
|
|
613
|
+
// Coerce FIRST, into the local the human line is built from further down.
|
|
614
|
+
// Anything can arrive as a message: an object from a handler, a Buffer off
|
|
615
|
+
// a socket, a 10MB string. See coerceMessage.
|
|
616
|
+
const message = coerceMessage(rawMessage);
|
|
617
|
+
|
|
415
618
|
const entry: LogEntry = {
|
|
416
619
|
timestamp: Log.timestamp(),
|
|
417
620
|
level,
|
|
@@ -441,26 +644,33 @@ export class Log {
|
|
|
441
644
|
const dataPart = data !== undefined ? ` ${JSON.stringify(data)}` : "";
|
|
442
645
|
const humanLine = `${entry.timestamp} [${paddedLevel}]${reqPart}${fnPart} ${message}${dataPart}`;
|
|
443
646
|
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
647
|
+
// ONE format decision, both sinks, one input: TINA4_LOG_FORMAT. Text is the
|
|
648
|
+
// default; only an explicit `json` selects JSON. The `|| Log.isProduction()`
|
|
649
|
+
// that used to live here made an UNSET TINA4_DEBUG silently reformat every
|
|
650
|
+
// line — the owner's measured defect (four frameworks, four meanings of
|
|
651
|
+
// "production", four different formats off one .env). An object passed as
|
|
652
|
+
// the message is still JSON-encoded INLINE by coerceMessage, which is the
|
|
653
|
+
// only structure a default install emits.
|
|
654
|
+
const formattedLine = cfg.format === "json" ? JSON.stringify(entry) : humanLine;
|
|
449
655
|
|
|
450
656
|
const shouldLog = Log.passesThreshold(level, cfg.minLevel);
|
|
451
657
|
|
|
452
658
|
// Console output. v3.13.14: stdout is NOT suppressed in production —
|
|
453
659
|
// containers read PID 1 stdout (docker logs / k8s) and the old
|
|
454
|
-
// `!isProduction()` gate meant deployed apps logged nothing.
|
|
455
|
-
// production we print the clean structured line (JSON, no ANSI) so it
|
|
456
|
-
// stays parseable; in dev we keep the coloured human-readable line.
|
|
660
|
+
// `!isProduction()` gate meant deployed apps logged nothing.
|
|
457
661
|
// TINA4_LOG_OUTPUT="file" still opts out of stdout entirely.
|
|
662
|
+
//
|
|
663
|
+
// stdout carries the SAME formattedLine as the file — the format env is the
|
|
664
|
+
// only thing that picks text vs JSON. TINA4_DEBUG decides COLOUR only: ANSI
|
|
665
|
+
// is for a human at a terminal, and a production pipe / log shipper must get
|
|
666
|
+
// clean bytes. Truncate on the CONSOLE only; the file keeps the full line so
|
|
667
|
+
// a consumer parsing it loses nothing, and a terminal does not need 10MB.
|
|
458
668
|
if (shouldLog && cfg.output !== "file") {
|
|
669
|
+
const consoleLine = truncateForStdout(formattedLine);
|
|
459
670
|
if (Log.isProduction()) {
|
|
460
|
-
console.log(
|
|
671
|
+
console.log(consoleLine);
|
|
461
672
|
} else {
|
|
462
|
-
|
|
463
|
-
console.log(`${color}${humanLine}${RESET}`);
|
|
673
|
+
console.log(`${COLORS[level]}${consoleLine}${RESET}`);
|
|
464
674
|
}
|
|
465
675
|
}
|
|
466
676
|
|
|
@@ -477,7 +687,18 @@ export class Log {
|
|
|
477
687
|
// that one flag gates the whole file writer (the only persisted sink).
|
|
478
688
|
if (cfg.fileEnabled) {
|
|
479
689
|
const filePath = resolveLogFilePath(cfg.logDir, cfg.logFile);
|
|
480
|
-
Log.writeToFile(filePath,
|
|
690
|
+
Log.writeToFile(filePath, formattedLine, cfg.rotateSize, cfg.rotateKeep, cfg.strict);
|
|
691
|
+
|
|
692
|
+
// Mirror WARNING and above into a dedicated error.log so
|
|
693
|
+
// `tail -f logs/error.log` gives just the stuff worth looking at. Node
|
|
694
|
+
// wrote ONE file where Python and PHP wrote two, so anyone whose
|
|
695
|
+
// alerting tails error.log got silence here (feature 2 of the audit, D3).
|
|
696
|
+
// Skipped when the operator named ONE file explicitly: they asked for a
|
|
697
|
+
// single path, so a sibling error.log appearing beside it is a surprise.
|
|
698
|
+
if (!cfg.explicitFile && LEVEL_PRIORITY[level] >= LEVEL_PRIORITY.WARNING) {
|
|
699
|
+
const errorPath = join(cfg.logDir, "error.log");
|
|
700
|
+
Log.writeToFile(errorPath, formattedLine, cfg.rotateSize, cfg.rotateKeep, cfg.strict);
|
|
701
|
+
}
|
|
481
702
|
}
|
|
482
703
|
}
|
|
483
704
|
}
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -1650,7 +1650,7 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1650
1650
|
return { error: "Seeder API not available (install @tina4/orm)" };
|
|
1651
1651
|
}
|
|
1652
1652
|
// RAW adapter, NOT globalThis.__tina4_db: autoFieldMap introspects via
|
|
1653
|
-
// adapter.
|
|
1653
|
+
// adapter.getColumns() and seedTable inserts via adapter.execute() — the
|
|
1654
1654
|
// Database WRAPPER has neither (its introspection method is getColumns).
|
|
1655
1655
|
// getAdapter() is the same accessor migration_* use above.
|
|
1656
1656
|
let adapter;
|
|
@@ -717,7 +717,7 @@ export class Messenger {
|
|
|
717
717
|
* Fetch latest messages from a folder.
|
|
718
718
|
* Returns list of message summaries.
|
|
719
719
|
*/
|
|
720
|
-
async inbox(
|
|
720
|
+
async inbox(folder: string = "INBOX", limit: number = 20, offset: number = 0): Promise<ImapMessage[]> {
|
|
721
721
|
let socket: net.Socket | tls.TLSSocket;
|
|
722
722
|
try {
|
|
723
723
|
socket = await this.imapConnect();
|
|
@@ -755,7 +755,7 @@ export class Messenger {
|
|
|
755
755
|
/**
|
|
756
756
|
* Read a single message by sequence number or UID.
|
|
757
757
|
*/
|
|
758
|
-
async read(uid: string, folder: string = "INBOX"): Promise<ImapFullMessage> {
|
|
758
|
+
async read(uid: string, folder: string = "INBOX"): Promise<ImapFullMessage | null> {
|
|
759
759
|
let socket: net.Socket | tls.TLSSocket;
|
|
760
760
|
try {
|
|
761
761
|
socket = await this.imapConnect();
|
|
@@ -767,9 +767,14 @@ export class Messenger {
|
|
|
767
767
|
const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
|
|
768
768
|
|
|
769
769
|
// A genuinely missing UID is a tagged OK with no message body literal —
|
|
770
|
-
// that is NOT an error
|
|
770
|
+
// that is NOT an error, so it must not throw. It returns null: FALSY, so
|
|
771
|
+
// `if (!msg)` detects it. This used to return emptyFullMessage(uid) under
|
|
772
|
+
// a comment claiming "parity with Python's {}" -- but that object is
|
|
773
|
+
// TRUTHY, while Python's {} , PHP's null and Ruby's nil are all falsy, so
|
|
774
|
+
// Node was the one framework where a caller could not tell "no such
|
|
775
|
+
// message" from a real one without inspecting individual fields.
|
|
771
776
|
if (!/\{\d+\}/.test(fetchResp)) {
|
|
772
|
-
return
|
|
777
|
+
return null;
|
|
773
778
|
}
|
|
774
779
|
|
|
775
780
|
// Mark as seen
|
|
@@ -1039,15 +1044,6 @@ function parseHeaderResponse(uid: string, response: string): ImapMessage {
|
|
|
1039
1044
|
};
|
|
1040
1045
|
}
|
|
1041
1046
|
|
|
1042
|
-
/**
|
|
1043
|
-
* An empty full message — returned when a FETCH succeeds (tagged OK) but the
|
|
1044
|
-
* UID does not exist, so there is no message body. Mirrors Python's {} return:
|
|
1045
|
-
* a missing UID is NOT an error.
|
|
1046
|
-
*/
|
|
1047
|
-
function emptyFullMessage(uid: string): ImapFullMessage {
|
|
1048
|
-
return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
1047
|
function parseFullMessage(uid: string, response: string): ImapFullMessage {
|
|
1052
1048
|
// Extract the raw message body from FETCH response
|
|
1053
1049
|
const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
|