tina4-nodejs 3.13.91 → 3.13.94

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.
Files changed (135) hide show
  1. package/CLAUDE.md +16 -3
  2. package/README.md +1 -1
  3. package/package.json +12 -9
  4. package/packages/cli/dist/bin.js +1312 -987
  5. package/packages/core/dist/index.js +1312 -987
  6. package/packages/core/src/auth.ts +141 -21
  7. package/packages/core/src/devMailbox.ts +20 -44
  8. package/packages/core/src/index.ts +2 -2
  9. package/packages/core/src/messenger.ts +72 -0
  10. package/packages/core/src/queueBackends/kafkaBackend.ts +108 -12
  11. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  12. package/packages/core/src/sessionHandlers/mongoClient.ts +9 -3
  13. package/packages/core/src/sessionHandlers/redisHandler.ts +18 -5
  14. package/packages/core/src/sessionHandlers/respClient.ts +5 -1
  15. package/packages/frond/dist/index.js +74 -31
  16. package/packages/frond/src/engine.ts +99 -33
  17. package/packages/orm/dist/index.js +3102 -2777
  18. package/packages/orm/src/adapters/sqlite.ts +4 -1
  19. package/packages/orm/src/database.ts +108 -8
  20. package/types/cli/src/bin.d.ts +92 -0
  21. package/types/cli/src/commands/build.d.ts +2 -0
  22. package/types/cli/src/commands/generate.d.ts +47 -0
  23. package/types/cli/src/commands/init.d.ts +1 -0
  24. package/types/cli/src/commands/metrics.d.ts +6 -0
  25. package/types/cli/src/commands/migrate.d.ts +1 -0
  26. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  27. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  28. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  29. package/types/cli/src/commands/queue.d.ts +20 -0
  30. package/types/cli/src/commands/routes.d.ts +1 -0
  31. package/types/cli/src/commands/seed.d.ts +1 -0
  32. package/types/cli/src/commands/serve.d.ts +6 -0
  33. package/types/cli/src/commands/test.d.ts +1 -0
  34. package/types/core/src/ai.d.ts +64 -0
  35. package/types/core/src/api.d.ts +262 -0
  36. package/types/core/src/auth.d.ts +154 -0
  37. package/types/core/src/authGate.d.ts +20 -0
  38. package/types/core/src/background.d.ts +34 -0
  39. package/types/core/src/cache.d.ts +160 -0
  40. package/types/core/src/constants.d.ts +38 -0
  41. package/types/core/src/container.d.ts +44 -0
  42. package/types/core/src/context/chunker.d.ts +31 -0
  43. package/types/core/src/context/index.d.ts +93 -0
  44. package/types/core/src/devAdmin.d.ts +179 -0
  45. package/types/core/src/devMailbox.d.ts +54 -0
  46. package/types/core/src/docs.d.ts +141 -0
  47. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  48. package/types/core/src/dotenv.d.ts +65 -0
  49. package/types/core/src/env.d.ts +28 -0
  50. package/types/core/src/errorOverlay.d.ts +36 -0
  51. package/types/core/src/events.d.ts +75 -0
  52. package/types/core/src/fakeData.d.ts +55 -0
  53. package/types/core/src/feedback.d.ts +90 -0
  54. package/types/core/src/graphql.d.ts +207 -0
  55. package/types/core/src/health.d.ts +22 -0
  56. package/types/core/src/htmlElement.d.ts +75 -0
  57. package/types/core/src/i18n.d.ts +37 -0
  58. package/types/core/src/index.d.ts +93 -0
  59. package/types/core/src/job.d.ts +39 -0
  60. package/types/core/src/logger.d.ts +123 -0
  61. package/types/core/src/mcp.d.ts +248 -0
  62. package/types/core/src/messenger.d.ts +191 -0
  63. package/types/core/src/metrics.d.ts +77 -0
  64. package/types/core/src/middleware.d.ts +207 -0
  65. package/types/core/src/mqtt.d.ts +257 -0
  66. package/types/core/src/mqttMessage.d.ts +67 -0
  67. package/types/core/src/plan.d.ts +96 -0
  68. package/types/core/src/projectIndex.d.ts +56 -0
  69. package/types/core/src/queue.d.ts +219 -0
  70. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  71. package/types/core/src/queueBackends/liteBackend.d.ts +119 -0
  72. package/types/core/src/queueBackends/mongoBackend.d.ts +97 -0
  73. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  74. package/types/core/src/rateLimiter.d.ts +49 -0
  75. package/types/core/src/request.d.ts +25 -0
  76. package/types/core/src/response.d.ts +28 -0
  77. package/types/core/src/routeDiscovery.d.ts +12 -0
  78. package/types/core/src/router.d.ts +355 -0
  79. package/types/core/src/scss.d.ts +19 -0
  80. package/types/core/src/server.d.ts +131 -0
  81. package/types/core/src/service.d.ts +115 -0
  82. package/types/core/src/session.d.ts +256 -0
  83. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  84. package/types/core/src/sessionHandlers/databaseHandler.d.ts +42 -0
  85. package/types/core/src/sessionHandlers/mongoClient.d.ts +24 -0
  86. package/types/core/src/sessionHandlers/mongoHandler.d.ts +61 -0
  87. package/types/core/src/sessionHandlers/redisHandler.d.ts +60 -0
  88. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  89. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  90. package/types/core/src/static.d.ts +2 -0
  91. package/types/core/src/test.d.ts +94 -0
  92. package/types/core/src/testClient.d.ts +36 -0
  93. package/types/core/src/testing.d.ts +58 -0
  94. package/types/core/src/types.d.ts +219 -0
  95. package/types/core/src/validator.d.ts +52 -0
  96. package/types/core/src/websocket.d.ts +376 -0
  97. package/types/core/src/websocketBackplane.d.ts +166 -0
  98. package/types/core/src/websocketConnection.d.ts +54 -0
  99. package/types/core/src/wsdl.d.ts +101 -0
  100. package/types/frond/src/engine.d.ts +263 -0
  101. package/types/frond/src/index.d.ts +2 -0
  102. package/types/orm/src/adapters/firebird.d.ts +138 -0
  103. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  104. package/types/orm/src/adapters/mssql.d.ts +70 -0
  105. package/types/orm/src/adapters/mysql.d.ts +66 -0
  106. package/types/orm/src/adapters/odbc.d.ts +97 -0
  107. package/types/orm/src/adapters/postgres.d.ts +85 -0
  108. package/types/orm/src/adapters/sqlite.d.ts +56 -0
  109. package/types/orm/src/autoCrud.d.ts +73 -0
  110. package/types/orm/src/baseModel.d.ts +391 -0
  111. package/types/orm/src/cachedDatabase.d.ts +177 -0
  112. package/types/orm/src/database.d.ts +609 -0
  113. package/types/orm/src/databaseResult.d.ts +85 -0
  114. package/types/orm/src/docstore.d.ts +182 -0
  115. package/types/orm/src/fakeData.d.ts +22 -0
  116. package/types/orm/src/index.d.ts +40 -0
  117. package/types/orm/src/migration.d.ts +275 -0
  118. package/types/orm/src/model.d.ts +7 -0
  119. package/types/orm/src/query.d.ts +14 -0
  120. package/types/orm/src/queryBuilder.d.ts +173 -0
  121. package/types/orm/src/realtime/index.d.ts +7 -0
  122. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  123. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  124. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  125. package/types/orm/src/realtime/models/message.d.ts +36 -0
  126. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  127. package/types/orm/src/realtime/realtime.d.ts +24 -0
  128. package/types/orm/src/realtime/storage.d.ts +61 -0
  129. package/types/orm/src/seeder.d.ts +118 -0
  130. package/types/orm/src/sqlTranslator.d.ts +134 -0
  131. package/types/orm/src/types.d.ts +138 -0
  132. package/types/orm/src/validation.d.ts +6 -0
  133. package/types/swagger/src/generator.d.ts +46 -0
  134. package/types/swagger/src/index.d.ts +2 -0
  135. package/types/swagger/src/ui.d.ts +11 -0
@@ -1298,11 +1298,14 @@ var init_sqlite = __esm({
1298
1298
  const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
1299
1299
  const rows = this.db.prepare(pragma).all();
1300
1300
  return rows.map((r) => ({
1301
+ // PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
1302
+ // key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
1303
+ // reported only the first column of a composite key.
1301
1304
  name: r.name,
1302
1305
  type: r.type,
1303
1306
  nullable: r.notnull === 0,
1304
1307
  default: r.dflt_value,
1305
- primaryKey: r.pk === 1
1308
+ primaryKey: Number(r.pk) > 0
1306
1309
  }));
1307
1310
  }
1308
1311
  lastInsertId() {
@@ -4298,6 +4301,8 @@ var init_database = __esm({
4298
4301
  poolIndex = 0;
4299
4302
  /** Factory for creating new adapters (used by pool) */
4300
4303
  adapterFactory = null;
4304
+ /** table -> primary-key column name (or null), introspected once */
4305
+ _pkCache = /* @__PURE__ */ new Map();
4301
4306
  /**
4302
4307
  * Whether a standalone write auto-commits. ON by default — a write made
4303
4308
  * outside an explicit transaction commits on its own connection before
@@ -4556,29 +4561,112 @@ var init_database = __esm({
4556
4561
  }
4557
4562
  return result;
4558
4563
  }
4559
- /** Update rows in a table matching filter. */
4564
+ /**
4565
+ * The table's primary-key column, introspected once and cached.
4566
+ *
4567
+ * Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
4568
+ * primaryKey per column on every adapter. Resolves to null when the table has
4569
+ * no primary key or cannot be introspected.
4570
+ */
4571
+ async primaryKey(table2) {
4572
+ if (!this._pkCache.has(table2)) {
4573
+ let pk = [];
4574
+ try {
4575
+ const columns = await this.getColumns(table2);
4576
+ pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
4577
+ } catch {
4578
+ pk = [];
4579
+ }
4580
+ this._pkCache.set(table2, pk);
4581
+ }
4582
+ return this._pkCache.get(table2) ?? [];
4583
+ }
4584
+ /**
4585
+ * A failed write must be loud.
4586
+ *
4587
+ * The adapters catch a SQL error and return { success: false, affectedRows: 0 },
4588
+ * so a filterless update produced invalid SQL ("... WHERE ") and reported
4589
+ * nothing rather than raising. A caller who does not inspect the result
4590
+ * believes the write landed (audit feature 4, P1).
4591
+ */
4592
+ static assertWrote(result, verb, table2) {
4593
+ if (result && result.success === false) {
4594
+ throw new Error(
4595
+ `${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
4596
+ );
4597
+ }
4598
+ return result;
4599
+ }
4600
+ /**
4601
+ * Update rows. A write with no filter is an error, not a full-table write.
4602
+ *
4603
+ * With no explicit filter the primary key is taken out of `data` and used as
4604
+ * the WHERE clause. With neither a filter nor a primary key in `data` this
4605
+ * throws rather than silently changing nothing (audit feature 4, P1).
4606
+ */
4560
4607
  async update(table2, data, filter, params) {
4608
+ let effectiveFilter = filter ?? {};
4609
+ let effectiveData = data;
4610
+ if (Object.keys(effectiveFilter).length === 0) {
4611
+ const pkColumns = await this.primaryKey(table2);
4612
+ const missing = pkColumns.filter((c) => !(c in data));
4613
+ if (pkColumns.length === 0 || missing.length > 0) {
4614
+ throw new Error(
4615
+ `update requires a filter or the complete primary key in the data; pass filter explicitly to update multiple rows (table=${table2}, primary key=[${pkColumns.join(", ")}], missing from data=[${missing.join(", ")}]). To empty a table use truncate(${table2}).`
4616
+ );
4617
+ }
4618
+ effectiveData = { ...data };
4619
+ const keyed = {};
4620
+ for (const col of pkColumns) {
4621
+ keyed[col] = effectiveData[col];
4622
+ delete effectiveData[col];
4623
+ }
4624
+ if (Object.keys(effectiveData).length === 0) {
4625
+ throw new Error(
4626
+ `update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
4627
+ );
4628
+ }
4629
+ effectiveFilter = keyed;
4630
+ }
4561
4631
  const adapter = this.getNextAdapter();
4562
- const result = adapter.updateAsync ? await adapter.updateAsync(table2, data, filter ?? {}, params) : adapter.update(table2, data, filter ?? {}, params);
4632
+ const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
4563
4633
  if (this.autoCommit && !this.inExplicitTransaction()) {
4564
4634
  try {
4565
4635
  await adapterCommit(adapter);
4566
4636
  } catch {
4567
4637
  }
4568
4638
  }
4569
- return result;
4639
+ return _Database.assertWrote(result, "update", table2);
4570
4640
  }
4571
- /** Delete rows from a table matching filter. */
4641
+ /** Delete rows. A filterless delete throws; use truncate() to empty a table. */
4572
4642
  async delete(table2, filter, params) {
4643
+ const effectiveFilter = filter ?? {};
4644
+ if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
4645
+ throw new Error(
4646
+ `delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
4647
+ );
4648
+ }
4573
4649
  const adapter = this.getNextAdapter();
4574
- const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, filter ?? {}, params) : adapter.delete(table2, filter ?? {}, params);
4650
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
4575
4651
  if (this.autoCommit && !this.inExplicitTransaction()) {
4576
4652
  try {
4577
4653
  await adapterCommit(adapter);
4578
4654
  } catch {
4579
4655
  }
4580
4656
  }
4581
- return result;
4657
+ return _Database.assertWrote(result, "delete", table2);
4658
+ }
4659
+ /** Remove every row. The explicit spelling of a whole-table delete. */
4660
+ async truncate(table2) {
4661
+ const adapter = this.getNextAdapter();
4662
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
4663
+ if (this.autoCommit && !this.inExplicitTransaction()) {
4664
+ try {
4665
+ await adapterCommit(adapter);
4666
+ } catch {
4667
+ }
4668
+ }
4669
+ return _Database.assertWrote(result, "truncate", table2);
4582
4670
  }
4583
4671
  /** Close all database connections (pool or single). */
4584
4672
  close() {
@@ -12122,6 +12210,7 @@ var init_logger = __esm({
12122
12210
  var auth_exports = {};
12123
12211
  __export(auth_exports, {
12124
12212
  Auth: () => Auth,
12213
+ JWT_LEEWAY_SECONDS: () => JWT_LEEWAY_SECONDS,
12125
12214
  authMiddleware: () => authMiddleware,
12126
12215
  authenticateRequest: () => authenticateRequest,
12127
12216
  checkPassword: () => checkPassword,
@@ -12130,6 +12219,7 @@ __export(auth_exports, {
12130
12219
  getToken: () => getToken,
12131
12220
  hashPassword: () => hashPassword,
12132
12221
  refreshToken: () => refreshToken,
12222
+ resolveAlgorithm: () => resolveAlgorithm,
12133
12223
  validToken: () => validToken,
12134
12224
  validateApiKey: () => validateApiKey
12135
12225
  });
@@ -12190,6 +12280,18 @@ function ensureDevSecret(cwd) {
12190
12280
  }
12191
12281
  return newSecret;
12192
12282
  }
12283
+ function unsupportedAlgorithmError(algorithm) {
12284
+ return new Error(
12285
+ `Unsupported JWT algorithm "${algorithm}". Tina4 signs with ${SUPPORTED_ALGORITHMS.join(", ")} (HMAC via node:crypto; RS256 needs a PEM key pair). Set TINA4_JWT_ALGORITHM to one of those.`
12286
+ );
12287
+ }
12288
+ function resolveAlgorithm(algorithm) {
12289
+ const chosen = (algorithm || process.env.TINA4_JWT_ALGORITHM || "HS256").trim();
12290
+ if (!HMAC_DIGESTS.has(chosen) && !RSA_SIGN_ALGORITHMS.has(chosen)) {
12291
+ throw unsupportedAlgorithmError(chosen);
12292
+ }
12293
+ return chosen;
12294
+ }
12193
12295
  function base64urlEncode(data) {
12194
12296
  return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
12195
12297
  }
@@ -12212,7 +12314,7 @@ function getToken(payload, secretOrExpiresIn, expiresIn = 60, algorithm) {
12212
12314
  if (!resolvedSecret) {
12213
12315
  _warnBlankSecret();
12214
12316
  }
12215
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12317
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12216
12318
  const header = { alg: resolvedAlgorithm, typ: "JWT" };
12217
12319
  const now = Math.floor(Date.now() / 1e3);
12218
12320
  const claims = { ...payload, iat: now };
@@ -12230,19 +12332,27 @@ function validToken(token, secret, algorithm) {
12230
12332
  if (!resolvedSecret) {
12231
12333
  _warnBlankSecret();
12232
12334
  }
12233
- const resolvedAlgorithm = algorithm ?? process.env.TINA4_JWT_ALGORITHM ?? "HS256";
12335
+ const resolvedAlgorithm = resolveAlgorithm(algorithm);
12234
12336
  try {
12235
12337
  const parts = token.split(".");
12236
12338
  if (parts.length !== 3) return null;
12237
12339
  const [h, p, sig] = parts;
12340
+ const header = JSON.parse(base64urlDecode(h).toString());
12341
+ if (header.alg !== resolvedAlgorithm) return null;
12238
12342
  const signingInput = `${h}.${p}`;
12239
12343
  if (!verifySignature(signingInput, sig, resolvedSecret, resolvedAlgorithm)) {
12240
12344
  return null;
12241
12345
  }
12242
12346
  const payload = JSON.parse(base64urlDecode(p).toString());
12243
- if (typeof payload.exp === "number" && Date.now() / 1e3 > payload.exp) {
12347
+ const now = Date.now() / 1e3;
12348
+ if (typeof payload.exp === "number" && now > payload.exp) {
12244
12349
  return null;
12245
12350
  }
12351
+ if (Object.hasOwn(payload, "nbf")) {
12352
+ const notBefore = payload.nbf;
12353
+ if (typeof notBefore !== "number" || !Number.isFinite(notBefore)) return null;
12354
+ if (now + JWT_LEEWAY_SECONDS < notBefore) return null;
12355
+ }
12246
12356
  return payload;
12247
12357
  } catch {
12248
12358
  return null;
@@ -12258,32 +12368,33 @@ function getPayload(token) {
12258
12368
  }
12259
12369
  }
12260
12370
  function sign(input, secret, algorithm) {
12261
- if (algorithm === "HS256") {
12262
- const sig = createHmac2("sha256", secret).update(input).digest();
12263
- return base64urlEncode(sig);
12371
+ const digest = HMAC_DIGESTS.get(algorithm);
12372
+ if (digest) {
12373
+ return base64urlEncode(createHmac2(digest, secret).update(input).digest());
12264
12374
  }
12265
- if (algorithm === "RS256") {
12266
- const signer = createSign("RSA-SHA256");
12375
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12376
+ if (rsaAlgorithm) {
12377
+ const signer = createSign(rsaAlgorithm);
12267
12378
  signer.update(input);
12268
- const sig = signer.sign(secret);
12269
- return base64urlEncode(sig);
12379
+ return base64urlEncode(signer.sign(secret));
12270
12380
  }
12271
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12381
+ throw unsupportedAlgorithmError(algorithm);
12272
12382
  }
12273
12383
  function verifySignature(input, sig, secret, algorithm) {
12274
- if (algorithm === "HS256") {
12384
+ if (HMAC_DIGESTS.has(algorithm)) {
12275
12385
  const expected = sign(input, secret, algorithm);
12276
12386
  const a = Buffer.from(sig);
12277
12387
  const b = Buffer.from(expected);
12278
12388
  if (a.length !== b.length) return false;
12279
12389
  return timingSafeEqual(a, b);
12280
12390
  }
12281
- if (algorithm === "RS256") {
12282
- const verifier = createVerify("RSA-SHA256");
12391
+ const rsaAlgorithm = RSA_SIGN_ALGORITHMS.get(algorithm);
12392
+ if (rsaAlgorithm) {
12393
+ const verifier = createVerify(rsaAlgorithm);
12283
12394
  verifier.update(input);
12284
12395
  return verifier.verify(secret, base64urlDecode(sig));
12285
12396
  }
12286
- throw new Error(`Unsupported algorithm: ${algorithm}`);
12397
+ throw unsupportedAlgorithmError(algorithm);
12287
12398
  }
12288
12399
  function hashPassword(password, salt, iterations = 26e4) {
12289
12400
  const actualSalt = salt ?? randomBytes3(16).toString("hex");
@@ -12308,7 +12419,7 @@ function checkPassword(password, hash) {
12308
12419
  return false;
12309
12420
  }
12310
12421
  }
12311
- function authMiddleware(secret, algorithm = "HS256") {
12422
+ function authMiddleware(secret, algorithm) {
12312
12423
  return (req2, res, next) => {
12313
12424
  const authHeader = req2.headers.authorization ?? "";
12314
12425
  if (!authHeader.startsWith("Bearer ")) {
@@ -12331,11 +12442,11 @@ function refreshToken(token, expiresIn = 60) {
12331
12442
  const { iat: _iat, exp: _exp, ...claims } = payload;
12332
12443
  return getToken(claims, expiresIn);
12333
12444
  }
12334
- function authenticateRequest(headers, secret, algorithm = "HS256") {
12445
+ function authenticateRequest(headers, secret, algorithm) {
12335
12446
  const authHeader = headers.authorization ?? headers.Authorization ?? "";
12336
12447
  if (!authHeader.startsWith("Bearer ")) return null;
12337
12448
  const token = authHeader.slice(7);
12338
- if (validToken(token)) return getPayload(token);
12449
+ if (validToken(token, secret, algorithm)) return getPayload(token);
12339
12450
  if (validateApiKey(token)) {
12340
12451
  return { _auth: "api_key" };
12341
12452
  }
@@ -12349,12 +12460,23 @@ function validateApiKey(provided, expected) {
12349
12460
  if (a.length !== b.length) return false;
12350
12461
  return timingSafeEqual(a, b);
12351
12462
  }
12352
- var BLANK_SECRET_WARNING, Auth;
12463
+ var BLANK_SECRET_WARNING, HMAC_DIGESTS, RSA_SIGN_ALGORITHMS, SUPPORTED_ALGORITHMS, JWT_LEEWAY_SECONDS, Auth;
12353
12464
  var init_auth = __esm({
12354
12465
  "../core/src/auth.ts"() {
12355
12466
  "use strict";
12356
12467
  init_dotenv();
12357
12468
  BLANK_SECRET_WARNING = "Auth: TINA4_SECRET is not set \u2014 JWT signing is insecure. Set TINA4_SECRET to a random value (e.g. `openssl rand -hex 32`) in your environment or .env before serving traffic. For LOCAL DEV, set TINA4_DEBUG=true and a per-machine secret is generated automatically into .env.local (gitignored). Seeing this warning means the run was NOT detected as dev \u2014 typically a container or CI without TINA4_DEBUG set, or TINA4_ENV=production.";
12469
+ HMAC_DIGESTS = /* @__PURE__ */ new Map([
12470
+ ["HS256", "sha256"],
12471
+ ["HS384", "sha384"],
12472
+ ["HS512", "sha512"]
12473
+ ]);
12474
+ RSA_SIGN_ALGORITHMS = /* @__PURE__ */ new Map([["RS256", "RSA-SHA256"]]);
12475
+ SUPPORTED_ALGORITHMS = [
12476
+ ...HMAC_DIGESTS.keys(),
12477
+ ...RSA_SIGN_ALGORITHMS.keys()
12478
+ ];
12479
+ JWT_LEEWAY_SECONDS = 60;
12358
12480
  Auth = class {
12359
12481
  static getToken = getToken;
12360
12482
  static validToken = validToken;
@@ -13793,7 +13915,7 @@ function _generateFormToken(descriptor = "") {
13793
13915
  function _generateFormTokenValue(descriptor = "") {
13794
13916
  return new SafeString(_buildFormTokenJwt(descriptor));
13795
13917
  }
13796
- var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13918
+ var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13797
13919
  var init_engine = __esm({
13798
13920
  "../frond/src/engine.ts"() {
13799
13921
  "use strict";
@@ -13836,6 +13958,29 @@ var init_engine = __esm({
13836
13958
  "endset",
13837
13959
  "endspaceless"
13838
13960
  ]);
13961
+ GATEABLE_TAGS = /* @__PURE__ */ new Set([
13962
+ "autoescape",
13963
+ "cache",
13964
+ "for",
13965
+ "from",
13966
+ "if",
13967
+ "import",
13968
+ "include",
13969
+ "live",
13970
+ "macro",
13971
+ "set",
13972
+ "spaceless"
13973
+ ]);
13974
+ BLOCK_TAG_ENDS = {
13975
+ autoescape: "endautoescape",
13976
+ cache: "endcache",
13977
+ for: "endfor",
13978
+ if: "endif",
13979
+ live: "endlive",
13980
+ macro: "endmacro",
13981
+ set: "endset",
13982
+ spaceless: "endspaceless"
13983
+ };
13839
13984
  JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
13840
13985
  JSON_UNSAFE_MAP = {
13841
13986
  "<": "\\u003c",
@@ -14471,42 +14616,27 @@ var init_engine = __esm({
14471
14616
  if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
14472
14617
  tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
14473
14618
  }
14474
- if (tag === "if") {
14475
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("if")) {
14476
- const skip = this.skipBlock(tokens, i, "if", "endif");
14477
- i = skip;
14478
- } else {
14479
- const [result, skip] = this.handleIf(tokens, i, context);
14480
- output.push(result);
14481
- i = skip;
14482
- }
14619
+ if (!this.tagPermitted(tag)) {
14620
+ i = this.skipDeniedTag(tokens, i, tag, content);
14621
+ } else if (tag === "if") {
14622
+ const [result, skip] = this.handleIf(tokens, i, context);
14623
+ output.push(result);
14624
+ i = skip;
14483
14625
  } else if (tag === "for") {
14484
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("for")) {
14485
- const skip = this.skipBlock(tokens, i, "for", "endfor");
14486
- i = skip;
14487
- } else {
14488
- const [result, skip] = this.handleFor(tokens, i, context);
14489
- output.push(result);
14490
- i = skip;
14491
- }
14626
+ const [result, skip] = this.handleFor(tokens, i, context);
14627
+ output.push(result);
14628
+ i = skip;
14492
14629
  } else if (tag === "set") {
14493
- const isBlockSet = !content.includes("=");
14494
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
14495
- i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
14496
- } else if (isBlockSet) {
14630
+ if (!content.includes("=")) {
14497
14631
  i = this.handleSetBlock(tokens, i, context);
14498
14632
  } else {
14499
14633
  this.handleSet(content, context);
14500
14634
  i++;
14501
14635
  }
14502
14636
  } else if (tag === "include") {
14503
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("include")) {
14504
- i++;
14505
- } else {
14506
- const result = this.handleInclude(content, context);
14507
- output.push(result);
14508
- i++;
14509
- }
14637
+ const result = this.handleInclude(content, context);
14638
+ output.push(result);
14639
+ i++;
14510
14640
  } else if (tag === "macro") {
14511
14641
  const skip = this.handleMacro(tokens, i, context);
14512
14642
  i = skip;
@@ -14551,6 +14681,41 @@ var init_engine = __esm({
14551
14681
  }
14552
14682
  return output.join("");
14553
14683
  }
14684
+ /**
14685
+ * May this filter RUN under the current sandbox?
14686
+ *
14687
+ * The escaping decision has to ask this rather than read the filter name out of
14688
+ * the source. Node carries safety as a FLAG rather than as a value-level marker
14689
+ * (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
14690
+ * produced only by actually running the filter), so here the name alone was
14691
+ * enough to suppress auto-escaping even when the filter was denied and skipped.
14692
+ */
14693
+ filterPermitted(name) {
14694
+ if (!this._sandbox || this._allowedFilters === null) return true;
14695
+ return this._allowedFilters.has(name);
14696
+ }
14697
+ /**
14698
+ * May this tag run under the current sandbox?
14699
+ *
14700
+ * One gate for every tag, so the allow-list governs the whole tag vocabulary
14701
+ * instead of the four names that happened to be checked individually.
14702
+ */
14703
+ tagPermitted(tag) {
14704
+ if (!this._sandbox || this._allowedTags === null) return true;
14705
+ if (!GATEABLE_TAGS.has(tag)) return true;
14706
+ return this._allowedTags.has(tag);
14707
+ }
14708
+ /**
14709
+ * Consume a denied tag WITHOUT running it, returning the index past its body.
14710
+ *
14711
+ * Advancing a single token past a body-owning tag would leave the body's tokens
14712
+ * to render at the TOP level, leaking exactly the content the sandbox denied.
14713
+ */
14714
+ skipDeniedTag(tokens, start2, tag, content) {
14715
+ const closeTag = BLOCK_TAG_ENDS[tag];
14716
+ if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
14717
+ return this.skipBlock(tokens, start2, tag, closeTag);
14718
+ }
14554
14719
  skipBlock(tokens, start2, openTag, closeTag) {
14555
14720
  let depth = 0;
14556
14721
  let i = start2 + 1;
@@ -14558,7 +14723,7 @@ var init_engine = __esm({
14558
14723
  if (tokens[i][0] === "BLOCK") {
14559
14724
  const [content] = stripTag(tokens[i][1]);
14560
14725
  const tag = content.split(/\s+/)[0] || "";
14561
- if (tag === openTag) depth++;
14726
+ if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
14562
14727
  else if (tag === closeTag) {
14563
14728
  if (depth === 0) return i + 1;
14564
14729
  depth--;
@@ -14743,11 +14908,11 @@ var init_engine = __esm({
14743
14908
  for (const [fname, rawArgs] of filters) {
14744
14909
  const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
14745
14910
  if (fname === "raw" || fname === "safe") {
14746
- isSafe = true;
14911
+ if (this.filterPermitted(fname)) isSafe = true;
14747
14912
  continue;
14748
14913
  }
14749
14914
  if (fname === "escape" || fname === "e") {
14750
- isSafe = true;
14915
+ if (this.filterPermitted(fname)) isSafe = true;
14751
14916
  }
14752
14917
  if (this._sandbox && this._allowedFilters !== null) {
14753
14918
  if (!this._allowedFilters.has(fname)) {
@@ -16558,858 +16723,128 @@ var init_rateLimiter = __esm({
16558
16723
  }
16559
16724
  });
16560
16725
 
16561
- // ../core/src/messenger.ts
16562
- import net2 from "node:net";
16563
- import tls from "node:tls";
16564
- import { readFileSync as readFileSync8 } from "node:fs";
16565
- import { basename as basename3 } from "node:path";
16726
+ // ../core/src/devMailbox.ts
16727
+ import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
16728
+ import { join as join13 } from "node:path";
16566
16729
  import { randomUUID as randomUUID2 } from "node:crypto";
16567
- function tlsRejectUnauthorized() {
16568
- return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
16569
- }
16570
- function readResponse(socket) {
16571
- return new Promise((resolve31, reject) => {
16572
- let buffer = "";
16573
- const onData = (chunk) => {
16574
- buffer += chunk.toString("utf-8");
16575
- const lines = buffer.split("\r\n");
16576
- for (let i = 0; i < lines.length; i++) {
16577
- const line = lines[i];
16578
- if (line.length < 3) continue;
16579
- const code = parseInt(line.substring(0, 3), 10);
16580
- if (line.length >= 4 && line[3] === " ") {
16581
- socket.removeListener("data", onData);
16582
- socket.removeListener("error", onError);
16583
- resolve31({ code, text: buffer.trim() });
16584
- return;
16585
- }
16586
- }
16587
- };
16588
- const onError = (err) => {
16589
- socket.removeListener("data", onData);
16590
- reject(err);
16591
- };
16592
- socket.on("data", onData);
16593
- socket.on("error", onError);
16594
- });
16595
- }
16596
- function sendCommand(socket, command) {
16597
- return new Promise((resolve31, reject) => {
16598
- socket.write(command + "\r\n", "utf-8", (err) => {
16599
- if (err) return reject(err);
16600
- readResponse(socket).then(resolve31, reject);
16601
- });
16602
- });
16603
- }
16604
- function buildMimeMessage(options) {
16605
- const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16606
- const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16607
- const hasAttachments = options.attachments && options.attachments.length > 0;
16608
- const hasTextAlt = options.text !== void 0 && options.html;
16609
- const lines = [];
16610
- const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
16611
- lines.push(`From: ${fromHeader}`);
16612
- lines.push(`To: ${options.to.join(", ")}`);
16613
- if (options.cc.length > 0) {
16614
- lines.push(`Cc: ${options.cc.join(", ")}`);
16615
- }
16616
- lines.push(`Subject: ${options.subject}`);
16617
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
16618
- lines.push(`Message-ID: <${options.messageId}>`);
16619
- lines.push("MIME-Version: 1.0");
16620
- if (options.replyTo) {
16621
- lines.push(`Reply-To: ${options.replyTo}`);
16622
- }
16623
- if (options.headers) {
16624
- for (const [key, value] of Object.entries(options.headers)) {
16625
- lines.push(`${key}: ${value}`);
16626
- }
16627
- }
16628
- if (hasAttachments) {
16629
- lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
16630
- lines.push("");
16631
- lines.push(`--${boundary}`);
16632
- if (hasTextAlt) {
16633
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16634
- lines.push("");
16635
- lines.push(`--${altBoundary}`);
16636
- lines.push("Content-Type: text/plain; charset=UTF-8");
16637
- lines.push("Content-Transfer-Encoding: 7bit");
16638
- lines.push("");
16639
- lines.push(options.text);
16640
- lines.push("");
16641
- lines.push(`--${altBoundary}`);
16642
- lines.push("Content-Type: text/html; charset=UTF-8");
16643
- lines.push("Content-Transfer-Encoding: 7bit");
16644
- lines.push("");
16645
- lines.push(options.body);
16646
- lines.push("");
16647
- lines.push(`--${altBoundary}--`);
16648
- } else {
16649
- const contentType = options.html ? "text/html" : "text/plain";
16650
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16651
- lines.push("Content-Transfer-Encoding: 7bit");
16652
- lines.push("");
16653
- lines.push(options.body);
16654
- }
16655
- for (const filePath of options.attachments) {
16656
- const fileName = basename3(filePath);
16657
- const fileData = readFileSync8(filePath);
16658
- const base64Data = fileData.toString("base64");
16659
- lines.push("");
16660
- lines.push(`--${boundary}`);
16661
- lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
16662
- lines.push("Content-Transfer-Encoding: base64");
16663
- lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
16664
- lines.push("");
16665
- for (let i = 0; i < base64Data.length; i += 76) {
16666
- lines.push(base64Data.substring(i, i + 76));
16667
- }
16668
- }
16669
- lines.push("");
16670
- lines.push(`--${boundary}--`);
16671
- } else if (hasTextAlt) {
16672
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16673
- lines.push("");
16674
- lines.push(`--${altBoundary}`);
16675
- lines.push("Content-Type: text/plain; charset=UTF-8");
16676
- lines.push("Content-Transfer-Encoding: 7bit");
16677
- lines.push("");
16678
- lines.push(options.text);
16679
- lines.push("");
16680
- lines.push(`--${altBoundary}`);
16681
- lines.push("Content-Type: text/html; charset=UTF-8");
16682
- lines.push("Content-Transfer-Encoding: 7bit");
16683
- lines.push("");
16684
- lines.push(options.body);
16685
- lines.push("");
16686
- lines.push(`--${altBoundary}--`);
16687
- } else {
16688
- const contentType = options.html ? "text/html" : "text/plain";
16689
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16690
- lines.push("");
16691
- lines.push(options.body);
16692
- }
16693
- return lines.join("\r\n");
16694
- }
16695
- function imapQuote(s) {
16696
- if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
16697
- return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
16698
- }
16699
- function imapReadLine(socket) {
16700
- return new Promise((resolve31, reject) => {
16701
- let buffer = "";
16702
- const onData = (chunk) => {
16703
- buffer += chunk.toString("utf-8");
16704
- const nlIndex = buffer.indexOf("\r\n");
16705
- if (nlIndex !== -1) {
16706
- socket.removeListener("data", onData);
16707
- socket.removeListener("error", onError);
16708
- resolve31(buffer);
16730
+ var DevMailbox;
16731
+ var init_devMailbox = __esm({
16732
+ "../core/src/devMailbox.ts"() {
16733
+ "use strict";
16734
+ DevMailbox = class {
16735
+ mailboxDir;
16736
+ constructor(mailboxDir) {
16737
+ this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
16709
16738
  }
16710
- };
16711
- const onError = (err) => {
16712
- socket.removeListener("data", onData);
16713
- reject(err);
16714
- };
16715
- socket.on("data", onData);
16716
- socket.on("error", onError);
16717
- });
16718
- }
16719
- function imapCommand(socket, command) {
16720
- return new Promise((resolve31, reject) => {
16721
- imapTagCounter++;
16722
- const tag = `T${imapTagCounter}`;
16723
- const fullCommand = `${tag} ${command}\r
16724
- `;
16725
- let buffer = "";
16726
- const onData = (chunk) => {
16727
- buffer += chunk.toString("utf-8");
16728
- if (buffer.includes(`${tag} OK`)) {
16729
- socket.removeListener("data", onData);
16730
- socket.removeListener("error", onError);
16731
- resolve31(buffer);
16732
- return;
16739
+ /**
16740
+ * Ensure a folder directory exists.
16741
+ */
16742
+ ensureFolder(folder) {
16743
+ const dir = join13(this.mailboxDir, folder);
16744
+ mkdirSync9(dir, { recursive: true });
16745
+ return dir;
16733
16746
  }
16734
- if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
16735
- socket.removeListener("data", onData);
16736
- socket.removeListener("error", onError);
16737
- reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
16747
+ /**
16748
+ * Capture an email to the dev mailbox instead of sending it.
16749
+ *
16750
+ * The parameter order MATCHES Messenger.send() on purpose. It did not before:
16751
+ * send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
16752
+ * meant different things depending on which door it came through -- that mismatch
16753
+ * IS nodejs#42.
16754
+ *
16755
+ * BREAKING: `text` is now the 5th positional. A caller passing cc positionally
16756
+ * must move it. Aligning the two signatures is the fix; leaving them apart would
16757
+ * preserve the bug.
16758
+ */
16759
+ capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
16760
+ const id = randomUUID2();
16761
+ const toList = Array.isArray(to) ? to : [to];
16762
+ const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
16763
+ const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
16764
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16765
+ const message = {
16766
+ id,
16767
+ type: "outbox",
16768
+ from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
16769
+ to: toList,
16770
+ cc: ccList,
16771
+ bcc: bccList,
16772
+ reply_to: replyTo,
16773
+ subject,
16774
+ body,
16775
+ text,
16776
+ html,
16777
+ attachments,
16778
+ date: now,
16779
+ read: false
16780
+ };
16781
+ const outboxDir = this.ensureFolder("outbox");
16782
+ writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
16783
+ const inboxDir = this.ensureFolder("inbox");
16784
+ const inboxMessage = { ...message, type: "inbox" };
16785
+ writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
16786
+ return { success: true, message: "Email captured to dev mailbox", id };
16738
16787
  }
16739
- };
16740
- const onError = (err) => {
16741
- socket.removeListener("data", onData);
16742
- reject(err);
16743
- };
16744
- socket.on("data", onData);
16745
- socket.on("error", onError);
16746
- socket.write(fullCommand, "utf-8");
16747
- });
16748
- }
16749
- function imapFail(method, err) {
16750
- const e = err instanceof Error ? err : new Error(String(err));
16751
- Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
16752
- if (e instanceof MessengerConnectionError) return e;
16753
- return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
16754
- }
16755
- function parseSearchResponse(response) {
16756
- const match = response.match(/\* SEARCH (.+)/);
16757
- if (!match) return [];
16758
- return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
16759
- }
16760
- function parseHeaderResponse(uid, response) {
16761
- const headers = {};
16762
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
16763
- if (headerBlock) {
16764
- const lines = headerBlock[1].split(/\r\n/);
16765
- let currentKey = "";
16766
- for (const line of lines) {
16767
- if (/^\s/.test(line) && currentKey) {
16768
- headers[currentKey] += " " + line.trim();
16769
- } else {
16770
- const colonIdx = line.indexOf(":");
16771
- if (colonIdx > 0) {
16772
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16773
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16788
+ /**
16789
+ * List messages from a folder (default: inbox).
16790
+ */
16791
+ inbox(limit = 50, offset = 0, folder = "inbox") {
16792
+ const dir = this.ensureFolder(folder);
16793
+ const results = [];
16794
+ let files;
16795
+ try {
16796
+ files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
16797
+ } catch {
16798
+ return [];
16774
16799
  }
16775
- }
16776
- }
16777
- }
16778
- const seen = /\\Seen/i.test(response);
16779
- return {
16780
- uid,
16781
- subject: headers["subject"] ?? "",
16782
- from: headers["from"] ?? "",
16783
- to: headers["to"] ?? "",
16784
- date: headers["date"] ?? "",
16785
- snippet: "",
16786
- seen
16787
- };
16788
- }
16789
- function emptyFullMessage(uid) {
16790
- return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
16791
- }
16792
- function parseFullMessage(uid, response) {
16793
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
16794
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
16795
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
16796
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
16797
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
16798
- const headers = {};
16799
- const headerLines = headerSection.split(/\r\n/);
16800
- let currentKey = "";
16801
- for (const line of headerLines) {
16802
- if (/^\s/.test(line) && currentKey) {
16803
- headers[currentKey] += " " + line.trim();
16804
- } else {
16805
- const colonIdx = line.indexOf(":");
16806
- if (colonIdx > 0) {
16807
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16808
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16809
- }
16810
- }
16811
- }
16812
- const contentType = headers["content-type"] ?? "text/plain";
16813
- let bodyText = "";
16814
- let bodyHtml = "";
16815
- if (contentType.includes("multipart")) {
16816
- const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
16817
- if (boundaryMatch) {
16818
- const boundary = boundaryMatch[1];
16819
- const parts = bodySection.split("--" + boundary);
16820
- for (const part of parts) {
16821
- if (part.trim() === "" || part.trim() === "--") continue;
16822
- const partHeaderEnd = part.indexOf("\r\n\r\n");
16823
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
16824
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
16825
- if (partHeaders.includes("text/html")) {
16826
- bodyHtml = partBody;
16827
- } else if (partHeaders.includes("text/plain")) {
16828
- bodyText = partBody;
16800
+ const sliced = files.slice(offset, offset + limit);
16801
+ for (const file of sliced) {
16802
+ try {
16803
+ const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
16804
+ results.push(msg);
16805
+ } catch {
16806
+ }
16829
16807
  }
16808
+ return results;
16830
16809
  }
16831
- }
16832
- } else if (contentType.includes("text/html")) {
16833
- bodyHtml = bodySection;
16834
- } else {
16835
- bodyText = bodySection;
16836
- }
16837
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16838
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16839
- return {
16840
- uid,
16841
- subject: headers["subject"] ?? "",
16842
- from: headers["from"] ?? "",
16843
- to: headers["to"] ?? "",
16844
- cc: headers["cc"] ?? "",
16845
- date: headers["date"] ?? "",
16846
- bodyText,
16847
- bodyHtml,
16848
- headers
16849
- };
16850
- }
16851
- var MessengerConnectionError, Messenger, imapTagCounter;
16852
- var init_messenger = __esm({
16853
- "../core/src/messenger.ts"() {
16854
- "use strict";
16855
- init_dotenv();
16856
- init_logger();
16857
- MessengerConnectionError = class extends Error {
16858
- constructor(message) {
16859
- super(message);
16860
- this.name = "MessengerConnectionError";
16861
- }
16862
- };
16863
- Messenger = class {
16864
- host;
16865
- port;
16866
- username;
16867
- password;
16868
- fromAddress;
16869
- fromName;
16870
- encryption;
16871
- useTls;
16872
- imapHost;
16873
- imapPort;
16874
- imapUser;
16875
- imapPass;
16876
- imapEncryption;
16877
- constructor(options) {
16878
- this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
16879
- this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
16880
- this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
16881
- this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
16882
- this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
16883
- this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
16884
- const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
16885
- if (envEncryption) {
16886
- this.encryption = envEncryption.toLowerCase();
16887
- } else if (options?.useTls !== void 0) {
16888
- this.encryption = options.useTls ? "tls" : "none";
16889
- } else {
16890
- this.encryption = "tls";
16810
+ /**
16811
+ * Read a single message by ID. Searches all folders.
16812
+ */
16813
+ read(msgId) {
16814
+ const folders = ["inbox", "outbox"];
16815
+ for (const folder of folders) {
16816
+ const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
16817
+ if (existsSync11(filePath)) {
16818
+ try {
16819
+ const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
16820
+ msg.read = true;
16821
+ writeFileSync7(filePath, JSON.stringify(msg, null, 2));
16822
+ return msg;
16823
+ } catch {
16824
+ return null;
16825
+ }
16826
+ }
16891
16827
  }
16892
- this.useTls = ["tls", "starttls"].includes(this.encryption);
16893
- this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
16894
- this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
16895
- this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
16896
- this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
16897
- this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
16828
+ return null;
16898
16829
  }
16899
16830
  /**
16900
- * Read-only IMAP encryption mode for inspection / tests.
16901
- * Returns one of "tls", "starttls", "none", "ssl".
16831
+ * Count unread messages in the inbox.
16902
16832
  */
16903
- getImapEncryption() {
16904
- return this.imapEncryption;
16905
- }
16906
- /**
16907
- * Send an email via SMTP.
16908
- */
16909
- async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
16910
- const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
16911
- const toList = Array.isArray(options.to) ? options.to : [options.to];
16912
- const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
16913
- const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
16914
- const allRecipients = [...toList, ...ccList, ...bccList];
16915
- const messageId = `${randomUUID2()}@${this.host}`;
16916
- if (allRecipients.length === 0) {
16917
- return { success: false, message: "No recipients specified" };
16918
- }
16919
- if (!this.fromAddress) {
16920
- return { success: false, message: "No from address configured" };
16921
- }
16922
- try {
16923
- let socket;
16924
- if (this.port === 465) {
16925
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
16926
- await new Promise((resolve31, reject) => {
16927
- socket.once("secureConnect", resolve31);
16928
- socket.once("error", reject);
16929
- });
16930
- } else {
16931
- socket = net2.createConnection({ host: this.host, port: this.port });
16932
- await new Promise((resolve31, reject) => {
16933
- socket.once("connect", resolve31);
16934
- socket.once("error", reject);
16935
- });
16936
- }
16937
- const greeting = await readResponse(socket);
16938
- if (greeting.code !== 220) {
16939
- socket.destroy();
16940
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
16941
- }
16942
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
16943
- if (ehlo.code !== 250) {
16944
- socket.destroy();
16945
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
16946
- }
16947
- if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
16948
- const starttls = await sendCommand(socket, "STARTTLS");
16949
- if (starttls.code !== 220) {
16950
- socket.destroy();
16951
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
16952
- }
16953
- const plainSocket = socket;
16954
- socket = tls.connect(
16955
- { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
16956
- );
16957
- await new Promise((resolve31, reject) => {
16958
- socket.once("secureConnect", resolve31);
16959
- socket.once("error", reject);
16960
- });
16961
- const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
16962
- if (ehlo2.code !== 250) {
16963
- socket.destroy();
16964
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
16965
- }
16966
- }
16967
- if (this.username && this.password) {
16968
- const auth = await sendCommand(socket, "AUTH LOGIN");
16969
- if (auth.code !== 334) {
16970
- socket.destroy();
16971
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
16972
- }
16973
- const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
16974
- if (userResp.code !== 334) {
16975
- socket.destroy();
16976
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
16977
- }
16978
- const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
16979
- if (passResp.code !== 235) {
16980
- socket.destroy();
16981
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
16982
- }
16983
- }
16984
- const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
16985
- if (mailFrom.code !== 250) {
16986
- socket.destroy();
16987
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
16988
- }
16989
- for (const recipient of allRecipients) {
16990
- const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
16991
- if (rcpt.code !== 250 && rcpt.code !== 251) {
16992
- socket.destroy();
16993
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
16994
- }
16995
- }
16996
- const dataCmd = await sendCommand(socket, "DATA");
16997
- if (dataCmd.code !== 354) {
16998
- socket.destroy();
16999
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
17000
- }
17001
- const mimeMessage = buildMimeMessage({
17002
- from: this.fromAddress,
17003
- fromName: this.fromName,
17004
- to: toList,
17005
- cc: ccList,
17006
- subject: options.subject,
17007
- body: options.body,
17008
- html: options.html ?? false,
17009
- text: options.text,
17010
- replyTo: options.replyTo,
17011
- attachments: options.attachments,
17012
- headers: options.headers,
17013
- messageId
17014
- });
17015
- const endData = await sendCommand(socket, mimeMessage + "\r\n.");
17016
- if (endData.code !== 250) {
17017
- socket.destroy();
17018
- return { success: false, message: `Message delivery failed: ${endData.text}` };
17019
- }
17020
- await sendCommand(socket, "QUIT");
17021
- socket.destroy();
17022
- return { success: true, message: "Email sent successfully", id: messageId };
17023
- } catch (err) {
17024
- const errMsg = err instanceof Error ? err.message : String(err);
17025
- return { success: false, message: `SMTP error: ${errMsg}` };
17026
- }
17027
- }
17028
- /**
17029
- * Test the SMTP connection without sending an email.
17030
- */
17031
- async testConnection() {
17032
- try {
17033
- let socket;
17034
- if (this.port === 465) {
17035
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
17036
- await new Promise((resolve31, reject) => {
17037
- socket.once("secureConnect", resolve31);
17038
- socket.once("error", reject);
17039
- });
17040
- } else {
17041
- socket = net2.createConnection({ host: this.host, port: this.port });
17042
- await new Promise((resolve31, reject) => {
17043
- socket.once("connect", resolve31);
17044
- socket.once("error", reject);
17045
- });
17046
- }
17047
- const greeting = await readResponse(socket);
17048
- if (greeting.code !== 220) {
17049
- socket.destroy();
17050
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
17051
- }
17052
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
17053
- if (ehlo.code !== 250) {
17054
- socket.destroy();
17055
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
17056
- }
17057
- await sendCommand(socket, "QUIT");
17058
- socket.destroy();
17059
- return { success: true, message: `Connected to ${this.host}:${this.port}` };
17060
- } catch (err) {
17061
- const errMsg = err instanceof Error ? err.message : String(err);
17062
- return { success: false, message: `Connection failed: ${errMsg}` };
17063
- }
17064
- }
17065
- // ── IMAP (Read) ────────────────────────────────────────────
17066
- /**
17067
- * Connect to the IMAP server via raw TCP/TLS.
17068
- * Returns the socket and reads the greeting.
17069
- */
17070
- async imapConnect() {
17071
- if (!this.imapHost) {
17072
- throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
17073
- }
17074
- let socket;
17075
- const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
17076
- if (useTls) {
17077
- socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
17078
- await new Promise((resolve31, reject) => {
17079
- socket.once("secureConnect", resolve31);
17080
- socket.once("error", reject);
17081
- });
17082
- } else {
17083
- socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
17084
- await new Promise((resolve31, reject) => {
17085
- socket.once("connect", resolve31);
17086
- socket.once("error", reject);
17087
- });
17088
- }
17089
- await imapReadLine(socket);
17090
- if (this.imapUser && this.imapPass) {
17091
- const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
17092
- if (!loginResp.includes("OK")) {
17093
- socket.destroy();
17094
- throw new Error(`IMAP login failed: ${loginResp}`);
17095
- }
17096
- }
17097
- return socket;
17098
- }
17099
- /**
17100
- * Disconnect from IMAP cleanly.
17101
- */
17102
- async imapDisconnect(socket) {
17103
- try {
17104
- await imapCommand(socket, "LOGOUT");
17105
- } catch {
17106
- }
17107
- socket.destroy();
17108
- }
17109
- /**
17110
- * Fetch latest messages from a folder.
17111
- * Returns list of message summaries.
17112
- */
17113
- async inbox(limit = 20, offset = 0, folder = "INBOX") {
17114
- let socket;
17115
- try {
17116
- socket = await this.imapConnect();
17117
- } catch (err) {
17118
- throw imapFail("inbox", err);
17119
- }
17120
- try {
17121
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17122
- const searchResp = await imapCommand(socket, "SEARCH ALL");
17123
- const uids = parseSearchResponse(searchResp);
17124
- if (uids.length === 0) return [];
17125
- uids.reverse();
17126
- const selected = uids.slice(offset, offset + limit);
17127
- if (selected.length === 0) return [];
17128
- const messages = [];
17129
- for (const uid of selected) {
17130
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17131
- messages.push(parseHeaderResponse(uid, fetchResp));
17132
- }
17133
- return messages;
17134
- } catch (err) {
17135
- throw imapFail("inbox", err);
17136
- } finally {
17137
- await this.imapDisconnect(socket);
17138
- }
17139
- }
17140
- /**
17141
- * Read a single message by sequence number or UID.
17142
- */
17143
- async read(uid, folder = "INBOX") {
17144
- let socket;
17145
- try {
17146
- socket = await this.imapConnect();
17147
- } catch (err) {
17148
- throw imapFail("read", err);
17149
- }
17150
- try {
17151
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17152
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
17153
- if (!/\{\d+\}/.test(fetchResp)) {
17154
- return emptyFullMessage(uid);
17155
- }
17156
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17157
- return parseFullMessage(uid, fetchResp);
17158
- } catch (err) {
17159
- throw imapFail("read", err);
17160
- } finally {
17161
- await this.imapDisconnect(socket);
17162
- }
17163
- }
17164
- /**
17165
- * Search messages using IMAP search criteria.
17166
- */
17167
- async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
17168
- const criteria = ["ALL"];
17169
- if (subject) criteria.push(`SUBJECT "${subject}"`);
17170
- if (sender) criteria.push(`FROM "${sender}"`);
17171
- if (since) criteria.push(`SINCE ${since}`);
17172
- if (before) criteria.push(`BEFORE ${before}`);
17173
- if (unseenOnly) criteria.push("UNSEEN");
17174
- const query = criteria.join(" ");
17175
- let socket;
17176
- try {
17177
- socket = await this.imapConnect();
17178
- } catch (err) {
17179
- throw imapFail("search", err);
17180
- }
17181
- try {
17182
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17183
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
17184
- const uids = parseSearchResponse(searchResp);
17185
- if (uids.length === 0) return [];
17186
- uids.reverse();
17187
- const messages = [];
17188
- for (const uid of uids.slice(0, limit)) {
17189
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17190
- messages.push(parseHeaderResponse(uid, fetchResp));
17191
- }
17192
- return messages;
17193
- } catch (err) {
17194
- throw imapFail("search", err);
17195
- } finally {
17196
- await this.imapDisconnect(socket);
17197
- }
17198
- }
17199
- /**
17200
- * Delete a message by UID.
17201
- */
17202
- async deleteMessage(uid, folder = "INBOX") {
17203
- const socket = await this.imapConnect();
17204
- try {
17205
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17206
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
17207
- await imapCommand(socket, "EXPUNGE");
17208
- } finally {
17209
- await this.imapDisconnect(socket);
17210
- }
17211
- }
17212
- /**
17213
- * Mark a message as read.
17214
- */
17215
- async markRead(uid, folder = "INBOX") {
17216
- const socket = await this.imapConnect();
17217
- try {
17218
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17219
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17220
- } finally {
17221
- await this.imapDisconnect(socket);
17222
- }
17223
- }
17224
- /**
17225
- * Count unseen messages in a folder.
17226
- */
17227
- async unread(folder = "INBOX") {
17228
- let socket;
17229
- try {
17230
- socket = await this.imapConnect();
17231
- } catch (err) {
17232
- throw imapFail("unread", err);
17233
- }
17234
- try {
17235
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17236
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
17237
- return parseSearchResponse(searchResp).length;
17238
- } catch (err) {
17239
- throw imapFail("unread", err);
17240
- } finally {
17241
- await this.imapDisconnect(socket);
17242
- }
17243
- }
17244
- /**
17245
- * List available IMAP folders/mailboxes.
17246
- */
17247
- async folders() {
17248
- let socket;
17249
- try {
17250
- socket = await this.imapConnect();
17251
- } catch (err) {
17252
- throw imapFail("folders", err);
17253
- }
17254
- try {
17255
- const resp = await imapCommand(socket, 'LIST "" "*"');
17256
- const result = [];
17257
- for (const line of resp.split("\r\n")) {
17258
- const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
17259
- if (m) result.push(m[1]);
17260
- }
17261
- return result;
17262
- } catch (err) {
17263
- throw imapFail("folders", err);
17264
- } finally {
17265
- await this.imapDisconnect(socket);
17266
- }
17267
- }
17268
- /**
17269
- * Test IMAP connectivity without reading.
17270
- */
17271
- async testImapConnection() {
17272
- try {
17273
- const socket = await this.imapConnect();
17274
- await this.imapDisconnect(socket);
17275
- return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
17276
- } catch (err) {
17277
- const errMsg = err instanceof Error ? err.message : String(err);
17278
- return { success: false, message: `IMAP connection failed: ${errMsg}` };
17279
- }
17280
- }
17281
- };
17282
- imapTagCounter = 0;
17283
- }
17284
- });
17285
-
17286
- // ../core/src/devMailbox.ts
17287
- import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
17288
- import { join as join13 } from "node:path";
17289
- import { randomUUID as randomUUID3 } from "node:crypto";
17290
- function createMessenger() {
17291
- const debug = process.env.TINA4_DEBUG;
17292
- const smtpHost = process.env.TINA4_MAIL_HOST;
17293
- const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
17294
- if (isTruthy(debug)) {
17295
- return new DevMailbox();
17296
- }
17297
- if (!smtpHost) {
17298
- return new DevMailbox();
17299
- }
17300
- if (!isProd) {
17301
- return new DevMailbox();
17302
- }
17303
- return new Messenger();
17304
- }
17305
- var DevMailbox;
17306
- var init_devMailbox = __esm({
17307
- "../core/src/devMailbox.ts"() {
17308
- "use strict";
17309
- init_messenger();
17310
- init_dotenv();
17311
- DevMailbox = class {
17312
- mailboxDir;
17313
- constructor(mailboxDir) {
17314
- this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
17315
- }
17316
- /**
17317
- * Ensure a folder directory exists.
17318
- */
17319
- ensureFolder(folder) {
17320
- const dir = join13(this.mailboxDir, folder);
17321
- mkdirSync9(dir, { recursive: true });
17322
- return dir;
17323
- }
17324
- /**
17325
- * Capture an email to the dev mailbox instead of sending it.
17326
- */
17327
- capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
17328
- const id = randomUUID3();
17329
- const toList = Array.isArray(to) ? to : [to];
17330
- const now = (/* @__PURE__ */ new Date()).toISOString();
17331
- const message = {
17332
- id,
17333
- type: "outbox",
17334
- from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
17335
- to: toList,
17336
- cc,
17337
- bcc,
17338
- reply_to: replyTo,
17339
- subject,
17340
- body,
17341
- html,
17342
- attachments,
17343
- date: now,
17344
- read: false
17345
- };
17346
- const outboxDir = this.ensureFolder("outbox");
17347
- writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
17348
- const inboxDir = this.ensureFolder("inbox");
17349
- const inboxMessage = { ...message, type: "inbox" };
17350
- writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
17351
- return { success: true, message: "Email captured to dev mailbox", id };
17352
- }
17353
- /**
17354
- * List messages from a folder (default: inbox).
17355
- */
17356
- inbox(limit = 50, offset = 0, folder = "inbox") {
17357
- const dir = this.ensureFolder(folder);
17358
- const results = [];
17359
- let files;
17360
- try {
17361
- files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
17362
- } catch {
17363
- return [];
17364
- }
17365
- const sliced = files.slice(offset, offset + limit);
17366
- for (const file of sliced) {
17367
- try {
17368
- const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
17369
- results.push(msg);
17370
- } catch {
17371
- }
17372
- }
17373
- return results;
17374
- }
17375
- /**
17376
- * Read a single message by ID. Searches all folders.
17377
- */
17378
- read(msgId) {
17379
- const folders = ["inbox", "outbox"];
17380
- for (const folder of folders) {
17381
- const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
17382
- if (existsSync11(filePath)) {
17383
- try {
17384
- const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
17385
- msg.read = true;
17386
- writeFileSync7(filePath, JSON.stringify(msg, null, 2));
17387
- return msg;
17388
- } catch {
17389
- return null;
17390
- }
17391
- }
17392
- }
17393
- return null;
17394
- }
17395
- /**
17396
- * Count unread messages in the inbox.
17397
- */
17398
- unreadCount() {
17399
- const dir = this.ensureFolder("inbox");
17400
- let count = 0;
17401
- try {
17402
- const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
17403
- for (const file of files) {
17404
- try {
17405
- const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
17406
- if (!msg.read) count++;
17407
- } catch {
17408
- }
17409
- }
17410
- } catch {
17411
- }
17412
- return count;
16833
+ unreadCount() {
16834
+ const dir = this.ensureFolder("inbox");
16835
+ let count = 0;
16836
+ try {
16837
+ const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
16838
+ for (const file of files) {
16839
+ try {
16840
+ const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
16841
+ if (!msg.read) count++;
16842
+ } catch {
16843
+ }
16844
+ }
16845
+ } catch {
16846
+ }
16847
+ return count;
17413
16848
  }
17414
16849
  /**
17415
16850
  * Delete a message by ID. Removes from all folders.
@@ -17474,7 +16909,7 @@ var init_devMailbox = __esm({
17474
16909
  const subject = subjects[i % subjects.length];
17475
16910
  const from = senders[i % senders.length];
17476
16911
  const date = new Date(Date.now() - i * 36e5).toISOString();
17477
- const id = randomUUID3();
16912
+ const id = randomUUID2();
17478
16913
  const message = {
17479
16914
  id,
17480
16915
  type: "inbox",
@@ -18595,7 +18030,7 @@ var init_metrics = __esm({
18595
18030
  });
18596
18031
 
18597
18032
  // ../core/src/feedback.ts
18598
- import { readFileSync as readFileSync11, existsSync as existsSync13 } from "node:fs";
18033
+ import { readFileSync as readFileSync10, existsSync as existsSync13 } from "node:fs";
18599
18034
  import { dirname as dirname6, join as join15, resolve as resolve9 } from "node:path";
18600
18035
  import { fileURLToPath as fileURLToPath2 } from "node:url";
18601
18036
  function feedbackEnabled() {
@@ -18736,7 +18171,7 @@ var init_feedback = __esm({
18736
18171
  handleFeedbackWidgetJs = (_req, res) => {
18737
18172
  let body;
18738
18173
  if (existsSync13(WIDGET_BUNDLE_PATH)) {
18739
- body = readFileSync11(WIDGET_BUNDLE_PATH);
18174
+ body = readFileSync10(WIDGET_BUNDLE_PATH);
18740
18175
  } else {
18741
18176
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
18742
18177
  }
@@ -20444,7 +19879,7 @@ __export(errorOverlay_exports, {
20444
19879
  renderErrorOverlay: () => renderErrorOverlay,
20445
19880
  renderProductionError: () => renderProductionError
20446
19881
  });
20447
- import { readFileSync as readFileSync13, statSync as statSync10 } from "node:fs";
19882
+ import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
20448
19883
  import { resolve as resolve11 } from "node:path";
20449
19884
  function esc(text) {
20450
19885
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -20468,7 +19903,7 @@ function parseStack(stack) {
20468
19903
  function readSourceLines(filename, lineno) {
20469
19904
  try {
20470
19905
  const absPath = resolve11(filename);
20471
- const content = readFileSync13(absPath, "utf-8");
19906
+ const content = readFileSync12(absPath, "utf-8");
20472
19907
  const allLines = content.split("\n");
20473
19908
  const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
20474
19909
  const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
@@ -21732,8 +21167,8 @@ __export(context_exports, {
21732
21167
  fts5Supported: () => fts5Supported
21733
21168
  });
21734
21169
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
21735
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21736
- import { basename as basename5, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
21170
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21171
+ import { basename as basename4, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
21737
21172
  function fts5Supported() {
21738
21173
  try {
21739
21174
  const conn = new DatabaseSync3(":memory:");
@@ -21756,7 +21191,7 @@ function realResolve(abs) {
21756
21191
  } catch {
21757
21192
  }
21758
21193
  try {
21759
- return join17(realpathSync2(dirname8(abs)), basename5(abs));
21194
+ return join17(realpathSync2(dirname8(abs)), basename4(abs));
21760
21195
  } catch {
21761
21196
  return abs;
21762
21197
  }
@@ -21869,7 +21304,7 @@ var init_context = __esm({
21869
21304
  // ── indexing ───────────────────────────────────────────────
21870
21305
  static chunksFor(label, text) {
21871
21306
  const ext = extname6(label).toLowerCase();
21872
- const special = SPECIAL_FILES.has(basename5(label).toLowerCase());
21307
+ const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
21873
21308
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
21874
21309
  return chunkCode(text, label);
21875
21310
  }
@@ -21886,7 +21321,7 @@ var init_context = __esm({
21886
21321
  const stored = label != null ? String(label) : String(file);
21887
21322
  let text;
21888
21323
  try {
21889
- text = readFileSync14(file, "utf-8");
21324
+ text = readFileSync13(file, "utf-8");
21890
21325
  } catch {
21891
21326
  return 0;
21892
21327
  }
@@ -21966,7 +21401,7 @@ var init_context = __esm({
21966
21401
  if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
21967
21402
  return -1;
21968
21403
  }
21969
- if (!_Context.eligible(basename5(rel))) return -1;
21404
+ if (!_Context.eligible(basename4(rel))) return -1;
21970
21405
  const stored = rel;
21971
21406
  if (!existsSync15(abs)) {
21972
21407
  this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
@@ -22058,7 +21493,7 @@ var init_context = __esm({
22058
21493
  });
22059
21494
 
22060
21495
  // ../core/src/websocketBackplane.ts
22061
- import { randomUUID as randomUUID4 } from "node:crypto";
21496
+ import { randomUUID as randomUUID3 } from "node:crypto";
22062
21497
  function createBackplane(url) {
22063
21498
  const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
22064
21499
  switch (backend) {
@@ -22088,7 +21523,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
22088
21523
  return envelope;
22089
21524
  }
22090
21525
  function randomInstanceId() {
22091
- return randomUUID4().replace(/-/g, "").slice(0, 16);
21526
+ return randomUUID3().replace(/-/g, "").slice(0, 16);
22092
21527
  }
22093
21528
  var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
22094
21529
  var init_websocketBackplane = __esm({
@@ -22316,7 +21751,7 @@ __export(websocket_exports, {
22316
21751
  });
22317
21752
  import { createServer } from "node:http";
22318
21753
  import { createHash as createHash4 } from "node:crypto";
22319
- import { randomUUID as randomUUID5 } from "node:crypto";
21754
+ import { randomUUID as randomUUID4 } from "node:crypto";
22320
21755
  function computeAcceptKey(key) {
22321
21756
  return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
22322
21757
  }
@@ -22444,7 +21879,7 @@ function parseFrame(data) {
22444
21879
  return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
22445
21880
  }
22446
21881
  function createRouteConnection(socket, path8, headers, params, auth) {
22447
- const id = randomUUID5().slice(0, 8);
21882
+ const id = randomUUID4().slice(0, 8);
22448
21883
  const send = (message) => {
22449
21884
  try {
22450
21885
  socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
@@ -23001,7 +22436,7 @@ var init_websocket = __esm({
23001
22436
  }
23002
22437
  responseLines.push("", "");
23003
22438
  socket.write(responseLines.join("\r\n"));
23004
- const clientId = randomUUID5().slice(0, 8);
22439
+ const clientId = randomUUID4().slice(0, 8);
23005
22440
  const client = {
23006
22441
  id: clientId,
23007
22442
  socket,
@@ -23289,7 +22724,7 @@ var init_websocket = __esm({
23289
22724
 
23290
22725
  // ../core/src/queueBackends/rabbitmqBackend.ts
23291
22726
  import { execFileSync } from "node:child_process";
23292
- import { randomUUID as randomUUID6 } from "node:crypto";
22727
+ import { randomUUID as randomUUID5 } from "node:crypto";
23293
22728
  function parseAmqpUrl(url) {
23294
22729
  const config = {};
23295
22730
  let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
@@ -23733,7 +23168,7 @@ var init_rabbitmqBackend = __esm({
23733
23168
  }
23734
23169
  }
23735
23170
  push(queue, payload, _delay) {
23736
- const id = randomUUID6();
23171
+ const id = randomUUID5();
23737
23172
  const now = (/* @__PURE__ */ new Date()).toISOString();
23738
23173
  const job = {
23739
23174
  id,
@@ -23772,7 +23207,7 @@ var init_rabbitmqBackend = __esm({
23772
23207
 
23773
23208
  // ../core/src/queueBackends/kafkaBackend.ts
23774
23209
  import { execFileSync as execFileSync2 } from "node:child_process";
23775
- import { randomUUID as randomUUID7 } from "node:crypto";
23210
+ import { randomUUID as randomUUID6 } from "node:crypto";
23776
23211
  function kafkaSecurityConfig(env = process.env) {
23777
23212
  const mapping = [
23778
23213
  ["security.protocol", "SECURITY_PROTOCOL"],
@@ -23796,7 +23231,7 @@ var init_kafkaBackend = __esm({
23796
23231
  "use strict";
23797
23232
  API_PRODUCE = 0;
23798
23233
  API_FETCH = 1;
23799
- KafkaBackend = class {
23234
+ KafkaBackend = class _KafkaBackend {
23800
23235
  brokers;
23801
23236
  groupId;
23802
23237
  constructor(config) {
@@ -24158,12 +23593,15 @@ var init_kafkaBackend = __esm({
24158
23593
  if (errCode === 0) {
24159
23594
  finish("__PUBLISHED__", 0);
24160
23595
  } else {
23596
+ // Report the CODE, not just "it failed" \u2014 the caller decides
23597
+ // whether it is retriable (3/5, the async topic-creation race)
23598
+ // or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
24161
23599
  process.stderr.write("Produce error code " + errCode);
24162
- finish("__ERROR__" + errCode, 0);
23600
+ finish("__PRODUCEERROR__" + errCode, 0);
24163
23601
  }
24164
23602
  } catch (e) {
24165
23603
  process.stderr.write("produce parse: " + e.message);
24166
- finish("__ERROR__", 0);
23604
+ finish("__PARSEERROR__produce: " + e.message, 0);
24167
23605
  }
24168
23606
  return;
24169
23607
  } else if (operation === "get") {
@@ -24177,6 +23615,7 @@ var init_kafkaBackend = __esm({
24177
23615
  pos += 4; // throttleTimeMs (v1+)
24178
23616
  const topicCount = buffer.readInt32BE(pos); pos += 4;
24179
23617
  let out = "__EMPTY__";
23618
+ let fatalCode = 0;
24180
23619
  for (let t = 0; t < topicCount; t++) {
24181
23620
  const tl = buffer.readInt16BE(pos); pos += 2 + tl;
24182
23621
  const pc = buffer.readInt32BE(pos); pos += 4;
@@ -24188,6 +23627,14 @@ var init_kafkaBackend = __esm({
24188
23627
  const abortedCount = buffer.readInt32BE(pos); pos += 4;
24189
23628
  if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
24190
23629
  const recSetSize = buffer.readInt32BE(pos); pos += 4;
23630
+ // 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
23631
+ // "nothing to read here yet", which a consumer that starts
23632
+ // before its producer hits on every cold start. Any OTHER code
23633
+ // (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
23634
+ // is a real failure and must NOT be reported as an empty queue.
23635
+ if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
23636
+ fatalCode = errCode;
23637
+ }
24191
23638
  if (errCode === 0 && recSetSize > 0) {
24192
23639
  const val = firstRecordValue(buffer, pos, pos + recSetSize);
24193
23640
  if (val !== null) out = val;
@@ -24195,21 +23642,33 @@ var init_kafkaBackend = __esm({
24195
23642
  pos += recSetSize > 0 ? recSetSize : 0;
24196
23643
  }
24197
23644
  }
23645
+ if (fatalCode !== 0) {
23646
+ process.stderr.write("Fetch error code " + fatalCode);
23647
+ finish("__FETCHERROR__" + fatalCode, 0);
23648
+ return;
23649
+ }
24198
23650
  finish(out, 0);
24199
23651
  } catch (e) {
23652
+ // A parse failure is NOT an empty queue either \u2014 say so.
24200
23653
  process.stderr.write("fetch parse: " + e.message);
24201
- finish("__EMPTY__", 0);
23654
+ finish("__PARSEERROR__fetch: " + e.message, 0);
24202
23655
  }
24203
23656
  return;
24204
23657
  }
24205
23658
  });
24206
23659
 
23660
+ // Report the reason on STDOUT and exit 0. Writing it to stderr and
23661
+ // exiting non-zero LOST it: stderr to a pipe is an async write and
23662
+ // process.exit() truncates it, so the parent saw an empty stderr and fell
23663
+ // back to execFileSync's message -- which embeds this entire script.
23664
+ // stdout is flushed by finish()'s write callback, so it survives.
24207
23665
  sock.on("error", (err) => {
24208
- process.stderr.write(err.message);
24209
- finish("", 1);
23666
+ finish("__TRANSPORTERROR__" + err.message, 0);
24210
23667
  });
24211
23668
 
24212
- var timer = setTimeout(() => { finish("", 1); }, 10000);
23669
+ var timer = setTimeout(() => {
23670
+ finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
23671
+ }, 10000);
24213
23672
  `;
24214
23673
  try {
24215
23674
  const result = execFileSync2(process.execPath, ["-e", script], {
@@ -24218,12 +23677,51 @@ var init_kafkaBackend = __esm({
24218
23677
  stdio: ["pipe", "pipe", "pipe"]
24219
23678
  });
24220
23679
  return result;
24221
- } catch {
24222
- return "";
23680
+ } catch (err) {
23681
+ const e = err;
23682
+ const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
23683
+ const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
23684
+ return "__TRANSPORTERROR__" + firstLine2;
23685
+ }
23686
+ }
23687
+ /**
23688
+ * Sleep synchronously between produce retries.
23689
+ *
23690
+ * `push()` is synchronous (the whole backend drives its socket through a child
23691
+ * process), so there is no event loop to await on. `Atomics.wait` on a
23692
+ * SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
23693
+ * no dependency, no busy-wait burning CPU.
23694
+ */
23695
+ static sleepSync(ms) {
23696
+ const shared = new Int32Array(new SharedArrayBuffer(4));
23697
+ Atomics.wait(shared, 0, 0, ms);
23698
+ }
23699
+ /**
23700
+ * Turn a sentinel from the protocol child into a thrown error, or return.
23701
+ *
23702
+ * The wording matches the Python and PHP backends exactly -- the parity rule
23703
+ * covers user-visible error messages, not just behaviour.
23704
+ */
23705
+ static assertNoError(result, operation, topic) {
23706
+ const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
23707
+ if (fatal) {
23708
+ throw new Error(
23709
+ `Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
23710
+ );
23711
+ }
23712
+ if (result.startsWith("__TRANSPORTERROR__")) {
23713
+ throw new Error(
23714
+ `Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
23715
+ );
23716
+ }
23717
+ if (result.startsWith("__PARSEERROR__")) {
23718
+ throw new Error(
23719
+ `Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
23720
+ );
24223
23721
  }
24224
23722
  }
24225
23723
  push(queue, payload, _delay) {
24226
- const id = randomUUID7();
23724
+ const id = randomUUID6();
24227
23725
  const now = (/* @__PURE__ */ new Date()).toISOString();
24228
23726
  const job = {
24229
23727
  id,
@@ -24233,14 +23731,25 @@ var init_kafkaBackend = __esm({
24233
23731
  attempts: 0,
24234
23732
  delayUntil: null
24235
23733
  };
24236
- const result = this.execSync("publish", queue, JSON.stringify(job));
24237
- if (!result.includes("__PUBLISHED__")) {
24238
- throw new Error("Kafka publish failed");
23734
+ const body = JSON.stringify(job);
23735
+ let result = "";
23736
+ for (let attempt = 1; attempt <= 10; attempt++) {
23737
+ result = this.execSync("publish", queue, body);
23738
+ if (result.includes("__PUBLISHED__")) {
23739
+ return id;
23740
+ }
23741
+ const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
23742
+ if (!retriable || attempt === 10) {
23743
+ break;
23744
+ }
23745
+ _KafkaBackend.sleepSync(200);
24239
23746
  }
24240
- return id;
23747
+ _KafkaBackend.assertNoError(result, "produce", queue);
23748
+ throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
24241
23749
  }
24242
23750
  pop(queue) {
24243
23751
  const result = this.execSync("get", queue);
23752
+ _KafkaBackend.assertNoError(result, "fetch", queue);
24244
23753
  if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
24245
23754
  try {
24246
23755
  return JSON.parse(result);
@@ -24258,7 +23767,7 @@ var init_kafkaBackend = __esm({
24258
23767
  });
24259
23768
 
24260
23769
  // ../core/src/queueBackends/mongoBackend.ts
24261
- import { randomUUID as randomUUID8 } from "node:crypto";
23770
+ import { randomUUID as randomUUID7 } from "node:crypto";
24262
23771
  import { execFileSync as execFileSync3 } from "node:child_process";
24263
23772
  var MongoBackend2;
24264
23773
  var init_mongoBackend = __esm({
@@ -24571,7 +24080,7 @@ var init_mongoBackend = __esm({
24571
24080
  }
24572
24081
  }
24573
24082
  push(queue, payload, delay) {
24574
- const id = randomUUID8();
24083
+ const id = randomUUID7();
24575
24084
  const now = (/* @__PURE__ */ new Date()).toISOString();
24576
24085
  const job = {
24577
24086
  id,
@@ -24706,9 +24215,9 @@ var init_job = __esm({
24706
24215
  });
24707
24216
 
24708
24217
  // ../core/src/queueBackends/liteBackend.ts
24709
- import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
24218
+ import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
24710
24219
  import { join as join18 } from "node:path";
24711
- import { randomUUID as randomUUID9 } from "node:crypto";
24220
+ import { randomUUID as randomUUID8 } from "node:crypto";
24712
24221
  var LiteBackend;
24713
24222
  var init_liteBackend = __esm({
24714
24223
  "../core/src/queueBackends/liteBackend.ts"() {
@@ -24760,7 +24269,7 @@ var init_liteBackend = __esm({
24760
24269
  }
24761
24270
  push(queue, payload, delay, priority) {
24762
24271
  const dir = this.ensureDir(queue);
24763
- const id = randomUUID9();
24272
+ const id = randomUUID8();
24764
24273
  const now = (/* @__PURE__ */ new Date()).toISOString();
24765
24274
  const job = {
24766
24275
  id,
@@ -24796,7 +24305,7 @@ var init_liteBackend = __esm({
24796
24305
  const filePath = join18(dir, filename);
24797
24306
  let job;
24798
24307
  try {
24799
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24308
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
24800
24309
  } catch {
24801
24310
  continue;
24802
24311
  }
@@ -24860,7 +24369,7 @@ var init_liteBackend = __esm({
24860
24369
  const filePath = join18(reservedDir, filename);
24861
24370
  let record;
24862
24371
  try {
24863
- record = JSON.parse(readFileSync15(filePath, "utf-8"));
24372
+ record = JSON.parse(readFileSync14(filePath, "utf-8"));
24864
24373
  } catch {
24865
24374
  continue;
24866
24375
  }
@@ -24973,7 +24482,7 @@ var init_liteBackend = __esm({
24973
24482
  let count = 0;
24974
24483
  for (const file of files) {
24975
24484
  try {
24976
- const job = JSON.parse(readFileSync15(join18(scanDir, file), "utf-8"));
24485
+ const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
24977
24486
  if (job.status === status2) count++;
24978
24487
  } catch {
24979
24488
  }
@@ -25030,7 +24539,7 @@ var init_liteBackend = __esm({
25030
24539
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
25031
24540
  for (const file of files) {
25032
24541
  try {
25033
- const job = JSON.parse(readFileSync15(join18(dir, file), "utf-8"));
24542
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
25034
24543
  const attempts = job.attempts || 0;
25035
24544
  if (attempts > 0 && attempts < maxRetries) {
25036
24545
  results.push(job);
@@ -25056,7 +24565,7 @@ var init_liteBackend = __esm({
25056
24565
  const failedDir = join18(this.basePath, q, "failed");
25057
24566
  const filePath = join18(failedDir, `${jobId}.queue-data`);
25058
24567
  if (existsSync16(filePath)) {
25059
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24568
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25060
24569
  job.status = "pending";
25061
24570
  job.attempts = (job.attempts || 0) + 1;
25062
24571
  job.error = void 0;
@@ -25080,7 +24589,7 @@ var init_liteBackend = __esm({
25080
24589
  const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
25081
24590
  for (const file of files) {
25082
24591
  try {
25083
- const job = JSON.parse(readFileSync15(join18(failedDir, file), "utf-8"));
24592
+ const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
25084
24593
  if ((job.attempts || 0) >= maxRetries) {
25085
24594
  job.status = "dead";
25086
24595
  results.push(job);
@@ -25114,7 +24623,7 @@ var init_liteBackend = __esm({
25114
24623
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
25115
24624
  for (const file of files) {
25116
24625
  try {
25117
- const job = JSON.parse(readFileSync15(join18(dir, file), "utf-8"));
24626
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
25118
24627
  if (job.status === status2) {
25119
24628
  unlinkSync5(join18(dir, file));
25120
24629
  count++;
@@ -25141,7 +24650,7 @@ var init_liteBackend = __esm({
25141
24650
  for (const file of files) {
25142
24651
  try {
25143
24652
  const filePath = join18(failedDir, file);
25144
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24653
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25145
24654
  if ((job.attempts || 0) >= maxRetries) {
25146
24655
  continue;
25147
24656
  }
@@ -25173,7 +24682,7 @@ var init_liteBackend = __esm({
25173
24682
  const filePath = join18(dir, file);
25174
24683
  let job;
25175
24684
  try {
25176
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24685
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
25177
24686
  } catch {
25178
24687
  continue;
25179
24688
  }
@@ -27663,7 +27172,7 @@ ${end}
27663
27172
 
27664
27173
  // ../core/src/devAdmin.ts
27665
27174
  import { cpus as osCpus } from "node:os";
27666
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
27175
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
27667
27176
  import { join as join22, dirname as dirname10, resolve as resolve16, relative as relative8 } from "node:path";
27668
27177
  import { fileURLToPath as fileURLToPath4 } from "node:url";
27669
27178
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -27849,7 +27358,7 @@ function resolveDevEnvVar(key) {
27849
27358
  if (live !== void 0 && live !== "") return live;
27850
27359
  const envPath = join22(process.cwd(), ".env");
27851
27360
  if (!existsSync20(envPath)) return "";
27852
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
27361
+ for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
27853
27362
  const t = line.trim();
27854
27363
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
27855
27364
  const eq = t.indexOf("=");
@@ -27859,7 +27368,7 @@ function resolveDevEnvVar(key) {
27859
27368
  }
27860
27369
  function upsertDevEnvVar(key, value) {
27861
27370
  const envPath = join22(process.cwd(), ".env");
27862
- const lines = existsSync20(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
27371
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
27863
27372
  let found = false;
27864
27373
  const out = [];
27865
27374
  for (const line of lines) {
@@ -27892,7 +27401,7 @@ function parseEnvFile() {
27892
27401
  const envPath = join22(process.cwd(), ".env");
27893
27402
  const result = {};
27894
27403
  if (!existsSync20(envPath)) return result;
27895
- const lines = readFileSync19(envPath, "utf-8").split("\n");
27404
+ const lines = readFileSync18(envPath, "utf-8").split("\n");
27896
27405
  for (const line of lines) {
27897
27406
  const trimmed = line.trim();
27898
27407
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -28249,7 +27758,7 @@ var init_devAdmin = __esm({
28249
27758
  for (const rel of ["../../../package.json", "../../package.json"]) {
28250
27759
  const p = resolve16(__dirname2, rel);
28251
27760
  if (existsSync20(p)) {
28252
- const pkg = JSON.parse(readFileSync19(p, "utf-8"));
27761
+ const pkg = JSON.parse(readFileSync18(p, "utf-8"));
28253
27762
  if (pkg.version) return pkg.version;
28254
27763
  }
28255
27764
  }
@@ -28907,7 +28416,7 @@ var init_devAdmin = __esm({
28907
28416
  for (const filename of readdirSync14(queueDir).sort()) {
28908
28417
  if (!filename.endsWith(".queue-data")) continue;
28909
28418
  try {
28910
- const job = JSON.parse(readFileSync19(join22(queueDir, filename), "utf-8"));
28419
+ const job = JSON.parse(readFileSync18(join22(queueDir, filename), "utf-8"));
28911
28420
  jobs.push(mapQueueJob(job, topic, "pending"));
28912
28421
  } catch {
28913
28422
  }
@@ -29393,7 +28902,7 @@ var init_devAdmin = __esm({
29393
28902
  }
29394
28903
  try {
29395
28904
  const envPath = join22(process.cwd(), ".env");
29396
- const lines = existsSync20(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
28905
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
29397
28906
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
29398
28907
  const newLines = [];
29399
28908
  for (const line of lines) {
@@ -29439,7 +28948,7 @@ var init_devAdmin = __esm({
29439
28948
  const metaFile = join22(entryPath, "meta.json");
29440
28949
  if (statSync15(entryPath).isDirectory() && existsSync20(metaFile)) {
29441
28950
  try {
29442
- const meta = JSON.parse(readFileSync19(metaFile, "utf-8"));
28951
+ const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
29443
28952
  meta.id = entry;
29444
28953
  const srcDir = join22(entryPath, "src");
29445
28954
  if (existsSync20(srcDir)) {
@@ -29654,7 +29163,7 @@ var init_devAdmin = __esm({
29654
29163
  return;
29655
29164
  }
29656
29165
  try {
29657
- const content = readFileSync19(target, "utf-8");
29166
+ const content = readFileSync18(target, "utf-8");
29658
29167
  const path8 = relative8(root, target);
29659
29168
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
29660
29169
  } catch (e) {
@@ -29696,7 +29205,7 @@ var init_devAdmin = __esm({
29696
29205
  return;
29697
29206
  }
29698
29207
  try {
29699
- const buf = readFileSync19(target);
29208
+ const buf = readFileSync18(target);
29700
29209
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
29701
29210
  const mime = {
29702
29211
  js: "application/javascript",
@@ -30124,7 +29633,7 @@ var init_devAdmin = __esm({
30124
29633
  });
30125
29634
 
30126
29635
  // ../core/src/i18n.ts
30127
- import { readFileSync as readFileSync20, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
29636
+ import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
30128
29637
  import { join as join23, resolve as resolve17 } from "node:path";
30129
29638
  var I18n;
30130
29639
  var init_i18n = __esm({
@@ -30221,7 +29730,7 @@ var init_i18n = __esm({
30221
29730
  const filePath = join23(this._localeDir, `${locale}.json`);
30222
29731
  if (existsSync21(filePath)) {
30223
29732
  try {
30224
- const raw = readFileSync20(filePath, "utf-8");
29733
+ const raw = readFileSync19(filePath, "utf-8");
30225
29734
  const data = JSON.parse(raw);
30226
29735
  this._translations.set(locale, _I18n._flatten(data));
30227
29736
  return;
@@ -30234,7 +29743,7 @@ var init_i18n = __esm({
30234
29743
  const yamlPath = join23(this._localeDir, `${locale}${ext}`);
30235
29744
  if (existsSync21(yamlPath)) {
30236
29745
  try {
30237
- const raw = readFileSync20(yamlPath, "utf-8");
29746
+ const raw = readFileSync19(yamlPath, "utf-8");
30238
29747
  const data = _I18n._parseSimpleYaml(raw);
30239
29748
  this._translations.set(locale, _I18n._flatten(data));
30240
29749
  return;
@@ -30902,6 +30411,36 @@ var init_docsAutoDiscovery = __esm({
30902
30411
  }
30903
30412
  });
30904
30413
 
30414
+ // ../core/src/sessionHandlers/childError.ts
30415
+ function childFailureReason(err) {
30416
+ const e = err ?? {};
30417
+ const stderr = String(e.stderr ?? "").trim();
30418
+ if (stderr !== "") {
30419
+ return firstLine(stderr);
30420
+ }
30421
+ if (e.code === "ETIMEDOUT" || e.signal) {
30422
+ return `timed out or was killed (${e.code ?? e.signal})`;
30423
+ }
30424
+ if (typeof e.status === "number" && e.status !== 0) {
30425
+ return `child exited with code ${e.status} and no output`;
30426
+ }
30427
+ return firstLine(String(e.message ?? "unknown error"));
30428
+ }
30429
+ function firstLine(text) {
30430
+ const line = text.split("\n", 1)[0] ?? "";
30431
+ return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
30432
+ }
30433
+ function childFailureError(label, err) {
30434
+ return new Error(`${label} command failed: ${childFailureReason(err)}`);
30435
+ }
30436
+ var MAX_FALLBACK;
30437
+ var init_childError = __esm({
30438
+ "../core/src/sessionHandlers/childError.ts"() {
30439
+ "use strict";
30440
+ MAX_FALLBACK = 200;
30441
+ }
30442
+ });
30443
+
30905
30444
  // ../core/src/sessionHandlers/respClient.ts
30906
30445
  import { execFileSync as execFileSync4 } from "node:child_process";
30907
30446
  function respCommandSync(target, args, label = "Redis") {
@@ -31021,7 +30560,7 @@ function respCommandSync(target, args, label = "Redis") {
31021
30560
  stdio: ["pipe", "pipe", "pipe"]
31022
30561
  });
31023
30562
  } catch (err) {
31024
- throw new Error(`${label} command failed: ${err.message}`);
30563
+ throw childFailureError(label, err);
31025
30564
  }
31026
30565
  if (result === "__NULL__") return "";
31027
30566
  if (result.startsWith("__ERR__")) {
@@ -31032,6 +30571,7 @@ function respCommandSync(target, args, label = "Redis") {
31032
30571
  var init_respClient = __esm({
31033
30572
  "../core/src/sessionHandlers/respClient.ts"() {
31034
30573
  "use strict";
30574
+ init_childError();
31035
30575
  }
31036
30576
  });
31037
30577
 
@@ -31050,6 +30590,7 @@ var moduleRequire, RedisNpmSessionHandler;
31050
30590
  var init_redisHandler = __esm({
31051
30591
  "../core/src/sessionHandlers/redisHandler.ts"() {
31052
30592
  "use strict";
30593
+ init_childError();
31053
30594
  init_respClient();
31054
30595
  moduleRequire = createRequire8(import.meta.url);
31055
30596
  RedisNpmSessionHandler = class {
@@ -31110,9 +30651,17 @@ var init_redisHandler = __esm({
31110
30651
  (async () => {
31111
30652
  try {
31112
30653
  const redis = require("redis");
30654
+ // reconnectStrategy: false \u2014 this child runs ONE command and exits, so
30655
+ // retrying inside it is pointless: the handler is called again on the
30656
+ // next request anyway. With the driver's default strategy a refused
30657
+ // connection never rejects, the child hangs until execFileSync's 5s
30658
+ // timeout kills it, and the caller is told "timed out" when the truth
30659
+ // is "connection refused". Off, connect() rejects in ~5ms with the real
30660
+ // reason -- a better message AND no 5s stall per request when Redis is
30661
+ // down.
31113
30662
  const clientOpts = useUrl
31114
- ? { url }
31115
- : { socket: { host, port }, password: password || undefined, database: db };
30663
+ ? { url, socket: { reconnectStrategy: false } }
30664
+ : { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
31116
30665
  const client = redis.createClient(clientOpts);
31117
30666
  client.on("error", () => {});
31118
30667
  await client.connect();
@@ -31126,8 +30675,10 @@ var init_redisHandler = __esm({
31126
30675
  const out = (result === null || result === undefined) ? "__NULL__" : String(result);
31127
30676
  process.stdout.write(out, () => process.exit(0));
31128
30677
  } catch (err) {
31129
- process.stderr.write(String((err && err.message) || err));
31130
- process.exit(1);
30678
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30679
+ // a bare process.exit() truncates it, which left the parent with an
30680
+ // empty stderr and nothing but execFileSync's script-dump message.
30681
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31131
30682
  }
31132
30683
  })();
31133
30684
  `;
@@ -31139,7 +30690,7 @@ var init_redisHandler = __esm({
31139
30690
  stdio: ["pipe", "pipe", "pipe"]
31140
30691
  });
31141
30692
  } catch (err) {
31142
- throw new Error(`Redis command failed: ${err.message}`);
30693
+ throw childFailureError("Redis", err);
31143
30694
  }
31144
30695
  if (result === "__NULL__") return "";
31145
30696
  return result;
@@ -31271,8 +30822,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31271
30822
  process.stdout.write(out, () => process.exit(0));
31272
30823
  } catch (err) {
31273
30824
  try { if (client) await client.close(); } catch (e) {}
31274
- process.stderr.write(String((err && err.message) || err));
31275
- process.exit(1);
30825
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30826
+ // a bare process.exit() truncates it, which left the parent with an
30827
+ // empty stderr and nothing but execFileSync's script-dump message.
30828
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31276
30829
  }
31277
30830
  })();
31278
30831
  } else {
@@ -31413,12 +30966,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31413
30966
  stdio: ["pipe", "pipe", "pipe"]
31414
30967
  });
31415
30968
  } catch (err) {
31416
- throw new Error(`${label} command failed: ${err.message}`);
30969
+ throw childFailureError(label, err);
31417
30970
  }
31418
30971
  }
31419
30972
  var init_mongoClient = __esm({
31420
30973
  "../core/src/sessionHandlers/mongoClient.ts"() {
31421
30974
  "use strict";
30975
+ init_childError();
31422
30976
  }
31423
30977
  });
31424
30978
 
@@ -31571,7 +31125,7 @@ __export(session_exports, {
31571
31125
  sessionCookieName: () => sessionCookieName
31572
31126
  });
31573
31127
  import { randomBytes as randomBytes6 } from "node:crypto";
31574
- import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31128
+ import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31575
31129
  import { join as join25 } from "node:path";
31576
31130
  function isSecureScheme(forwardedProto, socketEncrypted) {
31577
31131
  const forwarded = (forwardedProto ?? "").trim();
@@ -31624,7 +31178,7 @@ var init_session = __esm({
31624
31178
  const filePath = this.filePath(sessionId);
31625
31179
  try {
31626
31180
  if (!existsSync23(filePath)) return null;
31627
- const raw = readFileSync22(filePath, "utf-8");
31181
+ const raw = readFileSync21(filePath, "utf-8");
31628
31182
  const wrapper = JSON.parse(raw);
31629
31183
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
31630
31184
  try {
@@ -31660,7 +31214,7 @@ var init_session = __esm({
31660
31214
  if (!file.endsWith(".json")) continue;
31661
31215
  const fullPath = join25(this.storagePath, file);
31662
31216
  try {
31663
- const raw = readFileSync22(fullPath, "utf-8");
31217
+ const raw = readFileSync21(fullPath, "utf-8");
31664
31218
  const wrapper = JSON.parse(raw);
31665
31219
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
31666
31220
  unlinkSync7(fullPath);
@@ -32177,7 +31731,7 @@ var init_events = __esm({
32177
31731
  // ../core/src/server.ts
32178
31732
  import { createServer as createServer2 } from "node:http";
32179
31733
  import { resolve as resolve19, dirname as dirname11, join as join26, relative as relative9 } from "node:path";
32180
- import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync16 } from "node:fs";
31734
+ import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
32181
31735
  import { isatty } from "node:tty";
32182
31736
  import { fileURLToPath as fileURLToPath5 } from "node:url";
32183
31737
  import { execFileSync as execFileSync7, exec } from "node:child_process";
@@ -32239,7 +31793,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
32239
31793
  function readPackageVersion() {
32240
31794
  try {
32241
31795
  const pkgPath = resolve19(dirname11(fileURLToPath5(import.meta.url)), "..", "..", "..", "package.json");
32242
- const pkg = JSON.parse(readFileSync23(pkgPath, "utf-8"));
31796
+ const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
32243
31797
  return pkg.version ?? "0.0.0";
32244
31798
  } catch {
32245
31799
  return "0.0.0";
@@ -33069,7 +32623,7 @@ ${reset2}
33069
32623
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33070
32624
  res.raw.end(html);
33071
32625
  } else {
33072
- const html = readFileSync23(resolve19(templatesDir, tplFile), "utf-8");
32626
+ const html = readFileSync22(resolve19(templatesDir, tplFile), "utf-8");
33073
32627
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33074
32628
  res.raw.end(html);
33075
32629
  }
@@ -33460,7 +33014,7 @@ var init_constants = __esm({
33460
33014
  });
33461
33015
 
33462
33016
  // ../core/src/scss.ts
33463
- import { readFileSync as readFileSync24, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
33017
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
33464
33018
  import { join as join27, resolve as resolve20, dirname as dirname12 } from "node:path";
33465
33019
  function compileString(scss, importPaths, variables) {
33466
33020
  const imported = /* @__PURE__ */ new Set();
@@ -33491,7 +33045,7 @@ function resolveImports(content, paths, imported) {
33491
33045
  for (const candidate of candidates) {
33492
33046
  if (existsSync25(candidate) && !imported.has(candidate)) {
33493
33047
  imported.add(candidate);
33494
- const fileContent = readFileSync24(candidate, "utf-8");
33048
+ const fileContent = readFileSync23(candidate, "utf-8");
33495
33049
  return resolveImports(fileContent, [dirname12(candidate), ...paths], imported);
33496
33050
  }
33497
33051
  }
@@ -33832,7 +33386,7 @@ var init_scss = __esm({
33832
33386
  /** Compile an SCSS file to CSS. */
33833
33387
  compileFile(filePath) {
33834
33388
  const absPath = resolve20(filePath);
33835
- const content = readFileSync24(absPath, "utf-8");
33389
+ const content = readFileSync23(absPath, "utf-8");
33836
33390
  const paths = [dirname12(absPath), ...this._importPaths];
33837
33391
  return compileString(content, paths, { ...this._variables });
33838
33392
  }
@@ -33855,7 +33409,7 @@ var init_scss = __esm({
33855
33409
  const imported = /* @__PURE__ */ new Set();
33856
33410
  let merged = "";
33857
33411
  for (const file of files) {
33858
- const content = readFileSync24(file, "utf-8");
33412
+ const content = readFileSync23(file, "utf-8");
33859
33413
  imported.add(file);
33860
33414
  merged += resolveImports(content, paths, imported) + "\n";
33861
33415
  }
@@ -33872,7 +33426,7 @@ var init_scss = __esm({
33872
33426
  if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
33873
33427
  let existing = null;
33874
33428
  try {
33875
- existing = existsSync25(absOutput) ? readFileSync24(absOutput, "utf-8") : null;
33429
+ existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
33876
33430
  } catch {
33877
33431
  existing = null;
33878
33432
  }
@@ -33956,10 +33510,10 @@ var init_mqttMessage = __esm({
33956
33510
  });
33957
33511
 
33958
33512
  // ../core/src/mqtt.ts
33959
- import net3 from "node:net";
33960
- import tls2 from "node:tls";
33513
+ import net2 from "node:net";
33514
+ import tls from "node:tls";
33961
33515
  import { randomBytes as randomBytes7 } from "node:crypto";
33962
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
33516
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
33963
33517
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
33964
33518
  var init_mqtt = __esm({
33965
33519
  "../core/src/mqtt.ts"() {
@@ -34424,10 +33978,10 @@ var init_mqtt = __esm({
34424
33978
  servername: this.host,
34425
33979
  rejectUnauthorized: this.tlsVerify
34426
33980
  };
34427
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
34428
- sock = tls2.connect(opts, () => settle(() => resolve31(sock)));
33981
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
33982
+ sock = tls.connect(opts, () => settle(() => resolve31(sock)));
34429
33983
  } else {
34430
- sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
33984
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
34431
33985
  }
34432
33986
  sock.once("error", (err) => {
34433
33987
  settle(() => {
@@ -34978,7 +34532,7 @@ import https from "node:https";
34978
34532
  import { URL as URL2 } from "node:url";
34979
34533
  import { randomBytes as randomBytes8 } from "node:crypto";
34980
34534
  import { promises as fsp, createWriteStream } from "node:fs";
34981
- import { basename as basename7 } from "node:path";
34535
+ import { basename as basename6 } from "node:path";
34982
34536
  import { pipeline } from "node:stream/promises";
34983
34537
  function sameOrigin(urlA, urlB) {
34984
34538
  try {
@@ -35268,7 +34822,7 @@ var init_api = __esm({
35268
34822
  error: err instanceof Error ? err.message : String(err)
35269
34823
  };
35270
34824
  }
35271
- uploadName = filename || basename7(filePath);
34825
+ uploadName = filename || basename6(filePath);
35272
34826
  } else {
35273
34827
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
35274
34828
  }
@@ -35624,6 +35178,777 @@ var init_api = __esm({
35624
35178
  }
35625
35179
  });
35626
35180
 
35181
+ // ../core/src/messenger.ts
35182
+ import net3 from "node:net";
35183
+ import tls2 from "node:tls";
35184
+ import { readFileSync as readFileSync25 } from "node:fs";
35185
+ import { basename as basename7 } from "node:path";
35186
+ import { randomUUID as randomUUID9 } from "node:crypto";
35187
+ function tlsRejectUnauthorized() {
35188
+ return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
35189
+ }
35190
+ function readResponse(socket) {
35191
+ return new Promise((resolve31, reject) => {
35192
+ let buffer = "";
35193
+ const onData = (chunk) => {
35194
+ buffer += chunk.toString("utf-8");
35195
+ const lines = buffer.split("\r\n");
35196
+ for (let i = 0; i < lines.length; i++) {
35197
+ const line = lines[i];
35198
+ if (line.length < 3) continue;
35199
+ const code = parseInt(line.substring(0, 3), 10);
35200
+ if (line.length >= 4 && line[3] === " ") {
35201
+ socket.removeListener("data", onData);
35202
+ socket.removeListener("error", onError);
35203
+ resolve31({ code, text: buffer.trim() });
35204
+ return;
35205
+ }
35206
+ }
35207
+ };
35208
+ const onError = (err) => {
35209
+ socket.removeListener("data", onData);
35210
+ reject(err);
35211
+ };
35212
+ socket.on("data", onData);
35213
+ socket.on("error", onError);
35214
+ });
35215
+ }
35216
+ function sendCommand(socket, command) {
35217
+ return new Promise((resolve31, reject) => {
35218
+ socket.write(command + "\r\n", "utf-8", (err) => {
35219
+ if (err) return reject(err);
35220
+ readResponse(socket).then(resolve31, reject);
35221
+ });
35222
+ });
35223
+ }
35224
+ function buildMimeMessage(options) {
35225
+ const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35226
+ const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35227
+ const hasAttachments = options.attachments && options.attachments.length > 0;
35228
+ const hasTextAlt = options.text !== void 0 && options.html;
35229
+ const lines = [];
35230
+ const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
35231
+ lines.push(`From: ${fromHeader}`);
35232
+ lines.push(`To: ${options.to.join(", ")}`);
35233
+ if (options.cc.length > 0) {
35234
+ lines.push(`Cc: ${options.cc.join(", ")}`);
35235
+ }
35236
+ lines.push(`Subject: ${options.subject}`);
35237
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
35238
+ lines.push(`Message-ID: <${options.messageId}>`);
35239
+ lines.push("MIME-Version: 1.0");
35240
+ if (options.replyTo) {
35241
+ lines.push(`Reply-To: ${options.replyTo}`);
35242
+ }
35243
+ if (options.headers) {
35244
+ for (const [key, value] of Object.entries(options.headers)) {
35245
+ lines.push(`${key}: ${value}`);
35246
+ }
35247
+ }
35248
+ if (hasAttachments) {
35249
+ lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
35250
+ lines.push("");
35251
+ lines.push(`--${boundary}`);
35252
+ if (hasTextAlt) {
35253
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35254
+ lines.push("");
35255
+ lines.push(`--${altBoundary}`);
35256
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35257
+ lines.push("Content-Transfer-Encoding: 7bit");
35258
+ lines.push("");
35259
+ lines.push(options.text);
35260
+ lines.push("");
35261
+ lines.push(`--${altBoundary}`);
35262
+ lines.push("Content-Type: text/html; charset=UTF-8");
35263
+ lines.push("Content-Transfer-Encoding: 7bit");
35264
+ lines.push("");
35265
+ lines.push(options.body);
35266
+ lines.push("");
35267
+ lines.push(`--${altBoundary}--`);
35268
+ } else {
35269
+ const contentType = options.html ? "text/html" : "text/plain";
35270
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35271
+ lines.push("Content-Transfer-Encoding: 7bit");
35272
+ lines.push("");
35273
+ lines.push(options.body);
35274
+ }
35275
+ for (const filePath of options.attachments) {
35276
+ const fileName = basename7(filePath);
35277
+ const fileData = readFileSync25(filePath);
35278
+ const base64Data = fileData.toString("base64");
35279
+ lines.push("");
35280
+ lines.push(`--${boundary}`);
35281
+ lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
35282
+ lines.push("Content-Transfer-Encoding: base64");
35283
+ lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
35284
+ lines.push("");
35285
+ for (let i = 0; i < base64Data.length; i += 76) {
35286
+ lines.push(base64Data.substring(i, i + 76));
35287
+ }
35288
+ }
35289
+ lines.push("");
35290
+ lines.push(`--${boundary}--`);
35291
+ } else if (hasTextAlt) {
35292
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35293
+ lines.push("");
35294
+ lines.push(`--${altBoundary}`);
35295
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35296
+ lines.push("Content-Transfer-Encoding: 7bit");
35297
+ lines.push("");
35298
+ lines.push(options.text);
35299
+ lines.push("");
35300
+ lines.push(`--${altBoundary}`);
35301
+ lines.push("Content-Type: text/html; charset=UTF-8");
35302
+ lines.push("Content-Transfer-Encoding: 7bit");
35303
+ lines.push("");
35304
+ lines.push(options.body);
35305
+ lines.push("");
35306
+ lines.push(`--${altBoundary}--`);
35307
+ } else {
35308
+ const contentType = options.html ? "text/html" : "text/plain";
35309
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35310
+ lines.push("");
35311
+ lines.push(options.body);
35312
+ }
35313
+ return lines.join("\r\n");
35314
+ }
35315
+ function imapQuote(s) {
35316
+ if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
35317
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
35318
+ }
35319
+ function imapReadLine(socket) {
35320
+ return new Promise((resolve31, reject) => {
35321
+ let buffer = "";
35322
+ const onData = (chunk) => {
35323
+ buffer += chunk.toString("utf-8");
35324
+ const nlIndex = buffer.indexOf("\r\n");
35325
+ if (nlIndex !== -1) {
35326
+ socket.removeListener("data", onData);
35327
+ socket.removeListener("error", onError);
35328
+ resolve31(buffer);
35329
+ }
35330
+ };
35331
+ const onError = (err) => {
35332
+ socket.removeListener("data", onData);
35333
+ reject(err);
35334
+ };
35335
+ socket.on("data", onData);
35336
+ socket.on("error", onError);
35337
+ });
35338
+ }
35339
+ function imapCommand(socket, command) {
35340
+ return new Promise((resolve31, reject) => {
35341
+ imapTagCounter++;
35342
+ const tag = `T${imapTagCounter}`;
35343
+ const fullCommand = `${tag} ${command}\r
35344
+ `;
35345
+ let buffer = "";
35346
+ const onData = (chunk) => {
35347
+ buffer += chunk.toString("utf-8");
35348
+ if (buffer.includes(`${tag} OK`)) {
35349
+ socket.removeListener("data", onData);
35350
+ socket.removeListener("error", onError);
35351
+ resolve31(buffer);
35352
+ return;
35353
+ }
35354
+ if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
35355
+ socket.removeListener("data", onData);
35356
+ socket.removeListener("error", onError);
35357
+ reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
35358
+ }
35359
+ };
35360
+ const onError = (err) => {
35361
+ socket.removeListener("data", onData);
35362
+ reject(err);
35363
+ };
35364
+ socket.on("data", onData);
35365
+ socket.on("error", onError);
35366
+ socket.write(fullCommand, "utf-8");
35367
+ });
35368
+ }
35369
+ function imapFail(method, err) {
35370
+ const e = err instanceof Error ? err : new Error(String(err));
35371
+ Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
35372
+ if (e instanceof MessengerConnectionError) return e;
35373
+ return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
35374
+ }
35375
+ function parseSearchResponse(response) {
35376
+ const match = response.match(/\* SEARCH (.+)/);
35377
+ if (!match) return [];
35378
+ return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
35379
+ }
35380
+ function parseHeaderResponse(uid, response) {
35381
+ const headers = {};
35382
+ const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
35383
+ if (headerBlock) {
35384
+ const lines = headerBlock[1].split(/\r\n/);
35385
+ let currentKey = "";
35386
+ for (const line of lines) {
35387
+ if (/^\s/.test(line) && currentKey) {
35388
+ headers[currentKey] += " " + line.trim();
35389
+ } else {
35390
+ const colonIdx = line.indexOf(":");
35391
+ if (colonIdx > 0) {
35392
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35393
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35394
+ }
35395
+ }
35396
+ }
35397
+ }
35398
+ const seen = /\\Seen/i.test(response);
35399
+ return {
35400
+ uid,
35401
+ subject: headers["subject"] ?? "",
35402
+ from: headers["from"] ?? "",
35403
+ to: headers["to"] ?? "",
35404
+ date: headers["date"] ?? "",
35405
+ snippet: "",
35406
+ seen
35407
+ };
35408
+ }
35409
+ function emptyFullMessage(uid) {
35410
+ return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
35411
+ }
35412
+ function parseFullMessage(uid, response) {
35413
+ const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
35414
+ const rawMessage = bodyMatch ? bodyMatch[2] : response;
35415
+ const headerEnd = rawMessage.indexOf("\r\n\r\n");
35416
+ const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
35417
+ const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
35418
+ const headers = {};
35419
+ const headerLines = headerSection.split(/\r\n/);
35420
+ let currentKey = "";
35421
+ for (const line of headerLines) {
35422
+ if (/^\s/.test(line) && currentKey) {
35423
+ headers[currentKey] += " " + line.trim();
35424
+ } else {
35425
+ const colonIdx = line.indexOf(":");
35426
+ if (colonIdx > 0) {
35427
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35428
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35429
+ }
35430
+ }
35431
+ }
35432
+ const contentType = headers["content-type"] ?? "text/plain";
35433
+ let bodyText = "";
35434
+ let bodyHtml = "";
35435
+ if (contentType.includes("multipart")) {
35436
+ const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
35437
+ if (boundaryMatch) {
35438
+ const boundary = boundaryMatch[1];
35439
+ const parts = bodySection.split("--" + boundary);
35440
+ for (const part of parts) {
35441
+ if (part.trim() === "" || part.trim() === "--") continue;
35442
+ const partHeaderEnd = part.indexOf("\r\n\r\n");
35443
+ const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
35444
+ const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
35445
+ if (partHeaders.includes("text/html")) {
35446
+ bodyHtml = partBody;
35447
+ } else if (partHeaders.includes("text/plain")) {
35448
+ bodyText = partBody;
35449
+ }
35450
+ }
35451
+ }
35452
+ } else if (contentType.includes("text/html")) {
35453
+ bodyHtml = bodySection;
35454
+ } else {
35455
+ bodyText = bodySection;
35456
+ }
35457
+ bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35458
+ bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35459
+ return {
35460
+ uid,
35461
+ subject: headers["subject"] ?? "",
35462
+ from: headers["from"] ?? "",
35463
+ to: headers["to"] ?? "",
35464
+ cc: headers["cc"] ?? "",
35465
+ date: headers["date"] ?? "",
35466
+ bodyText,
35467
+ bodyHtml,
35468
+ headers
35469
+ };
35470
+ }
35471
+ function createMessenger() {
35472
+ return new Messenger();
35473
+ }
35474
+ var MessengerConnectionError, Messenger, imapTagCounter;
35475
+ var init_messenger = __esm({
35476
+ "../core/src/messenger.ts"() {
35477
+ "use strict";
35478
+ init_dotenv();
35479
+ init_devMailbox();
35480
+ init_logger();
35481
+ MessengerConnectionError = class extends Error {
35482
+ constructor(message) {
35483
+ super(message);
35484
+ this.name = "MessengerConnectionError";
35485
+ }
35486
+ };
35487
+ Messenger = class {
35488
+ host;
35489
+ port;
35490
+ username;
35491
+ password;
35492
+ fromAddress;
35493
+ fromName;
35494
+ encryption;
35495
+ useTls;
35496
+ /** Whether an SMTP host was actually configured (see the constructor). */
35497
+ smtpConfigured = false;
35498
+ /** The local mailbox, present only when this messenger captures. */
35499
+ devMailbox = null;
35500
+ imapHost;
35501
+ imapPort;
35502
+ imapUser;
35503
+ imapPass;
35504
+ imapEncryption;
35505
+ constructor(options) {
35506
+ this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
35507
+ this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
35508
+ this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
35509
+ this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
35510
+ this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
35511
+ this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
35512
+ this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
35513
+ const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
35514
+ if (envEncryption) {
35515
+ this.encryption = envEncryption.toLowerCase();
35516
+ } else if (options?.useTls !== void 0) {
35517
+ this.encryption = options.useTls ? "tls" : "none";
35518
+ } else {
35519
+ this.encryption = "tls";
35520
+ }
35521
+ this.useTls = ["tls", "starttls"].includes(this.encryption);
35522
+ this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
35523
+ this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
35524
+ this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
35525
+ this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
35526
+ this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
35527
+ }
35528
+ /**
35529
+ * Read-only IMAP encryption mode for inspection / tests.
35530
+ * Returns one of "tls", "starttls", "none", "ssl".
35531
+ */
35532
+ getImapEncryption() {
35533
+ return this.imapEncryption;
35534
+ }
35535
+ /**
35536
+ * Send an email via SMTP.
35537
+ */
35538
+ /**
35539
+ * Should send() capture locally instead of talking to SMTP?
35540
+ *
35541
+ * Availability decides, not verbosity. With no SMTP host configured sending is
35542
+ * impossible, so simulate it into a folder rather than failing -- that is what
35543
+ * makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
35544
+ * even when a host IS configured.
35545
+ *
35546
+ * TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
35547
+ * must still be able to send, and the old `NODE_ENV !== "production"` clause
35548
+ * silently swallowed every staging email.
35549
+ */
35550
+ shouldCapture() {
35551
+ if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
35552
+ return !this.smtpConfigured;
35553
+ }
35554
+ /** The local mailbox, created on first capture and reused after. */
35555
+ getDevMailbox() {
35556
+ if (this.devMailbox === null) {
35557
+ this.devMailbox = new DevMailbox();
35558
+ }
35559
+ return this.devMailbox;
35560
+ }
35561
+ async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
35562
+ const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
35563
+ const toList = Array.isArray(options.to) ? options.to : [options.to];
35564
+ const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
35565
+ const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
35566
+ const allRecipients = [...toList, ...ccList, ...bccList];
35567
+ if (this.shouldCapture()) {
35568
+ return this.getDevMailbox().capture(
35569
+ to,
35570
+ subject,
35571
+ body,
35572
+ html,
35573
+ text,
35574
+ ccList,
35575
+ bccList,
35576
+ replyTo,
35577
+ attachments,
35578
+ this.fromAddress || void 0
35579
+ );
35580
+ }
35581
+ const messageId = `${randomUUID9()}@${this.host}`;
35582
+ if (allRecipients.length === 0) {
35583
+ return { success: false, message: "No recipients specified" };
35584
+ }
35585
+ if (!this.fromAddress) {
35586
+ return { success: false, message: "No from address configured" };
35587
+ }
35588
+ try {
35589
+ let socket;
35590
+ if (this.port === 465) {
35591
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35592
+ await new Promise((resolve31, reject) => {
35593
+ socket.once("secureConnect", resolve31);
35594
+ socket.once("error", reject);
35595
+ });
35596
+ } else {
35597
+ socket = net3.createConnection({ host: this.host, port: this.port });
35598
+ await new Promise((resolve31, reject) => {
35599
+ socket.once("connect", resolve31);
35600
+ socket.once("error", reject);
35601
+ });
35602
+ }
35603
+ const greeting = await readResponse(socket);
35604
+ if (greeting.code !== 220) {
35605
+ socket.destroy();
35606
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35607
+ }
35608
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35609
+ if (ehlo.code !== 250) {
35610
+ socket.destroy();
35611
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35612
+ }
35613
+ if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
35614
+ const starttls = await sendCommand(socket, "STARTTLS");
35615
+ if (starttls.code !== 220) {
35616
+ socket.destroy();
35617
+ return { success: false, message: `STARTTLS failed: ${starttls.text}` };
35618
+ }
35619
+ const plainSocket = socket;
35620
+ socket = tls2.connect(
35621
+ { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
35622
+ );
35623
+ await new Promise((resolve31, reject) => {
35624
+ socket.once("secureConnect", resolve31);
35625
+ socket.once("error", reject);
35626
+ });
35627
+ const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
35628
+ if (ehlo2.code !== 250) {
35629
+ socket.destroy();
35630
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
35631
+ }
35632
+ }
35633
+ if (this.username && this.password) {
35634
+ const auth = await sendCommand(socket, "AUTH LOGIN");
35635
+ if (auth.code !== 334) {
35636
+ socket.destroy();
35637
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
35638
+ }
35639
+ const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
35640
+ if (userResp.code !== 334) {
35641
+ socket.destroy();
35642
+ return { success: false, message: `AUTH username failed: ${userResp.text}` };
35643
+ }
35644
+ const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
35645
+ if (passResp.code !== 235) {
35646
+ socket.destroy();
35647
+ return { success: false, message: `AUTH password failed: ${passResp.text}` };
35648
+ }
35649
+ }
35650
+ const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
35651
+ if (mailFrom.code !== 250) {
35652
+ socket.destroy();
35653
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
35654
+ }
35655
+ for (const recipient of allRecipients) {
35656
+ const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
35657
+ if (rcpt.code !== 250 && rcpt.code !== 251) {
35658
+ socket.destroy();
35659
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
35660
+ }
35661
+ }
35662
+ const dataCmd = await sendCommand(socket, "DATA");
35663
+ if (dataCmd.code !== 354) {
35664
+ socket.destroy();
35665
+ return { success: false, message: `DATA failed: ${dataCmd.text}` };
35666
+ }
35667
+ const mimeMessage = buildMimeMessage({
35668
+ from: this.fromAddress,
35669
+ fromName: this.fromName,
35670
+ to: toList,
35671
+ cc: ccList,
35672
+ subject: options.subject,
35673
+ body: options.body,
35674
+ html: options.html ?? false,
35675
+ text: options.text,
35676
+ replyTo: options.replyTo,
35677
+ attachments: options.attachments,
35678
+ headers: options.headers,
35679
+ messageId
35680
+ });
35681
+ const endData = await sendCommand(socket, mimeMessage + "\r\n.");
35682
+ if (endData.code !== 250) {
35683
+ socket.destroy();
35684
+ return { success: false, message: `Message delivery failed: ${endData.text}` };
35685
+ }
35686
+ await sendCommand(socket, "QUIT");
35687
+ socket.destroy();
35688
+ return { success: true, message: "Email sent successfully", id: messageId };
35689
+ } catch (err) {
35690
+ const errMsg = err instanceof Error ? err.message : String(err);
35691
+ return { success: false, message: `SMTP error: ${errMsg}` };
35692
+ }
35693
+ }
35694
+ /**
35695
+ * Test the SMTP connection without sending an email.
35696
+ */
35697
+ async testConnection() {
35698
+ try {
35699
+ let socket;
35700
+ if (this.port === 465) {
35701
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35702
+ await new Promise((resolve31, reject) => {
35703
+ socket.once("secureConnect", resolve31);
35704
+ socket.once("error", reject);
35705
+ });
35706
+ } else {
35707
+ socket = net3.createConnection({ host: this.host, port: this.port });
35708
+ await new Promise((resolve31, reject) => {
35709
+ socket.once("connect", resolve31);
35710
+ socket.once("error", reject);
35711
+ });
35712
+ }
35713
+ const greeting = await readResponse(socket);
35714
+ if (greeting.code !== 220) {
35715
+ socket.destroy();
35716
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35717
+ }
35718
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35719
+ if (ehlo.code !== 250) {
35720
+ socket.destroy();
35721
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35722
+ }
35723
+ await sendCommand(socket, "QUIT");
35724
+ socket.destroy();
35725
+ return { success: true, message: `Connected to ${this.host}:${this.port}` };
35726
+ } catch (err) {
35727
+ const errMsg = err instanceof Error ? err.message : String(err);
35728
+ return { success: false, message: `Connection failed: ${errMsg}` };
35729
+ }
35730
+ }
35731
+ // ── IMAP (Read) ────────────────────────────────────────────
35732
+ /**
35733
+ * Connect to the IMAP server via raw TCP/TLS.
35734
+ * Returns the socket and reads the greeting.
35735
+ */
35736
+ async imapConnect() {
35737
+ if (!this.imapHost) {
35738
+ throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
35739
+ }
35740
+ let socket;
35741
+ const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
35742
+ if (useTls) {
35743
+ socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
35744
+ await new Promise((resolve31, reject) => {
35745
+ socket.once("secureConnect", resolve31);
35746
+ socket.once("error", reject);
35747
+ });
35748
+ } else {
35749
+ socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
35750
+ await new Promise((resolve31, reject) => {
35751
+ socket.once("connect", resolve31);
35752
+ socket.once("error", reject);
35753
+ });
35754
+ }
35755
+ await imapReadLine(socket);
35756
+ if (this.imapUser && this.imapPass) {
35757
+ const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
35758
+ if (!loginResp.includes("OK")) {
35759
+ socket.destroy();
35760
+ throw new Error(`IMAP login failed: ${loginResp}`);
35761
+ }
35762
+ }
35763
+ return socket;
35764
+ }
35765
+ /**
35766
+ * Disconnect from IMAP cleanly.
35767
+ */
35768
+ async imapDisconnect(socket) {
35769
+ try {
35770
+ await imapCommand(socket, "LOGOUT");
35771
+ } catch {
35772
+ }
35773
+ socket.destroy();
35774
+ }
35775
+ /**
35776
+ * Fetch latest messages from a folder.
35777
+ * Returns list of message summaries.
35778
+ */
35779
+ async inbox(limit = 20, offset = 0, folder = "INBOX") {
35780
+ let socket;
35781
+ try {
35782
+ socket = await this.imapConnect();
35783
+ } catch (err) {
35784
+ throw imapFail("inbox", err);
35785
+ }
35786
+ try {
35787
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35788
+ const searchResp = await imapCommand(socket, "SEARCH ALL");
35789
+ const uids = parseSearchResponse(searchResp);
35790
+ if (uids.length === 0) return [];
35791
+ uids.reverse();
35792
+ const selected = uids.slice(offset, offset + limit);
35793
+ if (selected.length === 0) return [];
35794
+ const messages = [];
35795
+ for (const uid of selected) {
35796
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35797
+ messages.push(parseHeaderResponse(uid, fetchResp));
35798
+ }
35799
+ return messages;
35800
+ } catch (err) {
35801
+ throw imapFail("inbox", err);
35802
+ } finally {
35803
+ await this.imapDisconnect(socket);
35804
+ }
35805
+ }
35806
+ /**
35807
+ * Read a single message by sequence number or UID.
35808
+ */
35809
+ async read(uid, folder = "INBOX") {
35810
+ let socket;
35811
+ try {
35812
+ socket = await this.imapConnect();
35813
+ } catch (err) {
35814
+ throw imapFail("read", err);
35815
+ }
35816
+ try {
35817
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35818
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
35819
+ if (!/\{\d+\}/.test(fetchResp)) {
35820
+ return emptyFullMessage(uid);
35821
+ }
35822
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35823
+ return parseFullMessage(uid, fetchResp);
35824
+ } catch (err) {
35825
+ throw imapFail("read", err);
35826
+ } finally {
35827
+ await this.imapDisconnect(socket);
35828
+ }
35829
+ }
35830
+ /**
35831
+ * Search messages using IMAP search criteria.
35832
+ */
35833
+ async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
35834
+ const criteria = ["ALL"];
35835
+ if (subject) criteria.push(`SUBJECT "${subject}"`);
35836
+ if (sender) criteria.push(`FROM "${sender}"`);
35837
+ if (since) criteria.push(`SINCE ${since}`);
35838
+ if (before) criteria.push(`BEFORE ${before}`);
35839
+ if (unseenOnly) criteria.push("UNSEEN");
35840
+ const query = criteria.join(" ");
35841
+ let socket;
35842
+ try {
35843
+ socket = await this.imapConnect();
35844
+ } catch (err) {
35845
+ throw imapFail("search", err);
35846
+ }
35847
+ try {
35848
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35849
+ const searchResp = await imapCommand(socket, `SEARCH ${query}`);
35850
+ const uids = parseSearchResponse(searchResp);
35851
+ if (uids.length === 0) return [];
35852
+ uids.reverse();
35853
+ const messages = [];
35854
+ for (const uid of uids.slice(0, limit)) {
35855
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35856
+ messages.push(parseHeaderResponse(uid, fetchResp));
35857
+ }
35858
+ return messages;
35859
+ } catch (err) {
35860
+ throw imapFail("search", err);
35861
+ } finally {
35862
+ await this.imapDisconnect(socket);
35863
+ }
35864
+ }
35865
+ /**
35866
+ * Delete a message by UID.
35867
+ */
35868
+ async deleteMessage(uid, folder = "INBOX") {
35869
+ const socket = await this.imapConnect();
35870
+ try {
35871
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35872
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
35873
+ await imapCommand(socket, "EXPUNGE");
35874
+ } finally {
35875
+ await this.imapDisconnect(socket);
35876
+ }
35877
+ }
35878
+ /**
35879
+ * Mark a message as read.
35880
+ */
35881
+ async markRead(uid, folder = "INBOX") {
35882
+ const socket = await this.imapConnect();
35883
+ try {
35884
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35885
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35886
+ } finally {
35887
+ await this.imapDisconnect(socket);
35888
+ }
35889
+ }
35890
+ /**
35891
+ * Count unseen messages in a folder.
35892
+ */
35893
+ async unread(folder = "INBOX") {
35894
+ let socket;
35895
+ try {
35896
+ socket = await this.imapConnect();
35897
+ } catch (err) {
35898
+ throw imapFail("unread", err);
35899
+ }
35900
+ try {
35901
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35902
+ const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
35903
+ return parseSearchResponse(searchResp).length;
35904
+ } catch (err) {
35905
+ throw imapFail("unread", err);
35906
+ } finally {
35907
+ await this.imapDisconnect(socket);
35908
+ }
35909
+ }
35910
+ /**
35911
+ * List available IMAP folders/mailboxes.
35912
+ */
35913
+ async folders() {
35914
+ let socket;
35915
+ try {
35916
+ socket = await this.imapConnect();
35917
+ } catch (err) {
35918
+ throw imapFail("folders", err);
35919
+ }
35920
+ try {
35921
+ const resp = await imapCommand(socket, 'LIST "" "*"');
35922
+ const result = [];
35923
+ for (const line of resp.split("\r\n")) {
35924
+ const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
35925
+ if (m) result.push(m[1]);
35926
+ }
35927
+ return result;
35928
+ } catch (err) {
35929
+ throw imapFail("folders", err);
35930
+ } finally {
35931
+ await this.imapDisconnect(socket);
35932
+ }
35933
+ }
35934
+ /**
35935
+ * Test IMAP connectivity without reading.
35936
+ */
35937
+ async testImapConnection() {
35938
+ try {
35939
+ const socket = await this.imapConnect();
35940
+ await this.imapDisconnect(socket);
35941
+ return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
35942
+ } catch (err) {
35943
+ const errMsg = err instanceof Error ? err.message : String(err);
35944
+ return { success: false, message: `IMAP connection failed: ${errMsg}` };
35945
+ }
35946
+ }
35947
+ };
35948
+ imapTagCounter = 0;
35949
+ }
35950
+ });
35951
+
35627
35952
  // ../core/src/wsdl.ts
35628
35953
  function escapeXml(value) {
35629
35954
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");