tina4-nodejs 3.13.37 → 3.13.38
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 +50 -18
- package/package.json +5 -3
- package/packages/cli/src/bin.ts +7 -0
- package/packages/cli/src/commands/init.ts +1 -0
- package/packages/cli/src/commands/metrics.ts +154 -0
- package/packages/cli/src/commands/routes.ts +3 -3
- package/packages/core/src/auth.ts +112 -2
- package/packages/core/src/cache.ts +2 -2
- package/packages/core/src/devAdmin.ts +30 -25
- package/packages/core/src/devMailbox.ts +4 -0
- package/packages/core/src/dotenv.ts +13 -4
- package/packages/core/src/events.ts +86 -4
- package/packages/core/src/graphql.ts +182 -128
- package/packages/core/src/htmlElement.ts +62 -3
- package/packages/core/src/index.ts +13 -7
- package/packages/core/src/logger.ts +1 -1
- package/packages/core/src/mcp.test.ts +1 -1
- package/packages/core/src/messenger.ts +111 -11
- package/packages/core/src/metrics.ts +232 -33
- package/packages/core/src/middleware.ts +129 -39
- package/packages/core/src/plan.ts +1 -1
- package/packages/core/src/queue.ts +1 -1
- package/packages/core/src/queueBackends/kafkaBackend.ts +1 -1
- package/packages/core/src/queueBackends/mongoBackend.ts +1 -1
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +1 -1
- package/packages/core/src/rateLimiter.ts +1 -1
- package/packages/core/src/response.ts +90 -6
- package/packages/core/src/router.ts +2 -2
- package/packages/core/src/server.ts +26 -4
- package/packages/core/src/session.ts +130 -18
- package/packages/core/src/sessionHandlers/databaseHandler.ts +10 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +21 -4
- package/packages/core/src/sessionHandlers/redisHandler.ts +28 -7
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +27 -8
- package/packages/core/src/testClient.ts +1 -1
- package/packages/core/src/websocket.ts +247 -33
- package/packages/core/src/websocketBackplane.ts +210 -10
- package/packages/core/src/wsdl.ts +55 -21
- package/packages/orm/src/adapters/pg-types.d.ts +60 -0
- package/packages/orm/src/adapters/postgres.ts +26 -4
- package/packages/orm/src/adapters/sqlite.ts +112 -13
- package/packages/orm/src/baseModel.ts +8 -3
- package/packages/orm/src/cachedDatabase.ts +15 -6
- package/packages/orm/src/database.ts +257 -55
- package/packages/orm/src/index.ts +2 -1
- package/packages/orm/src/migration.ts +2 -2
- package/packages/orm/src/seeder.ts +443 -65
- package/packages/swagger/src/ui.ts +1 -1
|
@@ -22,32 +22,45 @@ export class MiddlewareChain {
|
|
|
22
22
|
this.middlewares.push(fn);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Run the chain in REGISTRATION order — each middleware runs exactly once,
|
|
27
|
+
* in the order it was attached via use(). The chain advances from ONE
|
|
28
|
+
* source only: `next()`. (The old runner double-advanced — a for-loop index
|
|
29
|
+
* AND next() both incremented — so every other middleware was silently
|
|
30
|
+
* skipped. Fixed by driving the chain purely by next(), mirroring Python's
|
|
31
|
+
* _make_mw_continuation Russian-doll continuation.)
|
|
32
|
+
*
|
|
33
|
+
* A middleware may stop the chain by:
|
|
34
|
+
* - not calling next() (it owns the response), or
|
|
35
|
+
* - ending the response (res.raw.writableEnded).
|
|
36
|
+
* Returns true when the whole chain ran to completion (handler may proceed),
|
|
37
|
+
* false when it was short-circuited.
|
|
38
|
+
*/
|
|
25
39
|
async run(req: Tina4Request, res: Tina4Response): Promise<boolean> {
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
const dispatch = async (i: number): Promise<void> => {
|
|
41
|
+
if (i >= this.middlewares.length) return;
|
|
42
|
+
let advanced = false;
|
|
28
43
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
const next = (): void => {
|
|
45
|
+
advanced = true;
|
|
46
|
+
};
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
const prevIndex = index;
|
|
35
|
-
await this.middlewares[index](req, res, next);
|
|
48
|
+
await this.middlewares[i](req, res, next);
|
|
36
49
|
|
|
37
|
-
//
|
|
38
|
-
if (res.raw.writableEnded)
|
|
39
|
-
completed = false;
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
50
|
+
// The middleware owns the response — stop the chain.
|
|
51
|
+
if (res.raw.writableEnded) return;
|
|
42
52
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// next() was called → advance to the following middleware (exactly one
|
|
54
|
+
// step). next() not called → the middleware short-circuited; stop here.
|
|
55
|
+
if (advanced) {
|
|
56
|
+
await dispatch(i + 1);
|
|
47
57
|
}
|
|
48
|
-
}
|
|
58
|
+
};
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
await dispatch(0);
|
|
61
|
+
|
|
62
|
+
// Completed (handler may proceed) iff no middleware ended the response.
|
|
63
|
+
return !res.raw.writableEnded;
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
|
|
@@ -64,6 +77,39 @@ export class MiddlewareChain {
|
|
|
64
77
|
* If a "before" method returns a response whose status code is >= 400
|
|
65
78
|
* the chain short-circuits and runBefore returns shouldContinue = false.
|
|
66
79
|
*/
|
|
80
|
+
/**
|
|
81
|
+
* Produce the deterministic clean 500 for a throwing class-based middleware
|
|
82
|
+
* (M2). Mirrors Python's _middleware_500: LOG via Log.error (class + method +
|
|
83
|
+
* error type + message — never silent) then return a 500 with the exact JSON
|
|
84
|
+
* body shape shared across all four frameworks. The worker never crashes and
|
|
85
|
+
* no unhandled exception leaks.
|
|
86
|
+
*/
|
|
87
|
+
function middleware500(
|
|
88
|
+
res: Tina4Response,
|
|
89
|
+
mwClass: any,
|
|
90
|
+
methodName: string,
|
|
91
|
+
error: unknown,
|
|
92
|
+
): Tina4Response {
|
|
93
|
+
const clsName = mwClass?.name ?? mwClass?.constructor?.name ?? "Middleware";
|
|
94
|
+
const err = error as { name?: string; message?: string };
|
|
95
|
+
const type = err?.name ?? (error as object)?.constructor?.name ?? "Error";
|
|
96
|
+
const message = err?.message ?? String(error);
|
|
97
|
+
try {
|
|
98
|
+
Log.error(`Middleware ${clsName}.${methodName} raised ${type}: ${message}`);
|
|
99
|
+
} catch {
|
|
100
|
+
/* never let a broken logger swallow the 500 */
|
|
101
|
+
}
|
|
102
|
+
// res is callable (json) in real Response; tolerate either shape.
|
|
103
|
+
if (typeof (res as any).json === "function") {
|
|
104
|
+
(res as any).json({ error: "Internal Server Error", status: 500 }, 500);
|
|
105
|
+
} else if (typeof (res as any) === "function") {
|
|
106
|
+
(res as any)({ error: "Internal Server Error", status: 500 }, 500);
|
|
107
|
+
} else if (typeof (res as any).status === "function") {
|
|
108
|
+
(res as any).status(500);
|
|
109
|
+
}
|
|
110
|
+
return res;
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
export class MiddlewareRunner {
|
|
68
114
|
/** Globally registered middleware classes (parity with PHP/Ruby/Python orchestrators). */
|
|
69
115
|
private static globalMiddleware: any[] = [];
|
|
@@ -90,16 +136,43 @@ export class MiddlewareRunner {
|
|
|
90
136
|
}
|
|
91
137
|
|
|
92
138
|
/**
|
|
93
|
-
*
|
|
94
|
-
* in order
|
|
95
|
-
*
|
|
139
|
+
* Discover the before-prefixed / after-prefixed method names on a
|
|
140
|
+
* middleware class in DEFINITION order (M1).
|
|
141
|
+
* `Object.getOwnPropertyNames` returns a class's own
|
|
142
|
+
* static method names in source-declaration order — we deliberately do NOT
|
|
143
|
+
* sort() them, so within a class the hooks run in the order they were
|
|
144
|
+
* written (parity with Python walking __dict__, PHP get_class_methods, Ruby
|
|
145
|
+
* instance_methods(false)). Cross-class order is the natural iteration of
|
|
146
|
+
* the registered classes = REGISTRATION order.
|
|
147
|
+
*/
|
|
148
|
+
private static methodNames(cls: any, prefix: string): string[] {
|
|
149
|
+
return Object.getOwnPropertyNames(cls).filter(
|
|
150
|
+
(name) => typeof cls[name] === "function" && name.startsWith(prefix),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Execute every beforeX static method found on the supplied classes.
|
|
156
|
+
*
|
|
157
|
+
* ORDER (M1): cross-class = REGISTRATION order (the order classes were
|
|
158
|
+
* attached via Router.use / MiddlewareRunner.use); within a class =
|
|
159
|
+
* DEFINITION order (source order, never alphabetical). before_* run before
|
|
160
|
+
* the handler.
|
|
161
|
+
*
|
|
162
|
+
* THROW (M2): each before* call is wrapped — a throwing middleware is
|
|
163
|
+
* LOGGED and produces a deterministic clean 500 (it never crashes the
|
|
164
|
+
* worker / leaks an unhandled exception), and the chain short-circuits
|
|
165
|
+
* (skip = true, handler skipped).
|
|
96
166
|
*
|
|
97
|
-
* Short-circuits when a before
|
|
167
|
+
* Short-circuits (skip = true, handler skipped) when a before* sets a
|
|
168
|
+
* status >= 400 or ends/500s the response.
|
|
98
169
|
*
|
|
99
170
|
* ASYNC — each hook is awaited so middleware can perform async work (e.g.
|
|
100
171
|
* the distributed responseCache before-hook awaiting `backend.get`). Awaiting
|
|
101
172
|
* a synchronous hook that returns an array is harmless (the array resolves
|
|
102
173
|
* immediately), so existing sync hooks keep working unchanged.
|
|
174
|
+
*
|
|
175
|
+
* Returns [req, res, shouldContinue].
|
|
103
176
|
*/
|
|
104
177
|
static async runBefore(
|
|
105
178
|
classes: any[],
|
|
@@ -107,13 +180,16 @@ export class MiddlewareRunner {
|
|
|
107
180
|
res: Tina4Response,
|
|
108
181
|
): Promise<[Tina4Request, Tina4Response, boolean]> {
|
|
109
182
|
for (const cls of classes) {
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
183
|
+
for (const method of MiddlewareRunner.methodNames(cls, "before")) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await cls[method](req, res);
|
|
186
|
+
if (Array.isArray(result)) {
|
|
187
|
+
[req, res] = result as [Tina4Request, Tina4Response];
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
// Throw → logged clean 500, skip the handler (deterministic).
|
|
191
|
+
res = middleware500(res, cls, method, error);
|
|
192
|
+
return [req, res, false];
|
|
117
193
|
}
|
|
118
194
|
// Short-circuit if the middleware set an error status
|
|
119
195
|
if (res.raw.statusCode >= 400 || res.raw.writableEnded) {
|
|
@@ -125,8 +201,19 @@ export class MiddlewareRunner {
|
|
|
125
201
|
}
|
|
126
202
|
|
|
127
203
|
/**
|
|
128
|
-
* Execute every afterX static method found on the supplied classes
|
|
129
|
-
*
|
|
204
|
+
* Execute every afterX static method found on the supplied classes.
|
|
205
|
+
*
|
|
206
|
+
* ORDER (M1): cross-class = REGISTRATION order; within a class = DEFINITION
|
|
207
|
+
* order. after_* run after the handler.
|
|
208
|
+
*
|
|
209
|
+
* THROW (M2): each after* call is wrapped — a throwing after middleware is
|
|
210
|
+
* LOGGED and produces a clean 500, then the remaining after* STILL run
|
|
211
|
+
* (they may add headers / logging). No unhandled exception leaks.
|
|
212
|
+
*
|
|
213
|
+
* AFTER-ON-4xx RULE (M2): after_* ALWAYS run, even when a before_*
|
|
214
|
+
* short-circuited with status >= 400 and the handler was skipped — so they
|
|
215
|
+
* can still add headers / logging. The dispatcher calls runAfter
|
|
216
|
+
* unconditionally after the before/handler block (see server.ts).
|
|
130
217
|
*
|
|
131
218
|
* ASYNC — each hook is awaited (e.g. the responseCache after-hook awaiting
|
|
132
219
|
* `backend.set`). Awaiting a synchronous hook is harmless, so existing sync
|
|
@@ -138,13 +225,16 @@ export class MiddlewareRunner {
|
|
|
138
225
|
res: Tina4Response,
|
|
139
226
|
): Promise<[Tina4Request, Tina4Response]> {
|
|
140
227
|
for (const cls of classes) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
228
|
+
for (const method of MiddlewareRunner.methodNames(cls, "after")) {
|
|
229
|
+
try {
|
|
230
|
+
const result = await cls[method](req, res);
|
|
231
|
+
if (Array.isArray(result)) {
|
|
232
|
+
[req, res] = result as [Tina4Request, Tina4Response];
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// Throw → logged clean 500, but remaining after* STILL run.
|
|
236
|
+
res = middleware500(res, cls, method, error);
|
|
237
|
+
continue;
|
|
148
238
|
}
|
|
149
239
|
}
|
|
150
240
|
}
|
|
@@ -487,7 +487,7 @@ export const Plan = {
|
|
|
487
487
|
}),
|
|
488
488
|
signal: AbortSignal.timeout(120_000),
|
|
489
489
|
});
|
|
490
|
-
result = await resp.json()
|
|
490
|
+
result = (await resp.json()) as Record<string, unknown>;
|
|
491
491
|
} catch (e) {
|
|
492
492
|
return { ok: false, error: `AI backend unreachable: ${(e as Error).message}` };
|
|
493
493
|
}
|
|
@@ -71,7 +71,7 @@ export interface QueueBackendInterface {
|
|
|
71
71
|
push(queue: string, payload: unknown, delay?: number): string;
|
|
72
72
|
pop(queue: string): QueueJob | null;
|
|
73
73
|
size(queue: string): number;
|
|
74
|
-
clear(queue: string):
|
|
74
|
+
clear(queue: string): void;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// ── Queue ────────────────────────────────────────────────────
|
|
@@ -172,7 +172,7 @@ export class RateLimiter {
|
|
|
172
172
|
|
|
173
173
|
/** Apply rate limiting to a request/response pair. Sets headers and 429 if exceeded. */
|
|
174
174
|
apply(request: Tina4Request, response: Tina4Response): [Tina4Request, Tina4Response] {
|
|
175
|
-
const ip =
|
|
175
|
+
const ip = request.ip ?? "unknown";
|
|
176
176
|
const result = this.check(ip);
|
|
177
177
|
|
|
178
178
|
response.header("X-RateLimit-Limit", String(result.limit));
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import type { ServerResponse } from "node:http";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import nodePath from "node:path";
|
|
4
|
+
import { once } from "node:events";
|
|
4
5
|
import type { Tina4Response, CookieOptions } from "./types.js";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort close of a streaming source on client disconnect or a mid-stream
|
|
9
|
+
* error. Async generators expose `.return()`; some custom iterables expose
|
|
10
|
+
* `.close()`/`.aclose()`. Any failure here is swallowed — cleanup is advisory.
|
|
11
|
+
*/
|
|
12
|
+
async function closeSource(source: AsyncIterable<unknown>): Promise<void> {
|
|
13
|
+
const s = source as {
|
|
14
|
+
return?: () => unknown;
|
|
15
|
+
close?: () => unknown;
|
|
16
|
+
aclose?: () => unknown;
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
if (typeof s.return === "function") await s.return();
|
|
20
|
+
else if (typeof s.aclose === "function") await s.aclose();
|
|
21
|
+
else if (typeof s.close === "function") await s.close();
|
|
22
|
+
} catch {
|
|
23
|
+
/* cleanup is best-effort */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
/** Cache Frond instances by template directory to avoid repeated instantiation. */
|
|
7
28
|
const _frondCache = new Map<string, InstanceType<any>>();
|
|
8
29
|
|
|
@@ -116,8 +137,15 @@ function toJsonable(data: unknown): unknown {
|
|
|
116
137
|
export function createResponse(res: ServerResponse): Tina4Response {
|
|
117
138
|
|
|
118
139
|
// ── Guard: prevent writing after headers are sent ──
|
|
119
|
-
const safeEnd = (
|
|
120
|
-
if (
|
|
140
|
+
const safeEnd = (chunk?: string | Buffer, encoding?: BufferEncoding) => {
|
|
141
|
+
if (res.headersSent) return;
|
|
142
|
+
if (chunk === undefined) {
|
|
143
|
+
res.end();
|
|
144
|
+
} else if (encoding === undefined) {
|
|
145
|
+
res.end(chunk);
|
|
146
|
+
} else {
|
|
147
|
+
res.end(chunk, encoding);
|
|
148
|
+
}
|
|
121
149
|
};
|
|
122
150
|
const safeSetHeader = (name: string, value: string | number | readonly string[]) => {
|
|
123
151
|
if (!res.headersSent) res.setHeader(name, value);
|
|
@@ -354,12 +382,68 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
354
382
|
"X-Accel-Buffering": "no",
|
|
355
383
|
});
|
|
356
384
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
385
|
+
// True once the client has gone away (socket destroyed) or the response
|
|
386
|
+
// has been finished — keep checking so we bail cleanly mid-stream rather
|
|
387
|
+
// than writing into a dead socket or buffering forever.
|
|
388
|
+
const streamClosed = (): boolean =>
|
|
389
|
+
res.writableEnded || (res.socket?.destroyed ?? false);
|
|
390
|
+
|
|
391
|
+
// Keep-alive heartbeat: periodically write a ':' SSE comment line on a
|
|
392
|
+
// long-lived stream so proxies/load-balancers don't reap an idle but
|
|
393
|
+
// healthy connection. Opt-out via TINA4_SSE_HEARTBEAT=0 (any non-positive
|
|
394
|
+
// value disables it). The interval is unref'd so it never holds the
|
|
395
|
+
// process open on its own.
|
|
396
|
+
const heartbeatSeconds = parseFloat(process.env.TINA4_SSE_HEARTBEAT ?? "15");
|
|
397
|
+
let heartbeat: ReturnType<typeof setInterval> | null = null;
|
|
398
|
+
if (Number.isFinite(heartbeatSeconds) && heartbeatSeconds > 0) {
|
|
399
|
+
heartbeat = setInterval(() => {
|
|
400
|
+
if (streamClosed()) return;
|
|
401
|
+
try {
|
|
402
|
+
res.write(": keep-alive\n\n");
|
|
403
|
+
} catch {
|
|
404
|
+
/* write race with a closing socket — the loop's guard handles it */
|
|
405
|
+
}
|
|
406
|
+
}, heartbeatSeconds * 1000);
|
|
407
|
+
heartbeat.unref?.();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const stopHeartbeat = (): void => {
|
|
411
|
+
if (heartbeat !== null) {
|
|
412
|
+
clearInterval(heartbeat);
|
|
413
|
+
heartbeat = null;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
for await (const chunk of source) {
|
|
419
|
+
// Client disconnected mid-stream — stop cleanly. Closing the source
|
|
420
|
+
// (best-effort) lets the producer release resources.
|
|
421
|
+
if (streamClosed()) {
|
|
422
|
+
await closeSource(source);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
const data = typeof chunk === "string" ? chunk : chunk.toString();
|
|
426
|
+
const ok = res.write(data);
|
|
427
|
+
// Slow-client backpressure: when write() returns false the kernel
|
|
428
|
+
// buffer is full. Wait for it to drain before pulling the next chunk
|
|
429
|
+
// so we don't unboundedly buffer ahead of a client that can't keep up.
|
|
430
|
+
if (!ok && !streamClosed()) {
|
|
431
|
+
await once(res, "drain").catch(() => {
|
|
432
|
+
/* socket errored/closed while waiting — loop guard handles it */
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
// The generator/source itself raised mid-stream. Log and stop cleanly —
|
|
438
|
+
// end the stream rather than crashing the request handler/worker.
|
|
439
|
+
const { Log } = await import("./logger.js");
|
|
440
|
+
Log.error(`SSE/stream source error: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
|
+
await closeSource(source);
|
|
442
|
+
} finally {
|
|
443
|
+
stopHeartbeat();
|
|
360
444
|
}
|
|
361
445
|
|
|
362
|
-
res.end();
|
|
446
|
+
if (!res.writableEnded) res.end();
|
|
363
447
|
return response;
|
|
364
448
|
};
|
|
365
449
|
|
|
@@ -615,10 +615,10 @@ export class RouteGroup {
|
|
|
615
615
|
constructor(
|
|
616
616
|
private router: Router,
|
|
617
617
|
private prefix: string,
|
|
618
|
-
private groupMiddlewares?:
|
|
618
|
+
private groupMiddlewares?: MiddlewareSpec[],
|
|
619
619
|
) {}
|
|
620
620
|
|
|
621
|
-
private mergeMiddlewares(routeMiddlewares?:
|
|
621
|
+
private mergeMiddlewares(routeMiddlewares?: MiddlewareSpec[]): MiddlewareSpec[] | undefined {
|
|
622
622
|
const group = this.groupMiddlewares ?? [];
|
|
623
623
|
const route = routeMiddlewares ?? [];
|
|
624
624
|
const merged = [...group, ...route];
|
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { execFileSync, exec } from "node:child_process";
|
|
7
7
|
import cluster from "node:cluster";
|
|
8
8
|
import os from "node:os";
|
|
9
|
+
import type { Socket } from "node:net";
|
|
9
10
|
import type { Tina4Config, Tina4Request, Tina4Response } from "./types.js";
|
|
10
11
|
import { Router, defaultRouter, runRouteMiddlewares } from "./router.js";
|
|
11
12
|
import { validToken, getPayload, refreshToken } from "./auth.js";
|
|
@@ -675,9 +676,23 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
675
676
|
router: Router;
|
|
676
677
|
port: number;
|
|
677
678
|
}> {
|
|
678
|
-
// Load
|
|
679
|
+
// Load env early so TINA4_DEBUG is available for the cluster decision.
|
|
680
|
+
// Precedence MUST be: real environment (set before boot) > .env.local > .env.
|
|
681
|
+
// loadEnv(override=false) is first-wins (only sets a key not already present),
|
|
682
|
+
// so load .env.local BEFORE .env — both with override=false. A real env var
|
|
683
|
+
// set before boot is already present and wins over both; .env.local fills
|
|
684
|
+
// local-only keys; .env fills whatever neither set. Loading .env.local with
|
|
685
|
+
// override=true would let a stray gitignored .env.local clobber an explicitly
|
|
686
|
+
// set real env var (e.g. a production TINA4_SECRET) — never do that.
|
|
687
|
+
loadEnv(".env.local");
|
|
679
688
|
loadEnv();
|
|
680
689
|
|
|
690
|
+
// Auto-generate a per-machine dev secret to a gitignored .env.local when one
|
|
691
|
+
// is missing (dev only, never CI/prod). Must run after env load and before
|
|
692
|
+
// any auth use. Local import avoids a load-time cycle through auth.
|
|
693
|
+
const { ensureDevSecret } = await import("./auth.js");
|
|
694
|
+
ensureDevSecret();
|
|
695
|
+
|
|
681
696
|
// Refuse to boot with pre-3.12 un-prefixed env vars set.
|
|
682
697
|
_checkLegacyEnvVars();
|
|
683
698
|
|
|
@@ -972,8 +987,8 @@ ${reset}
|
|
|
972
987
|
rawRes.setHeader("Content-Length", String(accumulated));
|
|
973
988
|
}
|
|
974
989
|
const realCb = typeof chunk === "function" ? chunk : cb;
|
|
975
|
-
return origEnd(undefined, undefined, realCb);
|
|
976
990
|
void origWrite; // referenced to keep tsc happy
|
|
991
|
+
return typeof realCb === "function" ? origEnd(realCb) : origEnd();
|
|
977
992
|
}) as typeof rawRes.end;
|
|
978
993
|
}
|
|
979
994
|
|
|
@@ -1151,7 +1166,14 @@ ${reset}
|
|
|
1151
1166
|
];
|
|
1152
1167
|
if (globalMiddleware.length > 0) {
|
|
1153
1168
|
const [, , proceed] = await MiddlewareRunner.runBefore(globalMiddleware, req, res);
|
|
1154
|
-
if (!proceed || res.raw.writableEnded)
|
|
1169
|
+
if (!proceed || res.raw.writableEnded) {
|
|
1170
|
+
// AFTER-ON-4xx RULE (M2): after_* ALWAYS run even when a before_*
|
|
1171
|
+
// short-circuited (4xx / clean 500 / response ended), so they can
|
|
1172
|
+
// still add headers / logging. Run them, then stop the handler.
|
|
1173
|
+
await MiddlewareRunner.runAfter(globalMiddleware, req, res);
|
|
1174
|
+
if (!res.raw.writableEnded) res.raw.end();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1155
1177
|
}
|
|
1156
1178
|
|
|
1157
1179
|
// Run per-route middlewares if any
|
|
@@ -1403,7 +1425,7 @@ ${reset}
|
|
|
1403
1425
|
(remoteAddress, p) => WsTracker.add(remoteAddress, p),
|
|
1404
1426
|
(id) => { WsTracker.remove(id); },
|
|
1405
1427
|
);
|
|
1406
|
-
server.on("upgrade", (req: IncomingMessage, socket, head) => {
|
|
1428
|
+
server.on("upgrade", (req: IncomingMessage, socket: Socket, head: Buffer) => {
|
|
1407
1429
|
const upPath = (req.url ?? "/").split("?")[0];
|
|
1408
1430
|
if (upPath === "/__dev_reload") {
|
|
1409
1431
|
devReloadWs.handleUpgrade(req, socket, head);
|