tina4-nodejs 3.13.137 → 3.13.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +33 -3
- package/COMMERCIAL-LICENSE.md +8 -0
- package/LICENSE +373 -0
- package/NOTICE +11 -0
- package/README.md +24 -2
- package/package.json +44 -6
- package/packages/cli/dist/bin.js +19979 -18102
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/build.ts +8 -0
- package/packages/cli/src/commands/generate.ts +8 -0
- package/packages/cli/src/commands/init.ts +8 -0
- package/packages/cli/src/commands/lint.ts +8 -0
- package/packages/cli/src/commands/migrate.ts +8 -0
- package/packages/cli/src/commands/migrateCreate.ts +8 -0
- package/packages/cli/src/commands/migrateRollback.ts +8 -0
- package/packages/cli/src/commands/migrateStatus.ts +8 -0
- package/packages/cli/src/commands/queue.ts +8 -0
- package/packages/cli/src/commands/routes.ts +8 -0
- package/packages/cli/src/commands/seed.ts +8 -0
- package/packages/cli/src/commands/serve.ts +11 -0
- package/packages/cli/src/commands/test.ts +8 -0
- package/packages/core/dist/index.js +19976 -18097
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
- package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
- package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
- package/packages/core/public/bundled-component-provenance.json +48 -0
- package/packages/core/public/js/tina4js.min.js +13 -3
- package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
- package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
- package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
- package/packages/core/src/__feedback/widget.js +8 -0
- package/packages/core/src/_missing.ts +8 -0
- package/packages/core/src/ai.ts +8 -0
- package/packages/core/src/aiClient.ts +8 -0
- package/packages/core/src/api.ts +48 -7
- package/packages/core/src/auth.ts +94 -14
- package/packages/core/src/authGate.ts +35 -20
- package/packages/core/src/background.ts +8 -0
- package/packages/core/src/cache.ts +43 -4
- package/packages/core/src/constants.ts +8 -0
- package/packages/core/src/container.ts +8 -0
- package/packages/core/src/context/chunker.ts +8 -0
- package/packages/core/src/context/index.ts +8 -0
- package/packages/core/src/devAdmin.ts +185 -59
- package/packages/core/src/devMailbox.ts +8 -0
- package/packages/core/src/dispatchPipeline.ts +8 -0
- package/packages/core/src/docs.ts +8 -0
- package/packages/core/src/docsAutoDiscovery.ts +8 -0
- package/packages/core/src/docsParser.ts +8 -0
- package/packages/core/src/docsScanner.ts +44 -0
- package/packages/core/src/docsSignatures.ts +8 -0
- package/packages/core/src/dotenv.ts +8 -0
- package/packages/core/src/env.ts +8 -0
- package/packages/core/src/errorOverlay.ts +8 -0
- package/packages/core/src/events.ts +8 -0
- package/packages/core/src/fakeData.ts +8 -0
- package/packages/core/src/feedback.ts +8 -0
- package/packages/core/src/graphql.ts +108 -5
- package/packages/core/src/health.ts +8 -0
- package/packages/core/src/htmlElement.ts +8 -0
- package/packages/core/src/i18n.ts +8 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/job.ts +16 -1
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +11 -1
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1181 -1097
- package/packages/core/src/mqtt.ts +8 -0
- package/packages/core/src/mqttMessage.ts +8 -0
- package/packages/core/src/optionalPackage.ts +48 -0
- package/packages/core/src/plan.ts +8 -0
- package/packages/core/src/portTakeover.ts +8 -0
- package/packages/core/src/projectIndex.ts +8 -0
- package/packages/core/src/projectIndexExtractors.ts +8 -0
- package/packages/core/src/projectIndexStorage.ts +8 -0
- package/packages/core/src/push.ts +24 -0
- package/packages/core/src/queue.ts +23 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +23 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +61 -14
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
- package/packages/core/src/rateLimiter.ts +8 -0
- package/packages/core/src/request.ts +59 -10
- package/packages/core/src/response.ts +79 -5
- package/packages/core/src/routeDiscovery.ts +8 -0
- package/packages/core/src/router.ts +8 -0
- package/packages/core/src/server.ts +79 -25
- package/packages/core/src/service.ts +8 -0
- package/packages/core/src/session.ts +8 -0
- package/packages/core/src/sessionHandlers/childError.ts +8 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +68 -3
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/respClient.ts +8 -0
- package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
- package/packages/core/src/sso.ts +20 -4
- package/packages/core/src/ssoIdentity.ts +19 -0
- package/packages/core/src/ssrf.ts +221 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +11 -0
- package/packages/core/src/testing.ts +8 -0
- package/packages/core/src/transport.ts +369 -0
- package/packages/core/src/trustedProxy.ts +8 -0
- package/packages/core/src/types.ts +13 -0
- package/packages/core/src/validator.ts +8 -0
- package/packages/core/src/version.ts +8 -0
- package/packages/core/src/websocket.ts +12 -2
- package/packages/core/src/websocketBackplane.ts +49 -32
- package/packages/core/src/websocketConnection.ts +8 -0
- package/packages/core/src/wsdl.ts +197 -115
- package/packages/core/templates/errors/302.twig +8 -0
- package/packages/core/templates/errors/401.twig +8 -0
- package/packages/core/templates/errors/403.twig +8 -0
- package/packages/core/templates/errors/404.twig +8 -0
- package/packages/core/templates/errors/500.twig +8 -0
- package/packages/core/templates/errors/502.twig +8 -0
- package/packages/core/templates/errors/503.twig +8 -0
- package/packages/core/templates/errors/base.twig +8 -0
- package/packages/frond/dist/index.js +157 -17
- package/packages/frond/src/engine.ts +186 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8641 -6765
- package/packages/orm/src/adapters/firebird.ts +50 -3
- package/packages/orm/src/adapters/mongodb.ts +8 -0
- package/packages/orm/src/adapters/mssql.ts +29 -7
- package/packages/orm/src/adapters/mysql.ts +17 -1
- package/packages/orm/src/adapters/odbc.ts +11 -0
- package/packages/orm/src/adapters/pg-types.d.ts +8 -0
- package/packages/orm/src/adapters/postgres.ts +34 -8
- package/packages/orm/src/adapters/sqlDialect.ts +57 -0
- package/packages/orm/src/adapters/sqlite.ts +26 -0
- package/packages/orm/src/autoCrud.ts +78 -36
- package/packages/orm/src/baseModel.ts +61 -29
- package/packages/orm/src/cachedDatabase.ts +67 -1
- package/packages/orm/src/connectTimeout.ts +8 -0
- package/packages/orm/src/database.ts +649 -401
- package/packages/orm/src/databaseResult.ts +8 -0
- package/packages/orm/src/databaseUrl.ts +8 -0
- package/packages/orm/src/docstore.ts +23 -1
- package/packages/orm/src/fakeData.ts +8 -0
- package/packages/orm/src/graph/adapters/arango.ts +8 -0
- package/packages/orm/src/graph/adapters/bolt.ts +8 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
- package/packages/orm/src/graph/connectTimeout.ts +8 -0
- package/packages/orm/src/graph/errors.ts +8 -0
- package/packages/orm/src/graph/graphAdapter.ts +8 -0
- package/packages/orm/src/graph/graphDatabase.ts +8 -0
- package/packages/orm/src/graph/graphUrl.ts +10 -1
- package/packages/orm/src/graph/shapes.ts +8 -0
- package/packages/orm/src/index.ts +147 -139
- package/packages/orm/src/migration.ts +8 -0
- package/packages/orm/src/model.ts +8 -0
- package/packages/orm/src/modelCollection.ts +8 -0
- package/packages/orm/src/point.ts +8 -0
- package/packages/orm/src/query.ts +149 -33
- package/packages/orm/src/queryBuilder.ts +8 -0
- package/packages/orm/src/realtime/index.ts +8 -0
- package/packages/orm/src/realtime/models/attachment.ts +8 -0
- package/packages/orm/src/realtime/models/channel.ts +8 -0
- package/packages/orm/src/realtime/models/channelMember.ts +8 -0
- package/packages/orm/src/realtime/models/message.ts +8 -0
- package/packages/orm/src/realtime/models/workspace.ts +8 -0
- package/packages/orm/src/realtime/realtime.ts +8 -0
- package/packages/orm/src/realtime/storage.ts +43 -12
- package/packages/orm/src/seeder.ts +8 -0
- package/packages/orm/src/seederTable.ts +8 -0
- package/packages/orm/src/seederTypes.ts +8 -0
- package/packages/orm/src/sqlTranslator.ts +185 -8
- package/packages/orm/src/types.ts +15 -0
- package/packages/orm/src/validation.ts +8 -0
- package/packages/swagger/src/generator.ts +8 -0
- package/packages/swagger/src/index.ts +8 -0
- package/packages/swagger/src/ui.ts +8 -0
- package/types/core/src/api.d.ts +6 -0
- package/types/core/src/auth.d.ts +30 -0
- package/types/core/src/background.d.ts +0 -16
- package/types/core/src/cache.d.ts +6 -0
- package/types/core/src/devAdmin.d.ts +18 -12
- package/types/core/src/docs.d.ts +0 -17
- package/types/core/src/errorOverlay.d.ts +0 -24
- package/types/core/src/events.d.ts +0 -12
- package/types/core/src/feedback.d.ts +0 -23
- package/types/core/src/graphql.d.ts +25 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- package/types/core/src/index.d.ts +1 -0
- package/types/core/src/job.d.ts +1 -0
- package/types/core/src/messenger.d.ts +12 -1
- package/types/core/src/metrics.d.ts +2 -0
- package/types/core/src/middleware.d.ts +45 -1
- package/types/core/src/mqtt.d.ts +0 -33
- package/types/core/src/optionalPackage.d.ts +25 -0
- package/types/core/src/plan.d.ts +0 -10
- package/types/core/src/projectIndex.d.ts +0 -10
- package/types/core/src/push.d.ts +5 -0
- package/types/core/src/queue.d.ts +7 -1
- package/types/core/src/queueBackends/liteBackend.d.ts +9 -0
- package/types/core/src/queueBackends/mongoBackend.d.ts +10 -1
- package/types/core/src/request.d.ts +12 -0
- package/types/core/src/response.d.ts +13 -0
- package/types/core/src/server.d.ts +19 -0
- package/types/core/src/sessionHandlers/childError.d.ts +0 -15
- package/types/core/src/sessionHandlers/databaseHandler.d.ts +25 -1
- package/types/core/src/sso.d.ts +1 -0
- package/types/core/src/ssoIdentity.d.ts +10 -0
- package/types/core/src/ssrf.d.ts +19 -0
- package/types/core/src/transport.d.ts +28 -0
- package/types/core/src/trustedProxy.d.ts +0 -16
- package/types/core/src/types.d.ts +5 -0
- package/types/core/src/websocketBackplane.d.ts +1 -0
- package/types/core/src/wsdl.d.ts +8 -22
- package/types/frond/src/engine.d.ts +7 -1
- package/types/orm/src/adapters/firebird.d.ts +12 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +2 -0
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
- package/types/orm/src/adapters/sqlite.d.ts +7 -0
- package/types/orm/src/baseModel.d.ts +5 -0
- package/types/orm/src/cachedDatabase.d.ts +27 -0
- package/types/orm/src/database.d.ts +46 -7
- package/types/orm/src/graph/graphUrl.d.ts +0 -8
- package/types/orm/src/index.d.ts +1 -1
- package/types/orm/src/query.d.ts +48 -2
- package/types/orm/src/realtime/storage.d.ts +2 -1
- package/types/orm/src/sqlTranslator.d.ts +70 -18
- package/types/orm/src/types.d.ts +6 -0
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
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
|
];
|
|
@@ -165,10 +175,17 @@ type ParsedDefinition = ParsedOperation | ParsedFragment;
|
|
|
165
175
|
class Parser {
|
|
166
176
|
private tokens: Token[];
|
|
167
177
|
private pos: number;
|
|
178
|
+
// F2: bound the parser's own recursion. A deeply nested query
|
|
179
|
+
// ("{a{a{a...}}}") otherwise recurses in parseSelectionSet until the call
|
|
180
|
+
// stack overflows — BEFORE the execution-time depth guard can run. Reusing
|
|
181
|
+
// TINA4_GRAPHQL_MAX_DEPTH keeps parse and execution on one bound.
|
|
182
|
+
private depth = 0;
|
|
183
|
+
private readonly maxDepth: number;
|
|
168
184
|
|
|
169
185
|
constructor(tokens: Token[]) {
|
|
170
186
|
this.tokens = tokens;
|
|
171
187
|
this.pos = 0;
|
|
188
|
+
this.maxDepth = graphqlMaxDepth();
|
|
172
189
|
}
|
|
173
190
|
|
|
174
191
|
peek(): Token | null {
|
|
@@ -262,6 +279,18 @@ class Parser {
|
|
|
262
279
|
}
|
|
263
280
|
|
|
264
281
|
private parseSelectionSet(): ParsedSelection[] {
|
|
282
|
+
this.depth++;
|
|
283
|
+
if (this.maxDepth > 0 && this.depth > this.maxDepth) {
|
|
284
|
+
throw new Error(`Query exceeds maximum depth of ${this.maxDepth}`);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
return this.parseSelectionSetInner();
|
|
288
|
+
} finally {
|
|
289
|
+
this.depth--;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private parseSelectionSetInner(): ParsedSelection[] {
|
|
265
294
|
this.expect("LBRACE");
|
|
266
295
|
const selections: ParsedSelection[] = [];
|
|
267
296
|
while (!this.match("RBRACE")) {
|
|
@@ -331,7 +360,6 @@ class Parser {
|
|
|
331
360
|
const name = this.expect("NAME").value;
|
|
332
361
|
this.expect("COLON");
|
|
333
362
|
args[name] = this.parseValue();
|
|
334
|
-
this.match("COMMA");
|
|
335
363
|
}
|
|
336
364
|
return args;
|
|
337
365
|
}
|
|
@@ -416,7 +444,6 @@ class Parser {
|
|
|
416
444
|
defaultVal = this.parseValue();
|
|
417
445
|
}
|
|
418
446
|
defs.push({ name, type: typeName, default: defaultVal });
|
|
419
|
-
this.match("COMMA");
|
|
420
447
|
}
|
|
421
448
|
return defs;
|
|
422
449
|
}
|
|
@@ -480,6 +507,24 @@ export function graphqlMaxDepth(): number {
|
|
|
480
507
|
return Number.isNaN(n) ? 50 : n;
|
|
481
508
|
}
|
|
482
509
|
|
|
510
|
+
/**
|
|
511
|
+
* F2: maximum number of expanded selection nodes a single query may request.
|
|
512
|
+
* The depth guard bounds NESTING but not WIDTH: a fragment that spreads another
|
|
513
|
+
* fragment many times (a "fragment bomb") stays shallow while expanding to
|
|
514
|
+
* millions of fields, and a query with thousands of aliases of one field is not
|
|
515
|
+
* deep at all. This budget bounds the expanded selection tree (fragments
|
|
516
|
+
* expanded, aliases counted) so both are rejected before any resolver runs.
|
|
517
|
+
* `TINA4_GRAPHQL_MAX_NODES` overrides the default (1000); `<= 0` disables it.
|
|
518
|
+
*/
|
|
519
|
+
export function graphqlMaxNodes(): number {
|
|
520
|
+
const raw = (process.env.TINA4_GRAPHQL_MAX_NODES ?? "1000").trim();
|
|
521
|
+
const n = parseInt(raw, 10);
|
|
522
|
+
return Number.isNaN(n) ? 1000 : n;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** Thrown when a query's expanded selection count exceeds the node budget (F2). */
|
|
526
|
+
class ComplexityError extends Error {}
|
|
527
|
+
|
|
483
528
|
// ── GraphQL Engine ───────────────────────────────────────────
|
|
484
529
|
|
|
485
530
|
export class GraphQL {
|
|
@@ -507,6 +552,13 @@ export class GraphQL {
|
|
|
507
552
|
*/
|
|
508
553
|
public maxDepth: number = graphqlMaxDepth();
|
|
509
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Maximum expanded selection nodes per query (from `TINA4_GRAPHQL_MAX_NODES`,
|
|
557
|
+
* default 1000; `<= 0` disables). Bounds query WIDTH — fragment bombs and
|
|
558
|
+
* alias explosions — which the depth guard does not. See {@link graphqlMaxNodes}.
|
|
559
|
+
*/
|
|
560
|
+
public maxNodes: number = graphqlMaxNodes();
|
|
561
|
+
|
|
510
562
|
/**
|
|
511
563
|
* Decorator-style resolver registration.
|
|
512
564
|
*
|
|
@@ -684,6 +736,20 @@ export class GraphQL {
|
|
|
684
736
|
}
|
|
685
737
|
|
|
686
738
|
const op = operations[0];
|
|
739
|
+
|
|
740
|
+
// F2: reject an over-complex query (fragment bomb / alias explosion) before
|
|
741
|
+
// any resolver runs.
|
|
742
|
+
if (this.maxNodes > 0) {
|
|
743
|
+
try {
|
|
744
|
+
this.assertComplexity(op.selections, fragments);
|
|
745
|
+
} catch (e: unknown) {
|
|
746
|
+
if (e instanceof ComplexityError) {
|
|
747
|
+
return { data: null, errors: [{ message: e.message }] };
|
|
748
|
+
}
|
|
749
|
+
throw e;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
687
753
|
const resolvers = op.operation === "query" ? this.queries : this.mutations;
|
|
688
754
|
|
|
689
755
|
// Apply variable defaults
|
|
@@ -715,6 +781,40 @@ export class GraphQL {
|
|
|
715
781
|
* instead of recursing until the interpreter stack overflows. Top-level
|
|
716
782
|
* starts at depth 1; `maxDepth <= 0` disables the guard.
|
|
717
783
|
*/
|
|
784
|
+
/**
|
|
785
|
+
* Count the expanded selection nodes, throwing {@link ComplexityError} past
|
|
786
|
+
* `maxNodes` (F2). Fragment spreads are expanded (so a fragment bomb is
|
|
787
|
+
* counted, not the small unexpanded document) and every field — including
|
|
788
|
+
* each alias — counts once. The count is capped by `maxNodes` and the walk by
|
|
789
|
+
* `maxDepth`, so a circular fragment trips the budget instead of looping and
|
|
790
|
+
* the check itself can never cost more than the budget.
|
|
791
|
+
*/
|
|
792
|
+
private assertComplexity(
|
|
793
|
+
selections: ParsedSelection[],
|
|
794
|
+
fragments: Map<string, ParsedFragment>,
|
|
795
|
+
): void {
|
|
796
|
+
const depthCap = this.maxDepth > 0 ? this.maxDepth : 1000;
|
|
797
|
+
let count = 0;
|
|
798
|
+
const walk = (sels: ParsedSelection[], depth: number): void => {
|
|
799
|
+
if (depth > depthCap) return;
|
|
800
|
+
for (const sel of sels) {
|
|
801
|
+
count++;
|
|
802
|
+
if (count > this.maxNodes) {
|
|
803
|
+
throw new ComplexityError(`Query exceeds maximum complexity of ${this.maxNodes} nodes`);
|
|
804
|
+
}
|
|
805
|
+
if (sel.kind === "fragment_spread") {
|
|
806
|
+
const frag = fragments.get(sel.name);
|
|
807
|
+
if (frag) walk(frag.selections, depth + 1);
|
|
808
|
+
} else if (sel.kind === "inline_fragment") {
|
|
809
|
+
walk(sel.selections, depth + 1);
|
|
810
|
+
} else if (sel.kind === "field" && sel.selections) {
|
|
811
|
+
walk(sel.selections, depth + 1);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
walk(selections, 1);
|
|
816
|
+
}
|
|
817
|
+
|
|
718
818
|
private async resolveSelectionsInto(
|
|
719
819
|
selections: ParsedSelection[],
|
|
720
820
|
resolvers: Map<string, QueryConfig>,
|
|
@@ -930,7 +1030,9 @@ export class GraphQL {
|
|
|
930
1030
|
className,
|
|
931
1031
|
(root, args) => {
|
|
932
1032
|
const db = getDb();
|
|
933
|
-
|
|
1033
|
+
// Declared fields only: the executor passes every argument the query
|
|
1034
|
+
// names, declared or not, and each one becomes a column (ADR-0069).
|
|
1035
|
+
const fieldNames = Object.keys(args).filter((k) => Object.hasOwn(mutationArgs, k));
|
|
934
1036
|
const placeholders = fieldNames.map(() => "?");
|
|
935
1037
|
const values = fieldNames.map((f) => args[f]);
|
|
936
1038
|
|
|
@@ -960,7 +1062,8 @@ export class GraphQL {
|
|
|
960
1062
|
const values: unknown[] = [];
|
|
961
1063
|
|
|
962
1064
|
for (const [k, v] of Object.entries(args)) {
|
|
963
|
-
|
|
1065
|
+
// Declared fields only (see create above).
|
|
1066
|
+
if (k !== "id" && Object.hasOwn(mutationArgs, k)) {
|
|
964
1067
|
setClauses.push(`"${k}" = ?`);
|
|
965
1068
|
values.push(v);
|
|
966
1069
|
}
|
|
@@ -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 */
|
|
@@ -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
|
* Programmatic HTML builder — avoids string concatenation.
|
|
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 i18n — Internationalization and localization, zero dependencies.
|
|
2
10
|
// Load JSON translation files, support nested keys, parameter substitution, locale fallback.
|
|
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
|
export type {
|
|
2
10
|
Tina4Request,
|
|
3
11
|
Tina4Response,
|
|
@@ -82,6 +90,7 @@ export type { ServiceOptions, ServiceContext, ServiceHandler, ServiceInfo } from
|
|
|
82
90
|
export { responseCache, clearCache, cacheStats, cacheGet, cacheSet, cacheDelete, cacheClear, cacheBackendStats, sweep, createBackend, _resetBackend } from "./cache.js";
|
|
83
91
|
export type { ResponseCacheConfig, CacheBackend } from "./cache.js";
|
|
84
92
|
export { Api, ApiStreamError, parseLineStream, parseSseStream } from "./api.js";
|
|
93
|
+
export { guardUrl, isBlockedAddress, allowPrivateRequests, SsrfError, ALLOW_PRIVATE_ENV } from "./ssrf.js";
|
|
85
94
|
export type { ApiResult, ApiOptions, ApiTransport, DownloadResult, UploadOptions, StreamOptions, SseEvent } from "./api.js";
|
|
86
95
|
export { Context, defaultContext, existingContext, fts5Supported, _sharedContexts } from "./context/index.js";
|
|
87
96
|
export type { SearchHit } from "./context/index.js";
|
package/packages/core/src/job.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 Queue Job — a single queue job with lifecycle methods.
|
|
3
11
|
*/
|
|
@@ -39,6 +47,7 @@ export type QueueJob = JobData & JobLifecycle;
|
|
|
39
47
|
|
|
40
48
|
export interface JobQueueBridge {
|
|
41
49
|
_failJob(topic: string, job: QueueJob, reason: string, maxRetries: number): void;
|
|
50
|
+
_rejectJob(topic: string, job: QueueJob, reason: string, maxRetries: number): void;
|
|
42
51
|
_retryJob(topic: string, job: QueueJob, delaySeconds?: number): void;
|
|
43
52
|
_completeJob(topic: string, job: QueueJob): void;
|
|
44
53
|
getMaxRetries(): number;
|
|
@@ -66,7 +75,13 @@ export function createJob(data: JobData, queue: JobQueueBridge): QueueJob {
|
|
|
66
75
|
queue._failJob(job.topic, job, reason, queue.getMaxRetries());
|
|
67
76
|
},
|
|
68
77
|
reject(reason = "") {
|
|
69
|
-
|
|
78
|
+
// Reject permanently — dead-letter NOW, no retry (ADR-0023). Distinct
|
|
79
|
+
// from fail(): fail() retries until maxRetries is spent; reject() is for
|
|
80
|
+
// a message the consumer KNOWS is poison and sends it straight to the
|
|
81
|
+
// dead-letter store. Was a literal alias for fail() before 3.13.139.
|
|
82
|
+
job.status = "dead";
|
|
83
|
+
job.error = reason;
|
|
84
|
+
queue._rejectJob(job.topic, job, reason, queue.getMaxRetries());
|
|
70
85
|
},
|
|
71
86
|
retry(delaySeconds?: number) {
|
|
72
87
|
queue._retryJob(job.topic, job, delaySeconds);
|
|
@@ -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 {
|
|
2
10
|
appendFileSync,
|
|
3
11
|
closeSync,
|
|
@@ -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
|
* Unit tests for the MCP (Model Context Protocol) implementation.
|
|
3
11
|
* Run with: npx tsx packages/core/src/mcp.test.ts
|
package/packages/core/src/mcp.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
|
// Tina4 MCP Server — Model Context Protocol for AI tool integration.
|
|
2
10
|
//
|
|
3
11
|
// Built-in MCP server for dev tools + developer API for custom MCP servers.
|
|
@@ -230,7 +238,9 @@ function envTruthy(val: string | undefined): boolean {
|
|
|
230
238
|
* Python master parity: tina4_python.mcp.is_loopback.
|
|
231
239
|
*/
|
|
232
240
|
export function isLoopback(ip: string | undefined | null): boolean {
|
|
233
|
-
|
|
241
|
+
// An empty or missing peer is UNKNOWN and is not loopback: a runtime path
|
|
242
|
+
// that lost its socket address must fail closed (ADR-0079 s4).
|
|
243
|
+
if (ip == null || ip === "") return false;
|
|
234
244
|
let addr = ip.trim().toLowerCase();
|
|
235
245
|
if (addr.startsWith("::ffff:")) addr = addr.slice(7);
|
|
236
246
|
return addr === "::1" || addr === "localhost" || addr.startsWith("127.");
|
|
@@ -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 Messenger — SMTP email client using Node.js built-in modules only.
|
|
3
11
|
*
|
|
@@ -32,15 +40,80 @@ import { isTruthy } from "./dotenv.js";
|
|
|
32
40
|
import { DevMailbox } from "./devMailbox.js";
|
|
33
41
|
import { Log } from "./logger.js";
|
|
34
42
|
|
|
43
|
+
// ── Transport security (ADR-0071) ───────────────────────────
|
|
44
|
+
//
|
|
45
|
+
// `encryption` means what it says. Port 465 is always implicit TLS; `ssl` is
|
|
46
|
+
// implicit TLS on ANY port; `tls` / `starttls` upgrade with STARTTLS and FAIL
|
|
47
|
+
// when the server does not offer it; `none` never upgrades. An unknown value
|
|
48
|
+
// is refused when the Messenger is built, so a typo never means cleartext.
|
|
49
|
+
//
|
|
50
|
+
// Every TLS connection verifies the certificate and the host name. There is
|
|
51
|
+
// no Tina4 switch to turn that off (TINA4_MAIL_TLS_INSECURE is withdrawn);
|
|
52
|
+
// `rejectUnauthorized: true` is passed explicitly so NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
53
|
+
// cannot turn it off either. Trust a private CA the standard Node way:
|
|
54
|
+
// NODE_EXTRA_CA_CERTS=/path/ca.pem.
|
|
55
|
+
|
|
56
|
+
const MAIL_ENCRYPTION_VALUES = ["ssl", "tls", "starttls", "none"];
|
|
57
|
+
|
|
58
|
+
/** Trim + lowercase an encryption setting, refusing anything that is not a known value. */
|
|
59
|
+
function normaliseEncryption(value: string, kind: "mail" | "IMAP"): string {
|
|
60
|
+
const normalised = value.trim().toLowerCase();
|
|
61
|
+
if (!MAIL_ENCRYPTION_VALUES.includes(normalised)) {
|
|
62
|
+
throw new Error(`Unknown ${kind} encryption '${value}'. Valid values: ${MAIL_ENCRYPTION_VALUES.join(", ")}.`);
|
|
63
|
+
}
|
|
64
|
+
return normalised;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type SmtpTransport = "implicit_tls" | "starttls" | "plain";
|
|
68
|
+
|
|
69
|
+
/** Matches Python's smtplib timeouts: 30 s for a send, 10 s for a connection check. */
|
|
70
|
+
const SMTP_SEND_TIMEOUT_MS = 30_000;
|
|
71
|
+
const SMTP_CHECK_TIMEOUT_MS = 10_000;
|
|
72
|
+
|
|
35
73
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* `rejectUnauthorized: false`, silently disabling certificate validation for
|
|
40
|
-
* every TLS connection (a man-in-the-middle risk).
|
|
74
|
+
* Give a socket an idle timeout that fails it with a clear error. Without one a
|
|
75
|
+
* client that waits for a greeting the server never sends (a plaintext client
|
|
76
|
+
* on a TLS-only port) hangs forever.
|
|
41
77
|
*/
|
|
42
|
-
function
|
|
43
|
-
|
|
78
|
+
function failOnIdle<T extends net.Socket>(socket: T, timeoutMs: number, what: string): T {
|
|
79
|
+
socket.setTimeout(timeoutMs, () => {
|
|
80
|
+
socket.destroy(new Error(`${what} timed out after ${timeoutMs / 1000}s`));
|
|
81
|
+
});
|
|
82
|
+
return socket;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Open a verified TLS connection (implicit TLS), or upgrade `socket` (STARTTLS). */
|
|
86
|
+
function connectVerifiedTls(host: string, port: number, socket?: net.Socket): Promise<tls.TLSSocket> {
|
|
87
|
+
const secureSocket = tls.connect({
|
|
88
|
+
host,
|
|
89
|
+
port,
|
|
90
|
+
socket,
|
|
91
|
+
servername: net.isIP(host) ? undefined : host,
|
|
92
|
+
rejectUnauthorized: true,
|
|
93
|
+
});
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
secureSocket.once("secureConnect", () => {
|
|
96
|
+
secureSocket.removeListener("error", reject);
|
|
97
|
+
resolve(secureSocket);
|
|
98
|
+
});
|
|
99
|
+
secureSocket.once("error", reject);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function connectPlain(host: string, port: number): Promise<net.Socket> {
|
|
104
|
+
const socket = net.createConnection({ host, port });
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
socket.once("connect", () => {
|
|
107
|
+
socket.removeListener("error", reject);
|
|
108
|
+
resolve(socket);
|
|
109
|
+
});
|
|
110
|
+
socket.once("error", reject);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Does this EHLO reply advertise the STARTTLS extension? */
|
|
115
|
+
function offersStarttls(ehloReply: string): boolean {
|
|
116
|
+
return /^\d{3}[ -]STARTTLS\b/im.test(ehloReply);
|
|
44
117
|
}
|
|
45
118
|
|
|
46
119
|
/**
|
|
@@ -384,7 +457,6 @@ export class Messenger {
|
|
|
384
457
|
private fromAddress: string;
|
|
385
458
|
private fromName: string;
|
|
386
459
|
private encryption: string;
|
|
387
|
-
private useTls: boolean;
|
|
388
460
|
/** Whether an SMTP host was actually configured (see the constructor). */
|
|
389
461
|
private smtpConfigured: boolean = false;
|
|
390
462
|
/** The local mailbox, present only when this messenger captures. */
|
|
@@ -421,17 +493,18 @@ export class Messenger {
|
|
|
421
493
|
?? process.env.TINA4_MAIL_FROM_NAME
|
|
422
494
|
?? "";
|
|
423
495
|
|
|
424
|
-
// Encryption: constructor > .env > backward-compat useTls > default "tls"
|
|
425
|
-
|
|
496
|
+
// Encryption: constructor > .env > backward-compat useTls > default "tls".
|
|
497
|
+
// A configured value is validated (ADR-0071): an unknown one, an empty one
|
|
498
|
+
// included, raises here rather than sending in cleartext later.
|
|
499
|
+
const configuredEncryption = options?.encryption
|
|
426
500
|
?? process.env.TINA4_MAIL_ENCRYPTION;
|
|
427
|
-
if (
|
|
428
|
-
this.encryption =
|
|
501
|
+
if (configuredEncryption !== undefined) {
|
|
502
|
+
this.encryption = normaliseEncryption(configuredEncryption, "mail");
|
|
429
503
|
} else if (options?.useTls !== undefined) {
|
|
430
504
|
this.encryption = options.useTls ? "tls" : "none";
|
|
431
505
|
} else {
|
|
432
506
|
this.encryption = "tls";
|
|
433
507
|
}
|
|
434
|
-
this.useTls = ["tls", "starttls"].includes(this.encryption);
|
|
435
508
|
|
|
436
509
|
this.imapHost = options?.imapHost
|
|
437
510
|
?? process.env.TINA4_MAIL_IMAP_HOST
|
|
@@ -448,10 +521,23 @@ export class Messenger {
|
|
|
448
521
|
// IMAP encryption — separate from SMTP encryption because IMAP almost
|
|
449
522
|
// always uses port 993 + implicit TLS while SMTP toggles between 587
|
|
450
523
|
// (STARTTLS) and 465 (implicit TLS). Default is "tls" to match
|
|
451
|
-
// industry-standard IMAPS port 993 behaviour.
|
|
452
|
-
|
|
524
|
+
// industry-standard IMAPS port 993 behaviour. "tls" and "ssl" are implicit
|
|
525
|
+
// TLS, "starttls" is a REQUIRED STARTTLS upgrade, "none" is plaintext; any
|
|
526
|
+
// other value raises here.
|
|
527
|
+
this.imapEncryption = normaliseEncryption(options?.imapEncryption
|
|
453
528
|
?? process.env.TINA4_MAIL_IMAP_ENCRYPTION
|
|
454
|
-
?? "tls")
|
|
529
|
+
?? "tls", "IMAP");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* How the SMTP connection is secured (ADR-0071 section 1). Port 465 is the
|
|
534
|
+
* implicit-TLS submission port whatever the setting; `ssl` is implicit TLS on
|
|
535
|
+
* any port; `tls` / `starttls` upgrade with a required STARTTLS; `none` is
|
|
536
|
+
* plaintext on purpose.
|
|
537
|
+
*/
|
|
538
|
+
private smtpTransport(): SmtpTransport {
|
|
539
|
+
if (this.port === 465 || this.encryption === "ssl") return "implicit_tls";
|
|
540
|
+
return this.encryption === "none" ? "plain" : "starttls";
|
|
455
541
|
}
|
|
456
542
|
|
|
457
543
|
/**
|
|
@@ -507,21 +593,12 @@ export class Messenger {
|
|
|
507
593
|
return { toList, ccList, bccList, allRecipients };
|
|
508
594
|
}
|
|
509
595
|
|
|
510
|
-
private async connectSmtpSocket(): Promise<SmtpSocket> {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
await
|
|
514
|
-
socket.once("secureConnect", resolve);
|
|
515
|
-
socket.once("error", reject);
|
|
516
|
-
});
|
|
517
|
-
return socket;
|
|
596
|
+
private async connectSmtpSocket(timeoutMs: number): Promise<SmtpSocket> {
|
|
597
|
+
const what = `SMTP connection to ${this.host}:${this.port}`;
|
|
598
|
+
if (this.smtpTransport() === "implicit_tls") {
|
|
599
|
+
return failOnIdle(await connectVerifiedTls(this.host, this.port), timeoutMs, what);
|
|
518
600
|
}
|
|
519
|
-
|
|
520
|
-
await new Promise<void>((resolve, reject) => {
|
|
521
|
-
socket.once("connect", resolve);
|
|
522
|
-
socket.once("error", reject);
|
|
523
|
-
});
|
|
524
|
-
return socket;
|
|
601
|
+
return failOnIdle(await connectPlain(this.host, this.port), timeoutMs, what);
|
|
525
602
|
}
|
|
526
603
|
|
|
527
604
|
private async requireSmtpResponse(socket: SmtpSocket, command: string, expected: number, failure: string): Promise<{ code: number; text: string }> {
|
|
@@ -530,20 +607,23 @@ export class Messenger {
|
|
|
530
607
|
return response;
|
|
531
608
|
}
|
|
532
609
|
|
|
533
|
-
|
|
534
|
-
|
|
610
|
+
/**
|
|
611
|
+
* Connect, read the greeting, EHLO, and - for the STARTTLS transport - upgrade.
|
|
612
|
+
* STARTTLS is REQUIRED: when the server does not offer it the session fails
|
|
613
|
+
* here, before AUTH or MAIL FROM is ever sent.
|
|
614
|
+
*/
|
|
615
|
+
private async openSmtpSession(timeoutMs: number = SMTP_SEND_TIMEOUT_MS): Promise<SmtpSocket> {
|
|
616
|
+
let socket = await this.connectSmtpSocket(timeoutMs);
|
|
535
617
|
try {
|
|
536
618
|
await this.requireSmtpResponse(socket, "", 220, "SMTP greeting failed");
|
|
537
619
|
const ehlo = await this.requireSmtpResponse(socket, `EHLO ${this.host}`, 250, "EHLO failed");
|
|
538
|
-
if (this.
|
|
620
|
+
if (this.smtpTransport() === "starttls") {
|
|
621
|
+
if (!offersStarttls(ehlo.text)) {
|
|
622
|
+
throw new SmtpCommandError(`STARTTLS was requested but ${this.host}:${this.port} does not offer it`);
|
|
623
|
+
}
|
|
539
624
|
await this.requireSmtpResponse(socket, "STARTTLS", 220, "STARTTLS failed");
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
await new Promise<void>((resolve, reject) => {
|
|
543
|
-
secureSocket.once("secureConnect", resolve);
|
|
544
|
-
secureSocket.once("error", reject);
|
|
545
|
-
});
|
|
546
|
-
socket = secureSocket;
|
|
625
|
+
socket = failOnIdle(await connectVerifiedTls(this.host, this.port, socket as net.Socket), timeoutMs,
|
|
626
|
+
`SMTP connection to ${this.host}:${this.port}`);
|
|
547
627
|
await this.requireSmtpResponse(socket, `EHLO ${this.host}`, 250, "EHLO after STARTTLS failed");
|
|
548
628
|
}
|
|
549
629
|
return socket;
|
|
@@ -679,42 +759,19 @@ export class Messenger {
|
|
|
679
759
|
* Test the SMTP connection without sending an email.
|
|
680
760
|
*/
|
|
681
761
|
async testConnection(): Promise<{ success: boolean; message: string }> {
|
|
762
|
+
// Same transport rules as send(): implicit TLS / required STARTTLS / plain,
|
|
763
|
+
// certificates verified.
|
|
764
|
+
let socket: SmtpSocket | null = null;
|
|
682
765
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (this.port === 465) {
|
|
686
|
-
socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
687
|
-
await new Promise<void>((resolve, reject) => {
|
|
688
|
-
socket.once("secureConnect", resolve);
|
|
689
|
-
socket.once("error", reject);
|
|
690
|
-
});
|
|
691
|
-
} else {
|
|
692
|
-
socket = net.createConnection({ host: this.host, port: this.port });
|
|
693
|
-
await new Promise<void>((resolve, reject) => {
|
|
694
|
-
socket.once("connect", resolve);
|
|
695
|
-
socket.once("error", reject);
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const greeting = await readResponse(socket);
|
|
700
|
-
if (greeting.code !== 220) {
|
|
701
|
-
socket.destroy();
|
|
702
|
-
return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
|
|
706
|
-
if (ehlo.code !== 250) {
|
|
707
|
-
socket.destroy();
|
|
708
|
-
return { success: false, message: `EHLO failed: ${ehlo.text}` };
|
|
709
|
-
}
|
|
710
|
-
|
|
766
|
+
socket = await this.openSmtpSession(SMTP_CHECK_TIMEOUT_MS);
|
|
711
767
|
await sendCommand(socket, "QUIT");
|
|
712
|
-
socket.destroy();
|
|
713
|
-
|
|
714
768
|
return { success: true, message: `Connected to ${this.host}:${this.port}` };
|
|
715
769
|
} catch (err) {
|
|
770
|
+
if (err instanceof SmtpCommandError) return { success: false, message: err.message };
|
|
716
771
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
717
772
|
return { success: false, message: `Connection failed: ${errMsg}` };
|
|
773
|
+
} finally {
|
|
774
|
+
socket?.destroy();
|
|
718
775
|
}
|
|
719
776
|
}
|
|
720
777
|
|
|
@@ -731,28 +788,28 @@ export class Messenger {
|
|
|
731
788
|
|
|
732
789
|
let socket: net.Socket | tls.TLSSocket;
|
|
733
790
|
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (useTls) {
|
|
740
|
-
socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
|
|
741
|
-
await new Promise<void>((resolve, reject) => {
|
|
742
|
-
socket.once("secureConnect", resolve);
|
|
743
|
-
socket.once("error", reject);
|
|
744
|
-
});
|
|
791
|
+
// "tls" / "ssl" → implicit TLS; "starttls" → plain connect, then a REQUIRED
|
|
792
|
+
// STARTTLS upgrade before LOGIN; "none" → plaintext. (Validated in the constructor.)
|
|
793
|
+
if (this.imapEncryption === "tls" || this.imapEncryption === "ssl") {
|
|
794
|
+
socket = await connectVerifiedTls(this.imapHost, this.imapPort);
|
|
745
795
|
} else {
|
|
746
|
-
socket =
|
|
747
|
-
await new Promise<void>((resolve, reject) => {
|
|
748
|
-
socket.once("connect", resolve);
|
|
749
|
-
socket.once("error", reject);
|
|
750
|
-
});
|
|
796
|
+
socket = await connectPlain(this.imapHost, this.imapPort);
|
|
751
797
|
}
|
|
752
798
|
|
|
753
799
|
// Read server greeting
|
|
754
800
|
await imapReadLine(socket);
|
|
755
801
|
|
|
802
|
+
if (this.imapEncryption === "starttls") {
|
|
803
|
+
try {
|
|
804
|
+
await imapCommand(socket, "STARTTLS");
|
|
805
|
+
} catch {
|
|
806
|
+
socket.destroy();
|
|
807
|
+
throw new MessengerConnectionError(
|
|
808
|
+
`STARTTLS was requested but ${this.imapHost}:${this.imapPort} does not offer it`);
|
|
809
|
+
}
|
|
810
|
+
socket = await connectVerifiedTls(this.imapHost, this.imapPort, socket);
|
|
811
|
+
}
|
|
812
|
+
|
|
756
813
|
// Login
|
|
757
814
|
if (this.imapUser && this.imapPass) {
|
|
758
815
|
const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
|