tina4-nodejs 3.13.92 → 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 (134) 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 +1260 -969
  5. package/packages/core/dist/index.js +1260 -969
  6. package/packages/core/src/devMailbox.ts +20 -44
  7. package/packages/core/src/index.ts +2 -2
  8. package/packages/core/src/messenger.ts +72 -0
  9. package/packages/core/src/queueBackends/kafkaBackend.ts +108 -12
  10. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  11. package/packages/core/src/sessionHandlers/mongoClient.ts +9 -3
  12. package/packages/core/src/sessionHandlers/redisHandler.ts +18 -5
  13. package/packages/core/src/sessionHandlers/respClient.ts +5 -1
  14. package/packages/frond/dist/index.js +74 -31
  15. package/packages/frond/src/engine.ts +99 -33
  16. package/packages/orm/dist/index.js +3055 -2764
  17. package/packages/orm/src/adapters/sqlite.ts +4 -1
  18. package/packages/orm/src/database.ts +108 -8
  19. package/types/cli/src/bin.d.ts +92 -0
  20. package/types/cli/src/commands/build.d.ts +2 -0
  21. package/types/cli/src/commands/generate.d.ts +47 -0
  22. package/types/cli/src/commands/init.d.ts +1 -0
  23. package/types/cli/src/commands/metrics.d.ts +6 -0
  24. package/types/cli/src/commands/migrate.d.ts +1 -0
  25. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  26. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  27. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  28. package/types/cli/src/commands/queue.d.ts +20 -0
  29. package/types/cli/src/commands/routes.d.ts +1 -0
  30. package/types/cli/src/commands/seed.d.ts +1 -0
  31. package/types/cli/src/commands/serve.d.ts +6 -0
  32. package/types/cli/src/commands/test.d.ts +1 -0
  33. package/types/core/src/ai.d.ts +64 -0
  34. package/types/core/src/api.d.ts +262 -0
  35. package/types/core/src/auth.d.ts +154 -0
  36. package/types/core/src/authGate.d.ts +20 -0
  37. package/types/core/src/background.d.ts +34 -0
  38. package/types/core/src/cache.d.ts +160 -0
  39. package/types/core/src/constants.d.ts +38 -0
  40. package/types/core/src/container.d.ts +44 -0
  41. package/types/core/src/context/chunker.d.ts +31 -0
  42. package/types/core/src/context/index.d.ts +93 -0
  43. package/types/core/src/devAdmin.d.ts +179 -0
  44. package/types/core/src/devMailbox.d.ts +54 -0
  45. package/types/core/src/docs.d.ts +141 -0
  46. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  47. package/types/core/src/dotenv.d.ts +65 -0
  48. package/types/core/src/env.d.ts +28 -0
  49. package/types/core/src/errorOverlay.d.ts +36 -0
  50. package/types/core/src/events.d.ts +75 -0
  51. package/types/core/src/fakeData.d.ts +55 -0
  52. package/types/core/src/feedback.d.ts +90 -0
  53. package/types/core/src/graphql.d.ts +207 -0
  54. package/types/core/src/health.d.ts +22 -0
  55. package/types/core/src/htmlElement.d.ts +75 -0
  56. package/types/core/src/i18n.d.ts +37 -0
  57. package/types/core/src/index.d.ts +93 -0
  58. package/types/core/src/job.d.ts +39 -0
  59. package/types/core/src/logger.d.ts +123 -0
  60. package/types/core/src/mcp.d.ts +248 -0
  61. package/types/core/src/messenger.d.ts +191 -0
  62. package/types/core/src/metrics.d.ts +77 -0
  63. package/types/core/src/middleware.d.ts +207 -0
  64. package/types/core/src/mqtt.d.ts +257 -0
  65. package/types/core/src/mqttMessage.d.ts +67 -0
  66. package/types/core/src/plan.d.ts +96 -0
  67. package/types/core/src/projectIndex.d.ts +56 -0
  68. package/types/core/src/queue.d.ts +219 -0
  69. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  70. package/types/core/src/queueBackends/liteBackend.d.ts +119 -0
  71. package/types/core/src/queueBackends/mongoBackend.d.ts +97 -0
  72. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  73. package/types/core/src/rateLimiter.d.ts +49 -0
  74. package/types/core/src/request.d.ts +25 -0
  75. package/types/core/src/response.d.ts +28 -0
  76. package/types/core/src/routeDiscovery.d.ts +12 -0
  77. package/types/core/src/router.d.ts +355 -0
  78. package/types/core/src/scss.d.ts +19 -0
  79. package/types/core/src/server.d.ts +131 -0
  80. package/types/core/src/service.d.ts +115 -0
  81. package/types/core/src/session.d.ts +256 -0
  82. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  83. package/types/core/src/sessionHandlers/databaseHandler.d.ts +42 -0
  84. package/types/core/src/sessionHandlers/mongoClient.d.ts +24 -0
  85. package/types/core/src/sessionHandlers/mongoHandler.d.ts +61 -0
  86. package/types/core/src/sessionHandlers/redisHandler.d.ts +60 -0
  87. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  88. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  89. package/types/core/src/static.d.ts +2 -0
  90. package/types/core/src/test.d.ts +94 -0
  91. package/types/core/src/testClient.d.ts +36 -0
  92. package/types/core/src/testing.d.ts +58 -0
  93. package/types/core/src/types.d.ts +219 -0
  94. package/types/core/src/validator.d.ts +52 -0
  95. package/types/core/src/websocket.d.ts +376 -0
  96. package/types/core/src/websocketBackplane.d.ts +166 -0
  97. package/types/core/src/websocketConnection.d.ts +54 -0
  98. package/types/core/src/wsdl.d.ts +101 -0
  99. package/types/frond/src/engine.d.ts +263 -0
  100. package/types/frond/src/index.d.ts +2 -0
  101. package/types/orm/src/adapters/firebird.d.ts +138 -0
  102. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  103. package/types/orm/src/adapters/mssql.d.ts +70 -0
  104. package/types/orm/src/adapters/mysql.d.ts +66 -0
  105. package/types/orm/src/adapters/odbc.d.ts +97 -0
  106. package/types/orm/src/adapters/postgres.d.ts +85 -0
  107. package/types/orm/src/adapters/sqlite.d.ts +56 -0
  108. package/types/orm/src/autoCrud.d.ts +73 -0
  109. package/types/orm/src/baseModel.d.ts +391 -0
  110. package/types/orm/src/cachedDatabase.d.ts +177 -0
  111. package/types/orm/src/database.d.ts +609 -0
  112. package/types/orm/src/databaseResult.d.ts +85 -0
  113. package/types/orm/src/docstore.d.ts +182 -0
  114. package/types/orm/src/fakeData.d.ts +22 -0
  115. package/types/orm/src/index.d.ts +40 -0
  116. package/types/orm/src/migration.d.ts +275 -0
  117. package/types/orm/src/model.d.ts +7 -0
  118. package/types/orm/src/query.d.ts +14 -0
  119. package/types/orm/src/queryBuilder.d.ts +173 -0
  120. package/types/orm/src/realtime/index.d.ts +7 -0
  121. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  122. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  123. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  124. package/types/orm/src/realtime/models/message.d.ts +36 -0
  125. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  126. package/types/orm/src/realtime/realtime.d.ts +24 -0
  127. package/types/orm/src/realtime/storage.d.ts +61 -0
  128. package/types/orm/src/seeder.d.ts +118 -0
  129. package/types/orm/src/sqlTranslator.d.ts +134 -0
  130. package/types/orm/src/types.d.ts +138 -0
  131. package/types/orm/src/validation.d.ts +6 -0
  132. package/types/swagger/src/generator.d.ts +46 -0
  133. package/types/swagger/src/index.d.ts +2 -0
  134. package/types/swagger/src/ui.d.ts +11 -0
@@ -1297,11 +1297,14 @@ var init_sqlite = __esm({
1297
1297
  const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
1298
1298
  const rows = this.db.prepare(pragma).all();
1299
1299
  return rows.map((r) => ({
1300
+ // PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
1301
+ // key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
1302
+ // reported only the first column of a composite key.
1300
1303
  name: r.name,
1301
1304
  type: r.type,
1302
1305
  nullable: r.notnull === 0,
1303
1306
  default: r.dflt_value,
1304
- primaryKey: r.pk === 1
1307
+ primaryKey: Number(r.pk) > 0
1305
1308
  }));
1306
1309
  }
1307
1310
  lastInsertId() {
@@ -4297,6 +4300,8 @@ var init_database = __esm({
4297
4300
  poolIndex = 0;
4298
4301
  /** Factory for creating new adapters (used by pool) */
4299
4302
  adapterFactory = null;
4303
+ /** table -> primary-key column name (or null), introspected once */
4304
+ _pkCache = /* @__PURE__ */ new Map();
4300
4305
  /**
4301
4306
  * Whether a standalone write auto-commits. ON by default — a write made
4302
4307
  * outside an explicit transaction commits on its own connection before
@@ -4555,29 +4560,112 @@ var init_database = __esm({
4555
4560
  }
4556
4561
  return result;
4557
4562
  }
4558
- /** Update rows in a table matching filter. */
4563
+ /**
4564
+ * The table's primary-key column, introspected once and cached.
4565
+ *
4566
+ * Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
4567
+ * primaryKey per column on every adapter. Resolves to null when the table has
4568
+ * no primary key or cannot be introspected.
4569
+ */
4570
+ async primaryKey(table2) {
4571
+ if (!this._pkCache.has(table2)) {
4572
+ let pk = [];
4573
+ try {
4574
+ const columns = await this.getColumns(table2);
4575
+ pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
4576
+ } catch {
4577
+ pk = [];
4578
+ }
4579
+ this._pkCache.set(table2, pk);
4580
+ }
4581
+ return this._pkCache.get(table2) ?? [];
4582
+ }
4583
+ /**
4584
+ * A failed write must be loud.
4585
+ *
4586
+ * The adapters catch a SQL error and return { success: false, affectedRows: 0 },
4587
+ * so a filterless update produced invalid SQL ("... WHERE ") and reported
4588
+ * nothing rather than raising. A caller who does not inspect the result
4589
+ * believes the write landed (audit feature 4, P1).
4590
+ */
4591
+ static assertWrote(result, verb, table2) {
4592
+ if (result && result.success === false) {
4593
+ throw new Error(
4594
+ `${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
4595
+ );
4596
+ }
4597
+ return result;
4598
+ }
4599
+ /**
4600
+ * Update rows. A write with no filter is an error, not a full-table write.
4601
+ *
4602
+ * With no explicit filter the primary key is taken out of `data` and used as
4603
+ * the WHERE clause. With neither a filter nor a primary key in `data` this
4604
+ * throws rather than silently changing nothing (audit feature 4, P1).
4605
+ */
4559
4606
  async update(table2, data, filter, params) {
4607
+ let effectiveFilter = filter ?? {};
4608
+ let effectiveData = data;
4609
+ if (Object.keys(effectiveFilter).length === 0) {
4610
+ const pkColumns = await this.primaryKey(table2);
4611
+ const missing = pkColumns.filter((c) => !(c in data));
4612
+ if (pkColumns.length === 0 || missing.length > 0) {
4613
+ throw new Error(
4614
+ `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}).`
4615
+ );
4616
+ }
4617
+ effectiveData = { ...data };
4618
+ const keyed = {};
4619
+ for (const col of pkColumns) {
4620
+ keyed[col] = effectiveData[col];
4621
+ delete effectiveData[col];
4622
+ }
4623
+ if (Object.keys(effectiveData).length === 0) {
4624
+ throw new Error(
4625
+ `update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
4626
+ );
4627
+ }
4628
+ effectiveFilter = keyed;
4629
+ }
4560
4630
  const adapter = this.getNextAdapter();
4561
- const result = adapter.updateAsync ? await adapter.updateAsync(table2, data, filter ?? {}, params) : adapter.update(table2, data, filter ?? {}, params);
4631
+ const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
4562
4632
  if (this.autoCommit && !this.inExplicitTransaction()) {
4563
4633
  try {
4564
4634
  await adapterCommit(adapter);
4565
4635
  } catch {
4566
4636
  }
4567
4637
  }
4568
- return result;
4638
+ return _Database.assertWrote(result, "update", table2);
4569
4639
  }
4570
- /** Delete rows from a table matching filter. */
4640
+ /** Delete rows. A filterless delete throws; use truncate() to empty a table. */
4571
4641
  async delete(table2, filter, params) {
4642
+ const effectiveFilter = filter ?? {};
4643
+ if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
4644
+ throw new Error(
4645
+ `delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
4646
+ );
4647
+ }
4572
4648
  const adapter = this.getNextAdapter();
4573
- const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, filter ?? {}, params) : adapter.delete(table2, filter ?? {}, params);
4649
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
4574
4650
  if (this.autoCommit && !this.inExplicitTransaction()) {
4575
4651
  try {
4576
4652
  await adapterCommit(adapter);
4577
4653
  } catch {
4578
4654
  }
4579
4655
  }
4580
- return result;
4656
+ return _Database.assertWrote(result, "delete", table2);
4657
+ }
4658
+ /** Remove every row. The explicit spelling of a whole-table delete. */
4659
+ async truncate(table2) {
4660
+ const adapter = this.getNextAdapter();
4661
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
4662
+ if (this.autoCommit && !this.inExplicitTransaction()) {
4663
+ try {
4664
+ await adapterCommit(adapter);
4665
+ } catch {
4666
+ }
4667
+ }
4668
+ return _Database.assertWrote(result, "truncate", table2);
4581
4669
  }
4582
4670
  /** Close all database connections (pool or single). */
4583
4671
  close() {
@@ -13826,7 +13914,7 @@ function _generateFormToken(descriptor = "") {
13826
13914
  function _generateFormTokenValue(descriptor = "") {
13827
13915
  return new SafeString(_buildFormTokenJwt(descriptor));
13828
13916
  }
13829
- 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;
13917
+ 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;
13830
13918
  var init_engine = __esm({
13831
13919
  "../frond/src/engine.ts"() {
13832
13920
  "use strict";
@@ -13869,6 +13957,29 @@ var init_engine = __esm({
13869
13957
  "endset",
13870
13958
  "endspaceless"
13871
13959
  ]);
13960
+ GATEABLE_TAGS = /* @__PURE__ */ new Set([
13961
+ "autoescape",
13962
+ "cache",
13963
+ "for",
13964
+ "from",
13965
+ "if",
13966
+ "import",
13967
+ "include",
13968
+ "live",
13969
+ "macro",
13970
+ "set",
13971
+ "spaceless"
13972
+ ]);
13973
+ BLOCK_TAG_ENDS = {
13974
+ autoescape: "endautoescape",
13975
+ cache: "endcache",
13976
+ for: "endfor",
13977
+ if: "endif",
13978
+ live: "endlive",
13979
+ macro: "endmacro",
13980
+ set: "endset",
13981
+ spaceless: "endspaceless"
13982
+ };
13872
13983
  JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
13873
13984
  JSON_UNSAFE_MAP = {
13874
13985
  "<": "\\u003c",
@@ -14504,42 +14615,27 @@ var init_engine = __esm({
14504
14615
  if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
14505
14616
  tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
14506
14617
  }
14507
- if (tag === "if") {
14508
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("if")) {
14509
- const skip = this.skipBlock(tokens, i, "if", "endif");
14510
- i = skip;
14511
- } else {
14512
- const [result, skip] = this.handleIf(tokens, i, context);
14513
- output.push(result);
14514
- i = skip;
14515
- }
14618
+ if (!this.tagPermitted(tag)) {
14619
+ i = this.skipDeniedTag(tokens, i, tag, content);
14620
+ } else if (tag === "if") {
14621
+ const [result, skip] = this.handleIf(tokens, i, context);
14622
+ output.push(result);
14623
+ i = skip;
14516
14624
  } else if (tag === "for") {
14517
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("for")) {
14518
- const skip = this.skipBlock(tokens, i, "for", "endfor");
14519
- i = skip;
14520
- } else {
14521
- const [result, skip] = this.handleFor(tokens, i, context);
14522
- output.push(result);
14523
- i = skip;
14524
- }
14625
+ const [result, skip] = this.handleFor(tokens, i, context);
14626
+ output.push(result);
14627
+ i = skip;
14525
14628
  } else if (tag === "set") {
14526
- const isBlockSet = !content.includes("=");
14527
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
14528
- i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
14529
- } else if (isBlockSet) {
14629
+ if (!content.includes("=")) {
14530
14630
  i = this.handleSetBlock(tokens, i, context);
14531
14631
  } else {
14532
14632
  this.handleSet(content, context);
14533
14633
  i++;
14534
14634
  }
14535
14635
  } else if (tag === "include") {
14536
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("include")) {
14537
- i++;
14538
- } else {
14539
- const result = this.handleInclude(content, context);
14540
- output.push(result);
14541
- i++;
14542
- }
14636
+ const result = this.handleInclude(content, context);
14637
+ output.push(result);
14638
+ i++;
14543
14639
  } else if (tag === "macro") {
14544
14640
  const skip = this.handleMacro(tokens, i, context);
14545
14641
  i = skip;
@@ -14584,6 +14680,41 @@ var init_engine = __esm({
14584
14680
  }
14585
14681
  return output.join("");
14586
14682
  }
14683
+ /**
14684
+ * May this filter RUN under the current sandbox?
14685
+ *
14686
+ * The escaping decision has to ask this rather than read the filter name out of
14687
+ * the source. Node carries safety as a FLAG rather than as a value-level marker
14688
+ * (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
14689
+ * produced only by actually running the filter), so here the name alone was
14690
+ * enough to suppress auto-escaping even when the filter was denied and skipped.
14691
+ */
14692
+ filterPermitted(name) {
14693
+ if (!this._sandbox || this._allowedFilters === null) return true;
14694
+ return this._allowedFilters.has(name);
14695
+ }
14696
+ /**
14697
+ * May this tag run under the current sandbox?
14698
+ *
14699
+ * One gate for every tag, so the allow-list governs the whole tag vocabulary
14700
+ * instead of the four names that happened to be checked individually.
14701
+ */
14702
+ tagPermitted(tag) {
14703
+ if (!this._sandbox || this._allowedTags === null) return true;
14704
+ if (!GATEABLE_TAGS.has(tag)) return true;
14705
+ return this._allowedTags.has(tag);
14706
+ }
14707
+ /**
14708
+ * Consume a denied tag WITHOUT running it, returning the index past its body.
14709
+ *
14710
+ * Advancing a single token past a body-owning tag would leave the body's tokens
14711
+ * to render at the TOP level, leaking exactly the content the sandbox denied.
14712
+ */
14713
+ skipDeniedTag(tokens, start2, tag, content) {
14714
+ const closeTag = BLOCK_TAG_ENDS[tag];
14715
+ if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
14716
+ return this.skipBlock(tokens, start2, tag, closeTag);
14717
+ }
14587
14718
  skipBlock(tokens, start2, openTag, closeTag) {
14588
14719
  let depth = 0;
14589
14720
  let i = start2 + 1;
@@ -14591,7 +14722,7 @@ var init_engine = __esm({
14591
14722
  if (tokens[i][0] === "BLOCK") {
14592
14723
  const [content] = stripTag(tokens[i][1]);
14593
14724
  const tag = content.split(/\s+/)[0] || "";
14594
- if (tag === openTag) depth++;
14725
+ if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
14595
14726
  else if (tag === closeTag) {
14596
14727
  if (depth === 0) return i + 1;
14597
14728
  depth--;
@@ -14776,11 +14907,11 @@ var init_engine = __esm({
14776
14907
  for (const [fname, rawArgs] of filters) {
14777
14908
  const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
14778
14909
  if (fname === "raw" || fname === "safe") {
14779
- isSafe = true;
14910
+ if (this.filterPermitted(fname)) isSafe = true;
14780
14911
  continue;
14781
14912
  }
14782
14913
  if (fname === "escape" || fname === "e") {
14783
- isSafe = true;
14914
+ if (this.filterPermitted(fname)) isSafe = true;
14784
14915
  }
14785
14916
  if (this._sandbox && this._allowedFilters !== null) {
14786
14917
  if (!this._allowedFilters.has(fname)) {
@@ -16591,861 +16722,131 @@ var init_rateLimiter = __esm({
16591
16722
  }
16592
16723
  });
16593
16724
 
16594
- // src/messenger.ts
16595
- import net2 from "node:net";
16596
- import tls from "node:tls";
16597
- import { readFileSync as readFileSync8 } from "node:fs";
16598
- import { basename as basename2 } from "node:path";
16725
+ // src/devMailbox.ts
16726
+ import { mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4, existsSync as existsSync10 } from "node:fs";
16727
+ import { join as join12 } from "node:path";
16599
16728
  import { randomUUID as randomUUID2 } from "node:crypto";
16600
- function tlsRejectUnauthorized() {
16601
- return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
16602
- }
16603
- function readResponse(socket) {
16604
- return new Promise((resolve21, reject) => {
16605
- let buffer = "";
16606
- const onData = (chunk) => {
16607
- buffer += chunk.toString("utf-8");
16608
- const lines = buffer.split("\r\n");
16609
- for (let i = 0; i < lines.length; i++) {
16610
- const line = lines[i];
16611
- if (line.length < 3) continue;
16612
- const code = parseInt(line.substring(0, 3), 10);
16613
- if (line.length >= 4 && line[3] === " ") {
16614
- socket.removeListener("data", onData);
16615
- socket.removeListener("error", onError);
16616
- resolve21({ code, text: buffer.trim() });
16617
- return;
16618
- }
16619
- }
16620
- };
16621
- const onError = (err) => {
16622
- socket.removeListener("data", onData);
16623
- reject(err);
16624
- };
16625
- socket.on("data", onData);
16626
- socket.on("error", onError);
16627
- });
16628
- }
16629
- function sendCommand(socket, command) {
16630
- return new Promise((resolve21, reject) => {
16631
- socket.write(command + "\r\n", "utf-8", (err) => {
16632
- if (err) return reject(err);
16633
- readResponse(socket).then(resolve21, reject);
16634
- });
16635
- });
16636
- }
16637
- function buildMimeMessage(options) {
16638
- const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16639
- const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16640
- const hasAttachments = options.attachments && options.attachments.length > 0;
16641
- const hasTextAlt = options.text !== void 0 && options.html;
16642
- const lines = [];
16643
- const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
16644
- lines.push(`From: ${fromHeader}`);
16645
- lines.push(`To: ${options.to.join(", ")}`);
16646
- if (options.cc.length > 0) {
16647
- lines.push(`Cc: ${options.cc.join(", ")}`);
16648
- }
16649
- lines.push(`Subject: ${options.subject}`);
16650
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
16651
- lines.push(`Message-ID: <${options.messageId}>`);
16652
- lines.push("MIME-Version: 1.0");
16653
- if (options.replyTo) {
16654
- lines.push(`Reply-To: ${options.replyTo}`);
16655
- }
16656
- if (options.headers) {
16657
- for (const [key, value] of Object.entries(options.headers)) {
16658
- lines.push(`${key}: ${value}`);
16659
- }
16660
- }
16661
- if (hasAttachments) {
16662
- lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
16663
- lines.push("");
16664
- lines.push(`--${boundary}`);
16665
- if (hasTextAlt) {
16666
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16667
- lines.push("");
16668
- lines.push(`--${altBoundary}`);
16669
- lines.push("Content-Type: text/plain; charset=UTF-8");
16670
- lines.push("Content-Transfer-Encoding: 7bit");
16671
- lines.push("");
16672
- lines.push(options.text);
16673
- lines.push("");
16674
- lines.push(`--${altBoundary}`);
16675
- lines.push("Content-Type: text/html; charset=UTF-8");
16676
- lines.push("Content-Transfer-Encoding: 7bit");
16677
- lines.push("");
16678
- lines.push(options.body);
16679
- lines.push("");
16680
- lines.push(`--${altBoundary}--`);
16681
- } else {
16682
- const contentType = options.html ? "text/html" : "text/plain";
16683
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16684
- lines.push("Content-Transfer-Encoding: 7bit");
16685
- lines.push("");
16686
- lines.push(options.body);
16687
- }
16688
- for (const filePath of options.attachments) {
16689
- const fileName = basename2(filePath);
16690
- const fileData = readFileSync8(filePath);
16691
- const base64Data = fileData.toString("base64");
16692
- lines.push("");
16693
- lines.push(`--${boundary}`);
16694
- lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
16695
- lines.push("Content-Transfer-Encoding: base64");
16696
- lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
16697
- lines.push("");
16698
- for (let i = 0; i < base64Data.length; i += 76) {
16699
- lines.push(base64Data.substring(i, i + 76));
16700
- }
16701
- }
16702
- lines.push("");
16703
- lines.push(`--${boundary}--`);
16704
- } else if (hasTextAlt) {
16705
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16706
- lines.push("");
16707
- lines.push(`--${altBoundary}`);
16708
- lines.push("Content-Type: text/plain; charset=UTF-8");
16709
- lines.push("Content-Transfer-Encoding: 7bit");
16710
- lines.push("");
16711
- lines.push(options.text);
16712
- lines.push("");
16713
- lines.push(`--${altBoundary}`);
16714
- lines.push("Content-Type: text/html; charset=UTF-8");
16715
- lines.push("Content-Transfer-Encoding: 7bit");
16716
- lines.push("");
16717
- lines.push(options.body);
16718
- lines.push("");
16719
- lines.push(`--${altBoundary}--`);
16720
- } else {
16721
- const contentType = options.html ? "text/html" : "text/plain";
16722
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16723
- lines.push("");
16724
- lines.push(options.body);
16725
- }
16726
- return lines.join("\r\n");
16727
- }
16728
- function imapQuote(s) {
16729
- if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
16730
- return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
16731
- }
16732
- function imapReadLine(socket) {
16733
- return new Promise((resolve21, reject) => {
16734
- let buffer = "";
16735
- const onData = (chunk) => {
16736
- buffer += chunk.toString("utf-8");
16737
- const nlIndex = buffer.indexOf("\r\n");
16738
- if (nlIndex !== -1) {
16739
- socket.removeListener("data", onData);
16740
- socket.removeListener("error", onError);
16741
- resolve21(buffer);
16729
+ var DevMailbox;
16730
+ var init_devMailbox = __esm({
16731
+ "src/devMailbox.ts"() {
16732
+ "use strict";
16733
+ DevMailbox = class {
16734
+ mailboxDir;
16735
+ constructor(mailboxDir) {
16736
+ this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
16742
16737
  }
16743
- };
16744
- const onError = (err) => {
16745
- socket.removeListener("data", onData);
16746
- reject(err);
16747
- };
16748
- socket.on("data", onData);
16749
- socket.on("error", onError);
16750
- });
16751
- }
16752
- function imapCommand(socket, command) {
16753
- return new Promise((resolve21, reject) => {
16754
- imapTagCounter++;
16755
- const tag = `T${imapTagCounter}`;
16756
- const fullCommand = `${tag} ${command}\r
16757
- `;
16758
- let buffer = "";
16759
- const onData = (chunk) => {
16760
- buffer += chunk.toString("utf-8");
16761
- if (buffer.includes(`${tag} OK`)) {
16762
- socket.removeListener("data", onData);
16763
- socket.removeListener("error", onError);
16764
- resolve21(buffer);
16765
- return;
16738
+ /**
16739
+ * Ensure a folder directory exists.
16740
+ */
16741
+ ensureFolder(folder) {
16742
+ const dir = join12(this.mailboxDir, folder);
16743
+ mkdirSync8(dir, { recursive: true });
16744
+ return dir;
16766
16745
  }
16767
- if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
16768
- socket.removeListener("data", onData);
16769
- socket.removeListener("error", onError);
16770
- reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
16746
+ /**
16747
+ * Capture an email to the dev mailbox instead of sending it.
16748
+ *
16749
+ * The parameter order MATCHES Messenger.send() on purpose. It did not before:
16750
+ * send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
16751
+ * meant different things depending on which door it came through -- that mismatch
16752
+ * IS nodejs#42.
16753
+ *
16754
+ * BREAKING: `text` is now the 5th positional. A caller passing cc positionally
16755
+ * must move it. Aligning the two signatures is the fix; leaving them apart would
16756
+ * preserve the bug.
16757
+ */
16758
+ capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
16759
+ const id = randomUUID2();
16760
+ const toList = Array.isArray(to) ? to : [to];
16761
+ const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
16762
+ const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
16763
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16764
+ const message = {
16765
+ id,
16766
+ type: "outbox",
16767
+ from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
16768
+ to: toList,
16769
+ cc: ccList,
16770
+ bcc: bccList,
16771
+ reply_to: replyTo,
16772
+ subject,
16773
+ body,
16774
+ text,
16775
+ html,
16776
+ attachments,
16777
+ date: now,
16778
+ read: false
16779
+ };
16780
+ const outboxDir = this.ensureFolder("outbox");
16781
+ writeFileSync6(join12(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
16782
+ const inboxDir = this.ensureFolder("inbox");
16783
+ const inboxMessage = { ...message, type: "inbox" };
16784
+ writeFileSync6(join12(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
16785
+ return { success: true, message: "Email captured to dev mailbox", id };
16771
16786
  }
16772
- };
16773
- const onError = (err) => {
16774
- socket.removeListener("data", onData);
16775
- reject(err);
16776
- };
16777
- socket.on("data", onData);
16778
- socket.on("error", onError);
16779
- socket.write(fullCommand, "utf-8");
16780
- });
16781
- }
16782
- function imapFail(method, err) {
16783
- const e = err instanceof Error ? err : new Error(String(err));
16784
- Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
16785
- if (e instanceof MessengerConnectionError) return e;
16786
- return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
16787
- }
16788
- function parseSearchResponse(response) {
16789
- const match = response.match(/\* SEARCH (.+)/);
16790
- if (!match) return [];
16791
- return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
16792
- }
16793
- function parseHeaderResponse(uid, response) {
16794
- const headers = {};
16795
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
16796
- if (headerBlock) {
16797
- const lines = headerBlock[1].split(/\r\n/);
16798
- let currentKey = "";
16799
- for (const line of lines) {
16800
- if (/^\s/.test(line) && currentKey) {
16801
- headers[currentKey] += " " + line.trim();
16802
- } else {
16803
- const colonIdx = line.indexOf(":");
16804
- if (colonIdx > 0) {
16805
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16806
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16787
+ /**
16788
+ * List messages from a folder (default: inbox).
16789
+ */
16790
+ inbox(limit = 50, offset = 0, folder = "inbox") {
16791
+ const dir = this.ensureFolder(folder);
16792
+ const results = [];
16793
+ let files;
16794
+ try {
16795
+ files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
16796
+ } catch {
16797
+ return [];
16807
16798
  }
16808
- }
16809
- }
16810
- }
16811
- const seen = /\\Seen/i.test(response);
16812
- return {
16813
- uid,
16814
- subject: headers["subject"] ?? "",
16815
- from: headers["from"] ?? "",
16816
- to: headers["to"] ?? "",
16817
- date: headers["date"] ?? "",
16818
- snippet: "",
16819
- seen
16820
- };
16821
- }
16822
- function emptyFullMessage(uid) {
16823
- return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
16824
- }
16825
- function parseFullMessage(uid, response) {
16826
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
16827
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
16828
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
16829
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
16830
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
16831
- const headers = {};
16832
- const headerLines = headerSection.split(/\r\n/);
16833
- let currentKey = "";
16834
- for (const line of headerLines) {
16835
- if (/^\s/.test(line) && currentKey) {
16836
- headers[currentKey] += " " + line.trim();
16837
- } else {
16838
- const colonIdx = line.indexOf(":");
16839
- if (colonIdx > 0) {
16840
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16841
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16842
- }
16843
- }
16844
- }
16845
- const contentType = headers["content-type"] ?? "text/plain";
16846
- let bodyText = "";
16847
- let bodyHtml = "";
16848
- if (contentType.includes("multipart")) {
16849
- const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
16850
- if (boundaryMatch) {
16851
- const boundary = boundaryMatch[1];
16852
- const parts = bodySection.split("--" + boundary);
16853
- for (const part of parts) {
16854
- if (part.trim() === "" || part.trim() === "--") continue;
16855
- const partHeaderEnd = part.indexOf("\r\n\r\n");
16856
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
16857
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
16858
- if (partHeaders.includes("text/html")) {
16859
- bodyHtml = partBody;
16860
- } else if (partHeaders.includes("text/plain")) {
16861
- bodyText = partBody;
16799
+ const sliced = files.slice(offset, offset + limit);
16800
+ for (const file of sliced) {
16801
+ try {
16802
+ const msg = JSON.parse(readFileSync8(join12(dir, file), "utf-8"));
16803
+ results.push(msg);
16804
+ } catch {
16805
+ }
16862
16806
  }
16807
+ return results;
16863
16808
  }
16864
- }
16865
- } else if (contentType.includes("text/html")) {
16866
- bodyHtml = bodySection;
16867
- } else {
16868
- bodyText = bodySection;
16869
- }
16870
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16871
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16872
- return {
16873
- uid,
16874
- subject: headers["subject"] ?? "",
16875
- from: headers["from"] ?? "",
16876
- to: headers["to"] ?? "",
16877
- cc: headers["cc"] ?? "",
16878
- date: headers["date"] ?? "",
16879
- bodyText,
16880
- bodyHtml,
16881
- headers
16882
- };
16883
- }
16884
- var MessengerConnectionError, Messenger, imapTagCounter;
16885
- var init_messenger = __esm({
16886
- "src/messenger.ts"() {
16887
- "use strict";
16888
- init_dotenv();
16889
- init_logger();
16890
- MessengerConnectionError = class extends Error {
16891
- constructor(message) {
16892
- super(message);
16893
- this.name = "MessengerConnectionError";
16894
- }
16895
- };
16896
- Messenger = class {
16897
- host;
16898
- port;
16899
- username;
16900
- password;
16901
- fromAddress;
16902
- fromName;
16903
- encryption;
16904
- useTls;
16905
- imapHost;
16906
- imapPort;
16907
- imapUser;
16908
- imapPass;
16909
- imapEncryption;
16910
- constructor(options) {
16911
- this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
16912
- this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
16913
- this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
16914
- this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
16915
- this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
16916
- this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
16917
- const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
16918
- if (envEncryption) {
16919
- this.encryption = envEncryption.toLowerCase();
16920
- } else if (options?.useTls !== void 0) {
16921
- this.encryption = options.useTls ? "tls" : "none";
16922
- } else {
16923
- this.encryption = "tls";
16809
+ /**
16810
+ * Read a single message by ID. Searches all folders.
16811
+ */
16812
+ read(msgId) {
16813
+ const folders = ["inbox", "outbox"];
16814
+ for (const folder of folders) {
16815
+ const filePath = join12(this.mailboxDir, folder, `${msgId}.json`);
16816
+ if (existsSync10(filePath)) {
16817
+ try {
16818
+ const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
16819
+ msg.read = true;
16820
+ writeFileSync6(filePath, JSON.stringify(msg, null, 2));
16821
+ return msg;
16822
+ } catch {
16823
+ return null;
16824
+ }
16825
+ }
16924
16826
  }
16925
- this.useTls = ["tls", "starttls"].includes(this.encryption);
16926
- this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
16927
- this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
16928
- this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
16929
- this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
16930
- this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
16827
+ return null;
16931
16828
  }
16932
16829
  /**
16933
- * Read-only IMAP encryption mode for inspection / tests.
16934
- * Returns one of "tls", "starttls", "none", "ssl".
16830
+ * Count unread messages in the inbox.
16935
16831
  */
16936
- getImapEncryption() {
16937
- return this.imapEncryption;
16832
+ unreadCount() {
16833
+ const dir = this.ensureFolder("inbox");
16834
+ let count = 0;
16835
+ try {
16836
+ const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
16837
+ for (const file of files) {
16838
+ try {
16839
+ const msg = JSON.parse(readFileSync8(join12(dir, file), "utf-8"));
16840
+ if (!msg.read) count++;
16841
+ } catch {
16842
+ }
16843
+ }
16844
+ } catch {
16845
+ }
16846
+ return count;
16938
16847
  }
16939
16848
  /**
16940
- * Send an email via SMTP.
16941
- */
16942
- async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
16943
- const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
16944
- const toList = Array.isArray(options.to) ? options.to : [options.to];
16945
- const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
16946
- const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
16947
- const allRecipients = [...toList, ...ccList, ...bccList];
16948
- const messageId = `${randomUUID2()}@${this.host}`;
16949
- if (allRecipients.length === 0) {
16950
- return { success: false, message: "No recipients specified" };
16951
- }
16952
- if (!this.fromAddress) {
16953
- return { success: false, message: "No from address configured" };
16954
- }
16955
- try {
16956
- let socket;
16957
- if (this.port === 465) {
16958
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
16959
- await new Promise((resolve21, reject) => {
16960
- socket.once("secureConnect", resolve21);
16961
- socket.once("error", reject);
16962
- });
16963
- } else {
16964
- socket = net2.createConnection({ host: this.host, port: this.port });
16965
- await new Promise((resolve21, reject) => {
16966
- socket.once("connect", resolve21);
16967
- socket.once("error", reject);
16968
- });
16969
- }
16970
- const greeting = await readResponse(socket);
16971
- if (greeting.code !== 220) {
16972
- socket.destroy();
16973
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
16974
- }
16975
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
16976
- if (ehlo.code !== 250) {
16977
- socket.destroy();
16978
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
16979
- }
16980
- if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
16981
- const starttls = await sendCommand(socket, "STARTTLS");
16982
- if (starttls.code !== 220) {
16983
- socket.destroy();
16984
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
16985
- }
16986
- const plainSocket = socket;
16987
- socket = tls.connect(
16988
- { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
16989
- );
16990
- await new Promise((resolve21, reject) => {
16991
- socket.once("secureConnect", resolve21);
16992
- socket.once("error", reject);
16993
- });
16994
- const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
16995
- if (ehlo2.code !== 250) {
16996
- socket.destroy();
16997
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
16998
- }
16999
- }
17000
- if (this.username && this.password) {
17001
- const auth = await sendCommand(socket, "AUTH LOGIN");
17002
- if (auth.code !== 334) {
17003
- socket.destroy();
17004
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
17005
- }
17006
- const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
17007
- if (userResp.code !== 334) {
17008
- socket.destroy();
17009
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
17010
- }
17011
- const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
17012
- if (passResp.code !== 235) {
17013
- socket.destroy();
17014
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
17015
- }
17016
- }
17017
- const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
17018
- if (mailFrom.code !== 250) {
17019
- socket.destroy();
17020
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
17021
- }
17022
- for (const recipient of allRecipients) {
17023
- const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
17024
- if (rcpt.code !== 250 && rcpt.code !== 251) {
17025
- socket.destroy();
17026
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
17027
- }
17028
- }
17029
- const dataCmd = await sendCommand(socket, "DATA");
17030
- if (dataCmd.code !== 354) {
17031
- socket.destroy();
17032
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
17033
- }
17034
- const mimeMessage = buildMimeMessage({
17035
- from: this.fromAddress,
17036
- fromName: this.fromName,
17037
- to: toList,
17038
- cc: ccList,
17039
- subject: options.subject,
17040
- body: options.body,
17041
- html: options.html ?? false,
17042
- text: options.text,
17043
- replyTo: options.replyTo,
17044
- attachments: options.attachments,
17045
- headers: options.headers,
17046
- messageId
17047
- });
17048
- const endData = await sendCommand(socket, mimeMessage + "\r\n.");
17049
- if (endData.code !== 250) {
17050
- socket.destroy();
17051
- return { success: false, message: `Message delivery failed: ${endData.text}` };
17052
- }
17053
- await sendCommand(socket, "QUIT");
17054
- socket.destroy();
17055
- return { success: true, message: "Email sent successfully", id: messageId };
17056
- } catch (err) {
17057
- const errMsg = err instanceof Error ? err.message : String(err);
17058
- return { success: false, message: `SMTP error: ${errMsg}` };
17059
- }
17060
- }
17061
- /**
17062
- * Test the SMTP connection without sending an email.
17063
- */
17064
- async testConnection() {
17065
- try {
17066
- let socket;
17067
- if (this.port === 465) {
17068
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
17069
- await new Promise((resolve21, reject) => {
17070
- socket.once("secureConnect", resolve21);
17071
- socket.once("error", reject);
17072
- });
17073
- } else {
17074
- socket = net2.createConnection({ host: this.host, port: this.port });
17075
- await new Promise((resolve21, reject) => {
17076
- socket.once("connect", resolve21);
17077
- socket.once("error", reject);
17078
- });
17079
- }
17080
- const greeting = await readResponse(socket);
17081
- if (greeting.code !== 220) {
17082
- socket.destroy();
17083
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
17084
- }
17085
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
17086
- if (ehlo.code !== 250) {
17087
- socket.destroy();
17088
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
17089
- }
17090
- await sendCommand(socket, "QUIT");
17091
- socket.destroy();
17092
- return { success: true, message: `Connected to ${this.host}:${this.port}` };
17093
- } catch (err) {
17094
- const errMsg = err instanceof Error ? err.message : String(err);
17095
- return { success: false, message: `Connection failed: ${errMsg}` };
17096
- }
17097
- }
17098
- // ── IMAP (Read) ────────────────────────────────────────────
17099
- /**
17100
- * Connect to the IMAP server via raw TCP/TLS.
17101
- * Returns the socket and reads the greeting.
17102
- */
17103
- async imapConnect() {
17104
- if (!this.imapHost) {
17105
- throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
17106
- }
17107
- let socket;
17108
- const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
17109
- if (useTls) {
17110
- socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
17111
- await new Promise((resolve21, reject) => {
17112
- socket.once("secureConnect", resolve21);
17113
- socket.once("error", reject);
17114
- });
17115
- } else {
17116
- socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
17117
- await new Promise((resolve21, reject) => {
17118
- socket.once("connect", resolve21);
17119
- socket.once("error", reject);
17120
- });
17121
- }
17122
- await imapReadLine(socket);
17123
- if (this.imapUser && this.imapPass) {
17124
- const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
17125
- if (!loginResp.includes("OK")) {
17126
- socket.destroy();
17127
- throw new Error(`IMAP login failed: ${loginResp}`);
17128
- }
17129
- }
17130
- return socket;
17131
- }
17132
- /**
17133
- * Disconnect from IMAP cleanly.
17134
- */
17135
- async imapDisconnect(socket) {
17136
- try {
17137
- await imapCommand(socket, "LOGOUT");
17138
- } catch {
17139
- }
17140
- socket.destroy();
17141
- }
17142
- /**
17143
- * Fetch latest messages from a folder.
17144
- * Returns list of message summaries.
17145
- */
17146
- async inbox(limit = 20, offset = 0, folder = "INBOX") {
17147
- let socket;
17148
- try {
17149
- socket = await this.imapConnect();
17150
- } catch (err) {
17151
- throw imapFail("inbox", err);
17152
- }
17153
- try {
17154
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17155
- const searchResp = await imapCommand(socket, "SEARCH ALL");
17156
- const uids = parseSearchResponse(searchResp);
17157
- if (uids.length === 0) return [];
17158
- uids.reverse();
17159
- const selected = uids.slice(offset, offset + limit);
17160
- if (selected.length === 0) return [];
17161
- const messages = [];
17162
- for (const uid of selected) {
17163
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17164
- messages.push(parseHeaderResponse(uid, fetchResp));
17165
- }
17166
- return messages;
17167
- } catch (err) {
17168
- throw imapFail("inbox", err);
17169
- } finally {
17170
- await this.imapDisconnect(socket);
17171
- }
17172
- }
17173
- /**
17174
- * Read a single message by sequence number or UID.
17175
- */
17176
- async read(uid, folder = "INBOX") {
17177
- let socket;
17178
- try {
17179
- socket = await this.imapConnect();
17180
- } catch (err) {
17181
- throw imapFail("read", err);
17182
- }
17183
- try {
17184
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17185
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
17186
- if (!/\{\d+\}/.test(fetchResp)) {
17187
- return emptyFullMessage(uid);
17188
- }
17189
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17190
- return parseFullMessage(uid, fetchResp);
17191
- } catch (err) {
17192
- throw imapFail("read", err);
17193
- } finally {
17194
- await this.imapDisconnect(socket);
17195
- }
17196
- }
17197
- /**
17198
- * Search messages using IMAP search criteria.
17199
- */
17200
- async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
17201
- const criteria = ["ALL"];
17202
- if (subject) criteria.push(`SUBJECT "${subject}"`);
17203
- if (sender) criteria.push(`FROM "${sender}"`);
17204
- if (since) criteria.push(`SINCE ${since}`);
17205
- if (before) criteria.push(`BEFORE ${before}`);
17206
- if (unseenOnly) criteria.push("UNSEEN");
17207
- const query = criteria.join(" ");
17208
- let socket;
17209
- try {
17210
- socket = await this.imapConnect();
17211
- } catch (err) {
17212
- throw imapFail("search", err);
17213
- }
17214
- try {
17215
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17216
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
17217
- const uids = parseSearchResponse(searchResp);
17218
- if (uids.length === 0) return [];
17219
- uids.reverse();
17220
- const messages = [];
17221
- for (const uid of uids.slice(0, limit)) {
17222
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17223
- messages.push(parseHeaderResponse(uid, fetchResp));
17224
- }
17225
- return messages;
17226
- } catch (err) {
17227
- throw imapFail("search", err);
17228
- } finally {
17229
- await this.imapDisconnect(socket);
17230
- }
17231
- }
17232
- /**
17233
- * Delete a message by UID.
17234
- */
17235
- async deleteMessage(uid, folder = "INBOX") {
17236
- const socket = await this.imapConnect();
17237
- try {
17238
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17239
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
17240
- await imapCommand(socket, "EXPUNGE");
17241
- } finally {
17242
- await this.imapDisconnect(socket);
17243
- }
17244
- }
17245
- /**
17246
- * Mark a message as read.
17247
- */
17248
- async markRead(uid, folder = "INBOX") {
17249
- const socket = await this.imapConnect();
17250
- try {
17251
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17252
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17253
- } finally {
17254
- await this.imapDisconnect(socket);
17255
- }
17256
- }
17257
- /**
17258
- * Count unseen messages in a folder.
17259
- */
17260
- async unread(folder = "INBOX") {
17261
- let socket;
17262
- try {
17263
- socket = await this.imapConnect();
17264
- } catch (err) {
17265
- throw imapFail("unread", err);
17266
- }
17267
- try {
17268
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17269
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
17270
- return parseSearchResponse(searchResp).length;
17271
- } catch (err) {
17272
- throw imapFail("unread", err);
17273
- } finally {
17274
- await this.imapDisconnect(socket);
17275
- }
17276
- }
17277
- /**
17278
- * List available IMAP folders/mailboxes.
17279
- */
17280
- async folders() {
17281
- let socket;
17282
- try {
17283
- socket = await this.imapConnect();
17284
- } catch (err) {
17285
- throw imapFail("folders", err);
17286
- }
17287
- try {
17288
- const resp = await imapCommand(socket, 'LIST "" "*"');
17289
- const result = [];
17290
- for (const line of resp.split("\r\n")) {
17291
- const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
17292
- if (m) result.push(m[1]);
17293
- }
17294
- return result;
17295
- } catch (err) {
17296
- throw imapFail("folders", err);
17297
- } finally {
17298
- await this.imapDisconnect(socket);
17299
- }
17300
- }
17301
- /**
17302
- * Test IMAP connectivity without reading.
17303
- */
17304
- async testImapConnection() {
17305
- try {
17306
- const socket = await this.imapConnect();
17307
- await this.imapDisconnect(socket);
17308
- return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
17309
- } catch (err) {
17310
- const errMsg = err instanceof Error ? err.message : String(err);
17311
- return { success: false, message: `IMAP connection failed: ${errMsg}` };
17312
- }
17313
- }
17314
- };
17315
- imapTagCounter = 0;
17316
- }
17317
- });
17318
-
17319
- // src/devMailbox.ts
17320
- import { mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync4, existsSync as existsSync10 } from "node:fs";
17321
- import { join as join12 } from "node:path";
17322
- import { randomUUID as randomUUID3 } from "node:crypto";
17323
- function createMessenger() {
17324
- const debug = process.env.TINA4_DEBUG;
17325
- const smtpHost = process.env.TINA4_MAIL_HOST;
17326
- const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
17327
- if (isTruthy(debug)) {
17328
- return new DevMailbox();
17329
- }
17330
- if (!smtpHost) {
17331
- return new DevMailbox();
17332
- }
17333
- if (!isProd) {
17334
- return new DevMailbox();
17335
- }
17336
- return new Messenger();
17337
- }
17338
- var DevMailbox;
17339
- var init_devMailbox = __esm({
17340
- "src/devMailbox.ts"() {
17341
- "use strict";
17342
- init_messenger();
17343
- init_dotenv();
17344
- DevMailbox = class {
17345
- mailboxDir;
17346
- constructor(mailboxDir) {
17347
- this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
17348
- }
17349
- /**
17350
- * Ensure a folder directory exists.
17351
- */
17352
- ensureFolder(folder) {
17353
- const dir = join12(this.mailboxDir, folder);
17354
- mkdirSync8(dir, { recursive: true });
17355
- return dir;
17356
- }
17357
- /**
17358
- * Capture an email to the dev mailbox instead of sending it.
17359
- */
17360
- capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
17361
- const id = randomUUID3();
17362
- const toList = Array.isArray(to) ? to : [to];
17363
- const now = (/* @__PURE__ */ new Date()).toISOString();
17364
- const message = {
17365
- id,
17366
- type: "outbox",
17367
- from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
17368
- to: toList,
17369
- cc,
17370
- bcc,
17371
- reply_to: replyTo,
17372
- subject,
17373
- body,
17374
- html,
17375
- attachments,
17376
- date: now,
17377
- read: false
17378
- };
17379
- const outboxDir = this.ensureFolder("outbox");
17380
- writeFileSync6(join12(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
17381
- const inboxDir = this.ensureFolder("inbox");
17382
- const inboxMessage = { ...message, type: "inbox" };
17383
- writeFileSync6(join12(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
17384
- return { success: true, message: "Email captured to dev mailbox", id };
17385
- }
17386
- /**
17387
- * List messages from a folder (default: inbox).
17388
- */
17389
- inbox(limit = 50, offset = 0, folder = "inbox") {
17390
- const dir = this.ensureFolder(folder);
17391
- const results = [];
17392
- let files;
17393
- try {
17394
- files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
17395
- } catch {
17396
- return [];
17397
- }
17398
- const sliced = files.slice(offset, offset + limit);
17399
- for (const file of sliced) {
17400
- try {
17401
- const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
17402
- results.push(msg);
17403
- } catch {
17404
- }
17405
- }
17406
- return results;
17407
- }
17408
- /**
17409
- * Read a single message by ID. Searches all folders.
17410
- */
17411
- read(msgId) {
17412
- const folders = ["inbox", "outbox"];
17413
- for (const folder of folders) {
17414
- const filePath = join12(this.mailboxDir, folder, `${msgId}.json`);
17415
- if (existsSync10(filePath)) {
17416
- try {
17417
- const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
17418
- msg.read = true;
17419
- writeFileSync6(filePath, JSON.stringify(msg, null, 2));
17420
- return msg;
17421
- } catch {
17422
- return null;
17423
- }
17424
- }
17425
- }
17426
- return null;
17427
- }
17428
- /**
17429
- * Count unread messages in the inbox.
17430
- */
17431
- unreadCount() {
17432
- const dir = this.ensureFolder("inbox");
17433
- let count = 0;
17434
- try {
17435
- const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
17436
- for (const file of files) {
17437
- try {
17438
- const msg = JSON.parse(readFileSync9(join12(dir, file), "utf-8"));
17439
- if (!msg.read) count++;
17440
- } catch {
17441
- }
17442
- }
17443
- } catch {
17444
- }
17445
- return count;
17446
- }
17447
- /**
17448
- * Delete a message by ID. Removes from all folders.
16849
+ * Delete a message by ID. Removes from all folders.
17449
16850
  */
17450
16851
  delete(msgId) {
17451
16852
  let deleted = false;
@@ -17507,7 +16908,7 @@ var init_devMailbox = __esm({
17507
16908
  const subject = subjects[i % subjects.length];
17508
16909
  const from = senders[i % senders.length];
17509
16910
  const date = new Date(Date.now() - i * 36e5).toISOString();
17510
- const id = randomUUID3();
16911
+ const id = randomUUID2();
17511
16912
  const message = {
17512
16913
  id,
17513
16914
  type: "inbox",
@@ -18546,7 +17947,7 @@ var init_metrics = __esm({
18546
17947
  });
18547
17948
 
18548
17949
  // src/feedback.ts
18549
- import { readFileSync as readFileSync11, existsSync as existsSync12 } from "node:fs";
17950
+ import { readFileSync as readFileSync10, existsSync as existsSync12 } from "node:fs";
18550
17951
  import { dirname as dirname5, join as join14, resolve as resolve8 } from "node:path";
18551
17952
  import { fileURLToPath } from "node:url";
18552
17953
  function feedbackEnabled() {
@@ -18687,7 +18088,7 @@ var init_feedback = __esm({
18687
18088
  handleFeedbackWidgetJs = (_req, res) => {
18688
18089
  let body;
18689
18090
  if (existsSync12(WIDGET_BUNDLE_PATH)) {
18690
- body = readFileSync11(WIDGET_BUNDLE_PATH);
18091
+ body = readFileSync10(WIDGET_BUNDLE_PATH);
18691
18092
  } else {
18692
18093
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
18693
18094
  }
@@ -20395,7 +19796,7 @@ __export(errorOverlay_exports, {
20395
19796
  renderErrorOverlay: () => renderErrorOverlay,
20396
19797
  renderProductionError: () => renderProductionError
20397
19798
  });
20398
- import { readFileSync as readFileSync13, statSync as statSync10 } from "node:fs";
19799
+ import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
20399
19800
  import { resolve as resolve10 } from "node:path";
20400
19801
  function esc(text) {
20401
19802
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -20419,7 +19820,7 @@ function parseStack(stack) {
20419
19820
  function readSourceLines(filename, lineno) {
20420
19821
  try {
20421
19822
  const absPath = resolve10(filename);
20422
- const content = readFileSync13(absPath, "utf-8");
19823
+ const content = readFileSync12(absPath, "utf-8");
20423
19824
  const allLines = content.split("\n");
20424
19825
  const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
20425
19826
  const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
@@ -21683,8 +21084,8 @@ __export(context_exports, {
21683
21084
  fts5Supported: () => fts5Supported
21684
21085
  });
21685
21086
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
21686
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21687
- import { basename as basename4, dirname as dirname7, extname as extname6, isAbsolute as isAbsolute5, join as join16, relative as relative4, resolve as resolve11 } from "node:path";
21087
+ import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21088
+ import { basename as basename3, dirname as dirname7, extname as extname6, isAbsolute as isAbsolute5, join as join16, relative as relative4, resolve as resolve11 } from "node:path";
21688
21089
  function fts5Supported() {
21689
21090
  try {
21690
21091
  const conn = new DatabaseSync3(":memory:");
@@ -21707,7 +21108,7 @@ function realResolve(abs) {
21707
21108
  } catch {
21708
21109
  }
21709
21110
  try {
21710
- return join16(realpathSync2(dirname7(abs)), basename4(abs));
21111
+ return join16(realpathSync2(dirname7(abs)), basename3(abs));
21711
21112
  } catch {
21712
21113
  return abs;
21713
21114
  }
@@ -21820,7 +21221,7 @@ var init_context = __esm({
21820
21221
  // ── indexing ───────────────────────────────────────────────
21821
21222
  static chunksFor(label, text) {
21822
21223
  const ext = extname6(label).toLowerCase();
21823
- const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
21224
+ const special = SPECIAL_FILES.has(basename3(label).toLowerCase());
21824
21225
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
21825
21226
  return chunkCode(text, label);
21826
21227
  }
@@ -21837,7 +21238,7 @@ var init_context = __esm({
21837
21238
  const stored = label != null ? String(label) : String(file);
21838
21239
  let text;
21839
21240
  try {
21840
- text = readFileSync14(file, "utf-8");
21241
+ text = readFileSync13(file, "utf-8");
21841
21242
  } catch {
21842
21243
  return 0;
21843
21244
  }
@@ -21917,7 +21318,7 @@ var init_context = __esm({
21917
21318
  if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
21918
21319
  return -1;
21919
21320
  }
21920
- if (!_Context.eligible(basename4(rel))) return -1;
21321
+ if (!_Context.eligible(basename3(rel))) return -1;
21921
21322
  const stored = rel;
21922
21323
  if (!existsSync14(abs)) {
21923
21324
  this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
@@ -22009,7 +21410,7 @@ var init_context = __esm({
22009
21410
  });
22010
21411
 
22011
21412
  // src/websocketBackplane.ts
22012
- import { randomUUID as randomUUID4 } from "node:crypto";
21413
+ import { randomUUID as randomUUID3 } from "node:crypto";
22013
21414
  function createBackplane(url) {
22014
21415
  const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
22015
21416
  switch (backend) {
@@ -22039,7 +21440,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
22039
21440
  return envelope;
22040
21441
  }
22041
21442
  function randomInstanceId() {
22042
- return randomUUID4().replace(/-/g, "").slice(0, 16);
21443
+ return randomUUID3().replace(/-/g, "").slice(0, 16);
22043
21444
  }
22044
21445
  var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
22045
21446
  var init_websocketBackplane = __esm({
@@ -22267,7 +21668,7 @@ __export(websocket_exports, {
22267
21668
  });
22268
21669
  import { createServer } from "node:http";
22269
21670
  import { createHash as createHash4 } from "node:crypto";
22270
- import { randomUUID as randomUUID5 } from "node:crypto";
21671
+ import { randomUUID as randomUUID4 } from "node:crypto";
22271
21672
  function computeAcceptKey(key) {
22272
21673
  return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
22273
21674
  }
@@ -22395,7 +21796,7 @@ function parseFrame(data) {
22395
21796
  return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
22396
21797
  }
22397
21798
  function createRouteConnection(socket, path8, headers, params, auth) {
22398
- const id = randomUUID5().slice(0, 8);
21799
+ const id = randomUUID4().slice(0, 8);
22399
21800
  const send = (message) => {
22400
21801
  try {
22401
21802
  socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
@@ -22952,7 +22353,7 @@ var init_websocket = __esm({
22952
22353
  }
22953
22354
  responseLines.push("", "");
22954
22355
  socket.write(responseLines.join("\r\n"));
22955
- const clientId = randomUUID5().slice(0, 8);
22356
+ const clientId = randomUUID4().slice(0, 8);
22956
22357
  const client = {
22957
22358
  id: clientId,
22958
22359
  socket,
@@ -23240,7 +22641,7 @@ var init_websocket = __esm({
23240
22641
 
23241
22642
  // src/queueBackends/rabbitmqBackend.ts
23242
22643
  import { execFileSync } from "node:child_process";
23243
- import { randomUUID as randomUUID6 } from "node:crypto";
22644
+ import { randomUUID as randomUUID5 } from "node:crypto";
23244
22645
  function parseAmqpUrl(url) {
23245
22646
  const config = {};
23246
22647
  let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
@@ -23684,7 +23085,7 @@ var init_rabbitmqBackend = __esm({
23684
23085
  }
23685
23086
  }
23686
23087
  push(queue, payload, _delay) {
23687
- const id = randomUUID6();
23088
+ const id = randomUUID5();
23688
23089
  const now = (/* @__PURE__ */ new Date()).toISOString();
23689
23090
  const job = {
23690
23091
  id,
@@ -23723,7 +23124,7 @@ var init_rabbitmqBackend = __esm({
23723
23124
 
23724
23125
  // src/queueBackends/kafkaBackend.ts
23725
23126
  import { execFileSync as execFileSync2 } from "node:child_process";
23726
- import { randomUUID as randomUUID7 } from "node:crypto";
23127
+ import { randomUUID as randomUUID6 } from "node:crypto";
23727
23128
  function kafkaSecurityConfig(env = process.env) {
23728
23129
  const mapping = [
23729
23130
  ["security.protocol", "SECURITY_PROTOCOL"],
@@ -23747,7 +23148,7 @@ var init_kafkaBackend = __esm({
23747
23148
  "use strict";
23748
23149
  API_PRODUCE = 0;
23749
23150
  API_FETCH = 1;
23750
- KafkaBackend = class {
23151
+ KafkaBackend = class _KafkaBackend {
23751
23152
  brokers;
23752
23153
  groupId;
23753
23154
  constructor(config) {
@@ -24109,12 +23510,15 @@ var init_kafkaBackend = __esm({
24109
23510
  if (errCode === 0) {
24110
23511
  finish("__PUBLISHED__", 0);
24111
23512
  } else {
23513
+ // Report the CODE, not just "it failed" \u2014 the caller decides
23514
+ // whether it is retriable (3/5, the async topic-creation race)
23515
+ // or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
24112
23516
  process.stderr.write("Produce error code " + errCode);
24113
- finish("__ERROR__" + errCode, 0);
23517
+ finish("__PRODUCEERROR__" + errCode, 0);
24114
23518
  }
24115
23519
  } catch (e) {
24116
23520
  process.stderr.write("produce parse: " + e.message);
24117
- finish("__ERROR__", 0);
23521
+ finish("__PARSEERROR__produce: " + e.message, 0);
24118
23522
  }
24119
23523
  return;
24120
23524
  } else if (operation === "get") {
@@ -24128,6 +23532,7 @@ var init_kafkaBackend = __esm({
24128
23532
  pos += 4; // throttleTimeMs (v1+)
24129
23533
  const topicCount = buffer.readInt32BE(pos); pos += 4;
24130
23534
  let out = "__EMPTY__";
23535
+ let fatalCode = 0;
24131
23536
  for (let t = 0; t < topicCount; t++) {
24132
23537
  const tl = buffer.readInt16BE(pos); pos += 2 + tl;
24133
23538
  const pc = buffer.readInt32BE(pos); pos += 4;
@@ -24139,6 +23544,14 @@ var init_kafkaBackend = __esm({
24139
23544
  const abortedCount = buffer.readInt32BE(pos); pos += 4;
24140
23545
  if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
24141
23546
  const recSetSize = buffer.readInt32BE(pos); pos += 4;
23547
+ // 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
23548
+ // "nothing to read here yet", which a consumer that starts
23549
+ // before its producer hits on every cold start. Any OTHER code
23550
+ // (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
23551
+ // is a real failure and must NOT be reported as an empty queue.
23552
+ if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
23553
+ fatalCode = errCode;
23554
+ }
24142
23555
  if (errCode === 0 && recSetSize > 0) {
24143
23556
  const val = firstRecordValue(buffer, pos, pos + recSetSize);
24144
23557
  if (val !== null) out = val;
@@ -24146,21 +23559,33 @@ var init_kafkaBackend = __esm({
24146
23559
  pos += recSetSize > 0 ? recSetSize : 0;
24147
23560
  }
24148
23561
  }
23562
+ if (fatalCode !== 0) {
23563
+ process.stderr.write("Fetch error code " + fatalCode);
23564
+ finish("__FETCHERROR__" + fatalCode, 0);
23565
+ return;
23566
+ }
24149
23567
  finish(out, 0);
24150
23568
  } catch (e) {
23569
+ // A parse failure is NOT an empty queue either \u2014 say so.
24151
23570
  process.stderr.write("fetch parse: " + e.message);
24152
- finish("__EMPTY__", 0);
23571
+ finish("__PARSEERROR__fetch: " + e.message, 0);
24153
23572
  }
24154
23573
  return;
24155
23574
  }
24156
23575
  });
24157
23576
 
23577
+ // Report the reason on STDOUT and exit 0. Writing it to stderr and
23578
+ // exiting non-zero LOST it: stderr to a pipe is an async write and
23579
+ // process.exit() truncates it, so the parent saw an empty stderr and fell
23580
+ // back to execFileSync's message -- which embeds this entire script.
23581
+ // stdout is flushed by finish()'s write callback, so it survives.
24158
23582
  sock.on("error", (err) => {
24159
- process.stderr.write(err.message);
24160
- finish("", 1);
23583
+ finish("__TRANSPORTERROR__" + err.message, 0);
24161
23584
  });
24162
23585
 
24163
- var timer = setTimeout(() => { finish("", 1); }, 10000);
23586
+ var timer = setTimeout(() => {
23587
+ finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
23588
+ }, 10000);
24164
23589
  `;
24165
23590
  try {
24166
23591
  const result = execFileSync2(process.execPath, ["-e", script], {
@@ -24169,12 +23594,51 @@ var init_kafkaBackend = __esm({
24169
23594
  stdio: ["pipe", "pipe", "pipe"]
24170
23595
  });
24171
23596
  return result;
24172
- } catch {
24173
- return "";
23597
+ } catch (err) {
23598
+ const e = err;
23599
+ const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
23600
+ const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
23601
+ return "__TRANSPORTERROR__" + firstLine2;
23602
+ }
23603
+ }
23604
+ /**
23605
+ * Sleep synchronously between produce retries.
23606
+ *
23607
+ * `push()` is synchronous (the whole backend drives its socket through a child
23608
+ * process), so there is no event loop to await on. `Atomics.wait` on a
23609
+ * SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
23610
+ * no dependency, no busy-wait burning CPU.
23611
+ */
23612
+ static sleepSync(ms) {
23613
+ const shared = new Int32Array(new SharedArrayBuffer(4));
23614
+ Atomics.wait(shared, 0, 0, ms);
23615
+ }
23616
+ /**
23617
+ * Turn a sentinel from the protocol child into a thrown error, or return.
23618
+ *
23619
+ * The wording matches the Python and PHP backends exactly -- the parity rule
23620
+ * covers user-visible error messages, not just behaviour.
23621
+ */
23622
+ static assertNoError(result, operation, topic) {
23623
+ const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
23624
+ if (fatal) {
23625
+ throw new Error(
23626
+ `Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
23627
+ );
23628
+ }
23629
+ if (result.startsWith("__TRANSPORTERROR__")) {
23630
+ throw new Error(
23631
+ `Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
23632
+ );
23633
+ }
23634
+ if (result.startsWith("__PARSEERROR__")) {
23635
+ throw new Error(
23636
+ `Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
23637
+ );
24174
23638
  }
24175
23639
  }
24176
23640
  push(queue, payload, _delay) {
24177
- const id = randomUUID7();
23641
+ const id = randomUUID6();
24178
23642
  const now = (/* @__PURE__ */ new Date()).toISOString();
24179
23643
  const job = {
24180
23644
  id,
@@ -24184,14 +23648,25 @@ var init_kafkaBackend = __esm({
24184
23648
  attempts: 0,
24185
23649
  delayUntil: null
24186
23650
  };
24187
- const result = this.execSync("publish", queue, JSON.stringify(job));
24188
- if (!result.includes("__PUBLISHED__")) {
24189
- throw new Error("Kafka publish failed");
23651
+ const body = JSON.stringify(job);
23652
+ let result = "";
23653
+ for (let attempt = 1; attempt <= 10; attempt++) {
23654
+ result = this.execSync("publish", queue, body);
23655
+ if (result.includes("__PUBLISHED__")) {
23656
+ return id;
23657
+ }
23658
+ const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
23659
+ if (!retriable || attempt === 10) {
23660
+ break;
23661
+ }
23662
+ _KafkaBackend.sleepSync(200);
24190
23663
  }
24191
- return id;
23664
+ _KafkaBackend.assertNoError(result, "produce", queue);
23665
+ throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
24192
23666
  }
24193
23667
  pop(queue) {
24194
23668
  const result = this.execSync("get", queue);
23669
+ _KafkaBackend.assertNoError(result, "fetch", queue);
24195
23670
  if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
24196
23671
  try {
24197
23672
  return JSON.parse(result);
@@ -24209,7 +23684,7 @@ var init_kafkaBackend = __esm({
24209
23684
  });
24210
23685
 
24211
23686
  // src/queueBackends/mongoBackend.ts
24212
- import { randomUUID as randomUUID8 } from "node:crypto";
23687
+ import { randomUUID as randomUUID7 } from "node:crypto";
24213
23688
  import { execFileSync as execFileSync3 } from "node:child_process";
24214
23689
  var MongoBackend2;
24215
23690
  var init_mongoBackend = __esm({
@@ -24522,7 +23997,7 @@ var init_mongoBackend = __esm({
24522
23997
  }
24523
23998
  }
24524
23999
  push(queue, payload, delay) {
24525
- const id = randomUUID8();
24000
+ const id = randomUUID7();
24526
24001
  const now = (/* @__PURE__ */ new Date()).toISOString();
24527
24002
  const job = {
24528
24003
  id,
@@ -24657,9 +24132,9 @@ var init_job = __esm({
24657
24132
  });
24658
24133
 
24659
24134
  // src/queueBackends/liteBackend.ts
24660
- import { mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, existsSync as existsSync15 } from "node:fs";
24135
+ import { mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync8, unlinkSync as unlinkSync5, existsSync as existsSync15 } from "node:fs";
24661
24136
  import { join as join17 } from "node:path";
24662
- import { randomUUID as randomUUID9 } from "node:crypto";
24137
+ import { randomUUID as randomUUID8 } from "node:crypto";
24663
24138
  var LiteBackend;
24664
24139
  var init_liteBackend = __esm({
24665
24140
  "src/queueBackends/liteBackend.ts"() {
@@ -24711,7 +24186,7 @@ var init_liteBackend = __esm({
24711
24186
  }
24712
24187
  push(queue, payload, delay, priority) {
24713
24188
  const dir = this.ensureDir(queue);
24714
- const id = randomUUID9();
24189
+ const id = randomUUID8();
24715
24190
  const now = (/* @__PURE__ */ new Date()).toISOString();
24716
24191
  const job = {
24717
24192
  id,
@@ -24747,7 +24222,7 @@ var init_liteBackend = __esm({
24747
24222
  const filePath = join17(dir, filename);
24748
24223
  let job;
24749
24224
  try {
24750
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24225
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
24751
24226
  } catch {
24752
24227
  continue;
24753
24228
  }
@@ -24811,7 +24286,7 @@ var init_liteBackend = __esm({
24811
24286
  const filePath = join17(reservedDir, filename);
24812
24287
  let record;
24813
24288
  try {
24814
- record = JSON.parse(readFileSync15(filePath, "utf-8"));
24289
+ record = JSON.parse(readFileSync14(filePath, "utf-8"));
24815
24290
  } catch {
24816
24291
  continue;
24817
24292
  }
@@ -24924,7 +24399,7 @@ var init_liteBackend = __esm({
24924
24399
  let count = 0;
24925
24400
  for (const file of files) {
24926
24401
  try {
24927
- const job = JSON.parse(readFileSync15(join17(scanDir, file), "utf-8"));
24402
+ const job = JSON.parse(readFileSync14(join17(scanDir, file), "utf-8"));
24928
24403
  if (job.status === status2) count++;
24929
24404
  } catch {
24930
24405
  }
@@ -24981,7 +24456,7 @@ var init_liteBackend = __esm({
24981
24456
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
24982
24457
  for (const file of files) {
24983
24458
  try {
24984
- const job = JSON.parse(readFileSync15(join17(dir, file), "utf-8"));
24459
+ const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
24985
24460
  const attempts = job.attempts || 0;
24986
24461
  if (attempts > 0 && attempts < maxRetries) {
24987
24462
  results.push(job);
@@ -25007,7 +24482,7 @@ var init_liteBackend = __esm({
25007
24482
  const failedDir = join17(this.basePath, q, "failed");
25008
24483
  const filePath = join17(failedDir, `${jobId}.queue-data`);
25009
24484
  if (existsSync15(filePath)) {
25010
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24485
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25011
24486
  job.status = "pending";
25012
24487
  job.attempts = (job.attempts || 0) + 1;
25013
24488
  job.error = void 0;
@@ -25031,7 +24506,7 @@ var init_liteBackend = __esm({
25031
24506
  const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
25032
24507
  for (const file of files) {
25033
24508
  try {
25034
- const job = JSON.parse(readFileSync15(join17(failedDir, file), "utf-8"));
24509
+ const job = JSON.parse(readFileSync14(join17(failedDir, file), "utf-8"));
25035
24510
  if ((job.attempts || 0) >= maxRetries) {
25036
24511
  job.status = "dead";
25037
24512
  results.push(job);
@@ -25065,7 +24540,7 @@ var init_liteBackend = __esm({
25065
24540
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
25066
24541
  for (const file of files) {
25067
24542
  try {
25068
- const job = JSON.parse(readFileSync15(join17(dir, file), "utf-8"));
24543
+ const job = JSON.parse(readFileSync14(join17(dir, file), "utf-8"));
25069
24544
  if (job.status === status2) {
25070
24545
  unlinkSync5(join17(dir, file));
25071
24546
  count++;
@@ -25092,7 +24567,7 @@ var init_liteBackend = __esm({
25092
24567
  for (const file of files) {
25093
24568
  try {
25094
24569
  const filePath = join17(failedDir, file);
25095
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24570
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25096
24571
  if ((job.attempts || 0) >= maxRetries) {
25097
24572
  continue;
25098
24573
  }
@@ -25124,7 +24599,7 @@ var init_liteBackend = __esm({
25124
24599
  const filePath = join17(dir, file);
25125
24600
  let job;
25126
24601
  try {
25127
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24602
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
25128
24603
  } catch {
25129
24604
  continue;
25130
24605
  }
@@ -27614,7 +27089,7 @@ ${end}
27614
27089
 
27615
27090
  // src/devAdmin.ts
27616
27091
  import { cpus as osCpus } from "node:os";
27617
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync14, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15 } from "node:fs";
27092
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, existsSync as existsSync19, readdirSync as readdirSync14, mkdirSync as mkdirSync14, copyFileSync, statSync as statSync15 } from "node:fs";
27618
27093
  import { join as join21, dirname as dirname9, resolve as resolve15, relative as relative8 } from "node:path";
27619
27094
  import { fileURLToPath as fileURLToPath3 } from "node:url";
27620
27095
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -27800,7 +27275,7 @@ function resolveDevEnvVar(key) {
27800
27275
  if (live !== void 0 && live !== "") return live;
27801
27276
  const envPath = join21(process.cwd(), ".env");
27802
27277
  if (!existsSync19(envPath)) return "";
27803
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
27278
+ for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
27804
27279
  const t = line.trim();
27805
27280
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
27806
27281
  const eq = t.indexOf("=");
@@ -27810,7 +27285,7 @@ function resolveDevEnvVar(key) {
27810
27285
  }
27811
27286
  function upsertDevEnvVar(key, value) {
27812
27287
  const envPath = join21(process.cwd(), ".env");
27813
- const lines = existsSync19(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
27288
+ const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
27814
27289
  let found = false;
27815
27290
  const out = [];
27816
27291
  for (const line of lines) {
@@ -27843,7 +27318,7 @@ function parseEnvFile() {
27843
27318
  const envPath = join21(process.cwd(), ".env");
27844
27319
  const result = {};
27845
27320
  if (!existsSync19(envPath)) return result;
27846
- const lines = readFileSync19(envPath, "utf-8").split("\n");
27321
+ const lines = readFileSync18(envPath, "utf-8").split("\n");
27847
27322
  for (const line of lines) {
27848
27323
  const trimmed = line.trim();
27849
27324
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -28200,7 +27675,7 @@ var init_devAdmin = __esm({
28200
27675
  for (const rel of ["../../../package.json", "../../package.json"]) {
28201
27676
  const p = resolve15(__dirname2, rel);
28202
27677
  if (existsSync19(p)) {
28203
- const pkg = JSON.parse(readFileSync19(p, "utf-8"));
27678
+ const pkg = JSON.parse(readFileSync18(p, "utf-8"));
28204
27679
  if (pkg.version) return pkg.version;
28205
27680
  }
28206
27681
  }
@@ -28858,7 +28333,7 @@ var init_devAdmin = __esm({
28858
28333
  for (const filename of readdirSync14(queueDir).sort()) {
28859
28334
  if (!filename.endsWith(".queue-data")) continue;
28860
28335
  try {
28861
- const job = JSON.parse(readFileSync19(join21(queueDir, filename), "utf-8"));
28336
+ const job = JSON.parse(readFileSync18(join21(queueDir, filename), "utf-8"));
28862
28337
  jobs.push(mapQueueJob(job, topic, "pending"));
28863
28338
  } catch {
28864
28339
  }
@@ -29344,7 +28819,7 @@ var init_devAdmin = __esm({
29344
28819
  }
29345
28820
  try {
29346
28821
  const envPath = join21(process.cwd(), ".env");
29347
- const lines = existsSync19(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
28822
+ const lines = existsSync19(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
29348
28823
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
29349
28824
  const newLines = [];
29350
28825
  for (const line of lines) {
@@ -29390,7 +28865,7 @@ var init_devAdmin = __esm({
29390
28865
  const metaFile = join21(entryPath, "meta.json");
29391
28866
  if (statSync15(entryPath).isDirectory() && existsSync19(metaFile)) {
29392
28867
  try {
29393
- const meta = JSON.parse(readFileSync19(metaFile, "utf-8"));
28868
+ const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
29394
28869
  meta.id = entry;
29395
28870
  const srcDir = join21(entryPath, "src");
29396
28871
  if (existsSync19(srcDir)) {
@@ -29605,7 +29080,7 @@ var init_devAdmin = __esm({
29605
29080
  return;
29606
29081
  }
29607
29082
  try {
29608
- const content = readFileSync19(target, "utf-8");
29083
+ const content = readFileSync18(target, "utf-8");
29609
29084
  const path8 = relative8(root, target);
29610
29085
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
29611
29086
  } catch (e) {
@@ -29647,7 +29122,7 @@ var init_devAdmin = __esm({
29647
29122
  return;
29648
29123
  }
29649
29124
  try {
29650
- const buf = readFileSync19(target);
29125
+ const buf = readFileSync18(target);
29651
29126
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
29652
29127
  const mime = {
29653
29128
  js: "application/javascript",
@@ -30075,7 +29550,7 @@ var init_devAdmin = __esm({
30075
29550
  });
30076
29551
 
30077
29552
  // src/i18n.ts
30078
- import { readFileSync as readFileSync20, readdirSync as readdirSync15, existsSync as existsSync20 } from "node:fs";
29553
+ import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync20 } from "node:fs";
30079
29554
  import { join as join22, resolve as resolve16 } from "node:path";
30080
29555
  var I18n;
30081
29556
  var init_i18n = __esm({
@@ -30172,7 +29647,7 @@ var init_i18n = __esm({
30172
29647
  const filePath = join22(this._localeDir, `${locale}.json`);
30173
29648
  if (existsSync20(filePath)) {
30174
29649
  try {
30175
- const raw = readFileSync20(filePath, "utf-8");
29650
+ const raw = readFileSync19(filePath, "utf-8");
30176
29651
  const data = JSON.parse(raw);
30177
29652
  this._translations.set(locale, _I18n._flatten(data));
30178
29653
  return;
@@ -30185,7 +29660,7 @@ var init_i18n = __esm({
30185
29660
  const yamlPath = join22(this._localeDir, `${locale}${ext}`);
30186
29661
  if (existsSync20(yamlPath)) {
30187
29662
  try {
30188
- const raw = readFileSync20(yamlPath, "utf-8");
29663
+ const raw = readFileSync19(yamlPath, "utf-8");
30189
29664
  const data = _I18n._parseSimpleYaml(raw);
30190
29665
  this._translations.set(locale, _I18n._flatten(data));
30191
29666
  return;
@@ -30853,6 +30328,36 @@ var init_docsAutoDiscovery = __esm({
30853
30328
  }
30854
30329
  });
30855
30330
 
30331
+ // src/sessionHandlers/childError.ts
30332
+ function childFailureReason(err) {
30333
+ const e = err ?? {};
30334
+ const stderr = String(e.stderr ?? "").trim();
30335
+ if (stderr !== "") {
30336
+ return firstLine(stderr);
30337
+ }
30338
+ if (e.code === "ETIMEDOUT" || e.signal) {
30339
+ return `timed out or was killed (${e.code ?? e.signal})`;
30340
+ }
30341
+ if (typeof e.status === "number" && e.status !== 0) {
30342
+ return `child exited with code ${e.status} and no output`;
30343
+ }
30344
+ return firstLine(String(e.message ?? "unknown error"));
30345
+ }
30346
+ function firstLine(text) {
30347
+ const line = text.split("\n", 1)[0] ?? "";
30348
+ return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
30349
+ }
30350
+ function childFailureError(label, err) {
30351
+ return new Error(`${label} command failed: ${childFailureReason(err)}`);
30352
+ }
30353
+ var MAX_FALLBACK;
30354
+ var init_childError = __esm({
30355
+ "src/sessionHandlers/childError.ts"() {
30356
+ "use strict";
30357
+ MAX_FALLBACK = 200;
30358
+ }
30359
+ });
30360
+
30856
30361
  // src/sessionHandlers/respClient.ts
30857
30362
  import { execFileSync as execFileSync4 } from "node:child_process";
30858
30363
  function respCommandSync(target, args, label = "Redis") {
@@ -30972,7 +30477,7 @@ function respCommandSync(target, args, label = "Redis") {
30972
30477
  stdio: ["pipe", "pipe", "pipe"]
30973
30478
  });
30974
30479
  } catch (err) {
30975
- throw new Error(`${label} command failed: ${err.message}`);
30480
+ throw childFailureError(label, err);
30976
30481
  }
30977
30482
  if (result === "__NULL__") return "";
30978
30483
  if (result.startsWith("__ERR__")) {
@@ -30983,6 +30488,7 @@ function respCommandSync(target, args, label = "Redis") {
30983
30488
  var init_respClient = __esm({
30984
30489
  "src/sessionHandlers/respClient.ts"() {
30985
30490
  "use strict";
30491
+ init_childError();
30986
30492
  }
30987
30493
  });
30988
30494
 
@@ -31001,6 +30507,7 @@ var moduleRequire, RedisNpmSessionHandler;
31001
30507
  var init_redisHandler = __esm({
31002
30508
  "src/sessionHandlers/redisHandler.ts"() {
31003
30509
  "use strict";
30510
+ init_childError();
31004
30511
  init_respClient();
31005
30512
  moduleRequire = createRequire8(import.meta.url);
31006
30513
  RedisNpmSessionHandler = class {
@@ -31061,9 +30568,17 @@ var init_redisHandler = __esm({
31061
30568
  (async () => {
31062
30569
  try {
31063
30570
  const redis = require("redis");
30571
+ // reconnectStrategy: false \u2014 this child runs ONE command and exits, so
30572
+ // retrying inside it is pointless: the handler is called again on the
30573
+ // next request anyway. With the driver's default strategy a refused
30574
+ // connection never rejects, the child hangs until execFileSync's 5s
30575
+ // timeout kills it, and the caller is told "timed out" when the truth
30576
+ // is "connection refused". Off, connect() rejects in ~5ms with the real
30577
+ // reason -- a better message AND no 5s stall per request when Redis is
30578
+ // down.
31064
30579
  const clientOpts = useUrl
31065
- ? { url }
31066
- : { socket: { host, port }, password: password || undefined, database: db };
30580
+ ? { url, socket: { reconnectStrategy: false } }
30581
+ : { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
31067
30582
  const client = redis.createClient(clientOpts);
31068
30583
  client.on("error", () => {});
31069
30584
  await client.connect();
@@ -31077,8 +30592,10 @@ var init_redisHandler = __esm({
31077
30592
  const out = (result === null || result === undefined) ? "__NULL__" : String(result);
31078
30593
  process.stdout.write(out, () => process.exit(0));
31079
30594
  } catch (err) {
31080
- process.stderr.write(String((err && err.message) || err));
31081
- process.exit(1);
30595
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30596
+ // a bare process.exit() truncates it, which left the parent with an
30597
+ // empty stderr and nothing but execFileSync's script-dump message.
30598
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31082
30599
  }
31083
30600
  })();
31084
30601
  `;
@@ -31090,7 +30607,7 @@ var init_redisHandler = __esm({
31090
30607
  stdio: ["pipe", "pipe", "pipe"]
31091
30608
  });
31092
30609
  } catch (err) {
31093
- throw new Error(`Redis command failed: ${err.message}`);
30610
+ throw childFailureError("Redis", err);
31094
30611
  }
31095
30612
  if (result === "__NULL__") return "";
31096
30613
  return result;
@@ -31222,8 +30739,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31222
30739
  process.stdout.write(out, () => process.exit(0));
31223
30740
  } catch (err) {
31224
30741
  try { if (client) await client.close(); } catch (e) {}
31225
- process.stderr.write(String((err && err.message) || err));
31226
- process.exit(1);
30742
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30743
+ // a bare process.exit() truncates it, which left the parent with an
30744
+ // empty stderr and nothing but execFileSync's script-dump message.
30745
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31227
30746
  }
31228
30747
  })();
31229
30748
  } else {
@@ -31364,12 +30883,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31364
30883
  stdio: ["pipe", "pipe", "pipe"]
31365
30884
  });
31366
30885
  } catch (err) {
31367
- throw new Error(`${label} command failed: ${err.message}`);
30886
+ throw childFailureError(label, err);
31368
30887
  }
31369
30888
  }
31370
30889
  var init_mongoClient = __esm({
31371
30890
  "src/sessionHandlers/mongoClient.ts"() {
31372
30891
  "use strict";
30892
+ init_childError();
31373
30893
  }
31374
30894
  });
31375
30895
 
@@ -31522,7 +31042,7 @@ __export(session_exports, {
31522
31042
  sessionCookieName: () => sessionCookieName
31523
31043
  });
31524
31044
  import { randomBytes as randomBytes6 } from "node:crypto";
31525
- import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync22, writeFileSync as writeFileSync15, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31045
+ import { existsSync as existsSync22, mkdirSync as mkdirSync17, readFileSync as readFileSync21, writeFileSync as writeFileSync15, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31526
31046
  import { join as join24 } from "node:path";
31527
31047
  function isSecureScheme(forwardedProto, socketEncrypted) {
31528
31048
  const forwarded = (forwardedProto ?? "").trim();
@@ -31575,7 +31095,7 @@ var init_session = __esm({
31575
31095
  const filePath = this.filePath(sessionId);
31576
31096
  try {
31577
31097
  if (!existsSync22(filePath)) return null;
31578
- const raw = readFileSync22(filePath, "utf-8");
31098
+ const raw = readFileSync21(filePath, "utf-8");
31579
31099
  const wrapper = JSON.parse(raw);
31580
31100
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
31581
31101
  try {
@@ -31611,7 +31131,7 @@ var init_session = __esm({
31611
31131
  if (!file.endsWith(".json")) continue;
31612
31132
  const fullPath = join24(this.storagePath, file);
31613
31133
  try {
31614
- const raw = readFileSync22(fullPath, "utf-8");
31134
+ const raw = readFileSync21(fullPath, "utf-8");
31615
31135
  const wrapper = JSON.parse(raw);
31616
31136
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
31617
31137
  unlinkSync7(fullPath);
@@ -32128,7 +31648,7 @@ var init_events = __esm({
32128
31648
  // src/server.ts
32129
31649
  import { createServer as createServer2 } from "node:http";
32130
31650
  import { resolve as resolve18, dirname as dirname10, join as join25, relative as relative9 } from "node:path";
32131
- import { existsSync as existsSync23, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync16 } from "node:fs";
31651
+ import { existsSync as existsSync23, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
32132
31652
  import { isatty } from "node:tty";
32133
31653
  import { fileURLToPath as fileURLToPath4 } from "node:url";
32134
31654
  import { execFileSync as execFileSync7, exec } from "node:child_process";
@@ -32190,7 +31710,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
32190
31710
  function readPackageVersion() {
32191
31711
  try {
32192
31712
  const pkgPath = resolve18(dirname10(fileURLToPath4(import.meta.url)), "..", "..", "..", "package.json");
32193
- const pkg = JSON.parse(readFileSync23(pkgPath, "utf-8"));
31713
+ const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
32194
31714
  return pkg.version ?? "0.0.0";
32195
31715
  } catch {
32196
31716
  return "0.0.0";
@@ -33020,7 +32540,7 @@ ${reset2}
33020
32540
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33021
32541
  res.raw.end(html);
33022
32542
  } else {
33023
- const html = readFileSync23(resolve18(templatesDir, tplFile), "utf-8");
32543
+ const html = readFileSync22(resolve18(templatesDir, tplFile), "utf-8");
33024
32544
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33025
32545
  res.raw.end(html);
33026
32546
  }
@@ -33411,7 +32931,7 @@ var init_constants = __esm({
33411
32931
  });
33412
32932
 
33413
32933
  // src/scss.ts
33414
- import { readFileSync as readFileSync24, writeFileSync as writeFileSync16, existsSync as existsSync24, mkdirSync as mkdirSync18, readdirSync as readdirSync18 } from "node:fs";
32934
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync16, existsSync as existsSync24, mkdirSync as mkdirSync18, readdirSync as readdirSync18 } from "node:fs";
33415
32935
  import { join as join26, resolve as resolve19, dirname as dirname11 } from "node:path";
33416
32936
  function compileString(scss, importPaths, variables) {
33417
32937
  const imported = /* @__PURE__ */ new Set();
@@ -33442,7 +32962,7 @@ function resolveImports(content, paths, imported) {
33442
32962
  for (const candidate of candidates) {
33443
32963
  if (existsSync24(candidate) && !imported.has(candidate)) {
33444
32964
  imported.add(candidate);
33445
- const fileContent = readFileSync24(candidate, "utf-8");
32965
+ const fileContent = readFileSync23(candidate, "utf-8");
33446
32966
  return resolveImports(fileContent, [dirname11(candidate), ...paths], imported);
33447
32967
  }
33448
32968
  }
@@ -33783,7 +33303,7 @@ var init_scss = __esm({
33783
33303
  /** Compile an SCSS file to CSS. */
33784
33304
  compileFile(filePath) {
33785
33305
  const absPath = resolve19(filePath);
33786
- const content = readFileSync24(absPath, "utf-8");
33306
+ const content = readFileSync23(absPath, "utf-8");
33787
33307
  const paths = [dirname11(absPath), ...this._importPaths];
33788
33308
  return compileString(content, paths, { ...this._variables });
33789
33309
  }
@@ -33806,7 +33326,7 @@ var init_scss = __esm({
33806
33326
  const imported = /* @__PURE__ */ new Set();
33807
33327
  let merged = "";
33808
33328
  for (const file of files) {
33809
- const content = readFileSync24(file, "utf-8");
33329
+ const content = readFileSync23(file, "utf-8");
33810
33330
  imported.add(file);
33811
33331
  merged += resolveImports(content, paths, imported) + "\n";
33812
33332
  }
@@ -33823,7 +33343,7 @@ var init_scss = __esm({
33823
33343
  if (!existsSync24(outDir)) mkdirSync18(outDir, { recursive: true });
33824
33344
  let existing = null;
33825
33345
  try {
33826
- existing = existsSync24(absOutput) ? readFileSync24(absOutput, "utf-8") : null;
33346
+ existing = existsSync24(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
33827
33347
  } catch {
33828
33348
  existing = null;
33829
33349
  }
@@ -33907,10 +33427,10 @@ var init_mqttMessage = __esm({
33907
33427
  });
33908
33428
 
33909
33429
  // src/mqtt.ts
33910
- import net3 from "node:net";
33911
- import tls2 from "node:tls";
33430
+ import net2 from "node:net";
33431
+ import tls from "node:tls";
33912
33432
  import { randomBytes as randomBytes7 } from "node:crypto";
33913
- import { existsSync as existsSync25, readFileSync as readFileSync25 } from "node:fs";
33433
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
33914
33434
  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;
33915
33435
  var init_mqtt = __esm({
33916
33436
  "src/mqtt.ts"() {
@@ -34375,10 +33895,10 @@ var init_mqtt = __esm({
34375
33895
  servername: this.host,
34376
33896
  rejectUnauthorized: this.tlsVerify
34377
33897
  };
34378
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
34379
- sock = tls2.connect(opts, () => settle(() => resolve21(sock)));
33898
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
33899
+ sock = tls.connect(opts, () => settle(() => resolve21(sock)));
34380
33900
  } else {
34381
- sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
33901
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve21(sock)));
34382
33902
  }
34383
33903
  sock.once("error", (err) => {
34384
33904
  settle(() => {
@@ -34929,7 +34449,7 @@ import https from "node:https";
34929
34449
  import { URL as URL2 } from "node:url";
34930
34450
  import { randomBytes as randomBytes8 } from "node:crypto";
34931
34451
  import { promises as fsp, createWriteStream } from "node:fs";
34932
- import { basename as basename6 } from "node:path";
34452
+ import { basename as basename5 } from "node:path";
34933
34453
  import { pipeline } from "node:stream/promises";
34934
34454
  function sameOrigin(urlA, urlB) {
34935
34455
  try {
@@ -35219,7 +34739,7 @@ var init_api = __esm({
35219
34739
  error: err instanceof Error ? err.message : String(err)
35220
34740
  };
35221
34741
  }
35222
- uploadName = filename || basename6(filePath);
34742
+ uploadName = filename || basename5(filePath);
35223
34743
  } else {
35224
34744
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
35225
34745
  }
@@ -35575,6 +35095,777 @@ var init_api = __esm({
35575
35095
  }
35576
35096
  });
35577
35097
 
35098
+ // src/messenger.ts
35099
+ import net3 from "node:net";
35100
+ import tls2 from "node:tls";
35101
+ import { readFileSync as readFileSync25 } from "node:fs";
35102
+ import { basename as basename6 } from "node:path";
35103
+ import { randomUUID as randomUUID9 } from "node:crypto";
35104
+ function tlsRejectUnauthorized() {
35105
+ return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
35106
+ }
35107
+ function readResponse(socket) {
35108
+ return new Promise((resolve21, reject) => {
35109
+ let buffer = "";
35110
+ const onData = (chunk) => {
35111
+ buffer += chunk.toString("utf-8");
35112
+ const lines = buffer.split("\r\n");
35113
+ for (let i = 0; i < lines.length; i++) {
35114
+ const line = lines[i];
35115
+ if (line.length < 3) continue;
35116
+ const code = parseInt(line.substring(0, 3), 10);
35117
+ if (line.length >= 4 && line[3] === " ") {
35118
+ socket.removeListener("data", onData);
35119
+ socket.removeListener("error", onError);
35120
+ resolve21({ code, text: buffer.trim() });
35121
+ return;
35122
+ }
35123
+ }
35124
+ };
35125
+ const onError = (err) => {
35126
+ socket.removeListener("data", onData);
35127
+ reject(err);
35128
+ };
35129
+ socket.on("data", onData);
35130
+ socket.on("error", onError);
35131
+ });
35132
+ }
35133
+ function sendCommand(socket, command) {
35134
+ return new Promise((resolve21, reject) => {
35135
+ socket.write(command + "\r\n", "utf-8", (err) => {
35136
+ if (err) return reject(err);
35137
+ readResponse(socket).then(resolve21, reject);
35138
+ });
35139
+ });
35140
+ }
35141
+ function buildMimeMessage(options) {
35142
+ const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35143
+ const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35144
+ const hasAttachments = options.attachments && options.attachments.length > 0;
35145
+ const hasTextAlt = options.text !== void 0 && options.html;
35146
+ const lines = [];
35147
+ const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
35148
+ lines.push(`From: ${fromHeader}`);
35149
+ lines.push(`To: ${options.to.join(", ")}`);
35150
+ if (options.cc.length > 0) {
35151
+ lines.push(`Cc: ${options.cc.join(", ")}`);
35152
+ }
35153
+ lines.push(`Subject: ${options.subject}`);
35154
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
35155
+ lines.push(`Message-ID: <${options.messageId}>`);
35156
+ lines.push("MIME-Version: 1.0");
35157
+ if (options.replyTo) {
35158
+ lines.push(`Reply-To: ${options.replyTo}`);
35159
+ }
35160
+ if (options.headers) {
35161
+ for (const [key, value] of Object.entries(options.headers)) {
35162
+ lines.push(`${key}: ${value}`);
35163
+ }
35164
+ }
35165
+ if (hasAttachments) {
35166
+ lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
35167
+ lines.push("");
35168
+ lines.push(`--${boundary}`);
35169
+ if (hasTextAlt) {
35170
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35171
+ lines.push("");
35172
+ lines.push(`--${altBoundary}`);
35173
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35174
+ lines.push("Content-Transfer-Encoding: 7bit");
35175
+ lines.push("");
35176
+ lines.push(options.text);
35177
+ lines.push("");
35178
+ lines.push(`--${altBoundary}`);
35179
+ lines.push("Content-Type: text/html; charset=UTF-8");
35180
+ lines.push("Content-Transfer-Encoding: 7bit");
35181
+ lines.push("");
35182
+ lines.push(options.body);
35183
+ lines.push("");
35184
+ lines.push(`--${altBoundary}--`);
35185
+ } else {
35186
+ const contentType = options.html ? "text/html" : "text/plain";
35187
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35188
+ lines.push("Content-Transfer-Encoding: 7bit");
35189
+ lines.push("");
35190
+ lines.push(options.body);
35191
+ }
35192
+ for (const filePath of options.attachments) {
35193
+ const fileName = basename6(filePath);
35194
+ const fileData = readFileSync25(filePath);
35195
+ const base64Data = fileData.toString("base64");
35196
+ lines.push("");
35197
+ lines.push(`--${boundary}`);
35198
+ lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
35199
+ lines.push("Content-Transfer-Encoding: base64");
35200
+ lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
35201
+ lines.push("");
35202
+ for (let i = 0; i < base64Data.length; i += 76) {
35203
+ lines.push(base64Data.substring(i, i + 76));
35204
+ }
35205
+ }
35206
+ lines.push("");
35207
+ lines.push(`--${boundary}--`);
35208
+ } else if (hasTextAlt) {
35209
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35210
+ lines.push("");
35211
+ lines.push(`--${altBoundary}`);
35212
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35213
+ lines.push("Content-Transfer-Encoding: 7bit");
35214
+ lines.push("");
35215
+ lines.push(options.text);
35216
+ lines.push("");
35217
+ lines.push(`--${altBoundary}`);
35218
+ lines.push("Content-Type: text/html; charset=UTF-8");
35219
+ lines.push("Content-Transfer-Encoding: 7bit");
35220
+ lines.push("");
35221
+ lines.push(options.body);
35222
+ lines.push("");
35223
+ lines.push(`--${altBoundary}--`);
35224
+ } else {
35225
+ const contentType = options.html ? "text/html" : "text/plain";
35226
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35227
+ lines.push("");
35228
+ lines.push(options.body);
35229
+ }
35230
+ return lines.join("\r\n");
35231
+ }
35232
+ function imapQuote(s) {
35233
+ if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
35234
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
35235
+ }
35236
+ function imapReadLine(socket) {
35237
+ return new Promise((resolve21, reject) => {
35238
+ let buffer = "";
35239
+ const onData = (chunk) => {
35240
+ buffer += chunk.toString("utf-8");
35241
+ const nlIndex = buffer.indexOf("\r\n");
35242
+ if (nlIndex !== -1) {
35243
+ socket.removeListener("data", onData);
35244
+ socket.removeListener("error", onError);
35245
+ resolve21(buffer);
35246
+ }
35247
+ };
35248
+ const onError = (err) => {
35249
+ socket.removeListener("data", onData);
35250
+ reject(err);
35251
+ };
35252
+ socket.on("data", onData);
35253
+ socket.on("error", onError);
35254
+ });
35255
+ }
35256
+ function imapCommand(socket, command) {
35257
+ return new Promise((resolve21, reject) => {
35258
+ imapTagCounter++;
35259
+ const tag = `T${imapTagCounter}`;
35260
+ const fullCommand = `${tag} ${command}\r
35261
+ `;
35262
+ let buffer = "";
35263
+ const onData = (chunk) => {
35264
+ buffer += chunk.toString("utf-8");
35265
+ if (buffer.includes(`${tag} OK`)) {
35266
+ socket.removeListener("data", onData);
35267
+ socket.removeListener("error", onError);
35268
+ resolve21(buffer);
35269
+ return;
35270
+ }
35271
+ if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
35272
+ socket.removeListener("data", onData);
35273
+ socket.removeListener("error", onError);
35274
+ reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
35275
+ }
35276
+ };
35277
+ const onError = (err) => {
35278
+ socket.removeListener("data", onData);
35279
+ reject(err);
35280
+ };
35281
+ socket.on("data", onData);
35282
+ socket.on("error", onError);
35283
+ socket.write(fullCommand, "utf-8");
35284
+ });
35285
+ }
35286
+ function imapFail(method, err) {
35287
+ const e = err instanceof Error ? err : new Error(String(err));
35288
+ Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
35289
+ if (e instanceof MessengerConnectionError) return e;
35290
+ return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
35291
+ }
35292
+ function parseSearchResponse(response) {
35293
+ const match = response.match(/\* SEARCH (.+)/);
35294
+ if (!match) return [];
35295
+ return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
35296
+ }
35297
+ function parseHeaderResponse(uid, response) {
35298
+ const headers = {};
35299
+ const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
35300
+ if (headerBlock) {
35301
+ const lines = headerBlock[1].split(/\r\n/);
35302
+ let currentKey = "";
35303
+ for (const line of lines) {
35304
+ if (/^\s/.test(line) && currentKey) {
35305
+ headers[currentKey] += " " + line.trim();
35306
+ } else {
35307
+ const colonIdx = line.indexOf(":");
35308
+ if (colonIdx > 0) {
35309
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35310
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35311
+ }
35312
+ }
35313
+ }
35314
+ }
35315
+ const seen = /\\Seen/i.test(response);
35316
+ return {
35317
+ uid,
35318
+ subject: headers["subject"] ?? "",
35319
+ from: headers["from"] ?? "",
35320
+ to: headers["to"] ?? "",
35321
+ date: headers["date"] ?? "",
35322
+ snippet: "",
35323
+ seen
35324
+ };
35325
+ }
35326
+ function emptyFullMessage(uid) {
35327
+ return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
35328
+ }
35329
+ function parseFullMessage(uid, response) {
35330
+ const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
35331
+ const rawMessage = bodyMatch ? bodyMatch[2] : response;
35332
+ const headerEnd = rawMessage.indexOf("\r\n\r\n");
35333
+ const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
35334
+ const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
35335
+ const headers = {};
35336
+ const headerLines = headerSection.split(/\r\n/);
35337
+ let currentKey = "";
35338
+ for (const line of headerLines) {
35339
+ if (/^\s/.test(line) && currentKey) {
35340
+ headers[currentKey] += " " + line.trim();
35341
+ } else {
35342
+ const colonIdx = line.indexOf(":");
35343
+ if (colonIdx > 0) {
35344
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35345
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35346
+ }
35347
+ }
35348
+ }
35349
+ const contentType = headers["content-type"] ?? "text/plain";
35350
+ let bodyText = "";
35351
+ let bodyHtml = "";
35352
+ if (contentType.includes("multipart")) {
35353
+ const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
35354
+ if (boundaryMatch) {
35355
+ const boundary = boundaryMatch[1];
35356
+ const parts = bodySection.split("--" + boundary);
35357
+ for (const part of parts) {
35358
+ if (part.trim() === "" || part.trim() === "--") continue;
35359
+ const partHeaderEnd = part.indexOf("\r\n\r\n");
35360
+ const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
35361
+ const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
35362
+ if (partHeaders.includes("text/html")) {
35363
+ bodyHtml = partBody;
35364
+ } else if (partHeaders.includes("text/plain")) {
35365
+ bodyText = partBody;
35366
+ }
35367
+ }
35368
+ }
35369
+ } else if (contentType.includes("text/html")) {
35370
+ bodyHtml = bodySection;
35371
+ } else {
35372
+ bodyText = bodySection;
35373
+ }
35374
+ bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35375
+ bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35376
+ return {
35377
+ uid,
35378
+ subject: headers["subject"] ?? "",
35379
+ from: headers["from"] ?? "",
35380
+ to: headers["to"] ?? "",
35381
+ cc: headers["cc"] ?? "",
35382
+ date: headers["date"] ?? "",
35383
+ bodyText,
35384
+ bodyHtml,
35385
+ headers
35386
+ };
35387
+ }
35388
+ function createMessenger() {
35389
+ return new Messenger();
35390
+ }
35391
+ var MessengerConnectionError, Messenger, imapTagCounter;
35392
+ var init_messenger = __esm({
35393
+ "src/messenger.ts"() {
35394
+ "use strict";
35395
+ init_dotenv();
35396
+ init_devMailbox();
35397
+ init_logger();
35398
+ MessengerConnectionError = class extends Error {
35399
+ constructor(message) {
35400
+ super(message);
35401
+ this.name = "MessengerConnectionError";
35402
+ }
35403
+ };
35404
+ Messenger = class {
35405
+ host;
35406
+ port;
35407
+ username;
35408
+ password;
35409
+ fromAddress;
35410
+ fromName;
35411
+ encryption;
35412
+ useTls;
35413
+ /** Whether an SMTP host was actually configured (see the constructor). */
35414
+ smtpConfigured = false;
35415
+ /** The local mailbox, present only when this messenger captures. */
35416
+ devMailbox = null;
35417
+ imapHost;
35418
+ imapPort;
35419
+ imapUser;
35420
+ imapPass;
35421
+ imapEncryption;
35422
+ constructor(options) {
35423
+ this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
35424
+ this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
35425
+ this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
35426
+ this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
35427
+ this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
35428
+ this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
35429
+ this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
35430
+ const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
35431
+ if (envEncryption) {
35432
+ this.encryption = envEncryption.toLowerCase();
35433
+ } else if (options?.useTls !== void 0) {
35434
+ this.encryption = options.useTls ? "tls" : "none";
35435
+ } else {
35436
+ this.encryption = "tls";
35437
+ }
35438
+ this.useTls = ["tls", "starttls"].includes(this.encryption);
35439
+ this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
35440
+ this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
35441
+ this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
35442
+ this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
35443
+ this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
35444
+ }
35445
+ /**
35446
+ * Read-only IMAP encryption mode for inspection / tests.
35447
+ * Returns one of "tls", "starttls", "none", "ssl".
35448
+ */
35449
+ getImapEncryption() {
35450
+ return this.imapEncryption;
35451
+ }
35452
+ /**
35453
+ * Send an email via SMTP.
35454
+ */
35455
+ /**
35456
+ * Should send() capture locally instead of talking to SMTP?
35457
+ *
35458
+ * Availability decides, not verbosity. With no SMTP host configured sending is
35459
+ * impossible, so simulate it into a folder rather than failing -- that is what
35460
+ * makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
35461
+ * even when a host IS configured.
35462
+ *
35463
+ * TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
35464
+ * must still be able to send, and the old `NODE_ENV !== "production"` clause
35465
+ * silently swallowed every staging email.
35466
+ */
35467
+ shouldCapture() {
35468
+ if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
35469
+ return !this.smtpConfigured;
35470
+ }
35471
+ /** The local mailbox, created on first capture and reused after. */
35472
+ getDevMailbox() {
35473
+ if (this.devMailbox === null) {
35474
+ this.devMailbox = new DevMailbox();
35475
+ }
35476
+ return this.devMailbox;
35477
+ }
35478
+ async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
35479
+ const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
35480
+ const toList = Array.isArray(options.to) ? options.to : [options.to];
35481
+ const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
35482
+ const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
35483
+ const allRecipients = [...toList, ...ccList, ...bccList];
35484
+ if (this.shouldCapture()) {
35485
+ return this.getDevMailbox().capture(
35486
+ to,
35487
+ subject,
35488
+ body,
35489
+ html,
35490
+ text,
35491
+ ccList,
35492
+ bccList,
35493
+ replyTo,
35494
+ attachments,
35495
+ this.fromAddress || void 0
35496
+ );
35497
+ }
35498
+ const messageId = `${randomUUID9()}@${this.host}`;
35499
+ if (allRecipients.length === 0) {
35500
+ return { success: false, message: "No recipients specified" };
35501
+ }
35502
+ if (!this.fromAddress) {
35503
+ return { success: false, message: "No from address configured" };
35504
+ }
35505
+ try {
35506
+ let socket;
35507
+ if (this.port === 465) {
35508
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35509
+ await new Promise((resolve21, reject) => {
35510
+ socket.once("secureConnect", resolve21);
35511
+ socket.once("error", reject);
35512
+ });
35513
+ } else {
35514
+ socket = net3.createConnection({ host: this.host, port: this.port });
35515
+ await new Promise((resolve21, reject) => {
35516
+ socket.once("connect", resolve21);
35517
+ socket.once("error", reject);
35518
+ });
35519
+ }
35520
+ const greeting = await readResponse(socket);
35521
+ if (greeting.code !== 220) {
35522
+ socket.destroy();
35523
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35524
+ }
35525
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35526
+ if (ehlo.code !== 250) {
35527
+ socket.destroy();
35528
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35529
+ }
35530
+ if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
35531
+ const starttls = await sendCommand(socket, "STARTTLS");
35532
+ if (starttls.code !== 220) {
35533
+ socket.destroy();
35534
+ return { success: false, message: `STARTTLS failed: ${starttls.text}` };
35535
+ }
35536
+ const plainSocket = socket;
35537
+ socket = tls2.connect(
35538
+ { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
35539
+ );
35540
+ await new Promise((resolve21, reject) => {
35541
+ socket.once("secureConnect", resolve21);
35542
+ socket.once("error", reject);
35543
+ });
35544
+ const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
35545
+ if (ehlo2.code !== 250) {
35546
+ socket.destroy();
35547
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
35548
+ }
35549
+ }
35550
+ if (this.username && this.password) {
35551
+ const auth = await sendCommand(socket, "AUTH LOGIN");
35552
+ if (auth.code !== 334) {
35553
+ socket.destroy();
35554
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
35555
+ }
35556
+ const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
35557
+ if (userResp.code !== 334) {
35558
+ socket.destroy();
35559
+ return { success: false, message: `AUTH username failed: ${userResp.text}` };
35560
+ }
35561
+ const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
35562
+ if (passResp.code !== 235) {
35563
+ socket.destroy();
35564
+ return { success: false, message: `AUTH password failed: ${passResp.text}` };
35565
+ }
35566
+ }
35567
+ const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
35568
+ if (mailFrom.code !== 250) {
35569
+ socket.destroy();
35570
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
35571
+ }
35572
+ for (const recipient of allRecipients) {
35573
+ const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
35574
+ if (rcpt.code !== 250 && rcpt.code !== 251) {
35575
+ socket.destroy();
35576
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
35577
+ }
35578
+ }
35579
+ const dataCmd = await sendCommand(socket, "DATA");
35580
+ if (dataCmd.code !== 354) {
35581
+ socket.destroy();
35582
+ return { success: false, message: `DATA failed: ${dataCmd.text}` };
35583
+ }
35584
+ const mimeMessage = buildMimeMessage({
35585
+ from: this.fromAddress,
35586
+ fromName: this.fromName,
35587
+ to: toList,
35588
+ cc: ccList,
35589
+ subject: options.subject,
35590
+ body: options.body,
35591
+ html: options.html ?? false,
35592
+ text: options.text,
35593
+ replyTo: options.replyTo,
35594
+ attachments: options.attachments,
35595
+ headers: options.headers,
35596
+ messageId
35597
+ });
35598
+ const endData = await sendCommand(socket, mimeMessage + "\r\n.");
35599
+ if (endData.code !== 250) {
35600
+ socket.destroy();
35601
+ return { success: false, message: `Message delivery failed: ${endData.text}` };
35602
+ }
35603
+ await sendCommand(socket, "QUIT");
35604
+ socket.destroy();
35605
+ return { success: true, message: "Email sent successfully", id: messageId };
35606
+ } catch (err) {
35607
+ const errMsg = err instanceof Error ? err.message : String(err);
35608
+ return { success: false, message: `SMTP error: ${errMsg}` };
35609
+ }
35610
+ }
35611
+ /**
35612
+ * Test the SMTP connection without sending an email.
35613
+ */
35614
+ async testConnection() {
35615
+ try {
35616
+ let socket;
35617
+ if (this.port === 465) {
35618
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35619
+ await new Promise((resolve21, reject) => {
35620
+ socket.once("secureConnect", resolve21);
35621
+ socket.once("error", reject);
35622
+ });
35623
+ } else {
35624
+ socket = net3.createConnection({ host: this.host, port: this.port });
35625
+ await new Promise((resolve21, reject) => {
35626
+ socket.once("connect", resolve21);
35627
+ socket.once("error", reject);
35628
+ });
35629
+ }
35630
+ const greeting = await readResponse(socket);
35631
+ if (greeting.code !== 220) {
35632
+ socket.destroy();
35633
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35634
+ }
35635
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35636
+ if (ehlo.code !== 250) {
35637
+ socket.destroy();
35638
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35639
+ }
35640
+ await sendCommand(socket, "QUIT");
35641
+ socket.destroy();
35642
+ return { success: true, message: `Connected to ${this.host}:${this.port}` };
35643
+ } catch (err) {
35644
+ const errMsg = err instanceof Error ? err.message : String(err);
35645
+ return { success: false, message: `Connection failed: ${errMsg}` };
35646
+ }
35647
+ }
35648
+ // ── IMAP (Read) ────────────────────────────────────────────
35649
+ /**
35650
+ * Connect to the IMAP server via raw TCP/TLS.
35651
+ * Returns the socket and reads the greeting.
35652
+ */
35653
+ async imapConnect() {
35654
+ if (!this.imapHost) {
35655
+ throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
35656
+ }
35657
+ let socket;
35658
+ const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
35659
+ if (useTls) {
35660
+ socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
35661
+ await new Promise((resolve21, reject) => {
35662
+ socket.once("secureConnect", resolve21);
35663
+ socket.once("error", reject);
35664
+ });
35665
+ } else {
35666
+ socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
35667
+ await new Promise((resolve21, reject) => {
35668
+ socket.once("connect", resolve21);
35669
+ socket.once("error", reject);
35670
+ });
35671
+ }
35672
+ await imapReadLine(socket);
35673
+ if (this.imapUser && this.imapPass) {
35674
+ const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
35675
+ if (!loginResp.includes("OK")) {
35676
+ socket.destroy();
35677
+ throw new Error(`IMAP login failed: ${loginResp}`);
35678
+ }
35679
+ }
35680
+ return socket;
35681
+ }
35682
+ /**
35683
+ * Disconnect from IMAP cleanly.
35684
+ */
35685
+ async imapDisconnect(socket) {
35686
+ try {
35687
+ await imapCommand(socket, "LOGOUT");
35688
+ } catch {
35689
+ }
35690
+ socket.destroy();
35691
+ }
35692
+ /**
35693
+ * Fetch latest messages from a folder.
35694
+ * Returns list of message summaries.
35695
+ */
35696
+ async inbox(limit = 20, offset = 0, folder = "INBOX") {
35697
+ let socket;
35698
+ try {
35699
+ socket = await this.imapConnect();
35700
+ } catch (err) {
35701
+ throw imapFail("inbox", err);
35702
+ }
35703
+ try {
35704
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35705
+ const searchResp = await imapCommand(socket, "SEARCH ALL");
35706
+ const uids = parseSearchResponse(searchResp);
35707
+ if (uids.length === 0) return [];
35708
+ uids.reverse();
35709
+ const selected = uids.slice(offset, offset + limit);
35710
+ if (selected.length === 0) return [];
35711
+ const messages = [];
35712
+ for (const uid of selected) {
35713
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35714
+ messages.push(parseHeaderResponse(uid, fetchResp));
35715
+ }
35716
+ return messages;
35717
+ } catch (err) {
35718
+ throw imapFail("inbox", err);
35719
+ } finally {
35720
+ await this.imapDisconnect(socket);
35721
+ }
35722
+ }
35723
+ /**
35724
+ * Read a single message by sequence number or UID.
35725
+ */
35726
+ async read(uid, folder = "INBOX") {
35727
+ let socket;
35728
+ try {
35729
+ socket = await this.imapConnect();
35730
+ } catch (err) {
35731
+ throw imapFail("read", err);
35732
+ }
35733
+ try {
35734
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35735
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
35736
+ if (!/\{\d+\}/.test(fetchResp)) {
35737
+ return emptyFullMessage(uid);
35738
+ }
35739
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35740
+ return parseFullMessage(uid, fetchResp);
35741
+ } catch (err) {
35742
+ throw imapFail("read", err);
35743
+ } finally {
35744
+ await this.imapDisconnect(socket);
35745
+ }
35746
+ }
35747
+ /**
35748
+ * Search messages using IMAP search criteria.
35749
+ */
35750
+ async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
35751
+ const criteria = ["ALL"];
35752
+ if (subject) criteria.push(`SUBJECT "${subject}"`);
35753
+ if (sender) criteria.push(`FROM "${sender}"`);
35754
+ if (since) criteria.push(`SINCE ${since}`);
35755
+ if (before) criteria.push(`BEFORE ${before}`);
35756
+ if (unseenOnly) criteria.push("UNSEEN");
35757
+ const query = criteria.join(" ");
35758
+ let socket;
35759
+ try {
35760
+ socket = await this.imapConnect();
35761
+ } catch (err) {
35762
+ throw imapFail("search", err);
35763
+ }
35764
+ try {
35765
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35766
+ const searchResp = await imapCommand(socket, `SEARCH ${query}`);
35767
+ const uids = parseSearchResponse(searchResp);
35768
+ if (uids.length === 0) return [];
35769
+ uids.reverse();
35770
+ const messages = [];
35771
+ for (const uid of uids.slice(0, limit)) {
35772
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35773
+ messages.push(parseHeaderResponse(uid, fetchResp));
35774
+ }
35775
+ return messages;
35776
+ } catch (err) {
35777
+ throw imapFail("search", err);
35778
+ } finally {
35779
+ await this.imapDisconnect(socket);
35780
+ }
35781
+ }
35782
+ /**
35783
+ * Delete a message by UID.
35784
+ */
35785
+ async deleteMessage(uid, folder = "INBOX") {
35786
+ const socket = await this.imapConnect();
35787
+ try {
35788
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35789
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
35790
+ await imapCommand(socket, "EXPUNGE");
35791
+ } finally {
35792
+ await this.imapDisconnect(socket);
35793
+ }
35794
+ }
35795
+ /**
35796
+ * Mark a message as read.
35797
+ */
35798
+ async markRead(uid, folder = "INBOX") {
35799
+ const socket = await this.imapConnect();
35800
+ try {
35801
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35802
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35803
+ } finally {
35804
+ await this.imapDisconnect(socket);
35805
+ }
35806
+ }
35807
+ /**
35808
+ * Count unseen messages in a folder.
35809
+ */
35810
+ async unread(folder = "INBOX") {
35811
+ let socket;
35812
+ try {
35813
+ socket = await this.imapConnect();
35814
+ } catch (err) {
35815
+ throw imapFail("unread", err);
35816
+ }
35817
+ try {
35818
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35819
+ const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
35820
+ return parseSearchResponse(searchResp).length;
35821
+ } catch (err) {
35822
+ throw imapFail("unread", err);
35823
+ } finally {
35824
+ await this.imapDisconnect(socket);
35825
+ }
35826
+ }
35827
+ /**
35828
+ * List available IMAP folders/mailboxes.
35829
+ */
35830
+ async folders() {
35831
+ let socket;
35832
+ try {
35833
+ socket = await this.imapConnect();
35834
+ } catch (err) {
35835
+ throw imapFail("folders", err);
35836
+ }
35837
+ try {
35838
+ const resp = await imapCommand(socket, 'LIST "" "*"');
35839
+ const result = [];
35840
+ for (const line of resp.split("\r\n")) {
35841
+ const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
35842
+ if (m) result.push(m[1]);
35843
+ }
35844
+ return result;
35845
+ } catch (err) {
35846
+ throw imapFail("folders", err);
35847
+ } finally {
35848
+ await this.imapDisconnect(socket);
35849
+ }
35850
+ }
35851
+ /**
35852
+ * Test IMAP connectivity without reading.
35853
+ */
35854
+ async testImapConnection() {
35855
+ try {
35856
+ const socket = await this.imapConnect();
35857
+ await this.imapDisconnect(socket);
35858
+ return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
35859
+ } catch (err) {
35860
+ const errMsg = err instanceof Error ? err.message : String(err);
35861
+ return { success: false, message: `IMAP connection failed: ${errMsg}` };
35862
+ }
35863
+ }
35864
+ };
35865
+ imapTagCounter = 0;
35866
+ }
35867
+ });
35868
+
35578
35869
  // src/wsdl.ts
35579
35870
  function escapeXml(value) {
35580
35871
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");