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,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
|
@@ -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
|
(function(){"use strict";(()=>{try{const t=window.location.pathname||"";return t.startsWith("/__dev")||t.startsWith("/__feedback")}catch{return!1}})()?console.info("tina4-feedback-widget: skipping on developer path"):window.__tina4FeedbackLoaded?console.warn("tina4-feedback-widget already loaded; skipping"):(window.__tina4FeedbackLoaded=!0,b());function b(){const c=(getComputedStyle(document.documentElement).getPropertyValue("--primary")||"").trim()||"#3b82f6";h(c);const l=m();document.body.appendChild(l);let e=null,u;const r=[];l.addEventListener("click",()=>{if(e){e.remove(),e=null,l.style.display="";return}e=g(),document.body.appendChild(e),l.style.display="none",setTimeout(()=>e?.querySelector("textarea")?.focus(),0)});function g(){const o=document.createElement("div");o.className="tina4-fb-modal",o.innerHTML=`
|
|
2
10
|
<div class="tina4-fb-head">
|
|
3
11
|
<span class="tina4-fb-title">Tell us what's not working</span>
|
|
@@ -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
|
* Last-resort finder for `@tina4/core/<subpath>` under Node's ESM resolver.
|
|
3
11
|
*
|
package/packages/core/src/ai.ts
CHANGED
|
@@ -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 AI — Install AI coding assistant context files.
|
|
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
|
/** Zero-dependency app-facing AI client (ADR-0053 + ADR-0060). */
|
|
2
10
|
import http, { type IncomingMessage } from "node:http";
|
|
3
11
|
import https from "node:https";
|
package/packages/core/src/api.ts
CHANGED
|
@@ -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 API Client — HTTP client using Node.js built-in modules only.
|
|
3
11
|
*
|
|
@@ -21,6 +29,7 @@ import { promises as fsp, createWriteStream } from "node:fs";
|
|
|
21
29
|
import { basename } from "node:path";
|
|
22
30
|
import { pipeline } from "node:stream/promises";
|
|
23
31
|
import { TINA4_VERSION } from "./version.js";
|
|
32
|
+
import { guardUrl } from "./ssrf.js";
|
|
24
33
|
|
|
25
34
|
export interface ApiResult {
|
|
26
35
|
http_code: number | null;
|
|
@@ -221,6 +230,11 @@ export interface ApiOptions {
|
|
|
221
230
|
* persisted; scoped to this instance.
|
|
222
231
|
*/
|
|
223
232
|
cookies?: boolean;
|
|
233
|
+
/**
|
|
234
|
+
* SSRF guard (ADR-0084): an explicit allow-list of hosts / host:port / CIDRs
|
|
235
|
+
* that bypass the private-address refusal even with the opt-out off.
|
|
236
|
+
*/
|
|
237
|
+
allowHosts?: string[];
|
|
224
238
|
}
|
|
225
239
|
|
|
226
240
|
/** True when two URLs share scheme + host + (effective) port. */
|
|
@@ -422,6 +436,7 @@ export class Api {
|
|
|
422
436
|
private transportFn?: ApiTransport;
|
|
423
437
|
private cookiesEnabled: boolean;
|
|
424
438
|
private cookies: Record<string, string>;
|
|
439
|
+
private allowHosts: string[];
|
|
425
440
|
|
|
426
441
|
/**
|
|
427
442
|
* Construct an Api client.
|
|
@@ -468,6 +483,7 @@ export class Api {
|
|
|
468
483
|
this.transportFn = undefined;
|
|
469
484
|
this.cookiesEnabled = false;
|
|
470
485
|
this.cookies = {};
|
|
486
|
+
this.allowHosts = [];
|
|
471
487
|
|
|
472
488
|
// Options-bag form — second arg is an object literal
|
|
473
489
|
if (typeof authHeaderOrOptions === "object" && authHeaderOrOptions !== null) {
|
|
@@ -479,6 +495,7 @@ export class Api {
|
|
|
479
495
|
this.retryBackoff = opts.retryBackoff ?? 0.5;
|
|
480
496
|
this.transportFn = opts.transport;
|
|
481
497
|
this.cookiesEnabled = opts.cookies ?? false;
|
|
498
|
+
this.allowHosts = opts.allowHosts ?? [];
|
|
482
499
|
|
|
483
500
|
// Bearer wins over basic-auth when both are passed
|
|
484
501
|
if (opts.bearerToken != null) {
|
|
@@ -667,7 +684,7 @@ export class Api {
|
|
|
667
684
|
url += (url.includes("?") ? "&" : "?") + qs;
|
|
668
685
|
}
|
|
669
686
|
|
|
670
|
-
const { headers, data } = this.buildRequest("GET", "application/json", undefined);
|
|
687
|
+
const { headers, data } = this.buildRequest("GET", "application/json", undefined, undefined, url);
|
|
671
688
|
|
|
672
689
|
// An injected transport can't stream (it returns a buffered result), so
|
|
673
690
|
// write its body out; only the real network path streams chunk-by-chunk.
|
|
@@ -760,7 +777,7 @@ export class Api {
|
|
|
760
777
|
const url = this.buildUrl(path);
|
|
761
778
|
const method = (opts.method ?? "GET").toUpperCase();
|
|
762
779
|
const contentType = opts.contentType ?? "application/json";
|
|
763
|
-
const { headers, data } = this.buildRequest(method, contentType, opts.body, opts.headers);
|
|
780
|
+
const { headers, data } = this.buildRequest(method, contentType, opts.body, opts.headers, url);
|
|
764
781
|
const totalSec = this.streamSeconds(opts.timeout, "TINA4_API_TIMEOUT", this.timeout);
|
|
765
782
|
const connectSec = this.streamSeconds(opts.connectTimeout, "TINA4_API_CONNECT_TIMEOUT", 10);
|
|
766
783
|
const opened = await this.openStreamRequest(method, url, headers, data, connectSec);
|
|
@@ -834,13 +851,19 @@ export class Api {
|
|
|
834
851
|
* body-phase timeout is applied by the caller (streamBytes) via
|
|
835
852
|
* `res.destroy()`.
|
|
836
853
|
*/
|
|
837
|
-
private openStreamRequest(
|
|
854
|
+
private async openStreamRequest(
|
|
838
855
|
method: string,
|
|
839
856
|
url: string,
|
|
840
857
|
headers: Record<string, string>,
|
|
841
858
|
data: Buffer | undefined,
|
|
842
859
|
connectSec: number,
|
|
843
860
|
): Promise<{ res: http.IncomingMessage }> {
|
|
861
|
+
// SSRF guard (ADR-0084): refuse a private/internal target before connect.
|
|
862
|
+
try {
|
|
863
|
+
await guardUrl(url, this.allowHosts);
|
|
864
|
+
} catch (err) {
|
|
865
|
+
throw new ApiStreamError(err instanceof Error ? err.message : String(err), null);
|
|
866
|
+
}
|
|
844
867
|
return new Promise((resolve, reject) => {
|
|
845
868
|
let parsed: URL;
|
|
846
869
|
try {
|
|
@@ -904,14 +927,21 @@ export class Api {
|
|
|
904
927
|
contentType: string,
|
|
905
928
|
body: unknown,
|
|
906
929
|
extraHeaders?: Record<string, string>,
|
|
930
|
+
targetUrl?: string,
|
|
907
931
|
): { headers: Record<string, string>; data: Buffer | undefined } {
|
|
908
932
|
const headers: Record<string, string> = { "User-Agent": `Tina4/${TINA4_VERSION}`, ...this.headers };
|
|
909
|
-
|
|
933
|
+
// Attach the configured Authorization / Cookie ONLY when the request
|
|
934
|
+
// target is same-origin as the configured base. A path that is itself
|
|
935
|
+
// an absolute off-origin URL (e.g. get("http://evil/x")) otherwise
|
|
936
|
+
// leaks the bearer token / session cookie to an attacker-chosen host —
|
|
937
|
+
// the same cross-origin strip already applied to followed redirects.
|
|
938
|
+
const sameOriginAsBase = targetUrl === undefined || sameOrigin(targetUrl, this.baseUrl);
|
|
939
|
+
if (this.authHeader && sameOriginAsBase) {
|
|
910
940
|
headers["Authorization"] = this.authHeader;
|
|
911
941
|
}
|
|
912
942
|
|
|
913
943
|
// Cookie jar: attach the accumulated Cookie header when enabled.
|
|
914
|
-
if (this.cookiesEnabled) {
|
|
944
|
+
if (this.cookiesEnabled && sameOriginAsBase) {
|
|
915
945
|
const cookieHeader = this.cookieHeader();
|
|
916
946
|
if (cookieHeader) {
|
|
917
947
|
headers["Cookie"] = cookieHeader;
|
|
@@ -943,6 +973,9 @@ export class Api {
|
|
|
943
973
|
Object.assign(headers, extraHeaders);
|
|
944
974
|
}
|
|
945
975
|
|
|
976
|
+
if (targetUrl !== undefined && !sameOrigin(targetUrl, this.baseUrl)) {
|
|
977
|
+
for (const name of STRIP_ON_CROSS_ORIGIN) deleteHeaderCaseInsensitive(headers, name);
|
|
978
|
+
}
|
|
946
979
|
return { headers, data };
|
|
947
980
|
}
|
|
948
981
|
|
|
@@ -985,7 +1018,7 @@ export class Api {
|
|
|
985
1018
|
contentType: string = "application/json",
|
|
986
1019
|
extraHeaders?: Record<string, string>,
|
|
987
1020
|
): Promise<ApiResult> {
|
|
988
|
-
const { headers, data } = this.buildRequest(method, contentType, body, extraHeaders);
|
|
1021
|
+
const { headers, data } = this.buildRequest(method, contentType, body, extraHeaders, url);
|
|
989
1022
|
|
|
990
1023
|
// A user-injected transport fully replaces the network call.
|
|
991
1024
|
if (this.transportFn) {
|
|
@@ -1037,13 +1070,21 @@ export class Api {
|
|
|
1037
1070
|
* bearer token / session cookie never leaks to a host you didn't
|
|
1038
1071
|
* authenticate to.
|
|
1039
1072
|
*/
|
|
1040
|
-
private performRequest(
|
|
1073
|
+
private async performRequest(
|
|
1041
1074
|
method: string,
|
|
1042
1075
|
url: string,
|
|
1043
1076
|
headers: Record<string, string>,
|
|
1044
1077
|
data: Buffer | undefined,
|
|
1045
1078
|
redirectsLeft: number,
|
|
1046
1079
|
): Promise<NetworkResult> {
|
|
1080
|
+
// SSRF guard (ADR-0084): re-validated per hop (this method recurses on a
|
|
1081
|
+
// redirect), so a hop to a private/internal address is refused even when
|
|
1082
|
+
// the initial URL was public.
|
|
1083
|
+
try {
|
|
1084
|
+
await guardUrl(url, this.allowHosts);
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
return { kind: "error", error: err instanceof Error ? err.message : String(err) };
|
|
1087
|
+
}
|
|
1047
1088
|
return new Promise<NetworkResult>((resolve) => {
|
|
1048
1089
|
let parsed: URL;
|
|
1049
1090
|
try {
|
|
@@ -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 Auth — Zero-dependency JWT, password hashing, and auth middleware.
|
|
3
11
|
*
|
|
@@ -12,7 +20,7 @@
|
|
|
12
20
|
* checkPassword("secret123", hash); // true
|
|
13
21
|
*/
|
|
14
22
|
import { createHmac, createSign, createVerify, pbkdf2Sync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
15
|
-
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { appendFileSync, existsSync, readFileSync, openSync, closeSync, fstatSync, fchmodSync, constants } from "node:fs";
|
|
16
24
|
import { join } from "node:path";
|
|
17
25
|
import type { Middleware, Tina4Request, Tina4Response } from "./types.js";
|
|
18
26
|
import { isTruthy } from "./dotenv.js";
|
|
@@ -74,6 +82,58 @@ function _warnBlankSecret(): void {
|
|
|
74
82
|
void _logWarning(BLANK_SECRET_WARNING);
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
// ── Auth-token rules (ADR-0079) ────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Token purposes that are never an identity (ADR-0079 s1). Frond's formToken()
|
|
89
|
+
* signs a JWT with the same TINA4_SECRET as an auth token, marked
|
|
90
|
+
* `type: "form"`. A form token proves where a write came from, never who the
|
|
91
|
+
* caller is, so every identity gate refuses these purposes. validToken itself
|
|
92
|
+
* stays purpose-neutral: the CSRF middleware needs it for form tokens.
|
|
93
|
+
*/
|
|
94
|
+
export const NON_IDENTITY_TOKEN_TYPES: readonly string[] = ["form"];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Minimum HMAC key length in bytes (ADR-0079 s2): the HS256 output size, RFC
|
|
98
|
+
* 7518 s3.2. A blank key is the shortest case - anyone can reproduce it.
|
|
99
|
+
*/
|
|
100
|
+
export const MIN_SECRET_BYTES = 32;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* True when a VERIFIED payload may stand for a caller's identity. A payload
|
|
104
|
+
* whose `type` is a reserved non-identity purpose (a Frond form token) is
|
|
105
|
+
* refused; application payloads are otherwise untouched.
|
|
106
|
+
*/
|
|
107
|
+
export function isIdentityPayload(payload: Record<string, unknown> | null | undefined): payload is Record<string, unknown> {
|
|
108
|
+
return payload != null && !NON_IDENTITY_TOKEN_TYPES.includes(payload.type as string);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The actionable error for a weak HMAC key, or null when the key is usable. */
|
|
112
|
+
export function insecureSecretMessage(secret: string | undefined | null): string | null {
|
|
113
|
+
const length = Buffer.byteLength(secret ?? "", "utf8");
|
|
114
|
+
if (length >= MIN_SECRET_BYTES) return null;
|
|
115
|
+
const what = length === 0 ? "is not set" : `is ${length} bytes`;
|
|
116
|
+
return `Auth: TINA4_SECRET ${what}; an HMAC JWT secret must be at least ${MIN_SECRET_BYTES} bytes. ` +
|
|
117
|
+
"Generate one with `openssl rand -hex 32` and set TINA4_SECRET in your environment or .env.";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Refuse to boot with a secret that makes tokens forgeable (ADR-0079 s2).
|
|
122
|
+
* Outside dev a blank TINA4_SECRET is refused; in any mode a set-but-short one
|
|
123
|
+
* is refused. Dev with a blank secret has already had one minted into
|
|
124
|
+
* .env.local by ensureDevSecret(). RS256 keys are PEMs and are not measured.
|
|
125
|
+
*
|
|
126
|
+
* @throws Error with the actionable message naming TINA4_SECRET.
|
|
127
|
+
*/
|
|
128
|
+
export function requireBootSecret(): void {
|
|
129
|
+
const algorithm = (process.env.TINA4_JWT_ALGORITHM ?? "").trim() || "HS256";
|
|
130
|
+
if (RSA_SIGN_ALGORITHMS.has(algorithm)) return;
|
|
131
|
+
const secret = process.env.TINA4_SECRET ?? "";
|
|
132
|
+
if (secret === "" && _isDev()) return;
|
|
133
|
+
const message = insecureSecretMessage(secret);
|
|
134
|
+
if (message) throw new Error(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
77
137
|
/**
|
|
78
138
|
* Ensure a usable TINA4_SECRET exists. Run ONCE at server boot, after env load
|
|
79
139
|
* and before auth is used. Mirrors Python's `ensure_dev_secret()`.
|
|
@@ -107,14 +167,19 @@ export function ensureDevSecret(cwd?: string): string | null {
|
|
|
107
167
|
const baseDir = cwd ?? process.cwd();
|
|
108
168
|
const envLocalPath = join(baseDir, ".env.local");
|
|
109
169
|
try {
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
170
|
+
// Read and append through one descriptor so replacing a pathname cannot
|
|
171
|
+
// redirect either operation to an unrelated file.
|
|
172
|
+
const fd = openSync(envLocalPath, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0), 0o600);
|
|
173
|
+
try {
|
|
174
|
+
const info = fstatSync(fd);
|
|
175
|
+
if (!info.isFile() || info.nlink !== 1) throw new Error("Refusing a non-regular or multiply-linked configuration file");
|
|
176
|
+
fchmodSync(fd, 0o600);
|
|
177
|
+
const existing = readFileSync(fd, "utf-8");
|
|
178
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
179
|
+
appendFileSync(fd, `${prefix}TINA4_SECRET=${newSecret}\n`);
|
|
180
|
+
} finally {
|
|
181
|
+
closeSync(fd);
|
|
116
182
|
}
|
|
117
|
-
appendFileSync(envLocalPath, `${prefix}TINA4_SECRET=${newSecret}\n`);
|
|
118
183
|
void _logInfo("Auth: generated a development secret, saved to .env.local (gitignored)");
|
|
119
184
|
} catch {
|
|
120
185
|
// Keep the in-memory secret for this run; warn but never crash boot.
|
|
@@ -447,6 +512,14 @@ export function validToken(token: string, secret?: string, algorithm?: string):
|
|
|
447
512
|
const header = JSON.parse(base64urlDecode(h).toString()) as Record<string, unknown>;
|
|
448
513
|
if (header.alg !== resolvedAlgorithm) return null;
|
|
449
514
|
|
|
515
|
+
// A blank or short HMAC key verifies tokens anyone can mint, so the token is
|
|
516
|
+
// REJECTED (fail closed with a 401, never a 500) and the operator is told
|
|
517
|
+
// why. ADR-0079 s2.
|
|
518
|
+
if (HMAC_DIGESTS.has(resolvedAlgorithm)) {
|
|
519
|
+
const weak = insecureSecretMessage(resolvedSecret);
|
|
520
|
+
if (weak) { void _logWarning(weak); return null; }
|
|
521
|
+
}
|
|
522
|
+
|
|
450
523
|
const signingInput = `${h}.${p}`;
|
|
451
524
|
if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
|
|
452
525
|
return null;
|
|
@@ -509,6 +582,8 @@ function sign(input: string, secret: string, algorithm: string): string {
|
|
|
509
582
|
// the header is the one that actually produced this signature.
|
|
510
583
|
const digest = HMAC_DIGESTS.get(algorithm);
|
|
511
584
|
if (digest) {
|
|
585
|
+
const weak = insecureSecretMessage(secret);
|
|
586
|
+
if (weak) throw new Error(weak);
|
|
512
587
|
return base64urlEncode(createHmac(digest, secret).update(input).digest());
|
|
513
588
|
}
|
|
514
589
|
const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
|
|
@@ -609,12 +684,14 @@ export function authMiddleware(secret?: string, algorithm?: string): Middleware
|
|
|
609
684
|
}
|
|
610
685
|
|
|
611
686
|
const token = authHeader.slice(7);
|
|
612
|
-
|
|
687
|
+
const payload = validToken(token, secret, algorithm);
|
|
688
|
+
// Invalid, expired, or a form token (not an identity, ADR-0079 s1).
|
|
689
|
+
if (!isIdentityPayload(payload)) {
|
|
613
690
|
res({ error: "Unauthorized" }, 401);
|
|
614
691
|
return;
|
|
615
692
|
}
|
|
616
693
|
|
|
617
|
-
(req as any).auth =
|
|
694
|
+
(req as any).auth = payload;
|
|
618
695
|
next();
|
|
619
696
|
};
|
|
620
697
|
}
|
|
@@ -635,11 +712,12 @@ export function refreshToken(
|
|
|
635
712
|
token: string,
|
|
636
713
|
expiresIn: number = 60,
|
|
637
714
|
): string | null {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const payload = getPayload(token);
|
|
715
|
+
const payload = validToken(token);
|
|
641
716
|
if (!payload) return null;
|
|
642
717
|
|
|
718
|
+
// Refresh preserves PURPOSE: a form token comes back as a form token (CSRF
|
|
719
|
+
// rotation) and so can still never pass an identity gate. The route gate
|
|
720
|
+
// issues a FreshToken only for an identity token. ADR-0079 s1.
|
|
643
721
|
// Strip standard timing claims so getToken sets fresh ones
|
|
644
722
|
const { iat: _iat, exp: _exp, ...claims } = payload;
|
|
645
723
|
return getToken(claims, expiresIn);
|
|
@@ -673,7 +751,9 @@ export function authenticateRequest(
|
|
|
673
751
|
// algorithm= silently got the env values instead, and `algorithm` defaulted to
|
|
674
752
|
// the literal "HS256" which additionally shadowed TINA4_JWT_ALGORITHM. Python,
|
|
675
753
|
// PHP and Ruby all honour these; Node was the last one that did not.
|
|
676
|
-
|
|
754
|
+
const payload = validToken(token, secret, algorithm);
|
|
755
|
+
// A form token is not an identity (ADR-0079 s1).
|
|
756
|
+
if (payload) return isIdentityPayload(payload) ? payload : null;
|
|
677
757
|
|
|
678
758
|
// Fallback: treat Bearer value as API key
|
|
679
759
|
if (validateApiKey(token)) {
|
|
@@ -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
|
* Secure-by-default route auth gate — shared by the live server dispatch and the
|
|
3
11
|
* in-process TestClient so the test surface enforces the IDENTICAL gate as
|
|
@@ -9,7 +17,8 @@
|
|
|
9
17
|
* verification layer lied. This is the same defect fixed in tina4-python under
|
|
10
18
|
* #PY2; extracting the gate here lets both callers share one implementation.
|
|
11
19
|
*/
|
|
12
|
-
import { validToken,
|
|
20
|
+
import { validToken, refreshToken, isIdentityPayload } from "./auth.js";
|
|
21
|
+
import { liveSessionIdentity } from "./ssoIdentity.js";
|
|
13
22
|
import type { Tina4Request, Tina4Response } from "./types.js";
|
|
14
23
|
|
|
15
24
|
/** Just the auth-relevant fields of a matched route. */
|
|
@@ -44,52 +53,58 @@ export function enforceRouteAuth(
|
|
|
44
53
|
return false;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
// Token slots in priority order: Bearer header, body formToken (frond.js
|
|
57
|
+
// puts the auth token it received as a FreshToken there), session token.
|
|
58
|
+
// Each slot is accepted only when it carries an IDENTITY token: a Frond
|
|
59
|
+
// form token (`type: "form"`) proves where a write came from, not who sent
|
|
60
|
+
// it, so it is skipped and the search moves on (ADR-0079 s1).
|
|
47
61
|
const authHeader = req.headers.authorization ?? "";
|
|
48
62
|
const headerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
49
63
|
|
|
50
|
-
let
|
|
64
|
+
let payload: Record<string, unknown> | null = null;
|
|
51
65
|
let tokenSource: "header" | "body" | "session" | "" = "";
|
|
66
|
+
let resolvedToken = "";
|
|
52
67
|
|
|
53
68
|
// Priority 1: Authorization Bearer header
|
|
54
|
-
if (headerToken
|
|
55
|
-
|
|
56
|
-
tokenSource = "header";
|
|
69
|
+
if (headerToken) {
|
|
70
|
+
const candidate = validToken(headerToken);
|
|
71
|
+
if (isIdentityPayload(candidate)) { payload = candidate; tokenSource = "header"; resolvedToken = headerToken; }
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
// Priority 2: formToken from request body
|
|
60
|
-
if (!
|
|
75
|
+
if (!payload) {
|
|
61
76
|
const bodyToken = (req.body as Record<string, unknown>)?.formToken as string | undefined;
|
|
62
|
-
if (bodyToken
|
|
63
|
-
|
|
64
|
-
tokenSource = "body";
|
|
77
|
+
if (bodyToken) {
|
|
78
|
+
const candidate = validToken(bodyToken);
|
|
79
|
+
if (isIdentityPayload(candidate)) { payload = candidate; tokenSource = "body"; resolvedToken = bodyToken; }
|
|
65
80
|
}
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
// Priority 3: Session
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const identity =
|
|
72
|
-
if (identity
|
|
83
|
+
// Priority 3: Session — a provider-verified SSO identity while it is live
|
|
84
|
+
// (ADR-0079 s5), then the session token.
|
|
85
|
+
if (!payload) {
|
|
86
|
+
const identity = liveSessionIdentity((req as any).session?.get?.("_tina4_sso"));
|
|
87
|
+
if (identity) {
|
|
73
88
|
req.user = identity;
|
|
74
89
|
// RBAC guards apply to the SSO identity too (Feature 138).
|
|
75
90
|
return rbacForbidden(match, identity, res);
|
|
76
91
|
}
|
|
77
92
|
const sessionToken = (req as any).session?.get?.("token") as string | undefined;
|
|
78
|
-
if (sessionToken
|
|
79
|
-
|
|
80
|
-
tokenSource = "session";
|
|
93
|
+
if (sessionToken) {
|
|
94
|
+
const candidate = validToken(sessionToken);
|
|
95
|
+
if (isIdentityPayload(candidate)) { payload = candidate; tokenSource = "session"; resolvedToken = sessionToken; }
|
|
81
96
|
}
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
if (!
|
|
99
|
+
if (!payload) {
|
|
85
100
|
res.raw.writeHead(401, { "Content-Type": "application/json" });
|
|
86
101
|
res.raw.end(JSON.stringify({ error: "Unauthorized" }));
|
|
87
102
|
return true;
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
req.user =
|
|
105
|
+
req.user = payload;
|
|
91
106
|
|
|
92
|
-
// When a body
|
|
107
|
+
// When a body token authenticates, return a FreshToken header with a refreshed JWT
|
|
93
108
|
if (tokenSource === "body") {
|
|
94
109
|
const fresh = refreshToken(resolvedToken);
|
|
95
110
|
if (fresh) {
|
|
@@ -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
|
* Background tasks — periodic callbacks that run alongside the HTTP server.
|
|
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
|
* Multi-backend response cache for GET requests.
|
|
3
11
|
*
|
|
@@ -56,6 +64,7 @@ import * as fs from "node:fs";
|
|
|
56
64
|
import * as path from "node:path";
|
|
57
65
|
import * as crypto from "node:crypto";
|
|
58
66
|
import * as net from "node:net";
|
|
67
|
+
import { optionalPackageMissing } from "./optionalPackage.js";
|
|
59
68
|
|
|
60
69
|
// ── Credential parsing (WHATWG URL or TINA4_CACHE_USERNAME / _PASSWORD) ──
|
|
61
70
|
|
|
@@ -181,6 +190,12 @@ interface CacheBackend {
|
|
|
181
190
|
/** One-time async connect/probe. Resolves once the backend has decided
|
|
182
191
|
* availability; createBackend() awaits this before falling back to file. */
|
|
183
192
|
ready?(): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* When unavailable because the app has not installed the backend's driver
|
|
195
|
+
* (an optional peer, ADR-0067): the actionable install message, appended to
|
|
196
|
+
* the fallback warning. null for every other cause.
|
|
197
|
+
*/
|
|
198
|
+
missingDriver?(): string | null;
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
// ── Memory backend ────────────────────────────────────────────────
|
|
@@ -1141,6 +1156,7 @@ class MongoBackend implements CacheBackend {
|
|
|
1141
1156
|
private misses = 0;
|
|
1142
1157
|
private maxEntries: number;
|
|
1143
1158
|
private available = false;
|
|
1159
|
+
private missingDriverMessage: string | null = null;
|
|
1144
1160
|
private readyPromise: Promise<void>;
|
|
1145
1161
|
// The mongodb driver types aren't depended on at compile time (optional dep).
|
|
1146
1162
|
private client: any = null;
|
|
@@ -1178,8 +1194,15 @@ class MongoBackend implements CacheBackend {
|
|
|
1178
1194
|
try {
|
|
1179
1195
|
// Dynamic import so @tina4/core has no hard dependency on `mongodb` and an
|
|
1180
1196
|
// absent driver degrades to file-fallback (never an import-time crash).
|
|
1181
|
-
|
|
1182
|
-
|
|
1197
|
+
let importError: any = null;
|
|
1198
|
+
const mod: any = await import("mongodb").catch((error) => { importError = error; return null; });
|
|
1199
|
+
if (!mod || !mod.MongoClient) {
|
|
1200
|
+
this.available = false;
|
|
1201
|
+
if (importError?.code === "ERR_MODULE_NOT_FOUND") {
|
|
1202
|
+
this.missingDriverMessage = optionalPackageMissing("the mongodb cache backend", ["mongodb"]).message;
|
|
1203
|
+
}
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1183
1206
|
const client = new mod.MongoClient(this.url, { serverSelectionTimeoutMS: 4000 });
|
|
1184
1207
|
await client.connect();
|
|
1185
1208
|
await client.db(this.dbName).command({ ping: 1 });
|
|
@@ -1200,6 +1223,10 @@ class MongoBackend implements CacheBackend {
|
|
|
1200
1223
|
return this.readyPromise;
|
|
1201
1224
|
}
|
|
1202
1225
|
|
|
1226
|
+
missingDriver(): string | null {
|
|
1227
|
+
return this.missingDriverMessage;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1203
1230
|
async isAvailable(): Promise<boolean> {
|
|
1204
1231
|
await this.readyPromise;
|
|
1205
1232
|
return this.available;
|
|
@@ -1291,6 +1318,7 @@ class DatabaseBackend implements CacheBackend {
|
|
|
1291
1318
|
private misses = 0;
|
|
1292
1319
|
private maxEntries: number;
|
|
1293
1320
|
private available = false;
|
|
1321
|
+
private missingDriverMessage: string | null = null;
|
|
1294
1322
|
private readyPromise: Promise<void>;
|
|
1295
1323
|
// @tina4/orm Database instance (optional dep, loaded dynamically).
|
|
1296
1324
|
private db: any = null;
|
|
@@ -1324,8 +1352,13 @@ class DatabaseBackend implements CacheBackend {
|
|
|
1324
1352
|
try { db.commit(); } catch { /* sqlite autocommits DDL */ }
|
|
1325
1353
|
this.db = db;
|
|
1326
1354
|
this.available = true;
|
|
1327
|
-
} catch {
|
|
1355
|
+
} catch (error) {
|
|
1328
1356
|
this.available = false;
|
|
1357
|
+
// The ORM adapters already phrase a missing driver as the install
|
|
1358
|
+
// command; carry THAT through, and nothing else (a connect error can
|
|
1359
|
+
// name the host and user).
|
|
1360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1361
|
+
if (/npm install \S/.test(message)) this.missingDriverMessage = message;
|
|
1329
1362
|
} finally {
|
|
1330
1363
|
if (prevAuto === undefined) delete process.env.TINA4_AUTO_CACHING; else process.env.TINA4_AUTO_CACHING = prevAuto;
|
|
1331
1364
|
if (prevDb === undefined) delete process.env.TINA4_DB_CACHE; else process.env.TINA4_DB_CACHE = prevDb;
|
|
@@ -1336,6 +1369,10 @@ class DatabaseBackend implements CacheBackend {
|
|
|
1336
1369
|
return this.readyPromise;
|
|
1337
1370
|
}
|
|
1338
1371
|
|
|
1372
|
+
missingDriver(): string | null {
|
|
1373
|
+
return this.missingDriverMessage;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1339
1376
|
async isAvailable(): Promise<boolean> {
|
|
1340
1377
|
await this.readyPromise;
|
|
1341
1378
|
return this.available;
|
|
@@ -1531,9 +1568,11 @@ export async function createBackend(config?: {
|
|
|
1531
1568
|
// degrading to a no-op cache. Mirrors the Python master.
|
|
1532
1569
|
if (typeof backend.isAvailable === "function" && !(await backend.isAvailable())) {
|
|
1533
1570
|
// eslint-disable-next-line no-console
|
|
1571
|
+
const installHint = backend.missingDriver?.() ?? null;
|
|
1534
1572
|
console.warn(
|
|
1535
1573
|
`[tina4] Cache backend '${backendName}' is unavailable ` +
|
|
1536
|
-
`(driver missing or service unreachable) — falling back to 'file'
|
|
1574
|
+
`(driver missing or service unreachable) — falling back to 'file'.` +
|
|
1575
|
+
(installHint ? ` ${installHint}` : ""),
|
|
1537
1576
|
);
|
|
1538
1577
|
return new FileBackend(cacheDir(), maxEntries);
|
|
1539
1578
|
}
|
|
@@ -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 Constants — HTTP status codes and content types.
|
|
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
|
* Lightweight dependency injection container.
|
|
3
11
|
*
|