tina4-nodejs 3.13.137 → 3.13.139
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 +33 -3
- package/COMMERCIAL-LICENSE.md +8 -0
- package/LICENSE +373 -0
- package/NOTICE +11 -0
- package/README.md +24 -2
- package/package.json +44 -6
- package/packages/cli/dist/bin.js +19979 -18102
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/build.ts +8 -0
- package/packages/cli/src/commands/generate.ts +8 -0
- package/packages/cli/src/commands/init.ts +8 -0
- package/packages/cli/src/commands/lint.ts +8 -0
- package/packages/cli/src/commands/migrate.ts +8 -0
- package/packages/cli/src/commands/migrateCreate.ts +8 -0
- package/packages/cli/src/commands/migrateRollback.ts +8 -0
- package/packages/cli/src/commands/migrateStatus.ts +8 -0
- package/packages/cli/src/commands/queue.ts +8 -0
- package/packages/cli/src/commands/routes.ts +8 -0
- package/packages/cli/src/commands/seed.ts +8 -0
- package/packages/cli/src/commands/serve.ts +11 -0
- package/packages/cli/src/commands/test.ts +8 -0
- package/packages/core/dist/index.js +19976 -18097
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
- package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
- package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
- package/packages/core/public/bundled-component-provenance.json +48 -0
- package/packages/core/public/js/tina4js.min.js +13 -3
- package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
- package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
- package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
- package/packages/core/src/__feedback/widget.js +8 -0
- package/packages/core/src/_missing.ts +8 -0
- package/packages/core/src/ai.ts +8 -0
- package/packages/core/src/aiClient.ts +8 -0
- package/packages/core/src/api.ts +48 -7
- package/packages/core/src/auth.ts +94 -14
- package/packages/core/src/authGate.ts +35 -20
- package/packages/core/src/background.ts +8 -0
- package/packages/core/src/cache.ts +43 -4
- package/packages/core/src/constants.ts +8 -0
- package/packages/core/src/container.ts +8 -0
- package/packages/core/src/context/chunker.ts +8 -0
- package/packages/core/src/context/index.ts +8 -0
- package/packages/core/src/devAdmin.ts +185 -59
- package/packages/core/src/devMailbox.ts +8 -0
- package/packages/core/src/dispatchPipeline.ts +8 -0
- package/packages/core/src/docs.ts +8 -0
- package/packages/core/src/docsAutoDiscovery.ts +8 -0
- package/packages/core/src/docsParser.ts +8 -0
- package/packages/core/src/docsScanner.ts +44 -0
- package/packages/core/src/docsSignatures.ts +8 -0
- package/packages/core/src/dotenv.ts +8 -0
- package/packages/core/src/env.ts +8 -0
- package/packages/core/src/errorOverlay.ts +8 -0
- package/packages/core/src/events.ts +8 -0
- package/packages/core/src/fakeData.ts +8 -0
- package/packages/core/src/feedback.ts +8 -0
- package/packages/core/src/graphql.ts +108 -5
- package/packages/core/src/health.ts +8 -0
- package/packages/core/src/htmlElement.ts +8 -0
- package/packages/core/src/i18n.ts +8 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/job.ts +16 -1
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +11 -1
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1181 -1097
- package/packages/core/src/mqtt.ts +8 -0
- package/packages/core/src/mqttMessage.ts +8 -0
- package/packages/core/src/optionalPackage.ts +48 -0
- package/packages/core/src/plan.ts +8 -0
- package/packages/core/src/portTakeover.ts +8 -0
- package/packages/core/src/projectIndex.ts +8 -0
- package/packages/core/src/projectIndexExtractors.ts +8 -0
- package/packages/core/src/projectIndexStorage.ts +8 -0
- package/packages/core/src/push.ts +24 -0
- package/packages/core/src/queue.ts +23 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +23 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +61 -14
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
- package/packages/core/src/rateLimiter.ts +8 -0
- package/packages/core/src/request.ts +59 -10
- package/packages/core/src/response.ts +79 -5
- package/packages/core/src/routeDiscovery.ts +8 -0
- package/packages/core/src/router.ts +8 -0
- package/packages/core/src/server.ts +79 -25
- package/packages/core/src/service.ts +8 -0
- package/packages/core/src/session.ts +8 -0
- package/packages/core/src/sessionHandlers/childError.ts +8 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +68 -3
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/respClient.ts +8 -0
- package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
- package/packages/core/src/sso.ts +20 -4
- package/packages/core/src/ssoIdentity.ts +19 -0
- package/packages/core/src/ssrf.ts +221 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +11 -0
- package/packages/core/src/testing.ts +8 -0
- package/packages/core/src/transport.ts +369 -0
- package/packages/core/src/trustedProxy.ts +8 -0
- package/packages/core/src/types.ts +13 -0
- package/packages/core/src/validator.ts +8 -0
- package/packages/core/src/version.ts +8 -0
- package/packages/core/src/websocket.ts +12 -2
- package/packages/core/src/websocketBackplane.ts +49 -32
- package/packages/core/src/websocketConnection.ts +8 -0
- package/packages/core/src/wsdl.ts +197 -115
- package/packages/core/templates/errors/302.twig +8 -0
- package/packages/core/templates/errors/401.twig +8 -0
- package/packages/core/templates/errors/403.twig +8 -0
- package/packages/core/templates/errors/404.twig +8 -0
- package/packages/core/templates/errors/500.twig +8 -0
- package/packages/core/templates/errors/502.twig +8 -0
- package/packages/core/templates/errors/503.twig +8 -0
- package/packages/core/templates/errors/base.twig +8 -0
- package/packages/frond/dist/index.js +157 -17
- package/packages/frond/src/engine.ts +186 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8641 -6765
- package/packages/orm/src/adapters/firebird.ts +50 -3
- package/packages/orm/src/adapters/mongodb.ts +8 -0
- package/packages/orm/src/adapters/mssql.ts +29 -7
- package/packages/orm/src/adapters/mysql.ts +17 -1
- package/packages/orm/src/adapters/odbc.ts +11 -0
- package/packages/orm/src/adapters/pg-types.d.ts +8 -0
- package/packages/orm/src/adapters/postgres.ts +34 -8
- package/packages/orm/src/adapters/sqlDialect.ts +57 -0
- package/packages/orm/src/adapters/sqlite.ts +26 -0
- package/packages/orm/src/autoCrud.ts +78 -36
- package/packages/orm/src/baseModel.ts +61 -29
- package/packages/orm/src/cachedDatabase.ts +67 -1
- package/packages/orm/src/connectTimeout.ts +8 -0
- package/packages/orm/src/database.ts +649 -401
- package/packages/orm/src/databaseResult.ts +8 -0
- package/packages/orm/src/databaseUrl.ts +8 -0
- package/packages/orm/src/docstore.ts +23 -1
- package/packages/orm/src/fakeData.ts +8 -0
- package/packages/orm/src/graph/adapters/arango.ts +8 -0
- package/packages/orm/src/graph/adapters/bolt.ts +8 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
- package/packages/orm/src/graph/connectTimeout.ts +8 -0
- package/packages/orm/src/graph/errors.ts +8 -0
- package/packages/orm/src/graph/graphAdapter.ts +8 -0
- package/packages/orm/src/graph/graphDatabase.ts +8 -0
- package/packages/orm/src/graph/graphUrl.ts +10 -1
- package/packages/orm/src/graph/shapes.ts +8 -0
- package/packages/orm/src/index.ts +147 -139
- package/packages/orm/src/migration.ts +8 -0
- package/packages/orm/src/model.ts +8 -0
- package/packages/orm/src/modelCollection.ts +8 -0
- package/packages/orm/src/point.ts +8 -0
- package/packages/orm/src/query.ts +149 -33
- package/packages/orm/src/queryBuilder.ts +8 -0
- package/packages/orm/src/realtime/index.ts +8 -0
- package/packages/orm/src/realtime/models/attachment.ts +8 -0
- package/packages/orm/src/realtime/models/channel.ts +8 -0
- package/packages/orm/src/realtime/models/channelMember.ts +8 -0
- package/packages/orm/src/realtime/models/message.ts +8 -0
- package/packages/orm/src/realtime/models/workspace.ts +8 -0
- package/packages/orm/src/realtime/realtime.ts +8 -0
- package/packages/orm/src/realtime/storage.ts +43 -12
- package/packages/orm/src/seeder.ts +8 -0
- package/packages/orm/src/seederTable.ts +8 -0
- package/packages/orm/src/seederTypes.ts +8 -0
- package/packages/orm/src/sqlTranslator.ts +185 -8
- package/packages/orm/src/types.ts +15 -0
- package/packages/orm/src/validation.ts +8 -0
- package/packages/swagger/src/generator.ts +8 -0
- package/packages/swagger/src/index.ts +8 -0
- package/packages/swagger/src/ui.ts +8 -0
- package/types/core/src/api.d.ts +6 -0
- package/types/core/src/auth.d.ts +30 -0
- package/types/core/src/background.d.ts +0 -16
- package/types/core/src/cache.d.ts +6 -0
- package/types/core/src/devAdmin.d.ts +18 -12
- package/types/core/src/docs.d.ts +0 -17
- package/types/core/src/errorOverlay.d.ts +0 -24
- package/types/core/src/events.d.ts +0 -12
- package/types/core/src/feedback.d.ts +0 -23
- package/types/core/src/graphql.d.ts +25 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/job.d.ts +1 -0
- package/types/core/src/messenger.d.ts +12 -1
- package/types/core/src/metrics.d.ts +2 -0
- package/types/core/src/middleware.d.ts +45 -1
- package/types/core/src/mqtt.d.ts +0 -33
- package/types/core/src/optionalPackage.d.ts +25 -0
- package/types/core/src/plan.d.ts +0 -10
- package/types/core/src/projectIndex.d.ts +0 -10
- package/types/core/src/push.d.ts +5 -0
- package/types/core/src/queue.d.ts +7 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +10 -1
- package/types/core/src/request.d.ts +12 -0
- package/types/core/src/response.d.ts +13 -0
- package/types/core/src/server.d.ts +19 -0
- package/types/core/src/sessionHandlers/childError.d.ts +0 -15
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
- package/types/core/src/sso.d.ts +1 -0
- package/types/core/src/ssoIdentity.d.ts +10 -0
- package/types/core/src/ssrf.d.ts +19 -0
- package/types/core/src/transport.d.ts +28 -0
- package/types/core/src/trustedProxy.d.ts +0 -16
- package/types/core/src/types.d.ts +5 -0
- package/types/core/src/websocketBackplane.d.ts +1 -0
- package/types/core/src/wsdl.d.ts +8 -22
- package/types/frond/src/engine.d.ts +7 -1
- package/types/orm/src/adapters/firebird.d.ts +12 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +2 -0
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
- package/types/orm/src/adapters/sqlite.d.ts +7 -0
- package/types/orm/src/baseModel.d.ts +5 -0
- package/types/orm/src/cachedDatabase.d.ts +27 -0
- package/types/orm/src/database.d.ts +46 -7
- package/types/orm/src/graph/graphUrl.d.ts +0 -8
- package/types/orm/src/index.d.ts +1 -1
- package/types/orm/src/query.d.ts +48 -2
- package/types/orm/src/realtime/storage.d.ts +2 -1
- package/types/orm/src/sqlTranslator.d.ts +70 -18
- package/types/orm/src/types.d.ts +6 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import type { ServerResponse } from "node:http";
|
|
2
10
|
import fs from "node:fs";
|
|
3
11
|
import nodePath from "node:path";
|
|
@@ -24,6 +32,39 @@ async function closeSource(source: AsyncIterable<unknown>): Promise<void> {
|
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/** RFC 9110 `token`: one or more `tchar`. */
|
|
36
|
+
const HEADER_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The caller's name as a JSON string literal with non-ASCII escaped, byte-
|
|
40
|
+
* identical to Python's json.dumps (ADR-0068), so a name that is itself broken
|
|
41
|
+
* cannot break the error message or the log line carrying it.
|
|
42
|
+
*/
|
|
43
|
+
export function quoteHeaderName(name: string): string {
|
|
44
|
+
return JSON.stringify(name).replace(/[\u007f-\uffff]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A TypeError carrying node:http's own error code, as setHeader() throws natively. */
|
|
48
|
+
function headerRefusal(message: string, code: string): TypeError {
|
|
49
|
+
return Object.assign(new TypeError(message), { code });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Refuse a response header whose name is not an HTTP token or whose value
|
|
54
|
+
* carries CR, LF or NUL, with the exact ADR-0068 wording. node:http refuses
|
|
55
|
+
* these too, but names a broken header raw (a CR/LF in the name reaches the
|
|
56
|
+
* log line) - this is the ADR's form, checked first.
|
|
57
|
+
*/
|
|
58
|
+
export function assertSafeHeader(name: string, value: unknown): void {
|
|
59
|
+
if (!HEADER_TOKEN.test(name)) {
|
|
60
|
+
throw headerRefusal(`Header name must be a valid HTTP token [${quoteHeaderName(name)}]`, "ERR_INVALID_HTTP_TOKEN");
|
|
61
|
+
}
|
|
62
|
+
const values = Array.isArray(value) ? value : [value];
|
|
63
|
+
if (values.some((item) => /[\r\n\0]/.test(String(item)))) {
|
|
64
|
+
throw headerRefusal(`Invalid character in header content [${quoteHeaderName(name)}]`, "ERR_INVALID_CHAR");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
/** Cache Frond instances by template directory to avoid repeated instantiation. */
|
|
28
69
|
const _frondCache = new Map<string, InstanceType<any>>();
|
|
29
70
|
|
|
@@ -147,10 +188,24 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
147
188
|
res.end(chunk, encoding);
|
|
148
189
|
}
|
|
149
190
|
};
|
|
191
|
+
// Every header this Response sets goes through here, so every one is
|
|
192
|
+
// checked (ADR-0068) - even after the headers are sent, since an unsafe
|
|
193
|
+
// value is a bug in the code that built it either way.
|
|
150
194
|
const safeSetHeader = (name: string, value: string | number | readonly string[]) => {
|
|
195
|
+
assertSafeHeader(name, value);
|
|
151
196
|
if (!res.headersSent) res.setHeader(name, value);
|
|
152
197
|
};
|
|
153
198
|
|
|
199
|
+
// True once the route set Content-Type with header(); response(data) then
|
|
200
|
+
// keeps it instead of detecting one (ADR-0072, #144).
|
|
201
|
+
let contentTypeFromHeader = false;
|
|
202
|
+
const setDetectedContentType = (contentType: string) => {
|
|
203
|
+
if (!contentTypeFromHeader) safeSetHeader("Content-Type", contentType);
|
|
204
|
+
};
|
|
205
|
+
const noteContentTypeHeader = (name: string) => {
|
|
206
|
+
if (name.toLowerCase() === "content-type") contentTypeFromHeader = true;
|
|
207
|
+
};
|
|
208
|
+
|
|
154
209
|
// ── The callable: response(data, status, contentType) ──
|
|
155
210
|
const response = function (data?: unknown, statusCode?: number, contentType?: string): Tina4Response {
|
|
156
211
|
if (res.headersSent) return response;
|
|
@@ -158,6 +213,10 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
158
213
|
// Normalise ORM models / collections / query results so handlers can
|
|
159
214
|
// `return response(model)` without serialising by hand.
|
|
160
215
|
data = toJsonable(data);
|
|
216
|
+
// Any byte array is a binary body: view it as a Buffer (no copy) so every branch below writes it as bytes.
|
|
217
|
+
if (data instanceof Uint8Array && !Buffer.isBuffer(data)) {
|
|
218
|
+
data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
219
|
+
}
|
|
161
220
|
|
|
162
221
|
if (statusCode !== undefined) {
|
|
163
222
|
res.statusCode = statusCode;
|
|
@@ -166,21 +225,23 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
166
225
|
if (contentType) {
|
|
167
226
|
// Explicit content type
|
|
168
227
|
safeSetHeader("Content-Type", contentType);
|
|
169
|
-
if (
|
|
228
|
+
if (Buffer.isBuffer(data)) {
|
|
229
|
+
safeEnd(data);
|
|
230
|
+
} else if (typeof data === "object" && data !== null) {
|
|
170
231
|
safeEnd(JSON.stringify(data));
|
|
171
232
|
} else {
|
|
172
233
|
safeEnd(data == null ? "" : String(data));
|
|
173
234
|
}
|
|
174
235
|
} else if (typeof data === "object" && data !== null && !Buffer.isBuffer(data)) {
|
|
175
236
|
// dict/array → auto JSON
|
|
176
|
-
|
|
237
|
+
setDetectedContentType("application/json");
|
|
177
238
|
safeEnd(JSON.stringify(data));
|
|
178
239
|
} else if (typeof data === "string") {
|
|
179
240
|
const trimmed = data.trim();
|
|
180
241
|
if (trimmed.startsWith("<") && trimmed.endsWith(">")) {
|
|
181
|
-
|
|
242
|
+
setDetectedContentType("text/html; charset=utf-8");
|
|
182
243
|
} else {
|
|
183
|
-
|
|
244
|
+
setDetectedContentType("text/plain; charset=utf-8");
|
|
184
245
|
}
|
|
185
246
|
safeEnd(data);
|
|
186
247
|
} else if (Buffer.isBuffer(data)) {
|
|
@@ -191,7 +252,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
191
252
|
} else if (data == null) {
|
|
192
253
|
safeEnd("");
|
|
193
254
|
} else {
|
|
194
|
-
|
|
255
|
+
setDetectedContentType("text/plain; charset=utf-8");
|
|
195
256
|
safeEnd(String(data));
|
|
196
257
|
}
|
|
197
258
|
|
|
@@ -246,6 +307,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
246
307
|
|
|
247
308
|
response.header = function (name: string, value: string | number | readonly string[]): Tina4Response {
|
|
248
309
|
safeSetHeader(name, value);
|
|
310
|
+
noteContentTypeHeader(name);
|
|
249
311
|
return response;
|
|
250
312
|
};
|
|
251
313
|
|
|
@@ -254,9 +316,11 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
254
316
|
*/
|
|
255
317
|
response.addHeader = function (name: string, value: string): void {
|
|
256
318
|
safeSetHeader(name, value);
|
|
319
|
+
noteContentTypeHeader(name);
|
|
257
320
|
};
|
|
258
321
|
|
|
259
322
|
response.redirect = function (url: string, code?: number): Tina4Response {
|
|
323
|
+
assertSafeHeader("Location", url); // before the status changes
|
|
260
324
|
if (res.headersSent) return response;
|
|
261
325
|
res.statusCode = code ?? 302;
|
|
262
326
|
safeSetHeader("Location", url);
|
|
@@ -265,6 +329,15 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
265
329
|
};
|
|
266
330
|
|
|
267
331
|
response.cookie = function (name: string, value: string, options?: CookieOptions): Tina4Response {
|
|
332
|
+
// The name and value are percent-encoded below, so they cannot carry CR,
|
|
333
|
+
// LF, NUL or ';' to the wire and keep being encoded (ADR-0068). Attribute
|
|
334
|
+
// values are written as given, so one carrying those is refused: a ';'
|
|
335
|
+
// would add an attribute (say Domain=) the application never set.
|
|
336
|
+
for (const attribute of [options?.path, options?.domain, options?.sameSite, options?.maxAge]) {
|
|
337
|
+
if (attribute !== undefined && /[\r\n\0;]/.test(String(attribute))) {
|
|
338
|
+
throw headerRefusal(`Invalid character in cookie content [${quoteHeaderName(name)}]`, "ERR_INVALID_CHAR");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
268
341
|
const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
|
|
269
342
|
if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);
|
|
270
343
|
if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
@@ -419,6 +492,7 @@ export function createResponse(res: ServerResponse): Tina4Response {
|
|
|
419
492
|
source: AsyncIterable<string | Buffer>,
|
|
420
493
|
contentType: string = "text/event-stream",
|
|
421
494
|
): Promise<Tina4Response> {
|
|
495
|
+
assertSafeHeader("Content-Type", contentType);
|
|
422
496
|
if (res.headersSent) return response;
|
|
423
497
|
res.writeHead(200, {
|
|
424
498
|
"Content-Type": contentType,
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
10
|
import { join, relative, basename, extname } from "node:path";
|
|
3
11
|
import type { RouteDefinition, RouteHandler, RouteMeta } from "./types.js";
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import type { RouteHandler, RouteDefinition, RouteMeta, Middleware, MiddlewareSpec, Tina4Request, Tina4Response, WebSocketRouteHandler, WebSocketRouteDefinition } from "./types.js";
|
|
2
10
|
import { isTruthy } from "./dotenv.js";
|
|
3
11
|
import { MiddlewareRunner, isMiddlewareClass } from "./middleware.js";
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
|
10
|
+
import { createTina4HttpServer, guardUnsafeHeaders, sendTransportRejection } from "./transport.js";
|
|
2
11
|
import { randomBytes } from "node:crypto";
|
|
3
12
|
import { resolve, dirname, join, relative } from "node:path";
|
|
4
13
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
@@ -21,7 +30,7 @@ import {
|
|
|
21
30
|
TAKEOVER_KILLED,
|
|
22
31
|
TAKEOVER_REFUSALS,
|
|
23
32
|
} from "./portTakeover.js";
|
|
24
|
-
import { createRequest } from "./request.js";
|
|
33
|
+
import { createRequest, PayloadTooLargeError } from "./request.js";
|
|
25
34
|
import {
|
|
26
35
|
resetRequestCaches,
|
|
27
36
|
headStripIntercept,
|
|
@@ -36,7 +45,7 @@ import { isDebugMode } from "./errorOverlay.js";
|
|
|
36
45
|
import { createHealthRoutes } from "./health.js";
|
|
37
46
|
import { rateLimiter } from "./rateLimiter.js";
|
|
38
47
|
import { Log } from "./logger.js";
|
|
39
|
-
import { DevAdmin, RequestInspector, WsTracker,
|
|
48
|
+
import { DevAdmin, RequestInspector, WsTracker, devRequestDenial, devHostAllowed } from "./devAdmin.js";
|
|
40
49
|
import { CLOSE_GOING_AWAY, devReloadWs, serveWebSocketRoute, wsRouteManager } from "./websocket.js";
|
|
41
50
|
import { feedbackEnabled, injectFeedbackWidget } from "./feedback.js";
|
|
42
51
|
import { I18n } from "./i18n.js";
|
|
@@ -355,6 +364,35 @@ function findAvailablePort(start: number): number {
|
|
|
355
364
|
}
|
|
356
365
|
}
|
|
357
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Variables that mark a CI run, in ADR-0070's order (ci_env_vars). A run is CI
|
|
369
|
+
* when any of them holds a value that, trimmed and lower-cased, is non-empty
|
|
370
|
+
* and not in CI_NOT_SET_VALUES.
|
|
371
|
+
*/
|
|
372
|
+
export const CI_ENVIRONMENT_VARIABLES = ["CI", "CONTINUOUS_INTEGRATION", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "JENKINS_URL", "TF_BUILD", "TEAMCITY_VERSION"];
|
|
373
|
+
|
|
374
|
+
/** Values that say "not under CI" (ADR-0070 ci_not_set_values), compared trimmed and lower-cased. */
|
|
375
|
+
export const CI_NOT_SET_VALUES = ["false", "0", "no", "off"];
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Whether the server may open a browser (ADR-0070): only for a developer at a
|
|
379
|
+
* desk. Every rule is a veto; nothing forces a browser open past one.
|
|
380
|
+
*
|
|
381
|
+
* 1. development only - the caller's resolved debug mode, and never with
|
|
382
|
+
* TINA4_PRODUCTION truthy or in a cluster worker (the primary decides);
|
|
383
|
+
* 2. never when TINA4_NO_BROWSER is truthy (true/1/yes/on);
|
|
384
|
+
* 3. never with --no-browser / a programmatic noBrowser (it can only veto);
|
|
385
|
+
* 4. never under CI.
|
|
386
|
+
*/
|
|
387
|
+
export function shouldOpenBrowser(isDevelopment: boolean, noBrowser = false): boolean {
|
|
388
|
+
if (!isDevelopment || isTruthy(process.env.TINA4_PRODUCTION) || cluster.isWorker) return false;
|
|
389
|
+
if (noBrowser || isTruthy(process.env.TINA4_NO_BROWSER)) return false;
|
|
390
|
+
return !CI_ENVIRONMENT_VARIABLES.some((name) => {
|
|
391
|
+
const value = (process.env[name] ?? "").trim().toLowerCase();
|
|
392
|
+
return value !== "" && !CI_NOT_SET_VALUES.includes(value);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
358
396
|
/**
|
|
359
397
|
* Open the user's default browser after a short delay so the server is ready.
|
|
360
398
|
*/
|
|
@@ -578,9 +616,9 @@ const HTTP_REASON_PHRASES: Record<number, string> = {
|
|
|
578
616
|
304: "Not Modified", 307: "Temporary Redirect", 308: "Permanent Redirect",
|
|
579
617
|
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
|
|
580
618
|
404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable",
|
|
581
|
-
409: "Conflict", 410: "Gone", 413: "Content Too Large",
|
|
619
|
+
408: "Request Timeout", 409: "Conflict", 410: "Gone", 413: "Content Too Large",
|
|
582
620
|
415: "Unsupported Media Type", 422: "Unprocessable Content",
|
|
583
|
-
429: "Too Many Requests",
|
|
621
|
+
429: "Too Many Requests", 431: "Request Header Fields Too Large",
|
|
584
622
|
500: "Internal Server Error", 501: "Not Implemented",
|
|
585
623
|
502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
|
|
586
624
|
};
|
|
@@ -628,6 +666,9 @@ export function resolveTemplate(pathname: string, templatesDir: string): string
|
|
|
628
666
|
// Skip underscore-prefixed files even within pages/ — they're private
|
|
629
667
|
// by Hugo/Jekyll convention (helpers, fragments) and shouldn't auto-serve.
|
|
630
668
|
if (cleanPath.split("/").some((seg) => seg.startsWith("_"))) return null;
|
|
669
|
+
// ADR-0082: a "." / ".." segment (or a backslash) could walk out of pages/
|
|
670
|
+
// - the only directory that auto-routes. Refuse it outright.
|
|
671
|
+
if (cleanPath.includes("\\") || cleanPath.split("/").some((seg) => seg === "" || seg === "." || seg === "..")) return null;
|
|
631
672
|
const pagesDir = resolve(templatesDir, TEMPLATE_PAGES_DIR);
|
|
632
673
|
for (const ext of [".twig", ".html"]) {
|
|
633
674
|
if (existsSync(resolve(pagesDir, cleanPath + ext))) {
|
|
@@ -1101,21 +1142,6 @@ async function runMatchedRoute(ctx: MatchedRouteContext): Promise<void> {
|
|
|
1101
1142
|
const { req, res, match, postMatchMiddleware } = ctx;
|
|
1102
1143
|
req.params = match.params as never;
|
|
1103
1144
|
|
|
1104
|
-
// DEVADMIN-DEC-01/02: fail-closed same-origin + loopback gate on every /__dev
|
|
1105
|
-
// write (POST/PUT/PATCH/DELETE), BEFORE the handler runs. Closes drive-by CSRF
|
|
1106
|
-
// (a cross-origin page POSTing to /file/save then /reload) and a network-exposed
|
|
1107
|
-
// debug box. Scoped to /__dev so /__feedback + /ai are unaffected; GET/HEAD/
|
|
1108
|
-
// OPTIONS are safe and skip the gate. The MCP endpoints keep their own 404 gate
|
|
1109
|
-
// (devMutationDenial skips the mcp prefixes for the loopback part).
|
|
1110
|
-
const devMethod = (req.method ?? "GET").toUpperCase();
|
|
1111
|
-
if (ctx.pathname.startsWith("/__dev") && !DEV_SAFE_METHODS.has(devMethod)) {
|
|
1112
|
-
const denial = devMutationDenial(req);
|
|
1113
|
-
if (denial) {
|
|
1114
|
-
res.json({ ok: false, error: denial.error }, denial.status);
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
1145
|
if (await runGlobalMiddlewarePass(postMatchMiddleware, req, res)) return;
|
|
1120
1146
|
|
|
1121
1147
|
// Auth enforcement lives in enforceRouteAuth (authGate.ts) so the in-process
|
|
@@ -1707,6 +1733,10 @@ async function dispatchInner(
|
|
|
1707
1733
|
// These four close over nothing but the raw req/res (never ctx), which is
|
|
1708
1734
|
// why they take no context object at all.
|
|
1709
1735
|
await resetRequestCaches();
|
|
1736
|
+
// Installed FIRST so it is the innermost wrapper of end()/write(): whatever
|
|
1737
|
+
// the later interceptors do, a response carrying a header node:http refused
|
|
1738
|
+
// leaves as the ADR-0068 500 {"error":"Invalid response header"}.
|
|
1739
|
+
guardUnsafeHeaders(rawRes);
|
|
1710
1740
|
headStripIntercept(rawReq, rawRes);
|
|
1711
1741
|
// Feature 40 (CE-DEC-01/02): gzip + ETag + conditional-GET for every
|
|
1712
1742
|
// dynamic response. Installed right after headStripIntercept so it runs
|
|
@@ -1718,6 +1748,11 @@ async function dispatchInner(
|
|
|
1718
1748
|
// res.render() is handled natively by response.ts via Frond
|
|
1719
1749
|
|
|
1720
1750
|
try {
|
|
1751
|
+
// Gate before middleware, matching and automatic OPTIONS handling (ADR-0082).
|
|
1752
|
+
if (req.path.replace(/\/+/g, "/").startsWith("/__dev")) {
|
|
1753
|
+
const denial = devRequestDenial(req);
|
|
1754
|
+
if (denial) { res.json({ ok: false, error: denial.error }, denial.status); return; }
|
|
1755
|
+
}
|
|
1721
1756
|
// sessionAutoStart is the one prologue stage INSIDE the try. It degrades
|
|
1722
1757
|
// on its own (ADR-0021), so the only thing that escapes it is a
|
|
1723
1758
|
// TINA4_SESSION_STRICT refusal - and that must become a 500 through the
|
|
@@ -1740,6 +1775,16 @@ async function dispatchInner(
|
|
|
1740
1775
|
try {
|
|
1741
1776
|
await req.parseBody();
|
|
1742
1777
|
} catch (err) {
|
|
1778
|
+
// The transport already answered (a 408 for a stalled body) and the
|
|
1779
|
+
// connection is going away: nothing left to say.
|
|
1780
|
+
if (rawRes.writableEnded) return;
|
|
1781
|
+
// A body over TINA4_MAX_UPLOAD_SIZE is a transport rejection (ADR-0068
|
|
1782
|
+
// section 4): exact body, security headers, and Connection: close, so
|
|
1783
|
+
// node:http does not go on reading the rest of the upload.
|
|
1784
|
+
if (err instanceof PayloadTooLargeError) {
|
|
1785
|
+
sendTransportRejection(rawRes, 413, err.message);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1743
1788
|
const status = (err as { statusCode?: number })?.statusCode;
|
|
1744
1789
|
if (typeof status === "number" && status >= 400 && status < 500) {
|
|
1745
1790
|
if (!rawRes.writableEnded) {
|
|
@@ -1910,7 +1955,7 @@ function startLoopbackSiblings(
|
|
|
1910
1955
|
siblingHosts.map(
|
|
1911
1956
|
(siblingHost) =>
|
|
1912
1957
|
new Promise<ReturnType<typeof createServer> | null>((resolveSibling) => {
|
|
1913
|
-
const sibling =
|
|
1958
|
+
const sibling = createTina4HttpServer(dispatch);
|
|
1914
1959
|
let settled = false;
|
|
1915
1960
|
sibling.on("error", (err: NodeJS.ErrnoException) => {
|
|
1916
1961
|
const expected =
|
|
@@ -2011,9 +2056,14 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
2011
2056
|
// Auto-generate a per-machine dev secret to a gitignored .env.local when one
|
|
2012
2057
|
// is missing (dev only, never CI/prod). Must run after env load and before
|
|
2013
2058
|
// any auth use. Local import avoids a load-time cycle through auth.
|
|
2014
|
-
const { ensureDevSecret } = await import("./auth.js");
|
|
2059
|
+
const { ensureDevSecret, requireBootSecret } = await import("./auth.js");
|
|
2015
2060
|
ensureDevSecret();
|
|
2016
2061
|
|
|
2062
|
+
// Refuse to serve with a secret anyone can reproduce: blank outside dev, or
|
|
2063
|
+
// set but shorter than 32 bytes in any mode (ADR-0079 s2). Throws the
|
|
2064
|
+
// actionable message naming TINA4_SECRET.
|
|
2065
|
+
requireBootSecret();
|
|
2066
|
+
|
|
2017
2067
|
// Refuse to boot with pre-3.12 un-prefixed env vars set.
|
|
2018
2068
|
_checkLegacyEnvVars();
|
|
2019
2069
|
|
|
@@ -2186,7 +2236,7 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
2186
2236
|
// dev; the stable AI port (port+1000, created below) suppresses reload/toolbar so an
|
|
2187
2237
|
// AI tool can drive it without its own edits triggering refreshes. The tina4 client
|
|
2188
2238
|
// posts /__dev/api/reload to the MAIN port. Matches Python (master).
|
|
2189
|
-
const server =
|
|
2239
|
+
const server = createTina4HttpServer(dispatch);
|
|
2190
2240
|
|
|
2191
2241
|
// WebSocket upgrade handling on the MAIN port. Two responsibilities:
|
|
2192
2242
|
//
|
|
@@ -2217,6 +2267,11 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
2217
2267
|
const upPath = (req.url ?? "/").split("?")[0];
|
|
2218
2268
|
// Dev-reload channel (debug only).
|
|
2219
2269
|
if (isDevMode() && upPath === "/__dev_reload") {
|
|
2270
|
+
// ADR-0082: the reload socket answers only a loopback / TINA4_HOST Host.
|
|
2271
|
+
if (!devHostAllowed(req.headers.host)) {
|
|
2272
|
+
try { socket.write("HTTP/1.1 403 Forbidden\r\n\r\n"); socket.destroy(); } catch { /* gone */ }
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2220
2275
|
devReloadWs.handleUpgrade(req, socket, head);
|
|
2221
2276
|
return;
|
|
2222
2277
|
}
|
|
@@ -2295,7 +2350,7 @@ export async function startServer(config?: Tina4Config): Promise<{
|
|
|
2295
2350
|
|
|
2296
2351
|
if (isDebug && !noAiPort && aiPortInRange) {
|
|
2297
2352
|
// Stable AI port (port+1000): tag requests so /__dev_reload + toolbar are suppressed.
|
|
2298
|
-
aiServer =
|
|
2353
|
+
aiServer = createTina4HttpServer(async (req, res) => {
|
|
2299
2354
|
(req as any)._tina4AiPort = true;
|
|
2300
2355
|
await dispatch(req, res);
|
|
2301
2356
|
});
|
|
@@ -2352,8 +2407,7 @@ ${reset}
|
|
|
2352
2407
|
Debug: ${isDebug ? "ON" : "OFF"} (Log level: ${logLevel})${dualPortLines}
|
|
2353
2408
|
`);
|
|
2354
2409
|
}
|
|
2355
|
-
|
|
2356
|
-
if (!noBrowser) {
|
|
2410
|
+
if (shouldOpenBrowser(isDebug, config?.noBrowser === true)) {
|
|
2357
2411
|
// Open the browser on the MAIN port — that's the hot-reload port.
|
|
2358
2412
|
openBrowser(`http://${displayHost}:${port}`);
|
|
2359
2413
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Service Runner — in-process background services using Node.js timers.
|
|
3
11
|
* Zero dependencies. Supports cron timing, simple intervals, and daemon mode.
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Session — Pluggable session backends, zero core dependencies.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 session handlers — turn a failed `execFileSync` child into a readable cause.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Database Session Handler — sessions in a `tina4_session` table on ANY
|
|
3
11
|
* engine the ORM Database layer supports: sqlite, postgres, mysql, mssql,
|
|
@@ -152,6 +160,16 @@ const CREATE_TABLE: Record<string, string> = {
|
|
|
152
160
|
`,
|
|
153
161
|
};
|
|
154
162
|
|
|
163
|
+
/** How many times ensureTable() tries the CREATE before a failure is real. */
|
|
164
|
+
const CREATE_ATTEMPTS = 5;
|
|
165
|
+
/** Base pause between tries; it grows linearly (50, 100, 150, 200 ms). */
|
|
166
|
+
const CREATE_RETRY_DELAY_MS = 50;
|
|
167
|
+
|
|
168
|
+
/** Block this thread for `ms` - the SessionHandler interface is synchronous. */
|
|
169
|
+
function sleepSync(ms: number): void {
|
|
170
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
171
|
+
}
|
|
172
|
+
|
|
155
173
|
/**
|
|
156
174
|
* Read a column out of a result row, case-insensitively.
|
|
157
175
|
*
|
|
@@ -215,6 +233,12 @@ export class DatabaseSessionHandler implements SessionHandler {
|
|
|
215
233
|
private get sqlite(): any {
|
|
216
234
|
if (this.sqliteHandle === null) {
|
|
217
235
|
this.sqliteHandle = new DatabaseSync(this.dbPath as string);
|
|
236
|
+
// Wait up to 5 s for another process's write lock instead of failing at
|
|
237
|
+
// once with "database is locked" - node:sqlite's default is NO wait, so
|
|
238
|
+
// racing processes collided on the first overlap (five of six died,
|
|
239
|
+
// measured). 5000 ms matches PHP's busyTimeout(5000) and Python's sqlite3
|
|
240
|
+
// default. Set BEFORE the WAL pragma so that write waits too.
|
|
241
|
+
this.sqliteHandle.exec("PRAGMA busy_timeout = 5000");
|
|
218
242
|
this.sqliteHandle.exec("PRAGMA journal_mode = WAL");
|
|
219
243
|
}
|
|
220
244
|
return this.sqliteHandle;
|
|
@@ -334,16 +358,57 @@ export class DatabaseSessionHandler implements SessionHandler {
|
|
|
334
358
|
}
|
|
335
359
|
|
|
336
360
|
/**
|
|
337
|
-
* Ensure the session table exists (called once on first use)
|
|
361
|
+
* Ensure the session table exists (called once on first use) - safely when
|
|
362
|
+
* several processes race to create it.
|
|
363
|
+
*
|
|
364
|
+
* Every per-engine statement above is check-then-act, so two processes can
|
|
365
|
+
* both find the table absent and both CREATE; the loser errors, and every
|
|
366
|
+
* engine spells that differently (a pg_type unique violation on PostgreSQL,
|
|
367
|
+
* Msg 2714 on SQL Server, an RDB$RELATIONS unique violation on Firebird). So
|
|
368
|
+
* a failure is judged by RE-CHECKING whether the table now exists, never by
|
|
369
|
+
* parsing the message - the rescue the Firebird note above has always
|
|
370
|
+
* assumed, and which was not here. MEASURED on the lab before it: six real
|
|
371
|
+
* processes racing lost one to five of them on PostgreSQL, SQLite and
|
|
372
|
+
* Firebird.
|
|
373
|
+
*
|
|
374
|
+
* The re-check is RETRIED because the loser can fail BEFORE the winner has
|
|
375
|
+
* committed. On MySQL, CREATE TABLE IF NOT EXISTS takes a shared metadata
|
|
376
|
+
* lock, checks, then upgrades it to exclusive; two upgrades deadlock, and when
|
|
377
|
+
* MySQL cannot back the victim off silently it answers 1213 "Deadlock found"
|
|
378
|
+
* while the winner's table is still invisible (tina4-php CI run 35972320442).
|
|
379
|
+
* Bounded, so a genuine failure (no CREATE grant) still throws. Same attempts
|
|
380
|
+
* and delays in all four frameworks.
|
|
338
381
|
*/
|
|
339
382
|
private ensureTable(): void {
|
|
340
383
|
if (this.initialized) return;
|
|
341
384
|
const ddl = CREATE_TABLE[this.engine];
|
|
342
|
-
|
|
343
|
-
|
|
385
|
+
for (let attempt = 1; ; attempt++) {
|
|
386
|
+
try {
|
|
387
|
+
if (this.target === null) this.sqlite.exec(ddl);
|
|
388
|
+
else sqlCommandSync(this.target, ddl, []);
|
|
389
|
+
break;
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (this.tableExists()) break;
|
|
392
|
+
if (attempt >= CREATE_ATTEMPTS) throw error;
|
|
393
|
+
sleepSync(CREATE_RETRY_DELAY_MS * attempt);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
344
396
|
this.initialized = true;
|
|
345
397
|
}
|
|
346
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Whether tina4_session exists, asked the one way every engine answers alike:
|
|
401
|
+
* a query against it that returns no rows succeeds only if it is there.
|
|
402
|
+
*/
|
|
403
|
+
private tableExists(): boolean {
|
|
404
|
+
try {
|
|
405
|
+
this.query("SELECT 1 FROM tina4_session WHERE 1 = 0", []);
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
347
412
|
read(sessionId: string): SessionData | null {
|
|
348
413
|
this.ensureTable();
|
|
349
414
|
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Memcached Session Handler — zero-dependency text protocol over TCP.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 synchronous MongoDB transport — ONE persistent client per target.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 MongoDB Session Handler — MongoDB wire protocol via raw TCP, zero dependencies.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 synchronous RESP transport — shared by every Redis/Valkey session handler.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 synchronous SQL transport for the database session backend.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 sync-over-async bridge — call an ASYNC worker from SYNCHRONOUS code.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 synchronous socket transport — ONE persistent connection per target.
|
|
3
11
|
*
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Valkey Session Handler — Valkey (Redis-compatible) via raw TCP, zero dependencies.
|
|
3
11
|
*
|