tina4-nodejs 3.13.94 → 3.13.96
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 +158 -30
- package/README.md +1 -1
- package/package.json +3 -1
- package/packages/cli/dist/bin.js +30911 -28444
- package/packages/cli/src/commands/metrics.ts +17 -11
- package/packages/cli/src/commands/serve.ts +10 -9
- package/packages/core/dist/index.js +30810 -28261
- package/packages/core/public/css/tina4.min.css +1 -1
- 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 +6 -7
- package/packages/core/src/logger.ts +257 -36
- package/packages/core/src/mcp.ts +1 -1
- package/packages/core/src/messenger.ts +294 -106
- 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 +34 -16
- package/packages/core/src/response.ts +46 -1
- package/packages/core/src/router.ts +29 -4
- package/packages/core/src/server.ts +886 -421
- 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 +22717 -20168
- 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/autoCrud.ts +12 -10
- 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 +241 -197
- package/packages/orm/src/databaseResult.ts +51 -28
- package/packages/orm/src/databaseUrl.ts +484 -0
- package/packages/orm/src/docstore.ts +386 -145
- package/packages/orm/src/index.ts +13 -6
- package/packages/orm/src/migration.ts +44 -11
- package/packages/orm/src/model.ts +4 -0
- package/packages/orm/src/queryBuilder.ts +47 -6
- package/packages/orm/src/sqlTranslator.ts +310 -4
- package/packages/orm/src/types.ts +21 -77
- package/packages/swagger/dist/index.js +78 -20
- package/packages/swagger/src/generator.ts +172 -29
- 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 +6 -9
- package/types/core/src/logger.d.ts +93 -16
- package/types/core/src/messenger.d.ts +47 -6
- 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 -4
- 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 +78 -28
- package/types/orm/src/databaseResult.d.ts +29 -15
- 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 +6 -4
- package/types/orm/src/migration.d.ts +4 -3
- 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 +21 -38
- package/packages/core/src/scss.ts +0 -623
- package/packages/core/src/sessionHandlers/redisHandler.ts +0 -219
- package/types/core/src/scss.d.ts +0 -19
- package/types/core/src/sessionHandlers/redisHandler.d.ts +0 -60
|
@@ -18,6 +18,18 @@ export declare class MiddlewareChain {
|
|
|
18
18
|
*/
|
|
19
19
|
run(req: Tina4Request, res: Tina4Response): Promise<boolean>;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* True when a middleware spec is a CLASS (the beforeX/afterX convention)
|
|
23
|
+
* rather than a plain `(req, res, next)` middleware function.
|
|
24
|
+
*
|
|
25
|
+
* A class's `prototype` property is non-writable by the language spec
|
|
26
|
+
* (ClassDefinitionEvaluation); an ordinary function's is writable, and an
|
|
27
|
+
* arrow function, async function or bound function has no `prototype` at all.
|
|
28
|
+
* That is a language-level distinction rather than a name or source-string
|
|
29
|
+
* sniff, so a class named `cors` and a function named `Cors` both classify
|
|
30
|
+
* correctly.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isMiddlewareClass(spec: unknown): boolean;
|
|
21
33
|
export declare class MiddlewareRunner {
|
|
22
34
|
/** Globally registered middleware classes (parity with PHP/Ruby/Python orchestrators). */
|
|
23
35
|
private static globalMiddleware;
|
|
@@ -28,18 +40,58 @@ export declare class MiddlewareRunner {
|
|
|
28
40
|
*/
|
|
29
41
|
static use(cls: any): void;
|
|
30
42
|
/** Return the list of globally registered middleware classes. */
|
|
43
|
+
/**
|
|
44
|
+
* Global middleware that runs BEFORE route matching.
|
|
45
|
+
*
|
|
46
|
+
* A middleware opts in with `static preMatch = true`. Everything else stays
|
|
47
|
+
* where it has always run - after matching - so this is additive and no
|
|
48
|
+
* existing middleware changes behaviour.
|
|
49
|
+
*
|
|
50
|
+
* The two groups need opposite things. CORS must run before matching so its
|
|
51
|
+
* headers survive a short-circuited 401/403; a browser shown a 401 without
|
|
52
|
+
* them reports a CORS error and the real status never reaches the developer.
|
|
53
|
+
* CSRF must run AFTER, because it reads the matched route's metadata to
|
|
54
|
+
* honour a route marked noAuth - PHP shipped exactly that bypass as dead
|
|
55
|
+
* code once, because the metadata was not assigned yet.
|
|
56
|
+
*
|
|
57
|
+
* NOT named `beforeMatch` - hook discovery treats every `before*` static as
|
|
58
|
+
* a middleware hook and would call the flag itself with (req, res).
|
|
59
|
+
*/
|
|
60
|
+
static partitionByMatchPhase(all: any[]): {
|
|
61
|
+
pre: any[];
|
|
62
|
+
post: any[];
|
|
63
|
+
};
|
|
31
64
|
static getGlobal(): any[];
|
|
32
65
|
/** Clear all globally registered middleware (primarily for tests). */
|
|
33
66
|
static reset(): void;
|
|
34
67
|
/**
|
|
35
|
-
* Discover the before-prefixed / after-prefixed
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
68
|
+
* Discover the before-prefixed / after-prefixed hook names on a middleware
|
|
69
|
+
* class, INHERITED HOOKS INCLUDED, base class first (M1).
|
|
70
|
+
*
|
|
71
|
+
* `Object.getOwnPropertyNames` returns a class's OWN statics only, in
|
|
72
|
+
* source-declaration order. On its own that silently DROPPED every hook a
|
|
73
|
+
* subclass inherited: for `class Sub extends Base` with `static beforeBase`
|
|
74
|
+
* on the base, discovery returned only ["beforeSub"] even though
|
|
75
|
+
* `Sub.beforeBase` is a live function — so a shared base middleware simply
|
|
76
|
+
* never ran, with no error. Python returns ['before_base','before_sub'] and
|
|
77
|
+
* Ruby [:before_base,:before_sub]; Node was the only one of the four that
|
|
78
|
+
* lost hooks.
|
|
79
|
+
*
|
|
80
|
+
* So walk the prototype chain (the STATIC side: Sub -> Base -> ...) and emit
|
|
81
|
+
* base-class hooks BEFORE the subclass's own, de-duping an override to its
|
|
82
|
+
* first (base) position. That is exactly Python's `_discover_methods`
|
|
83
|
+
* walking `reversed(__mro__)` over each `__dict__`, and Ruby's
|
|
84
|
+
* `discover_methods` walking `ancestors.reverse_each`.
|
|
85
|
+
*
|
|
86
|
+
* Within one class the order is still source-declaration order — we
|
|
87
|
+
* deliberately do NOT sort(), so hooks run in the order they were written
|
|
88
|
+
* (parity with Python walking __dict__, PHP get_class_methods, Ruby
|
|
41
89
|
* instance_methods(false)). Cross-class order is the natural iteration of
|
|
42
90
|
* the registered classes = REGISTRATION order.
|
|
91
|
+
*
|
|
92
|
+
* The chain walk stops at Function.prototype / Object.prototype, so the
|
|
93
|
+
* built-in members are never scanned. A plain object registered as
|
|
94
|
+
* middleware still works: its own keys are level 0.
|
|
43
95
|
*/
|
|
44
96
|
private static methodNames;
|
|
45
97
|
/**
|
|
@@ -63,6 +115,10 @@ export declare class MiddlewareRunner {
|
|
|
63
115
|
* a synchronous hook that returns an array is harmless (the array resolves
|
|
64
116
|
* immediately), so existing sync hooks keep working unchanged.
|
|
65
117
|
*
|
|
118
|
+
* RETURN VALUE — see `interpretHookResult` for the one table every hook at
|
|
119
|
+
* every scope obeys. A returned Response object is the PRIMARY
|
|
120
|
+
* short-circuit; `false` is a deny.
|
|
121
|
+
*
|
|
66
122
|
* Returns [req, res, shouldContinue].
|
|
67
123
|
*/
|
|
68
124
|
static runBefore(classes: any[], req: Tina4Request, res: Tina4Response): Promise<[Tina4Request, Tina4Response, boolean]>;
|
|
@@ -84,12 +140,18 @@ export declare class MiddlewareRunner {
|
|
|
84
140
|
* ASYNC — each hook is awaited (e.g. the responseCache after-hook awaiting
|
|
85
141
|
* `backend.set`). Awaiting a synchronous hook is harmless, so existing sync
|
|
86
142
|
* after-hooks keep working unchanged.
|
|
143
|
+
*
|
|
144
|
+
* RETURN VALUE — the SAME table as runBefore (`interpretHookResult`): the
|
|
145
|
+
* contract is one table for every hook at every scope. There is nothing left
|
|
146
|
+
* to skip after the handler, so a short-circuit here ends the after chain.
|
|
147
|
+
* A THROW is different and unchanged: it is logged, becomes a clean 500, and
|
|
148
|
+
* the remaining after hooks still run.
|
|
87
149
|
*/
|
|
88
150
|
static runAfter(classes: any[], req: Tina4Request, res: Tina4Response): Promise<[Tina4Request, Tina4Response]>;
|
|
89
151
|
}
|
|
90
152
|
/** Configuration for the CORS middleware */
|
|
91
153
|
export interface CorsConfig {
|
|
92
|
-
/** Allowed origins. Default:
|
|
154
|
+
/** Allowed origins. Default: NONE (deny) — or TINA4_CORS_ORIGINS env, comma-separated. "*" allows any. */
|
|
93
155
|
origins?: string | string[];
|
|
94
156
|
/** Allowed methods. Default: standard REST methods (or TINA4_CORS_METHODS env) */
|
|
95
157
|
methods?: string | string[];
|
|
@@ -97,22 +159,79 @@ export interface CorsConfig {
|
|
|
97
159
|
headers?: string | string[];
|
|
98
160
|
/** Access-Control-Max-Age in seconds. Default: 86400 (or TINA4_CORS_MAX_AGE env) */
|
|
99
161
|
maxAge?: number;
|
|
162
|
+
/** Send Access-Control-Allow-Credentials. Default: false (or TINA4_CORS_CREDENTIALS env). Never sent with a wildcard origin. */
|
|
163
|
+
credentials?: boolean;
|
|
164
|
+
}
|
|
165
|
+
/** Reset the CORS warn-once ledger. Test seam. */
|
|
166
|
+
export declare function resetCorsWarnings(): void;
|
|
167
|
+
/**
|
|
168
|
+
* The resolved CORS policy — ONE implementation of the rules.
|
|
169
|
+
*
|
|
170
|
+
* Both the function middleware `cors()` and the class middleware
|
|
171
|
+
* `CorsMiddleware` build one of these and apply what it returns. They used to
|
|
172
|
+
* be two independent implementations that had already drifted: `cors()` never
|
|
173
|
+
* read TINA4_CORS_CREDENTIALS at all, so the DEFAULT always-on pipeline
|
|
174
|
+
* silently ignored a documented env var (measured 2026-07-31). One feature,
|
|
175
|
+
* one code path.
|
|
176
|
+
*
|
|
177
|
+
* DENY BY DEFAULT (ADR-0018). With no origins configured, NO
|
|
178
|
+
* Access-Control-Allow-Origin is emitted and the browser's own CORS check
|
|
179
|
+
* blocks the cross-origin request. "*" still works, it just has to be asked for.
|
|
180
|
+
*
|
|
181
|
+
* CREDENTIALS AND THE WILDCARD ARE MUTUALLY EXCLUSIVE. The Fetch Standard's
|
|
182
|
+
* CORS check treats "*" as a literal (not a wildcard) once the request's
|
|
183
|
+
* credentials mode is "include", so ACAO: * with
|
|
184
|
+
* Access-Control-Allow-Credentials: true is rejected by every browser.
|
|
185
|
+
*
|
|
186
|
+
* VARY: ORIGIN whenever the ACAO value is COMPUTED from the request's Origin,
|
|
187
|
+
* i.e. whenever an allow-list is configured — on a MISS as well as a match.
|
|
188
|
+
* RFC 9110 s12.5.5: a Vary field name list tells cache recipients they "MUST
|
|
189
|
+
* NOT use this response to satisfy a later request unless the later request
|
|
190
|
+
* has the same values for the listed header fields as the original request".
|
|
191
|
+
* The miss case matters most: without it a shared cache can store the no-ACAO
|
|
192
|
+
* response for origin B and serve it to origin A. A constant "*" genuinely
|
|
193
|
+
* does not vary and gets no Vary, which would only fragment a CDN's cache.
|
|
194
|
+
*
|
|
195
|
+
* Access-Control-Allow-Methods / -Allow-Headers are static configured lists
|
|
196
|
+
* here, never derived from the request's Access-Control-Request-* headers, so
|
|
197
|
+
* those field names do NOT belong in Vary.
|
|
198
|
+
*/
|
|
199
|
+
export declare class CorsPolicy {
|
|
200
|
+
readonly allowedOrigins: string[];
|
|
201
|
+
readonly allowedMethods: string;
|
|
202
|
+
readonly allowedHeaders: string;
|
|
203
|
+
readonly maxAge: number;
|
|
204
|
+
readonly credentials: boolean;
|
|
205
|
+
constructor(config?: CorsConfig);
|
|
206
|
+
/** Whether an operator has actually declared a CORS policy. */
|
|
207
|
+
isConfigured(): boolean;
|
|
208
|
+
/** The origin to send in Access-Control-Allow-Origin, or undefined for none. */
|
|
209
|
+
resolveOrigin(requestOrigin: string): string | undefined;
|
|
210
|
+
/**
|
|
211
|
+
* The CORS headers for a request origin. `isPreflight` adds Max-Age, which
|
|
212
|
+
* the Fetch Standard only defines for a preflight response.
|
|
213
|
+
*/
|
|
214
|
+
headersFor(requestOrigin: string, isPreflight: boolean): Record<string, string>;
|
|
100
215
|
}
|
|
101
216
|
/**
|
|
102
217
|
* Built-in CORS middleware (function form).
|
|
103
|
-
*
|
|
218
|
+
*
|
|
219
|
+
* A thin adapter over CorsPolicy — see that class for the rules and the
|
|
220
|
+
* standards behind them. Reads configuration from env vars when not provided:
|
|
104
221
|
* TINA4_CORS_ORIGINS — comma-separated list of allowed origins, or "*"
|
|
105
222
|
* TINA4_CORS_METHODS — comma-separated list of allowed methods
|
|
106
223
|
* TINA4_CORS_HEADERS — comma-separated list of allowed headers
|
|
107
224
|
* TINA4_CORS_MAX_AGE — preflight cache duration in seconds
|
|
225
|
+
* TINA4_CORS_CREDENTIALS — send Access-Control-Allow-Credentials
|
|
108
226
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
227
|
+
* A real preflight is answered 204. The status is the same whether the origin
|
|
228
|
+
* was allowed or denied — the browser does the blocking.
|
|
111
229
|
*/
|
|
112
230
|
export declare function cors(config?: CorsConfig): Middleware;
|
|
113
231
|
/**
|
|
114
232
|
* Class-based CORS middleware using the before/after convention.
|
|
115
|
-
*
|
|
233
|
+
*
|
|
234
|
+
* The same CorsPolicy as `cors()` — one implementation, one set of semantics.
|
|
116
235
|
*
|
|
117
236
|
* Usage:
|
|
118
237
|
* Router.use(CorsMiddleware);
|
|
@@ -121,6 +240,10 @@ export declare class CorsMiddleware {
|
|
|
121
240
|
static beforeCors(req: Tina4Request, res: Tina4Response): [Tina4Request, Tina4Response];
|
|
122
241
|
/**
|
|
123
242
|
* Check if a request is an OPTIONS preflight.
|
|
243
|
+
*
|
|
244
|
+
* NOTE: returns true for ANY OPTIONS, with no Origin check, so the name
|
|
245
|
+
* overstates what it tests. The real short-circuit uses isCorsPreflight().
|
|
246
|
+
* Kept because existing tests pin this meaning.
|
|
124
247
|
*/
|
|
125
248
|
static isPreflight(method: string): boolean;
|
|
126
249
|
}
|
|
@@ -19,12 +19,23 @@ export interface QueueConfig {
|
|
|
19
19
|
* re-enqueuing, or dead-lettering past maxRetries (at-least-once delivery).
|
|
20
20
|
* Falls back to TINA4_QUEUE_VISIBILITY_TIMEOUT, else 300 (5 min). <= 0
|
|
21
21
|
* disables the reclaim (a reservation then lasts until the consumer acks —
|
|
22
|
-
* the old at-most-once behaviour). File + MongoDB backends
|
|
23
|
-
*
|
|
22
|
+
* the old at-most-once behaviour). File + MongoDB backends, which are the
|
|
23
|
+
* only backends Node offers (ADR-0022). Parity with Python's
|
|
24
24
|
* visibility_timeout.
|
|
25
25
|
*/
|
|
26
26
|
visibilityTimeout?: number;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Where the file-backed queue stores its jobs — `TINA4_QUEUE_PATH`, else
|
|
30
|
+
* `data/queue` (relative to the working directory).
|
|
31
|
+
*
|
|
32
|
+
* Exported because it is the ONE answer to "where do the queue files live",
|
|
33
|
+
* and anything that reads the store directly (the dev-admin queue panel) must
|
|
34
|
+
* ask here rather than re-deriving it. The dev admin hardcoded
|
|
35
|
+
* `cwd/data/queue/<topic>` and so listed a DIFFERENT directory from the one
|
|
36
|
+
* `Queue.size()` counted the moment `TINA4_QUEUE_PATH` was set.
|
|
37
|
+
*/
|
|
38
|
+
export declare function queueBasePath(): string;
|
|
28
39
|
export interface ProcessOptions {
|
|
29
40
|
pollInterval?: number;
|
|
30
41
|
maxJobs?: number;
|
|
@@ -46,10 +57,21 @@ export interface ConsumeOptions {
|
|
|
46
57
|
id?: string;
|
|
47
58
|
}
|
|
48
59
|
export interface QueueBackendInterface {
|
|
49
|
-
push(queue: string, payload: unknown, delay?: number): string;
|
|
60
|
+
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
50
61
|
pop(queue: string): QueueJob | null;
|
|
51
62
|
size(queue: string): number;
|
|
52
63
|
clear(queue: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Release whatever connection the backend holds, and be safe to call twice.
|
|
66
|
+
*
|
|
67
|
+
* REQUIRED, not optional, and deliberately so: it mirrors PHP's
|
|
68
|
+
* Tina4\Queue\QueueBackend, where close() has always been part of the
|
|
69
|
+
* interface. Optional would reintroduce exactly the bug this closes — a
|
|
70
|
+
* caller feature-detecting `backend.close?.()` silently skips the backend
|
|
71
|
+
* that forgot to implement it, which is how tina4-ruby's lite backend went
|
|
72
|
+
* un-closed by every `respond_to?(:close)` guard in its tree.
|
|
73
|
+
*/
|
|
74
|
+
close(): void;
|
|
53
75
|
complete?(queue: string, id: string): void;
|
|
54
76
|
fail?(queue: string, id: string, error: string, maxRetries: number, retryBackoff: number): void;
|
|
55
77
|
retry?(queue: string, id: string, delaySeconds?: number): void;
|
|
@@ -71,9 +93,11 @@ export declare class Queue {
|
|
|
71
93
|
* Unified Queue constructor.
|
|
72
94
|
*
|
|
73
95
|
* Accepts either:
|
|
74
|
-
* - new Queue({ topic: "tasks", backend: "
|
|
75
|
-
* - new Queue("
|
|
96
|
+
* - new Queue({ topic: "tasks", backend: "mongodb" })
|
|
97
|
+
* - new Queue("mongodb", { path: "data/queue" }) // legacy
|
|
76
98
|
* - new Queue() // file backend, default topic
|
|
99
|
+
*
|
|
100
|
+
* Throws on backend "rabbitmq" or "kafka" (ADR-0022).
|
|
77
101
|
*/
|
|
78
102
|
constructor(backendOrConfig?: string | QueueConfig, config?: QueueConfig);
|
|
79
103
|
/**
|
|
@@ -120,6 +144,31 @@ export declare class Queue {
|
|
|
120
144
|
* Remove all jobs from this queue's topic. Returns the number cleared.
|
|
121
145
|
*/
|
|
122
146
|
clear(): number;
|
|
147
|
+
/**
|
|
148
|
+
* Release the backend's connection and free its resources.
|
|
149
|
+
*
|
|
150
|
+
* MEASURED 2026-08-04: close() was absent on the top-level Queue in ALL FOUR
|
|
151
|
+
* frameworks, and in Node it was absent on every backend class too — so an
|
|
152
|
+
* application had no way at all to hand a queue's client back. Same class of
|
|
153
|
+
* leak as ADR-0025 corollary 4 (client-lifecycle-is-bounded).
|
|
154
|
+
*
|
|
155
|
+
* Safe on EVERY backend: the file backend holds no connection and closes as a
|
|
156
|
+
* documented no-op, so a TINA4_QUEUE_BACKEND change never turns a working
|
|
157
|
+
* shutdown path into an error. Idempotent — each backend drops its handles on
|
|
158
|
+
* the first call, so a second call finds nothing to close and returns.
|
|
159
|
+
*
|
|
160
|
+
* HONEST CAVEAT specific to Node: neither backend it can reach holds a
|
|
161
|
+
* connection between calls today. The Mongo backend runs each operation in
|
|
162
|
+
* its own child process (ADR-0022), which closes its own client before it
|
|
163
|
+
* exits, and rabbitmq/kafka are refused outright at construction. So this
|
|
164
|
+
* releases nothing YET — it is here for the contract, and because the day the
|
|
165
|
+
* persistent-connection rewrite lands the client is released here with no
|
|
166
|
+
* change at any call site. Python, PHP and Ruby release a REAL client through
|
|
167
|
+
* the identically-named method.
|
|
168
|
+
*
|
|
169
|
+
* Treat the queue as spent afterwards and build a new one to keep working.
|
|
170
|
+
*/
|
|
171
|
+
close(): void;
|
|
123
172
|
/**
|
|
124
173
|
* Get jobs that failed at least once but are still being retried
|
|
125
174
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
@@ -44,7 +44,7 @@ export interface KafkaClientConfig extends KafkaSecurityConfig {
|
|
|
44
44
|
*/
|
|
45
45
|
export declare function kafkaSecurityConfig(env?: NodeJS.ProcessEnv): KafkaSecurityConfig;
|
|
46
46
|
export interface QueueBackend {
|
|
47
|
-
push(queue: string, payload: unknown, delay?: number): string;
|
|
47
|
+
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
48
48
|
pop(queue: string): QueueJob | null;
|
|
49
49
|
size(queue: string): number;
|
|
50
50
|
clear(queue: string): void;
|
|
@@ -20,6 +20,15 @@ export declare class LiteBackend {
|
|
|
20
20
|
private nowIso;
|
|
21
21
|
private futureIso;
|
|
22
22
|
private nextPrefix;
|
|
23
|
+
/**
|
|
24
|
+
* No-op: the file backend holds no connection to release.
|
|
25
|
+
*
|
|
26
|
+
* It exists so `Queue.close()` can call ONE method on every backend instead
|
|
27
|
+
* of testing for it, and so switching TINA4_QUEUE_BACKEND to "file" never
|
|
28
|
+
* turns a working close() into "backend.close is not a function". Idempotent
|
|
29
|
+
* by construction — there is nothing to drop.
|
|
30
|
+
*/
|
|
31
|
+
close(): void;
|
|
23
32
|
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
24
33
|
/**
|
|
25
34
|
* Return [filename, jobData] for every pending, non-delayed job, ordered by
|
|
@@ -18,10 +18,12 @@ export interface MongoConfig {
|
|
|
18
18
|
maxRetries?: number;
|
|
19
19
|
}
|
|
20
20
|
export interface QueueBackend {
|
|
21
|
-
push(queue: string, payload: unknown, delay?: number): string;
|
|
21
|
+
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
22
22
|
pop(queue: string): QueueJob | null;
|
|
23
23
|
size(queue: string): number;
|
|
24
24
|
clear(queue: string): void;
|
|
25
|
+
/** Release whatever connection the backend holds. Must be idempotent. */
|
|
26
|
+
close(): void;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* MongoDB queue backend using the `mongodb` npm package.
|
|
@@ -69,7 +71,8 @@ export declare class MongoBackend implements QueueBackend {
|
|
|
69
71
|
* Execute a MongoDB operation synchronously via a child process.
|
|
70
72
|
*/
|
|
71
73
|
private execSync;
|
|
72
|
-
|
|
74
|
+
popById(queue: string, id: string): QueueJob | null;
|
|
75
|
+
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
73
76
|
pop(queue: string): QueueJob | null;
|
|
74
77
|
size(queue: string): number;
|
|
75
78
|
clear(queue: string): void;
|
|
@@ -94,4 +97,23 @@ export declare class MongoBackend implements QueueBackend {
|
|
|
94
97
|
retryFailed(queue: string, maxRetries?: number): number;
|
|
95
98
|
/** Remove jobs by status (default: every doc for the topic). Returns count removed. */
|
|
96
99
|
purge(queue: string, status?: string): number;
|
|
100
|
+
/**
|
|
101
|
+
* Release the MongoDB connection. Idempotent — a second call is a no-op.
|
|
102
|
+
*
|
|
103
|
+
* HONEST CAVEAT, and it is the whole reason ADR-0022 exists: THIS backend
|
|
104
|
+
* holds no connection between calls to release. Every operation runs in its
|
|
105
|
+
* own child process (see execSync/buildScript), and that child's `finally`
|
|
106
|
+
* already does `await client.close()` before it exits — so the pool it opened
|
|
107
|
+
* is gone by the time the method returns. Unlike tina4-python, tina4-php and
|
|
108
|
+
* tina4-ruby, whose Mongo/broker backends hold a long-lived client that this
|
|
109
|
+
* method genuinely hands back, Node has nothing to give back.
|
|
110
|
+
*
|
|
111
|
+
* It is implemented anyway, and required by the QueueBackend interface,
|
|
112
|
+
* because the CONTRACT is what matters: `Queue.close()` must be callable on
|
|
113
|
+
* every backend in every framework, and the day the persistent-connection
|
|
114
|
+
* rewrite lands (ADR-0022's tracked fix) the client goes here with no change
|
|
115
|
+
* at any call site. A method that is a no-op today and correct forever beats
|
|
116
|
+
* a missing method the caller has to feature-detect.
|
|
117
|
+
*/
|
|
118
|
+
close(): void;
|
|
97
119
|
}
|
|
@@ -16,12 +16,12 @@ export interface RabbitMQConfig {
|
|
|
16
16
|
* Parse an AMQP URL (amqp://[user:pass@]host[:port][/vhost]) into a partial
|
|
17
17
|
* RabbitMQConfig. Mirrors the Python/PHP/Ruby `parse_amqp_url` semantics:
|
|
18
18
|
* strips a leading amqp:// or amqps:// scheme, splits optional credentials,
|
|
19
|
-
* and
|
|
20
|
-
* in the URL are populated.
|
|
19
|
+
* and reads the path segment as the URL-decoded vhost name. Only fields
|
|
20
|
+
* present in the URL are populated.
|
|
21
21
|
*/
|
|
22
22
|
export declare function parseAmqpUrl(url: string): RabbitMQConfig;
|
|
23
23
|
export interface QueueBackend {
|
|
24
|
-
push(queue: string, payload: unknown, delay?: number): string;
|
|
24
|
+
push(queue: string, payload: unknown, delay?: number, priority?: number): string;
|
|
25
25
|
pop(queue: string): QueueJob | null;
|
|
26
26
|
size(queue: string): number;
|
|
27
27
|
clear(queue: string): void;
|
|
@@ -317,10 +317,21 @@ export declare class RouteGroup {
|
|
|
317
317
|
*/
|
|
318
318
|
export declare function resolveStringMiddleware(spec: string): Promise<Middleware>;
|
|
319
319
|
/**
|
|
320
|
-
* Run per-route middleware chain
|
|
320
|
+
* Run the per-route middleware chain. Returns false when it short-circuited
|
|
321
|
+
* and the handler must be skipped.
|
|
321
322
|
*
|
|
322
|
-
* Accepts middleware functions and
|
|
323
|
-
*
|
|
323
|
+
* Accepts middleware functions, middleware CLASSES, and string specs
|
|
324
|
+
* (e.g. "ResponseCache:300"). A function or string spec is resolved and
|
|
325
|
+
* invoked as `mw(req, res, next)` exactly as before.
|
|
326
|
+
*
|
|
327
|
+
* A CLASS runs its beforeX hooks through the SAME `MiddlewareRunner.runBefore`
|
|
328
|
+
* and the SAME return-value table as global middleware — no parallel runner.
|
|
329
|
+
* Its afterX hooks run with the global after pass once the handler is done
|
|
330
|
+
* (server.ts / testClient.ts append the route's classes to that list), because
|
|
331
|
+
* "after" means after the handler, not after this function. Every spec used to
|
|
332
|
+
* be invoked as `mw(req, res, next)`, which for a class throws "Class
|
|
333
|
+
* constructor cannot be invoked without 'new'", so a class attached per-route
|
|
334
|
+
* was inert. Python and PHP both ran per-route class hooks already.
|
|
324
335
|
*/
|
|
325
336
|
export declare function runRouteMiddlewares(middlewares: MiddlewareSpec[], req: Tina4Request, res: Tina4Response): Promise<boolean>;
|
|
326
337
|
/**
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
import type { Tina4Config } from "./types.js";
|
|
3
3
|
import { Router } from "./router.js";
|
|
4
|
+
/** How long a graceful shutdown waits for in-flight requests, in seconds. */
|
|
5
|
+
export declare const DEFAULT_SHUTDOWN_TIMEOUT_SECONDS = 30;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the shutdown budget from `TINA4_SHUTDOWN_TIMEOUT` (seconds).
|
|
8
|
+
*
|
|
9
|
+
* 30s matches Kubernetes' default `terminationGracePeriodSeconds` and
|
|
10
|
+
* Gunicorn's `graceful_timeout`, so the drain finishes just BEFORE the
|
|
11
|
+
* orchestrator's SIGKILL rather than being truncated by it. Same env var and
|
|
12
|
+
* same default as tina4-ruby's `Tina4::Shutdown`.
|
|
13
|
+
*
|
|
14
|
+
* A non-numeric or negative value falls back to the default rather than
|
|
15
|
+
* silently disabling the drain - a typo must not turn shutdown into a
|
|
16
|
+
* zero-second force-kill.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shutdownTimeoutSeconds(): number;
|
|
4
19
|
/**
|
|
5
20
|
* Build the startup banner's optional surface lines (issue #99).
|
|
6
21
|
*
|
|
@@ -105,10 +120,6 @@ export declare function resetTemplateCache(): void;
|
|
|
105
120
|
* The whole feature can be turned off with `TINA4_TEMPLATE_ROUTING=off`.
|
|
106
121
|
*/
|
|
107
122
|
export declare function resolveTemplate(pathname: string, templatesDir: string): string | null;
|
|
108
|
-
/**
|
|
109
|
-
* Start the Tina4 HTTP server.
|
|
110
|
-
* Thin wrapper around startServer() for cross-framework parity with PHP and Ruby.
|
|
111
|
-
*/
|
|
112
123
|
export declare function start(config?: Tina4Config): Promise<{
|
|
113
124
|
close: () => void;
|
|
114
125
|
router: Router;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface SessionConfig {
|
|
2
|
-
/** Session backend type: "file", "redis", "valkey", "mongo", "database" (or "db") */
|
|
2
|
+
/** Session backend type: "file", "redis", "valkey", "mongo", "memcached", "database" (or "db") */
|
|
3
3
|
backend?: string;
|
|
4
4
|
/** File storage path (default: "data/sessions") */
|
|
5
5
|
path?: string;
|
|
@@ -21,6 +21,21 @@ interface SessionData {
|
|
|
21
21
|
_accessed: number;
|
|
22
22
|
[key: string]: unknown;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Is `sessionId` a well-formed opaque session identifier?
|
|
26
|
+
*
|
|
27
|
+
* Callers pass UNTRUSTED input here (the session cookie is attacker-chosen), so
|
|
28
|
+
* anything that is not a string of the opaque alphabet is rejected.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isValidSessionId(sessionId: unknown): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Every accepted backend name, aliases included. Byte-identical membership in
|
|
33
|
+
* all four frameworks. Written once here so the switch below and the error
|
|
34
|
+
* message cannot disagree.
|
|
35
|
+
*/
|
|
36
|
+
export declare const VALID_SESSION_BACKENDS: readonly ["file", "filesystem", "redis", "valkey", "mongodb", "mongo", "memcached", "memcache", "database", "db"];
|
|
37
|
+
/** Canonical name of each backend, for the error message (aliases omitted). */
|
|
38
|
+
export declare const CANONICAL_SESSION_BACKENDS: readonly ["file", "redis", "valkey", "mongodb", "memcached", "database"];
|
|
24
39
|
/**
|
|
25
40
|
* Base interface for session storage backends.
|
|
26
41
|
* Implementations must provide read, write, and destroy.
|
|
@@ -36,6 +51,29 @@ export declare class FileSessionHandler implements SessionHandler {
|
|
|
36
51
|
private storagePath;
|
|
37
52
|
constructor(storagePath?: string);
|
|
38
53
|
private ensureDir;
|
|
54
|
+
/**
|
|
55
|
+
* Derive the file backing a session id. TWO independent guards, both required.
|
|
56
|
+
*
|
|
57
|
+
* This is the one place a session id becomes a filesystem path, and it used to
|
|
58
|
+
* interpolate the id RAW: `join(storagePath, "../../OUTSIDE/appconfig.json")`
|
|
59
|
+
* left the session directory entirely, so a cookie could read an existing
|
|
60
|
+
* .json from anywhere on disk into `session.all()` and then OVERWRITE it on
|
|
61
|
+
* save. Reproduced on Node 24.9.0 / macOS.
|
|
62
|
+
*
|
|
63
|
+
* 1. VALIDATE — a malformed id is refused outright. It throws rather than
|
|
64
|
+
* returning null so a hostile id can never be mistaken for an ordinary
|
|
65
|
+
* cache miss (the Session layer catches it, logs it, and degrades).
|
|
66
|
+
* 2. HASH — the filename is a SHA-256 of the id, matching the Python master
|
|
67
|
+
* (`hashlib.sha256(session_id.encode()).hexdigest()`). A hex digest cannot
|
|
68
|
+
* contain a separator or a dot, so even an id that somehow passed the
|
|
69
|
+
* validator can only ever name a file inside `storagePath`.
|
|
70
|
+
*
|
|
71
|
+
* DEPLOY NOTE: hashing CHANGES the filename for every id, so existing on-disk
|
|
72
|
+
* sessions are orphaned and every logged-in user is logged out ONCE on the
|
|
73
|
+
* deploy that ships this. That is accepted: under strict session mode an old
|
|
74
|
+
* cookie is discarded on a read miss anyway, so the sessions were going to be
|
|
75
|
+
* dropped regardless.
|
|
76
|
+
*/
|
|
39
77
|
private filePath;
|
|
40
78
|
read(sessionId: string): SessionData | null;
|
|
41
79
|
write(sessionId: string, data: SessionData, ttl?: number): void;
|
|
@@ -97,13 +135,28 @@ export declare class Session {
|
|
|
97
135
|
* frameworks. Strict mode is the escape hatch (same as events/seeding).
|
|
98
136
|
*/
|
|
99
137
|
private strict;
|
|
138
|
+
/**
|
|
139
|
+
* True when the LAST backend read RAISED rather than returning a miss.
|
|
140
|
+
* Lets `start()` tell "no such session" from "the store is unreachable".
|
|
141
|
+
*/
|
|
142
|
+
private lastReadFailed;
|
|
100
143
|
constructor(backend?: string, config?: SessionConfig);
|
|
101
144
|
/**
|
|
102
145
|
* Use a custom session handler (for advanced use cases).
|
|
103
146
|
*/
|
|
104
147
|
setHandler(handler: SessionHandler): void;
|
|
105
148
|
private logBackendError;
|
|
106
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Read through the backend; on FAILURE log + degrade to empty (or re-throw
|
|
151
|
+
* under strict).
|
|
152
|
+
*
|
|
153
|
+
* Sets {@link lastReadFailed} so `start()` can tell "the store answered, and
|
|
154
|
+
* has no such session" from "the store did not answer at all". Strict mode
|
|
155
|
+
* must discard an id only on the first: treating an outage as an unknown id
|
|
156
|
+
* rotates the session id on EVERY request for the whole outage, logging the
|
|
157
|
+
* entire userbase out over one Redis blip and orphaning their stored
|
|
158
|
+
* sessions. The policy is log-loud + degrade, never rotate.
|
|
159
|
+
*/
|
|
107
160
|
private safeRead;
|
|
108
161
|
/** Write through the backend; on FAILURE log + return false (or re-throw under strict). */
|
|
109
162
|
private safeWrite;
|
|
@@ -111,6 +164,26 @@ export declare class Session {
|
|
|
111
164
|
private safeDestroy;
|
|
112
165
|
/**
|
|
113
166
|
* Start or resume a session.
|
|
167
|
+
*
|
|
168
|
+
* `sessionId` is UNTRUSTED — it arrives from the session cookie, which the
|
|
169
|
+
* client fully controls. An id that is not a well-formed opaque identifier is
|
|
170
|
+
* DISCARDED and a fresh one minted, never adopted: adopting it let a cookie
|
|
171
|
+
* steer a filesystem path (a `tina4_session=../../OUTSIDE/appconfig` cookie
|
|
172
|
+
* read an existing .json from outside the session directory into
|
|
173
|
+
* `session.all()`, then OVERWROTE it on save) and let an attacker pre-plant a
|
|
174
|
+
* session id that survived the victim's login (session fixation).
|
|
175
|
+
*
|
|
176
|
+
* The check runs BEFORE the read, so a hostile id never reaches a handler at
|
|
177
|
+
* all. A legitimate id from any of the four frameworks passes unchanged.
|
|
178
|
+
*
|
|
179
|
+
* STRICT SESSION MODE (deliberate, and Node is the family reference for it):
|
|
180
|
+
* an id that is WELL-FORMED but UNKNOWN to the backend is also discarded and
|
|
181
|
+
* a fresh one minted — the `if (loaded)` below only adopts an id the store
|
|
182
|
+
* actually knows. That is OWASP's strict mode and PHP's own
|
|
183
|
+
* `session.use_strict_mode=1` default, and it is what stops an attacker
|
|
184
|
+
* planting a session id that survives the victim's login. The validation
|
|
185
|
+
* above sits IN FRONT of it; neither replaces the other.
|
|
186
|
+
*
|
|
114
187
|
* @param sessionId - Existing session ID to resume (optional)
|
|
115
188
|
* @returns The session ID
|
|
116
189
|
*/
|
|
@@ -232,6 +305,18 @@ export declare function isSecureScheme(forwardedProto?: string, socketEncrypted?
|
|
|
232
305
|
* `session.session_cookie_name()`.
|
|
233
306
|
*/
|
|
234
307
|
export declare function sessionCookieName(): string;
|
|
308
|
+
/**
|
|
309
|
+
* Is TINA4_SESSION_STRICT on? The single source of truth for the flag.
|
|
310
|
+
*
|
|
311
|
+
* Module level, not a Session field, because the REQUEST PATH has to be able to
|
|
312
|
+
* consult it when a Session could not be constructed at all - a handler whose
|
|
313
|
+
* constructor raises, or a refused TINA4_SESSION_BACKEND, both fail BEFORE
|
|
314
|
+
* there is an object to ask. That gap is why strict mode used to be inert on
|
|
315
|
+
* the request path. Parity with Python `session.session_strict_mode()`.
|
|
316
|
+
*
|
|
317
|
+
* TINA4_SESSION_STRICT re-throw instead of degrading (default: false)
|
|
318
|
+
*/
|
|
319
|
+
export declare function sessionStrictMode(): boolean;
|
|
235
320
|
/**
|
|
236
321
|
* Build the `Set-Cookie` header value for a Tina4 session. Centralised so
|
|
237
322
|
* the auto-cookie path in server.ts and `Session.cookieHeader()` agree on
|