tina4-nodejs 3.13.137 → 3.13.138
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 +19466 -18048
- 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 +19458 -18043
- 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 +15 -3
- package/packages/core/src/auth.ts +21 -8
- package/packages/core/src/authGate.ts +8 -0
- 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 +16 -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 +8 -0
- package/packages/core/src/job.ts +8 -0
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +8 -0
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1178 -1096
- 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 +8 -0
- package/packages/core/src/queue.ts +8 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +8 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
- 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 +73 -24
- 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 +8 -0
- 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 +8 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +8 -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 +8 -0
- 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 +151 -17
- package/packages/frond/src/engine.ts +177 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8125 -6708
- 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/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 +0 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- 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/queueBackends/mongoBackend.d.ts +2 -0
- 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/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
|
/**
|
|
2
10
|
* Tina4 Dev Admin — Built-in development dashboard, zero dependencies.
|
|
3
11
|
*
|
|
@@ -10,14 +18,14 @@
|
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
20
|
import { cpus as osCpus } from "node:os";
|
|
13
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFileSync, statSync } from "node:fs";
|
|
14
|
-
import { join, dirname, resolve, relative } from "node:path";
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFileSync, statSync, openSync, closeSync, fstatSync, fchmodSync, ftruncateSync, constants, realpathSync, lstatSync } from "node:fs";
|
|
22
|
+
import { join, dirname, resolve, relative, sep, basename } from "node:path";
|
|
15
23
|
import { fileURLToPath } from "node:url";
|
|
16
24
|
import type { Router } from "./router.js";
|
|
17
25
|
import type { RouteHandler, Tina4Request } from "./types.js";
|
|
18
26
|
import { DevMailbox } from "./devMailbox.js";
|
|
19
27
|
import { isTruthy } from "./dotenv.js";
|
|
20
|
-
import { fullAnalysis, fileDetail, MetricsEngineError } from "./metrics.js";
|
|
28
|
+
import { fullAnalysis, fileDetail, MetricsEngineError, metricsScanRoot } from "./metrics.js";
|
|
21
29
|
import { registerFeedbackRoutes } from "./feedback.js";
|
|
22
30
|
import { getDefaultDevServer, mcpEnabled, isRequestAllowed, isLoopback } from "./mcp.js";
|
|
23
31
|
import { timingSafeEqual } from "node:crypto";
|
|
@@ -67,8 +75,8 @@ function escapeHtml(value: string): string {
|
|
|
67
75
|
*
|
|
68
76
|
* A drive-by CSRF is a BROWSER cross-origin request, and a modern browser always
|
|
69
77
|
* sends `Sec-Fetch-Site` (and any browser sends `Origin` on a cross-origin POST):
|
|
70
|
-
* - Sec-Fetch-Site present ->
|
|
71
|
-
* (
|
|
78
|
+
* - Sec-Fetch-Site present -> only same-origin (the dashboard itself) and
|
|
79
|
+
* none (a typed URL) pass; same-site is a different origin (ADR-0082).
|
|
72
80
|
* - else Origin present -> require its host to match the request Host.
|
|
73
81
|
* - else neither header -> not a browser cross-origin request (curl, a test
|
|
74
82
|
* client, a server-side caller); it cannot be a drive-by, so allow here and
|
|
@@ -77,13 +85,18 @@ function escapeHtml(value: string): string {
|
|
|
77
85
|
export function devSameOriginOk(req: Tina4Request): boolean {
|
|
78
86
|
const secFetchSite = (req.header("sec-fetch-site") ?? "").trim().toLowerCase();
|
|
79
87
|
if (secFetchSite) {
|
|
80
|
-
|
|
88
|
+
if (secFetchSite !== "same-origin" && secFetchSite !== "none") return false;
|
|
81
89
|
}
|
|
82
90
|
const origin = (req.header("origin") ?? "").trim();
|
|
83
91
|
if (origin) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
try {
|
|
93
|
+
const supplied = new URL(origin);
|
|
94
|
+
const host = (req.header("host") ?? "").trim();
|
|
95
|
+
const scheme = new URL(req.url ?? "http://localhost").protocol;
|
|
96
|
+
return host !== "" && !supplied.username && !supplied.password
|
|
97
|
+
&& supplied.pathname === "/" && !supplied.search && !supplied.hash
|
|
98
|
+
&& supplied.origin === new URL(`${scheme}//${host}`).origin;
|
|
99
|
+
} catch { return false; }
|
|
87
100
|
}
|
|
88
101
|
return true;
|
|
89
102
|
}
|
|
@@ -98,14 +111,52 @@ export function devSameOriginOk(req: Tina4Request): boolean {
|
|
|
98
111
|
* Reads the RAW socket peer (never X-Forwarded-For), exactly like the MCP gate.
|
|
99
112
|
*/
|
|
100
113
|
export function devMutationDenial(req: Tina4Request): { status: number; error: string } | null {
|
|
114
|
+
return devRequestDenial(req);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* True when a Host header names the loopback machine or the configured
|
|
119
|
+
* TINA4_HOST (ADR-0082), so a rebound DNS name cannot reach /__dev. A missing
|
|
120
|
+
* Host is not a browser request (every browser sends one); the peer gate
|
|
121
|
+
* governs it instead.
|
|
122
|
+
*/
|
|
123
|
+
export function devHostAllowed(hostHeader: string | undefined | null): boolean {
|
|
124
|
+
const host = (hostHeader ?? "").trim().toLowerCase();
|
|
125
|
+
if (!host) return true;
|
|
126
|
+
let name: string;
|
|
127
|
+
if (host.startsWith("[")) {
|
|
128
|
+
const end = host.indexOf("]");
|
|
129
|
+
name = end < 0 ? host : host.slice(1, end);
|
|
130
|
+
} else if (host.split(":").length === 2) {
|
|
131
|
+
name = host.split(":")[0];
|
|
132
|
+
} else {
|
|
133
|
+
name = host;
|
|
134
|
+
}
|
|
135
|
+
const allowed = new Set(["localhost", "127.0.0.1", "::1"]);
|
|
136
|
+
const configured = (process.env.TINA4_HOST ?? "").trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
137
|
+
if (configured) allowed.add(configured);
|
|
138
|
+
return allowed.has(name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Return `{status, error}` to REFUSE any /__dev request (read or write), or
|
|
143
|
+
* `null` to allow (ADR-0082): Host allow-list, then same-origin, then the
|
|
144
|
+
* loopback peer (the MCP surface keeps its own 404 gate for the peer part).
|
|
145
|
+
*/
|
|
146
|
+
export function devRequestDenial(req: Tina4Request): { status: number; error: string } | null {
|
|
147
|
+
// Host and browser origin remain mandatory even with a valid token.
|
|
148
|
+
// Remote dashboards explicitly configure TINA4_HOST.
|
|
149
|
+
const path = (req.path ?? "").replace(/\/+/g, "/");
|
|
150
|
+
const isMcp = DEV_MCP_PREFIXES.some((prefix) => path === prefix || path.startsWith(prefix + "/"));
|
|
151
|
+
if (!devHostAllowed(req.header("host"))) {
|
|
152
|
+
return { status: 403, error: "dev-admin: refused (host not allowed)" };
|
|
153
|
+
}
|
|
101
154
|
if (!devSameOriginOk(req)) {
|
|
102
155
|
return { status: 403, error: "dev-admin: refused (cross-origin request)" };
|
|
103
156
|
}
|
|
104
|
-
const path = req.path ?? "";
|
|
105
|
-
const isMcp = DEV_MCP_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
106
157
|
if (!isMcp) {
|
|
107
158
|
const peer = (req as unknown as { socket?: { remoteAddress?: string } }).socket?.remoteAddress ?? "";
|
|
108
|
-
if (!(isLoopback(peer) || mcpTokenOk(req))) {
|
|
159
|
+
if (!(isLoopback(peer) || mcpTokenOk(req, true))) {
|
|
109
160
|
return { status: 403, error: "dev-admin: refused (non-loopback peer)" };
|
|
110
161
|
}
|
|
111
162
|
}
|
|
@@ -605,7 +656,13 @@ export class DevAdmin {
|
|
|
605
656
|
{ method: "GET", pattern: "/__dev/api/metrics/file", handler: (req: any, res: any) => {
|
|
606
657
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
607
658
|
const p = (url.searchParams.get("path") || "").toString();
|
|
608
|
-
|
|
659
|
+
// ADR-0082: the path must resolve inside the project or the last scan
|
|
660
|
+
// root; an absolute path elsewhere is refused before the engine runs.
|
|
661
|
+
const scanRoot = metricsScanRoot();
|
|
662
|
+
let target: string | null = p ? safeJoin(process.cwd(), p, [scanRoot]) : "";
|
|
663
|
+
if (target === null && scanRoot) target = safeJoin(process.cwd(), join(scanRoot, p), [scanRoot]);
|
|
664
|
+
if (target === null) { res.status(403).json({ error: "Path outside project" }); return; }
|
|
665
|
+
try { res.json(fileDetail(target)); }
|
|
609
666
|
catch (e) {
|
|
610
667
|
if (e instanceof MetricsEngineError) {
|
|
611
668
|
// A bad path is the caller's mistake (404); anything else is the
|
|
@@ -1264,6 +1321,12 @@ const handleTable: RouteHandler = async (req, res) => {
|
|
|
1264
1321
|
try {
|
|
1265
1322
|
const { getAdapter } = await import("../../orm/src/index.js");
|
|
1266
1323
|
const db = getAdapter();
|
|
1324
|
+
// ADR-0082: only a name the database itself reports is accepted.
|
|
1325
|
+
const known = (db.getTables() as unknown[]).map((t) => String(t));
|
|
1326
|
+
if (!known.includes(name)) {
|
|
1327
|
+
res.json({ error: "unknown table" }, 404);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1267
1330
|
const columns = db.getColumns(name);
|
|
1268
1331
|
if (!columns.length) {
|
|
1269
1332
|
res.json({ table: name, columns: [], rows: [], message: "Database not connected or table not found" });
|
|
@@ -1928,26 +1991,35 @@ const handleConnectionsSave: RouteHandler = (req, res) => {
|
|
|
1928
1991
|
}
|
|
1929
1992
|
try {
|
|
1930
1993
|
const envPath = join(process.cwd(), ".env");
|
|
1931
|
-
const
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1994
|
+
const fd = openSync(envPath, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0), 0o600);
|
|
1995
|
+
try {
|
|
1996
|
+
const info = fstatSync(fd);
|
|
1997
|
+
if (!info.isFile() || info.nlink !== 1) throw new Error("Refusing a non-regular or multiply-linked configuration file");
|
|
1998
|
+
fchmodSync(fd, 0o600);
|
|
1999
|
+
const lines = readFileSync(fd, "utf-8").split("\n");
|
|
2000
|
+
const keysFound: Record<string, boolean> = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
|
|
2001
|
+
const newLines: string[] = [];
|
|
2002
|
+
for (const line of lines) {
|
|
2003
|
+
const trimmed = line.trim();
|
|
2004
|
+
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) {
|
|
2005
|
+
newLines.push(line);
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const key = trimmed.split("=", 1)[0].trim();
|
|
2009
|
+
if (key === "TINA4_DATABASE_URL") { newLines.push(`TINA4_DATABASE_URL=${url}`); keysFound.TINA4_DATABASE_URL = true; }
|
|
2010
|
+
else if (key === "TINA4_DATABASE_USERNAME") { newLines.push(`TINA4_DATABASE_USERNAME=${username}`); keysFound.TINA4_DATABASE_USERNAME = true; }
|
|
2011
|
+
else if (key === "TINA4_DATABASE_PASSWORD") { newLines.push(`TINA4_DATABASE_PASSWORD=${password}`); keysFound.TINA4_DATABASE_PASSWORD = true; }
|
|
2012
|
+
else { newLines.push(line); }
|
|
1939
2013
|
}
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
2014
|
+
const values: Record<string, string> = { TINA4_DATABASE_URL: url, TINA4_DATABASE_USERNAME: username, TINA4_DATABASE_PASSWORD: password };
|
|
2015
|
+
for (const [key, found] of Object.entries(keysFound)) {
|
|
2016
|
+
if (!found) newLines.push(`${key}=${values[key]}`);
|
|
2017
|
+
}
|
|
2018
|
+
ftruncateSync(fd, 0);
|
|
2019
|
+
writeFileSync(fd, newLines.join("\n") + "\n");
|
|
2020
|
+
} finally {
|
|
2021
|
+
closeSync(fd);
|
|
1949
2022
|
}
|
|
1950
|
-
writeFileSync(envPath, newLines.join("\n") + "\n");
|
|
1951
2023
|
res.json({ success: true });
|
|
1952
2024
|
} catch (e: unknown) {
|
|
1953
2025
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -2117,10 +2189,44 @@ export const handleVersionCheck: RouteHandler = async (_req, res) => {
|
|
|
2117
2189
|
// Parity handlers — ported from Python tina4_python.dev_admin
|
|
2118
2190
|
// ---------------------------------------------------------------------------
|
|
2119
2191
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2192
|
+
/**
|
|
2193
|
+
* Real path of `target`: the deepest existing ancestor is realpath'd (symlinks
|
|
2194
|
+
* followed the way the OS will open the file) and the not-yet-existing tail is
|
|
2195
|
+
* appended (a new file being saved).
|
|
2196
|
+
*/
|
|
2197
|
+
function realPathOf(target: string): string {
|
|
2198
|
+
let existing = target;
|
|
2199
|
+
const tail: string[] = [];
|
|
2200
|
+
for (;;) {
|
|
2201
|
+
let present = false;
|
|
2202
|
+
try { lstatSync(existing); present = true; } catch { /* missing */ }
|
|
2203
|
+
if (present || dirname(existing) === existing) break;
|
|
2204
|
+
tail.unshift(basename(existing));
|
|
2205
|
+
existing = dirname(existing);
|
|
2206
|
+
}
|
|
2207
|
+
return join(realpathSync(existing), ...tail);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
/**
|
|
2211
|
+
* Resolve `rel` inside an allowed root (ADR-0082): `..` collapsed, symlinks
|
|
2212
|
+
* followed, and a trailing-separator root check so `/app` never contains
|
|
2213
|
+
* `/app-sibling`. Returns null when the path leaves every root.
|
|
2214
|
+
*/
|
|
2215
|
+
function safeJoin(projectRoot: string, rel: string, extraRoots: string[] = []): string | null {
|
|
2216
|
+
try {
|
|
2217
|
+
const root = realPathOf(resolve(projectRoot));
|
|
2218
|
+
const resolved = realPathOf(resolve(root, rel));
|
|
2219
|
+
const roots = [root, ...extraRoots.filter(Boolean).map((r) => realPathOf(resolve(r)))];
|
|
2220
|
+
for (const r of roots) {
|
|
2221
|
+
if (resolved === r || resolved.startsWith(r.endsWith(sep) ? r : r + sep)) return resolved;
|
|
2222
|
+
}
|
|
2223
|
+
} catch { /* Broken links or inaccessible ancestors fail closed. */ }
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
/** Project-relative form of a resolved path, for the secret denylist. */
|
|
2228
|
+
function resolvedRelative(projectRoot: string, resolvedPath: string): string {
|
|
2229
|
+
return relative(realPathOf(resolve(projectRoot)), resolvedPath);
|
|
2124
2230
|
}
|
|
2125
2231
|
|
|
2126
2232
|
const handleThoughts: RouteHandler = (req, res) => {
|
|
@@ -2343,22 +2449,43 @@ export function devAdminLanguage(rel: string): string {
|
|
|
2343
2449
|
return DEV_ADMIN_LANG_MAP[ext] ?? "text";
|
|
2344
2450
|
}
|
|
2345
2451
|
|
|
2452
|
+
/** Read the checked regular file through the same descriptor. Final-component
|
|
2453
|
+
* symlink replacement is refused. Parent-directory resolution is not an atomic
|
|
2454
|
+
* openat walk; the development project directory must remain locally trusted.
|
|
2455
|
+
*/
|
|
2456
|
+
function readDevFile(target: string): Buffer | null {
|
|
2457
|
+
let fd: number | undefined;
|
|
2458
|
+
try {
|
|
2459
|
+
fd = openSync(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
2460
|
+
if (!fstatSync(fd).isFile()) return null;
|
|
2461
|
+
return readFileSync(fd);
|
|
2462
|
+
} catch (error) {
|
|
2463
|
+
if (["ENOENT", "ENOTDIR", "ELOOP"].includes((error as NodeJS.ErrnoException).code ?? "")) return null;
|
|
2464
|
+
throw error;
|
|
2465
|
+
} finally {
|
|
2466
|
+
if (fd !== undefined) closeSync(fd);
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2346
2470
|
const handleFileRead: RouteHandler = (req, res) => {
|
|
2347
2471
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2348
2472
|
const rel = url.searchParams.get("path") ?? "";
|
|
2349
|
-
// DEVADMIN-DEC-03: never serve secret material (.env, keys, .git/, secrets/).
|
|
2350
|
-
if (isSecretPath(rel)) {
|
|
2351
|
-
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
2352
|
-
return;
|
|
2353
|
-
}
|
|
2354
2473
|
const root = resolve(process.cwd());
|
|
2355
2474
|
const target = safeJoin(root, rel);
|
|
2356
|
-
if (!target
|
|
2357
|
-
res.json({ error:
|
|
2475
|
+
if (!target) {
|
|
2476
|
+
res.json({ error: "Path outside project", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
// DEVADMIN-DEC-03: never serve secret material (.env, keys, .git/, secrets/),
|
|
2480
|
+
// judged on the RESOLVED path so `.env/.` or a symlink cannot slip past (ADR-0082).
|
|
2481
|
+
if (isSecretPath(rel) || isSecretPath(resolvedRelative(root, target))) {
|
|
2482
|
+
res.json({ error: "Refused: secret file", path: rel, content: "", language: "text", bytes: 0 }, 403);
|
|
2358
2483
|
return;
|
|
2359
2484
|
}
|
|
2360
2485
|
try {
|
|
2361
|
-
const
|
|
2486
|
+
const data = readDevFile(target);
|
|
2487
|
+
if (data === null) { res.json({ error: `File not found: ${rel}` }, 404); return; }
|
|
2488
|
+
const content = data.toString("utf-8");
|
|
2362
2489
|
const path = relative(root, target);
|
|
2363
2490
|
res.json({ path, content, language: devAdminLanguage(path), bytes: Buffer.byteLength(content, "utf-8") });
|
|
2364
2491
|
} catch (e) {
|
|
@@ -2373,7 +2500,7 @@ const handleFileSave: RouteHandler = async (req, res) => {
|
|
|
2373
2500
|
const root = resolve(process.cwd());
|
|
2374
2501
|
const target = safeJoin(root, rel);
|
|
2375
2502
|
if (!target) {
|
|
2376
|
-
res.json({ error: `Path escapes project directory: ${rel}` },
|
|
2503
|
+
res.json({ error: `Path escapes project directory: ${rel}` }, 403);
|
|
2377
2504
|
return;
|
|
2378
2505
|
}
|
|
2379
2506
|
try {
|
|
@@ -2393,20 +2520,20 @@ const handleFileSave: RouteHandler = async (req, res) => {
|
|
|
2393
2520
|
const handleFileRaw: RouteHandler = (req, res) => {
|
|
2394
2521
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2395
2522
|
const rel = url.searchParams.get("path") ?? "";
|
|
2396
|
-
// DEVADMIN-DEC-03: never serve secret material (.env, keys, .git/, secrets/).
|
|
2397
|
-
if (isSecretPath(rel)) {
|
|
2398
|
-
res.json({ error: "Refused: secret file" }, 403);
|
|
2399
|
-
return;
|
|
2400
|
-
}
|
|
2401
2523
|
const root = resolve(process.cwd());
|
|
2402
2524
|
const target = safeJoin(root, rel);
|
|
2403
|
-
if (!target
|
|
2404
|
-
res.
|
|
2405
|
-
|
|
2525
|
+
if (!target) {
|
|
2526
|
+
res.json({ error: "Path outside project" }, 403);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
// DEVADMIN-DEC-03: never serve secret material, judged on the RESOLVED path.
|
|
2530
|
+
if (isSecretPath(rel) || isSecretPath(resolvedRelative(root, target))) {
|
|
2531
|
+
res.json({ error: "Refused: secret file" }, 403);
|
|
2406
2532
|
return;
|
|
2407
2533
|
}
|
|
2408
2534
|
try {
|
|
2409
|
-
const buf =
|
|
2535
|
+
const buf = readDevFile(target);
|
|
2536
|
+
if (buf === null) { res.raw.writeHead(404); res.raw.end("Not found"); return; }
|
|
2410
2537
|
const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
2411
2538
|
const mime: Record<string, string> = {
|
|
2412
2539
|
js: "application/javascript", ts: "text/plain", json: "application/json",
|
|
@@ -2544,19 +2671,18 @@ function mcpSecureEqual(expected: string, provided: string): boolean {
|
|
|
2544
2671
|
}
|
|
2545
2672
|
|
|
2546
2673
|
/**
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
2674
|
+
* MCP transport retains the documented API-key fallback. Non-MCP dev routes
|
|
2675
|
+
* pass dedicated=true and require TINA4_MCP_TOKEN (ADR-0082).
|
|
2549
2676
|
* No configured token ⇒ a remote caller can never present a valid one.
|
|
2550
2677
|
*/
|
|
2551
|
-
function mcpTokenOk(req: Tina4Request): boolean {
|
|
2552
|
-
|
|
2553
|
-
if (!expected) expected = process.env.TINA4_API_KEY;
|
|
2678
|
+
function mcpTokenOk(req: Tina4Request, dedicated = false): boolean {
|
|
2679
|
+
const expected = process.env.TINA4_MCP_TOKEN || (!dedicated ? process.env.TINA4_API_KEY : undefined);
|
|
2554
2680
|
if (!expected) return false;
|
|
2555
2681
|
let provided = "";
|
|
2556
2682
|
const auth = req.header("authorization") ?? "";
|
|
2557
2683
|
if (auth.toLowerCase().startsWith("bearer ")) provided = auth.slice(7).trim();
|
|
2558
2684
|
if (!provided) provided = req.header("x-mcp-token") ?? "";
|
|
2559
|
-
if (!provided) provided = req.header("x-api-key") ?? "";
|
|
2685
|
+
if (!dedicated && !provided) provided = req.header("x-api-key") ?? "";
|
|
2560
2686
|
if (!provided) return false;
|
|
2561
2687
|
return mcpSecureEqual(expected, provided);
|
|
2562
2688
|
}
|
|
@@ -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 DevMailbox — File-backed development mailbox, 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
|
* The dispatch pipeline: the concerns of `dispatch`, named and extracted.
|
|
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 Live API RAG — `Docs` module.
|
|
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
|
* Live Docs MCP discovery — writes `.tina4/mcp.json` so MCP-aware AI tools
|
|
3
11
|
* (Claude Code, Cursor, etc.) auto-discover this server's docs MCP endpoint.
|
|
@@ -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
|
// ── TS regex parser ──────────────────────────────────────────────────
|
|
2
10
|
import { stripStrings } from "./docsScanner.js";
|
|
3
11
|
import { matchMethodSignature, matchTopLevelFunction } from "./docsSignatures.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
|
/**
|
|
2
10
|
* Replace string literals and template literal contents with spaces of equal
|
|
3
11
|
* length so brace/paren scanning isn't fooled by characters inside strings.
|
|
@@ -85,6 +93,34 @@ function stripTemplate(source: string, start: number): { text: string; next: num
|
|
|
85
93
|
return { text: out.join(""), next: i };
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
// A slash starts a regexp where an expression may begin. Division follows a
|
|
97
|
+
// value (identifier, number, closing bracket/paren) and must remain visible.
|
|
98
|
+
function startsRegex(source: string, start: number): boolean {
|
|
99
|
+
let i = start - 1;
|
|
100
|
+
while (i >= 0 && /\s/.test(source[i])) i--;
|
|
101
|
+
if (i < 0 || "=(:,[!&|?;{}".includes(source[i])) return true;
|
|
102
|
+
const end = i + 1;
|
|
103
|
+
while (i >= 0 && /[A-Za-z_$]/.test(source[i])) i--;
|
|
104
|
+
return ["return", "throw", "case", "yield", "await"].includes(source.slice(i + 1, end));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stripRegex(source: string, start: number): { text: string; next: number } | null {
|
|
108
|
+
let inClass = false;
|
|
109
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
110
|
+
const c = source[i];
|
|
111
|
+
if (c === "\n" || c === "\r") return null;
|
|
112
|
+
if (c === "\\") { i++; continue; }
|
|
113
|
+
if (c === "[") inClass = true;
|
|
114
|
+
else if (c === "]") inClass = false;
|
|
115
|
+
else if (c === "/" && !inClass) {
|
|
116
|
+
let next = i + 1;
|
|
117
|
+
while (next < source.length && /[a-z]/i.test(source[next])) next++;
|
|
118
|
+
return { text: blankSegment(source, start, next), next };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
88
124
|
export function stripStrings(source: string): string {
|
|
89
125
|
const out: string[] = [];
|
|
90
126
|
let i = 0;
|
|
@@ -102,6 +138,14 @@ export function stripStrings(source: string): string {
|
|
|
102
138
|
i = part.next;
|
|
103
139
|
continue;
|
|
104
140
|
}
|
|
141
|
+
if (c === "/" && startsRegex(source, i)) {
|
|
142
|
+
const part = stripRegex(source, i);
|
|
143
|
+
if (part) {
|
|
144
|
+
out.push(part.text);
|
|
145
|
+
i = part.next;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
105
149
|
if (c === '"' || c === "'") {
|
|
106
150
|
const part = stripQuoted(source, i);
|
|
107
151
|
out.push(part.text);
|
|
@@ -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
|
interface MethodMatch {
|
|
2
10
|
name: string;
|
|
3
11
|
signature: 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
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
2
10
|
import { resolve, join, dirname } from "node:path";
|
|
3
11
|
|
package/packages/core/src/env.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
|
* Typed environment-variable helpers — zero-deps.
|
|
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 Debug — Rich error overlay for development mode.
|
|
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 Events — Simple observer pattern for decoupled communication.
|
|
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
|
// Tina4 FakeData — Fake data generation and database seeding, zero dependencies.
|
|
2
10
|
// Instance-based with optional seeded PRNG for deterministic output.
|
|
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
|
* Customer feedback widget — Tier 4 port from tina4-python.
|
|
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 GraphQL — Zero-dependency GraphQL engine.
|
|
3
11
|
*
|
|
@@ -62,12 +70,14 @@ const TOKEN_PATTERNS: Array<[string, RegExp]> = [
|
|
|
62
70
|
["EQUALS", /=/y],
|
|
63
71
|
["AT", /@/y],
|
|
64
72
|
["DOLLAR", /\$/y],
|
|
65
|
-
["COMMA", /,/y],
|
|
66
73
|
["STRING", /"(?:[^"\\]|\\.)*"/y],
|
|
67
74
|
["NUMBER", /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y],
|
|
68
75
|
["BOOL", /\b(?:true|false)\b/y],
|
|
69
76
|
["NULL", /\bnull\b/y],
|
|
70
77
|
["NAME", /[_a-zA-Z]\w*/y],
|
|
78
|
+
// GraphQL spec 2.1.7: a comma is insignificant, exactly like whitespace. It
|
|
79
|
+
// used to be a COMMA token that only the argument and variable loops
|
|
80
|
+
// consumed, so a comma between fields or list values was a parse error.
|
|
71
81
|
["SKIP", /[\s,]+/y],
|
|
72
82
|
["COMMENT", /#[^\n]*/y],
|
|
73
83
|
];
|
|
@@ -331,7 +341,6 @@ class Parser {
|
|
|
331
341
|
const name = this.expect("NAME").value;
|
|
332
342
|
this.expect("COLON");
|
|
333
343
|
args[name] = this.parseValue();
|
|
334
|
-
this.match("COMMA");
|
|
335
344
|
}
|
|
336
345
|
return args;
|
|
337
346
|
}
|
|
@@ -416,7 +425,6 @@ class Parser {
|
|
|
416
425
|
defaultVal = this.parseValue();
|
|
417
426
|
}
|
|
418
427
|
defs.push({ name, type: typeName, default: defaultVal });
|
|
419
|
-
this.match("COMMA");
|
|
420
428
|
}
|
|
421
429
|
return defs;
|
|
422
430
|
}
|
|
@@ -930,7 +938,9 @@ export class GraphQL {
|
|
|
930
938
|
className,
|
|
931
939
|
(root, args) => {
|
|
932
940
|
const db = getDb();
|
|
933
|
-
|
|
941
|
+
// Declared fields only: the executor passes every argument the query
|
|
942
|
+
// names, declared or not, and each one becomes a column (ADR-0069).
|
|
943
|
+
const fieldNames = Object.keys(args).filter((k) => Object.hasOwn(mutationArgs, k));
|
|
934
944
|
const placeholders = fieldNames.map(() => "?");
|
|
935
945
|
const values = fieldNames.map((f) => args[f]);
|
|
936
946
|
|
|
@@ -960,7 +970,8 @@ export class GraphQL {
|
|
|
960
970
|
const values: unknown[] = [];
|
|
961
971
|
|
|
962
972
|
for (const [k, v] of Object.entries(args)) {
|
|
963
|
-
|
|
973
|
+
// Declared fields only (see create above).
|
|
974
|
+
if (k !== "id" && Object.hasOwn(mutationArgs, k)) {
|
|
964
975
|
setClauses.push(`"${k}" = ?`);
|
|
965
976
|
values.push(v);
|
|
966
977
|
}
|
|
@@ -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 { RouteDefinition, RouteHandler } from "./types.js";
|
|
2
10
|
|
|
3
11
|
/** Server start time, set when health route is created */
|