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
|
@@ -0,0 +1,369 @@
|
|
|
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
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The HTTP transport around node:http (ADR-0068 sections 2-4).
|
|
11
|
+
*
|
|
12
|
+
* node:http parses the request, so most of the rules are its own; this module
|
|
13
|
+
* makes what the client SEES match the contract every Tina4 framework keeps:
|
|
14
|
+
*
|
|
15
|
+
* - every transport rejection (400, 408, 413, 431, 500) has one shape: an
|
|
16
|
+
* exact JSON body, Content-Type, Content-Length, Connection: close and the
|
|
17
|
+
* canonical security headers (no HSTS - it is written before the scheme is
|
|
18
|
+
* known);
|
|
19
|
+
* - TINA4_MAX_REQUEST_HEADER caps the head (431) and TINA4_REQUEST_TIMEOUT
|
|
20
|
+
* answers a request that stalls part-way (408);
|
|
21
|
+
* - a declared body over TINA4_MAX_UPLOAD_SIZE is refused before any of it is
|
|
22
|
+
* read, and a refused upload is closed - node:http would otherwise read and
|
|
23
|
+
* discard the rest of it to keep the connection alive;
|
|
24
|
+
* - a header node:http refuses natively still produces the fixture's answer
|
|
25
|
+
* (500 {"error":"Invalid response header"}), not a route error page.
|
|
26
|
+
*/
|
|
27
|
+
import { createServer, validateHeaderName, validateHeaderValue } from "node:http";
|
|
28
|
+
import type { IncomingMessage, ServerResponse, Server, OutgoingHttpHeaders } from "node:http";
|
|
29
|
+
import type { Socket } from "node:net";
|
|
30
|
+
import { SecurityHeadersMiddleware } from "./middleware.js";
|
|
31
|
+
import { Log } from "./logger.js";
|
|
32
|
+
import { quoteHeaderName } from "./response.js";
|
|
33
|
+
import { resolveLimit, maxUploadSize, bodyTooLargeMessage } from "./request.js";
|
|
34
|
+
|
|
35
|
+
const DEFAULT_MAX_REQUEST_HEADER = 65_536;
|
|
36
|
+
const DEFAULT_REQUEST_TIMEOUT_SECONDS = 30;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* After a rejection the server stops reading the connection at once and keeps
|
|
40
|
+
* it open for at most HOLD_SECONDS before closing it (ADR-0068 section 4).
|
|
41
|
+
*
|
|
42
|
+
* Why hold rather than close: closing with unread bytes in the kernel buffer
|
|
43
|
+
* sends a reset, and a client still uploading would lose the answer. Why not
|
|
44
|
+
* drain (read and discard, as PHP and Python do): on loopback two seconds of
|
|
45
|
+
* draining is gigabytes read, and a refused upload must not make the server
|
|
46
|
+
* read more of it. Paused, TCP flow control stalls the client instead, it
|
|
47
|
+
* reads the answer, and the bytes read after the refusal stay bounded by what
|
|
48
|
+
* was already in flight.
|
|
49
|
+
*/
|
|
50
|
+
const HOLD_SECONDS = 2;
|
|
51
|
+
|
|
52
|
+
const held = new WeakSet<Socket>();
|
|
53
|
+
function stopReadingThenClose(socket: Socket): void {
|
|
54
|
+
if (held.has(socket)) return;
|
|
55
|
+
held.add(socket);
|
|
56
|
+
socket.pause();
|
|
57
|
+
// node:http resumes the socket to discard an unread request body (and after
|
|
58
|
+
// a parse error); on this socket that must not happen any more.
|
|
59
|
+
(socket as Socket & { resume: () => Socket }).resume = () => socket;
|
|
60
|
+
const timer = setTimeout(() => socket.destroy(), HOLD_SECONDS * 1000);
|
|
61
|
+
timer.unref();
|
|
62
|
+
socket.once("close", () => clearTimeout(timer));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const REASONS: Record<number, string> = {
|
|
66
|
+
400: "Bad Request",
|
|
67
|
+
408: "Request Timeout",
|
|
68
|
+
413: "Content Too Large",
|
|
69
|
+
431: "Request Header Fields Too Large",
|
|
70
|
+
500: "Internal Server Error",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function rejectionHeaders(body: string): Array<[string, string]> {
|
|
74
|
+
const headers: Array<[string, string]> = [
|
|
75
|
+
["Content-Type", "application/json"],
|
|
76
|
+
["Content-Length", String(Buffer.byteLength(body))],
|
|
77
|
+
["Connection", "close"],
|
|
78
|
+
];
|
|
79
|
+
for (const [name, value] of Object.entries(SecurityHeadersMiddleware.canonicalHeaders())) {
|
|
80
|
+
// A broken environment override must not break the rejection itself.
|
|
81
|
+
if (!/[\r\n\0]/.test(value)) headers.push([name, value]);
|
|
82
|
+
}
|
|
83
|
+
return headers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function rejectionBody(message: string): string {
|
|
87
|
+
return JSON.stringify({ error: message });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A whole rejection as raw bytes, for a socket no ServerResponse owns yet. */
|
|
91
|
+
function rawRejection(status: number, message: string): string {
|
|
92
|
+
const body = rejectionBody(message);
|
|
93
|
+
const head = rejectionHeaders(body).map(([name, value]) => `${name}: ${value}\r\n`).join("");
|
|
94
|
+
return `HTTP/1.1 ${status} ${REASONS[status] ?? "Error"}\r\n${head}\r\n${body}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Answer through a ServerResponse with the rejection shape and close. Every
|
|
99
|
+
* header already on the response is dropped: the shape is exact. Node closes
|
|
100
|
+
* the connection once the answer is flushed (Connection: close), so the rest
|
|
101
|
+
* of a refused upload is never read.
|
|
102
|
+
*/
|
|
103
|
+
export function sendTransportRejection(res: ServerResponse, status: number, message: string, end = res.end.bind(res)): void {
|
|
104
|
+
if (res.headersSent) {
|
|
105
|
+
res.socket?.destroy();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const name of res.getHeaderNames()) res.removeHeader(name);
|
|
109
|
+
res.statusCode = status;
|
|
110
|
+
res.statusMessage = REASONS[status] ?? "Error";
|
|
111
|
+
const body = rejectionBody(message);
|
|
112
|
+
for (const [name, value] of rejectionHeaders(body)) res.setHeader(name, value);
|
|
113
|
+
res.shouldKeepAlive = false;
|
|
114
|
+
const socket = res.socket;
|
|
115
|
+
if (socket) {
|
|
116
|
+
// node:http closes a Connection: close response with destroySoon() once
|
|
117
|
+
// it is flushed, which resets a client that is still uploading before it
|
|
118
|
+
// has read the answer. Half-close instead; stopReadingThenClose() closes.
|
|
119
|
+
(socket as Socket & { destroySoon: () => void }).destroySoon = () => socket.end();
|
|
120
|
+
stopReadingThenClose(socket);
|
|
121
|
+
}
|
|
122
|
+
end(body);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Write a rejection to a socket no response owns yet, then drain and close. */
|
|
126
|
+
const rejectedSockets = new WeakSet<Socket>();
|
|
127
|
+
function rejectSocket(socket: Socket, status: number, message: string): void {
|
|
128
|
+
if (rejectedSockets.has(socket)) return;
|
|
129
|
+
rejectedSockets.add(socket);
|
|
130
|
+
socket.end(rawRejection(status, message));
|
|
131
|
+
stopReadingThenClose(socket);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const HEADER_REFUSAL_CODES = new Set(["ERR_INVALID_CHAR", "ERR_INVALID_HTTP_TOKEN", "ERR_HTTP_INVALID_HEADER_VALUE"]);
|
|
135
|
+
|
|
136
|
+
function isHeaderRefusal(err: unknown): boolean {
|
|
137
|
+
return err instanceof TypeError && HEADER_REFUSAL_CODES.has((err as { code?: string }).code ?? "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* ADR-0068 section 2 for Node. node:http refuses an unsafe header where it is
|
|
142
|
+
* set (setHeader / appendHeader / writeHead). A header set straight on the raw
|
|
143
|
+
* response - past Tina4's own call-site check - used to surface as a route
|
|
144
|
+
* error page. Here the refusal is recorded instead, and whatever the handler
|
|
145
|
+
* sends next becomes the fixture's 500 {"error":"Invalid response header"},
|
|
146
|
+
* logged by header name. Tina4's own Response keeps refusing at the call site
|
|
147
|
+
* (its check runs before this layer is reached).
|
|
148
|
+
*/
|
|
149
|
+
export function guardUnsafeHeaders(res: ServerResponse): void {
|
|
150
|
+
let refusedName: string | null = null;
|
|
151
|
+
const originalEnd = res.end.bind(res);
|
|
152
|
+
const originalWrite = res.write.bind(res);
|
|
153
|
+
const originalWriteHead = res.writeHead.bind(res);
|
|
154
|
+
const originalSetHeader = res.setHeader.bind(res);
|
|
155
|
+
const originalAppendHeader = typeof res.appendHeader === "function" ? res.appendHeader.bind(res) : null;
|
|
156
|
+
|
|
157
|
+
const refuse = (name: unknown): void => {
|
|
158
|
+
if (refusedName !== null) return;
|
|
159
|
+
refusedName = String(name);
|
|
160
|
+
Log.error(
|
|
161
|
+
`Refusing to write an unsafe response header ${quoteHeaderName(refusedName)}: ` +
|
|
162
|
+
"the name is not an HTTP token or the value contains CR, LF or NUL",
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
// While the refusal itself is being written the wrappers step aside:
|
|
166
|
+
// node:http's end() calls this.writeHead() and this.setHeader() internally.
|
|
167
|
+
let answering = false;
|
|
168
|
+
const answerRefused = (): void => {
|
|
169
|
+
if (answering) return;
|
|
170
|
+
answering = true;
|
|
171
|
+
sendTransportRejection(res, 500, "Invalid response header", originalEnd as typeof res.end);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
res.setHeader = function (name: string, value: number | string | readonly string[]) {
|
|
175
|
+
try {
|
|
176
|
+
return originalSetHeader(name, value);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!isHeaderRefusal(err)) throw err;
|
|
179
|
+
refuse(name);
|
|
180
|
+
return res;
|
|
181
|
+
}
|
|
182
|
+
} as typeof res.setHeader;
|
|
183
|
+
|
|
184
|
+
if (originalAppendHeader) {
|
|
185
|
+
res.appendHeader = function (name: string, value: string | readonly string[]) {
|
|
186
|
+
try {
|
|
187
|
+
return originalAppendHeader(name, value);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (!isHeaderRefusal(err)) throw err;
|
|
190
|
+
refuse(name);
|
|
191
|
+
return res;
|
|
192
|
+
}
|
|
193
|
+
} as typeof res.appendHeader;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
res.writeHead = function (this: ServerResponse, statusCode: number, ...rest: unknown[]) {
|
|
197
|
+
// Validate the headers object BEFORE anything reaches the wire: node:http
|
|
198
|
+
// would otherwise send the head without the refused header.
|
|
199
|
+
const headers = rest.find((arg) => arg !== null && typeof arg === "object") as OutgoingHttpHeaders | string[] | undefined;
|
|
200
|
+
if (headers && !Array.isArray(headers)) {
|
|
201
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
202
|
+
try {
|
|
203
|
+
validateHeaderName(name);
|
|
204
|
+
if (value !== undefined) for (const item of Array.isArray(value) ? value : [value]) validateHeaderValue(name, String(item));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (!isHeaderRefusal(err)) throw err;
|
|
207
|
+
refuse(name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (refusedName !== null && !answering) return res;
|
|
212
|
+
try {
|
|
213
|
+
return (originalWriteHead as (...args: unknown[]) => ServerResponse)(statusCode, ...rest);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (!isHeaderRefusal(err)) throw err;
|
|
216
|
+
refuse("(writeHead)");
|
|
217
|
+
return res;
|
|
218
|
+
}
|
|
219
|
+
} as typeof res.writeHead;
|
|
220
|
+
|
|
221
|
+
res.write = function (...args: unknown[]) {
|
|
222
|
+
if (refusedName !== null && !answering) {
|
|
223
|
+
answerRefused();
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
return (originalWrite as (...a: unknown[]) => boolean)(...args);
|
|
227
|
+
} as typeof res.write;
|
|
228
|
+
|
|
229
|
+
res.end = function (...args: unknown[]) {
|
|
230
|
+
if (refusedName !== null && !answering) {
|
|
231
|
+
answerRefused();
|
|
232
|
+
return res;
|
|
233
|
+
}
|
|
234
|
+
return (originalEnd as (...a: unknown[]) => ServerResponse)(...args);
|
|
235
|
+
} as typeof res.end;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Refuse a request on its head alone, before a body byte is read: a
|
|
240
|
+
* Transfer-Encoding other than exactly `chunked` (node:http has already
|
|
241
|
+
* refused one combined with Content-Length, or repeated), and a declared
|
|
242
|
+
* Content-Length over TINA4_MAX_UPLOAD_SIZE. Returns true when it answered.
|
|
243
|
+
*/
|
|
244
|
+
function rejectOnHead(req: IncomingMessage, res: ServerResponse): boolean {
|
|
245
|
+
const transferEncoding = req.headers["transfer-encoding"];
|
|
246
|
+
if (transferEncoding !== undefined && transferEncoding.trim().toLowerCase() !== "chunked") {
|
|
247
|
+
sendTransportRejection(res, 400, "Invalid Transfer-Encoding");
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
const contentLength = req.headers["content-length"];
|
|
251
|
+
if (contentLength !== undefined) {
|
|
252
|
+
const limit = maxUploadSize();
|
|
253
|
+
const declared = BigInt(contentLength); // node:http accepts digits only
|
|
254
|
+
if (declared > BigInt(limit)) {
|
|
255
|
+
sendTransportRejection(res, 413, bodyTooLargeMessage(declared, limit));
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Answer 408 when a request's body stops arriving for TINA4_REQUEST_TIMEOUT
|
|
264
|
+
* seconds. Idle, not total: a slow but steady upload is never cut off.
|
|
265
|
+
* (A head that never completes is node:http's headersTimeout, set to the same
|
|
266
|
+
* value.)
|
|
267
|
+
*/
|
|
268
|
+
function watchBody(req: IncomingMessage, res: ServerResponse, idleSeconds: number): void {
|
|
269
|
+
if (idleSeconds <= 0 || req.complete) return;
|
|
270
|
+
const socket = req.socket;
|
|
271
|
+
let timer: NodeJS.Timeout | null = null;
|
|
272
|
+
const stop = (): void => {
|
|
273
|
+
if (timer) clearTimeout(timer);
|
|
274
|
+
timer = null;
|
|
275
|
+
socket.off("data", onData);
|
|
276
|
+
};
|
|
277
|
+
const fire = (): void => {
|
|
278
|
+
stop();
|
|
279
|
+
if (req.complete) return;
|
|
280
|
+
sendTransportRejection(res, 408, "Request timed out before it was complete");
|
|
281
|
+
};
|
|
282
|
+
const arm = (): void => {
|
|
283
|
+
if (timer) clearTimeout(timer);
|
|
284
|
+
timer = setTimeout(fire, idleSeconds * 1000);
|
|
285
|
+
timer.unref();
|
|
286
|
+
};
|
|
287
|
+
// Added after node:http's own listener, so the parser has already consumed
|
|
288
|
+
// the chunk and req.complete is current.
|
|
289
|
+
function onData(): void {
|
|
290
|
+
if (req.complete) stop();
|
|
291
|
+
else arm();
|
|
292
|
+
}
|
|
293
|
+
socket.on("data", onData);
|
|
294
|
+
req.once("end", stop);
|
|
295
|
+
req.once("close", stop);
|
|
296
|
+
res.once("close", stop);
|
|
297
|
+
arm();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Map a node:http parse failure to the ADR-0068 answer. */
|
|
301
|
+
function onClientError(err: NodeJS.ErrnoException, socket: Socket): void {
|
|
302
|
+
if (rejectedSockets.has(socket) || held.has(socket)) return; // answered already
|
|
303
|
+
if (err.code === "ECONNRESET" || !socket.writable) {
|
|
304
|
+
socket.destroy();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const inFlight = (socket as Socket & { _httpMessage?: ServerResponse | null })._httpMessage ?? null;
|
|
308
|
+
|
|
309
|
+
if (err.code === "ERR_HTTP_REQUEST_TIMEOUT") {
|
|
310
|
+
// A connection that never sent a byte has no request to answer.
|
|
311
|
+
if (socket.bytesRead === 0) {
|
|
312
|
+
socket.destroy();
|
|
313
|
+
} else if (inFlight) {
|
|
314
|
+
sendTransportRejection(inFlight, 408, "Request timed out before it was complete");
|
|
315
|
+
} else {
|
|
316
|
+
rejectSocket(socket, 408, "Request timed out before it was complete");
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The head was accepted and the body broke: bad chunk framing.
|
|
322
|
+
if (inFlight) {
|
|
323
|
+
if (inFlight.headersSent) socket.destroy();
|
|
324
|
+
else sendTransportRejection(inFlight, 400, "Invalid Transfer-Encoding");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
switch (err.code) {
|
|
329
|
+
case "HPE_HEADER_OVERFLOW":
|
|
330
|
+
rejectSocket(socket, 431, `Request header fields exceed TINA4_MAX_REQUEST_HEADER (${resolveLimit("TINA4_MAX_REQUEST_HEADER", DEFAULT_MAX_REQUEST_HEADER)} bytes)`);
|
|
331
|
+
return;
|
|
332
|
+
case "HPE_INVALID_CONTENT_LENGTH":
|
|
333
|
+
case "HPE_UNEXPECTED_CONTENT_LENGTH":
|
|
334
|
+
rejectSocket(socket, 400, "Invalid Content-Length");
|
|
335
|
+
return;
|
|
336
|
+
case "HPE_INVALID_TRANSFER_ENCODING":
|
|
337
|
+
case "HPE_INVALID_CHUNK_SIZE":
|
|
338
|
+
rejectSocket(socket, 400, "Invalid Transfer-Encoding");
|
|
339
|
+
return;
|
|
340
|
+
default:
|
|
341
|
+
rejectSocket(socket, 400, "Malformed request head");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Create the HTTP server Tina4 listens with - the main port, its loopback
|
|
347
|
+
* siblings, the AI port and every cluster worker - so every one of them keeps
|
|
348
|
+
* the same limits and answers.
|
|
349
|
+
*/
|
|
350
|
+
export function createTina4HttpServer(handler: (req: IncomingMessage, res: ServerResponse) => unknown): Server {
|
|
351
|
+
const maxRequestHeader = resolveLimit("TINA4_MAX_REQUEST_HEADER", DEFAULT_MAX_REQUEST_HEADER);
|
|
352
|
+
const idleSeconds = resolveLimit("TINA4_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT_SECONDS, true);
|
|
353
|
+
const timeouts =
|
|
354
|
+
idleSeconds > 0
|
|
355
|
+
? {
|
|
356
|
+
headersTimeout: idleSeconds * 1000,
|
|
357
|
+
requestTimeout: Math.max(300_000, idleSeconds * 1000),
|
|
358
|
+
connectionsCheckingInterval: 1000,
|
|
359
|
+
}
|
|
360
|
+
: {};
|
|
361
|
+
|
|
362
|
+
const server = createServer({ maxHeaderSize: maxRequestHeader, ...timeouts }, (req, res) => {
|
|
363
|
+
if (rejectOnHead(req, res)) return;
|
|
364
|
+
watchBody(req, res, idleSeconds);
|
|
365
|
+
return handler(req, res);
|
|
366
|
+
});
|
|
367
|
+
server.on("clientError", onClientError);
|
|
368
|
+
return server;
|
|
369
|
+
}
|
|
@@ -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
|
* Which upstream hops are allowed to speak for a client (ADR-0019).
|
|
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
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
10
|
|
|
3
11
|
export interface UploadedFile {
|
|
@@ -207,6 +215,11 @@ export interface Tina4Config {
|
|
|
207
215
|
* and staticDir are resolved relative to this path instead of process.cwd(). */
|
|
208
216
|
basePath?: string;
|
|
209
217
|
routesDir?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Never open a browser on start (ADR-0070 rule 3, the programmatic
|
|
220
|
+
* --no-browser). It can only veto: false does not force one open.
|
|
221
|
+
*/
|
|
222
|
+
noBrowser?: boolean;
|
|
210
223
|
modelsDir?: string;
|
|
211
224
|
templatesDir?: string;
|
|
212
225
|
staticDir?: string;
|
|
@@ -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 Validator — Request body validation.
|
|
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
|
* The single Tina4 framework version resolver (feature 130, VERSION-DEC-01).
|
|
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 WebSocket — Zero-dependency RFC 6455 implementation.
|
|
3
11
|
*
|
|
@@ -30,7 +38,7 @@ import type { WebSocketConnection } from "./websocketConnection.js";
|
|
|
30
38
|
import type { WebSocketRouteHandler } from "./types.js";
|
|
31
39
|
import { Router } from "./router.js";
|
|
32
40
|
import { Log } from "./logger.js";
|
|
33
|
-
import { validToken } from "./auth.js";
|
|
41
|
+
import { validToken, isIdentityPayload } from "./auth.js";
|
|
34
42
|
import { WsBackplaneManager, type WsEnvelope } from "./websocketBackplane.js";
|
|
35
43
|
|
|
36
44
|
// ── Constants ────────────────────────────────────────────────
|
|
@@ -203,7 +211,9 @@ export function wsAuthorized(
|
|
|
203
211
|
const token = wsToken(headers, queryString, subprotocol);
|
|
204
212
|
if (!token) return [null, false];
|
|
205
213
|
const payload = validToken(token);
|
|
206
|
-
|
|
214
|
+
// A form token is not an identity (ADR-0079 s1).
|
|
215
|
+
if (!isIdentityPayload(payload)) return [null, false];
|
|
216
|
+
return [payload, true];
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/**
|
|
@@ -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
|
* WebSocket Backplane Abstraction for Tina4 Node.js.
|
|
3
11
|
*
|
|
@@ -17,6 +25,31 @@
|
|
|
17
25
|
* }
|
|
18
26
|
*/
|
|
19
27
|
import { randomUUID } from "node:crypto";
|
|
28
|
+
import { resolveOptionalPackage } from "./optionalPackage.js";
|
|
29
|
+
import { redactCredentials } from "../../orm/src/databaseUrl.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The one "connected" line both backplanes print. The URL goes through the
|
|
33
|
+
* framework's single redaction primitive: it can carry a password
|
|
34
|
+
* (`redis://:secret@host`), and this line used to print it verbatim.
|
|
35
|
+
*/
|
|
36
|
+
function logConnected(backplaneName: string, url: string): void {
|
|
37
|
+
console.log(`[Tina4] ${backplaneName} connected to ${redactCredentials(url)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A backplane connects in the background, and publish()/subscribe() await that
|
|
42
|
+
* connect. Until someone does, a failed connect is a rejected promise nobody is
|
|
43
|
+
* holding - and Node's default for an unhandled rejection is to kill the
|
|
44
|
+
* process. Observe it so the failure is LOGGED once and still rejects every
|
|
45
|
+
* later publish()/subscribe(). The URL is deliberately not logged: it can carry
|
|
46
|
+
* a password.
|
|
47
|
+
*/
|
|
48
|
+
function observeConnectFailure(ready: Promise<void>, backplaneName: string): void {
|
|
49
|
+
ready.catch((error: unknown) => {
|
|
50
|
+
console.error(`[Tina4] ${backplaneName} could not connect: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
20
53
|
|
|
21
54
|
/**
|
|
22
55
|
* Base interface for scaling WebSocket broadcast across instances.
|
|
@@ -55,20 +88,12 @@ export class RedisBackplane implements WebSocketBackplane {
|
|
|
55
88
|
const resolvedUrl = url ?? process.env.TINA4_WS_BACKPLANE_URL ?? "redis://localhost:6379";
|
|
56
89
|
this.url = resolvedUrl;
|
|
57
90
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Optional peer dependency — resolved via a string specifier so the
|
|
62
|
-
// module isn't required at type-check time when it isn't installed.
|
|
63
|
-
const redisModule: string = "redis";
|
|
64
|
-
redis = await import(redisModule);
|
|
65
|
-
} catch {
|
|
66
|
-
throw new Error(
|
|
67
|
-
"The 'redis' package is required for RedisBackplane. " +
|
|
68
|
-
"Install it with: npm install redis"
|
|
69
|
-
);
|
|
70
|
-
}
|
|
91
|
+
// Optional peer (ADR-0067): resolved NOW so a missing package throws from
|
|
92
|
+
// the constructor with the install command, as the Python master's does.
|
|
93
|
+
const redisModuleUrl = resolveOptionalPackage("redis", "RedisBackplane");
|
|
71
94
|
|
|
95
|
+
this.ready = (async () => {
|
|
96
|
+
const redis: any = await import(redisModuleUrl);
|
|
72
97
|
this.publisher = redis.createClient({ url: resolvedUrl });
|
|
73
98
|
this.subscriber = this.publisher.duplicate();
|
|
74
99
|
|
|
@@ -76,8 +101,9 @@ export class RedisBackplane implements WebSocketBackplane {
|
|
|
76
101
|
this.publisher.connect(),
|
|
77
102
|
this.subscriber.connect(),
|
|
78
103
|
]);
|
|
79
|
-
|
|
104
|
+
logConnected("RedisBackplane", resolvedUrl);
|
|
80
105
|
})();
|
|
106
|
+
observeConnectFailure(this.ready, "RedisBackplane");
|
|
81
107
|
}
|
|
82
108
|
|
|
83
109
|
async publish(channel: string, message: string): Promise<void> {
|
|
@@ -118,42 +144,33 @@ export class NATSBackplane implements WebSocketBackplane {
|
|
|
118
144
|
private url: string;
|
|
119
145
|
private subs: Map<string, any> = new Map();
|
|
120
146
|
private ready: Promise<void>;
|
|
147
|
+
private natsModuleUrl: string;
|
|
121
148
|
|
|
122
149
|
constructor(url?: string) {
|
|
123
150
|
const resolvedUrl = url ?? process.env.TINA4_WS_BACKPLANE_URL ?? "nats://localhost:4222";
|
|
124
151
|
this.url = resolvedUrl;
|
|
125
152
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
// Optional peer dependency — resolved via a string specifier so the
|
|
130
|
-
// module isn't required at type-check time when it isn't installed.
|
|
131
|
-
const natsModule: string = "nats";
|
|
132
|
-
nats = await import(natsModule);
|
|
133
|
-
} catch {
|
|
134
|
-
throw new Error(
|
|
135
|
-
"The 'nats' package is required for NATSBackplane. " +
|
|
136
|
-
"Install it with: npm install nats"
|
|
137
|
-
);
|
|
138
|
-
}
|
|
153
|
+
const natsModuleUrl = resolveOptionalPackage("nats", "NATSBackplane");
|
|
154
|
+
this.natsModuleUrl = natsModuleUrl;
|
|
139
155
|
|
|
156
|
+
this.ready = (async () => {
|
|
157
|
+
const nats: any = await import(natsModuleUrl);
|
|
140
158
|
this.nc = await nats.connect({ servers: resolvedUrl });
|
|
141
|
-
|
|
159
|
+
logConnected("NATSBackplane", resolvedUrl);
|
|
142
160
|
})();
|
|
161
|
+
observeConnectFailure(this.ready, "NATSBackplane");
|
|
143
162
|
}
|
|
144
163
|
|
|
145
164
|
async publish(channel: string, message: string): Promise<void> {
|
|
146
165
|
await this.ready;
|
|
147
|
-
const
|
|
148
|
-
const { StringCodec } = await import(natsModule);
|
|
166
|
+
const { StringCodec } = await import(this.natsModuleUrl);
|
|
149
167
|
const sc = StringCodec();
|
|
150
168
|
this.nc.publish(channel, sc.encode(message));
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
async subscribe(channel: string, callback: (message: string) => void): Promise<void> {
|
|
154
172
|
await this.ready;
|
|
155
|
-
const
|
|
156
|
-
const { StringCodec } = await import(natsModule);
|
|
173
|
+
const { StringCodec } = await import(this.natsModuleUrl);
|
|
157
174
|
const sc = StringCodec();
|
|
158
175
|
const sub = this.nc.subscribe(channel);
|
|
159
176
|
this.subs.set(channel, sub);
|
|
@@ -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
|
* Represents a WebSocket connection with send/broadcast/close capabilities.
|
|
3
11
|
* Used by Router.websocket() route handlers.
|