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
@@ -1298,11 +1298,14 @@ var init_sqlite = __esm({
1298
1298
  const pragma = schema && isIdentifier(schema) && isIdentifier(tbl) ? `PRAGMA ${schema}.table_info("${tbl}")` : `PRAGMA table_info("${table2}")`;
1299
1299
  const rows = this.db.prepare(pragma).all();
1300
1300
  return rows.map((r) => ({
1301
+ // PRAGMA table_info reports `pk` as the 1-BASED POSITION within the primary
1302
+ // key, not a boolean: a composite key gives pk=1, pk=2, ... Testing `=== 1`
1303
+ // reported only the first column of a composite key.
1301
1304
  name: r.name,
1302
1305
  type: r.type,
1303
1306
  nullable: r.notnull === 0,
1304
1307
  default: r.dflt_value,
1305
- primaryKey: r.pk === 1
1308
+ primaryKey: Number(r.pk) > 0
1306
1309
  }));
1307
1310
  }
1308
1311
  lastInsertId() {
@@ -4298,6 +4301,8 @@ var init_database = __esm({
4298
4301
  poolIndex = 0;
4299
4302
  /** Factory for creating new adapters (used by pool) */
4300
4303
  adapterFactory = null;
4304
+ /** table -> primary-key column name (or null), introspected once */
4305
+ _pkCache = /* @__PURE__ */ new Map();
4301
4306
  /**
4302
4307
  * Whether a standalone write auto-commits. ON by default — a write made
4303
4308
  * outside an explicit transaction commits on its own connection before
@@ -4556,29 +4561,112 @@ var init_database = __esm({
4556
4561
  }
4557
4562
  return result;
4558
4563
  }
4559
- /** Update rows in a table matching filter. */
4564
+ /**
4565
+ * The table's primary-key column, introspected once and cached.
4566
+ *
4567
+ * Uses the cross-engine getColumns() contract (v3.13.14, #48), which reports
4568
+ * primaryKey per column on every adapter. Resolves to null when the table has
4569
+ * no primary key or cannot be introspected.
4570
+ */
4571
+ async primaryKey(table2) {
4572
+ if (!this._pkCache.has(table2)) {
4573
+ let pk = [];
4574
+ try {
4575
+ const columns = await this.getColumns(table2);
4576
+ pk = columns.filter((c) => c.primaryKey).map((c) => c.name);
4577
+ } catch {
4578
+ pk = [];
4579
+ }
4580
+ this._pkCache.set(table2, pk);
4581
+ }
4582
+ return this._pkCache.get(table2) ?? [];
4583
+ }
4584
+ /**
4585
+ * A failed write must be loud.
4586
+ *
4587
+ * The adapters catch a SQL error and return { success: false, affectedRows: 0 },
4588
+ * so a filterless update produced invalid SQL ("... WHERE ") and reported
4589
+ * nothing rather than raising. A caller who does not inspect the result
4590
+ * believes the write landed (audit feature 4, P1).
4591
+ */
4592
+ static assertWrote(result, verb, table2) {
4593
+ if (result && result.success === false) {
4594
+ throw new Error(
4595
+ `${verb} failed on ${table2}: ${result.error ?? "unknown error"}`
4596
+ );
4597
+ }
4598
+ return result;
4599
+ }
4600
+ /**
4601
+ * Update rows. A write with no filter is an error, not a full-table write.
4602
+ *
4603
+ * With no explicit filter the primary key is taken out of `data` and used as
4604
+ * the WHERE clause. With neither a filter nor a primary key in `data` this
4605
+ * throws rather than silently changing nothing (audit feature 4, P1).
4606
+ */
4560
4607
  async update(table2, data, filter, params) {
4608
+ let effectiveFilter = filter ?? {};
4609
+ let effectiveData = data;
4610
+ if (Object.keys(effectiveFilter).length === 0) {
4611
+ const pkColumns = await this.primaryKey(table2);
4612
+ const missing = pkColumns.filter((c) => !(c in data));
4613
+ if (pkColumns.length === 0 || missing.length > 0) {
4614
+ throw new Error(
4615
+ `update requires a filter or the complete primary key in the data; pass filter explicitly to update multiple rows (table=${table2}, primary key=[${pkColumns.join(", ")}], missing from data=[${missing.join(", ")}]). To empty a table use truncate(${table2}).`
4616
+ );
4617
+ }
4618
+ effectiveData = { ...data };
4619
+ const keyed = {};
4620
+ for (const col of pkColumns) {
4621
+ keyed[col] = effectiveData[col];
4622
+ delete effectiveData[col];
4623
+ }
4624
+ if (Object.keys(effectiveData).length === 0) {
4625
+ throw new Error(
4626
+ `update was given only the primary key [${pkColumns.join(", ")}] and no columns to set (table=${table2})`
4627
+ );
4628
+ }
4629
+ effectiveFilter = keyed;
4630
+ }
4561
4631
  const adapter = this.getNextAdapter();
4562
- const result = adapter.updateAsync ? await adapter.updateAsync(table2, data, filter ?? {}, params) : adapter.update(table2, data, filter ?? {}, params);
4632
+ const result = adapter.updateAsync ? await adapter.updateAsync(table2, effectiveData, effectiveFilter, params) : adapter.update(table2, effectiveData, effectiveFilter, params);
4563
4633
  if (this.autoCommit && !this.inExplicitTransaction()) {
4564
4634
  try {
4565
4635
  await adapterCommit(adapter);
4566
4636
  } catch {
4567
4637
  }
4568
4638
  }
4569
- return result;
4639
+ return _Database.assertWrote(result, "update", table2);
4570
4640
  }
4571
- /** Delete rows from a table matching filter. */
4641
+ /** Delete rows. A filterless delete throws; use truncate() to empty a table. */
4572
4642
  async delete(table2, filter, params) {
4643
+ const effectiveFilter = filter ?? {};
4644
+ if (!Array.isArray(effectiveFilter) && typeof effectiveFilter !== "string" && Object.keys(effectiveFilter).length === 0) {
4645
+ throw new Error(
4646
+ `delete requires a filter (table=${table2}). To remove every row use truncate(${table2}).`
4647
+ );
4648
+ }
4573
4649
  const adapter = this.getNextAdapter();
4574
- const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, filter ?? {}, params) : adapter.delete(table2, filter ?? {}, params);
4650
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, effectiveFilter, params) : adapter.delete(table2, effectiveFilter, params);
4575
4651
  if (this.autoCommit && !this.inExplicitTransaction()) {
4576
4652
  try {
4577
4653
  await adapterCommit(adapter);
4578
4654
  } catch {
4579
4655
  }
4580
4656
  }
4581
- return result;
4657
+ return _Database.assertWrote(result, "delete", table2);
4658
+ }
4659
+ /** Remove every row. The explicit spelling of a whole-table delete. */
4660
+ async truncate(table2) {
4661
+ const adapter = this.getNextAdapter();
4662
+ const result = adapter.deleteAsync ? await adapter.deleteAsync(table2, "1 = 1", []) : adapter.delete(table2, "1 = 1", []);
4663
+ if (this.autoCommit && !this.inExplicitTransaction()) {
4664
+ try {
4665
+ await adapterCommit(adapter);
4666
+ } catch {
4667
+ }
4668
+ }
4669
+ return _Database.assertWrote(result, "truncate", table2);
4582
4670
  }
4583
4671
  /** Close all database connections (pool or single). */
4584
4672
  close() {
@@ -13827,7 +13915,7 @@ function _generateFormToken(descriptor = "") {
13827
13915
  function _generateFormTokenValue(descriptor = "") {
13828
13916
  return new SafeString(_buildFormTokenJwt(descriptor));
13829
13917
  }
13830
- var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13918
+ var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
13831
13919
  var init_engine = __esm({
13832
13920
  "../frond/src/engine.ts"() {
13833
13921
  "use strict";
@@ -13870,6 +13958,29 @@ var init_engine = __esm({
13870
13958
  "endset",
13871
13959
  "endspaceless"
13872
13960
  ]);
13961
+ GATEABLE_TAGS = /* @__PURE__ */ new Set([
13962
+ "autoescape",
13963
+ "cache",
13964
+ "for",
13965
+ "from",
13966
+ "if",
13967
+ "import",
13968
+ "include",
13969
+ "live",
13970
+ "macro",
13971
+ "set",
13972
+ "spaceless"
13973
+ ]);
13974
+ BLOCK_TAG_ENDS = {
13975
+ autoescape: "endautoescape",
13976
+ cache: "endcache",
13977
+ for: "endfor",
13978
+ if: "endif",
13979
+ live: "endlive",
13980
+ macro: "endmacro",
13981
+ set: "endset",
13982
+ spaceless: "endspaceless"
13983
+ };
13873
13984
  JSON_UNSAFE_RE = /[<>&'\u2028\u2029]/g;
13874
13985
  JSON_UNSAFE_MAP = {
13875
13986
  "<": "\\u003c",
@@ -14505,42 +14616,27 @@ var init_engine = __esm({
14505
14616
  if (stripA && i + 1 < tokens.length && tokens[i + 1][0] === "TEXT") {
14506
14617
  tokens[i + 1] = ["TEXT", tokens[i + 1][1].replace(LEADING_WS_RE, "")];
14507
14618
  }
14508
- if (tag === "if") {
14509
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("if")) {
14510
- const skip = this.skipBlock(tokens, i, "if", "endif");
14511
- i = skip;
14512
- } else {
14513
- const [result, skip] = this.handleIf(tokens, i, context);
14514
- output.push(result);
14515
- i = skip;
14516
- }
14619
+ if (!this.tagPermitted(tag)) {
14620
+ i = this.skipDeniedTag(tokens, i, tag, content);
14621
+ } else if (tag === "if") {
14622
+ const [result, skip] = this.handleIf(tokens, i, context);
14623
+ output.push(result);
14624
+ i = skip;
14517
14625
  } else if (tag === "for") {
14518
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("for")) {
14519
- const skip = this.skipBlock(tokens, i, "for", "endfor");
14520
- i = skip;
14521
- } else {
14522
- const [result, skip] = this.handleFor(tokens, i, context);
14523
- output.push(result);
14524
- i = skip;
14525
- }
14626
+ const [result, skip] = this.handleFor(tokens, i, context);
14627
+ output.push(result);
14628
+ i = skip;
14526
14629
  } else if (tag === "set") {
14527
- const isBlockSet = !content.includes("=");
14528
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("set")) {
14529
- i = isBlockSet ? this.skipBlock(tokens, i, "set", "endset") : i + 1;
14530
- } else if (isBlockSet) {
14630
+ if (!content.includes("=")) {
14531
14631
  i = this.handleSetBlock(tokens, i, context);
14532
14632
  } else {
14533
14633
  this.handleSet(content, context);
14534
14634
  i++;
14535
14635
  }
14536
14636
  } else if (tag === "include") {
14537
- if (this._sandbox && this._allowedTags !== null && !this._allowedTags.has("include")) {
14538
- i++;
14539
- } else {
14540
- const result = this.handleInclude(content, context);
14541
- output.push(result);
14542
- i++;
14543
- }
14637
+ const result = this.handleInclude(content, context);
14638
+ output.push(result);
14639
+ i++;
14544
14640
  } else if (tag === "macro") {
14545
14641
  const skip = this.handleMacro(tokens, i, context);
14546
14642
  i = skip;
@@ -14585,6 +14681,41 @@ var init_engine = __esm({
14585
14681
  }
14586
14682
  return output.join("");
14587
14683
  }
14684
+ /**
14685
+ * May this filter RUN under the current sandbox?
14686
+ *
14687
+ * The escaping decision has to ask this rather than read the filter name out of
14688
+ * the source. Node carries safety as a FLAG rather than as a value-level marker
14689
+ * (Python and Ruby return a SafeString, PHP prepends a RAW_MARKER -- all three
14690
+ * produced only by actually running the filter), so here the name alone was
14691
+ * enough to suppress auto-escaping even when the filter was denied and skipped.
14692
+ */
14693
+ filterPermitted(name) {
14694
+ if (!this._sandbox || this._allowedFilters === null) return true;
14695
+ return this._allowedFilters.has(name);
14696
+ }
14697
+ /**
14698
+ * May this tag run under the current sandbox?
14699
+ *
14700
+ * One gate for every tag, so the allow-list governs the whole tag vocabulary
14701
+ * instead of the four names that happened to be checked individually.
14702
+ */
14703
+ tagPermitted(tag) {
14704
+ if (!this._sandbox || this._allowedTags === null) return true;
14705
+ if (!GATEABLE_TAGS.has(tag)) return true;
14706
+ return this._allowedTags.has(tag);
14707
+ }
14708
+ /**
14709
+ * Consume a denied tag WITHOUT running it, returning the index past its body.
14710
+ *
14711
+ * Advancing a single token past a body-owning tag would leave the body's tokens
14712
+ * to render at the TOP level, leaking exactly the content the sandbox denied.
14713
+ */
14714
+ skipDeniedTag(tokens, start2, tag, content) {
14715
+ const closeTag = BLOCK_TAG_ENDS[tag];
14716
+ if (closeTag === void 0 || tag === "set" && content.includes("=")) return start2 + 1;
14717
+ return this.skipBlock(tokens, start2, tag, closeTag);
14718
+ }
14588
14719
  skipBlock(tokens, start2, openTag, closeTag) {
14589
14720
  let depth = 0;
14590
14721
  let i = start2 + 1;
@@ -14592,7 +14723,7 @@ var init_engine = __esm({
14592
14723
  if (tokens[i][0] === "BLOCK") {
14593
14724
  const [content] = stripTag(tokens[i][1]);
14594
14725
  const tag = content.split(/\s+/)[0] || "";
14595
- if (tag === openTag) depth++;
14726
+ if (tag === openTag && !(openTag === "set" && content.includes("="))) depth++;
14596
14727
  else if (tag === closeTag) {
14597
14728
  if (depth === 0) return i + 1;
14598
14729
  depth--;
@@ -14777,11 +14908,11 @@ var init_engine = __esm({
14777
14908
  for (const [fname, rawArgs] of filters) {
14778
14909
  const args = rawArgs.map((a) => a instanceof VarRef ? evalExpr(a.name, context) : a);
14779
14910
  if (fname === "raw" || fname === "safe") {
14780
- isSafe = true;
14911
+ if (this.filterPermitted(fname)) isSafe = true;
14781
14912
  continue;
14782
14913
  }
14783
14914
  if (fname === "escape" || fname === "e") {
14784
- isSafe = true;
14915
+ if (this.filterPermitted(fname)) isSafe = true;
14785
14916
  }
14786
14917
  if (this._sandbox && this._allowedFilters !== null) {
14787
14918
  if (!this._allowedFilters.has(fname)) {
@@ -16592,861 +16723,131 @@ var init_rateLimiter = __esm({
16592
16723
  }
16593
16724
  });
16594
16725
 
16595
- // ../core/src/messenger.ts
16596
- import net2 from "node:net";
16597
- import tls from "node:tls";
16598
- import { readFileSync as readFileSync8 } from "node:fs";
16599
- import { basename as basename3 } from "node:path";
16726
+ // ../core/src/devMailbox.ts
16727
+ import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
16728
+ import { join as join13 } from "node:path";
16600
16729
  import { randomUUID as randomUUID2 } from "node:crypto";
16601
- function tlsRejectUnauthorized() {
16602
- return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
16603
- }
16604
- function readResponse(socket) {
16605
- return new Promise((resolve31, reject) => {
16606
- let buffer = "";
16607
- const onData = (chunk) => {
16608
- buffer += chunk.toString("utf-8");
16609
- const lines = buffer.split("\r\n");
16610
- for (let i = 0; i < lines.length; i++) {
16611
- const line = lines[i];
16612
- if (line.length < 3) continue;
16613
- const code = parseInt(line.substring(0, 3), 10);
16614
- if (line.length >= 4 && line[3] === " ") {
16615
- socket.removeListener("data", onData);
16616
- socket.removeListener("error", onError);
16617
- resolve31({ code, text: buffer.trim() });
16618
- return;
16619
- }
16620
- }
16621
- };
16622
- const onError = (err) => {
16623
- socket.removeListener("data", onData);
16624
- reject(err);
16625
- };
16626
- socket.on("data", onData);
16627
- socket.on("error", onError);
16628
- });
16629
- }
16630
- function sendCommand(socket, command) {
16631
- return new Promise((resolve31, reject) => {
16632
- socket.write(command + "\r\n", "utf-8", (err) => {
16633
- if (err) return reject(err);
16634
- readResponse(socket).then(resolve31, reject);
16635
- });
16636
- });
16637
- }
16638
- function buildMimeMessage(options) {
16639
- const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16640
- const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
16641
- const hasAttachments = options.attachments && options.attachments.length > 0;
16642
- const hasTextAlt = options.text !== void 0 && options.html;
16643
- const lines = [];
16644
- const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
16645
- lines.push(`From: ${fromHeader}`);
16646
- lines.push(`To: ${options.to.join(", ")}`);
16647
- if (options.cc.length > 0) {
16648
- lines.push(`Cc: ${options.cc.join(", ")}`);
16649
- }
16650
- lines.push(`Subject: ${options.subject}`);
16651
- lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
16652
- lines.push(`Message-ID: <${options.messageId}>`);
16653
- lines.push("MIME-Version: 1.0");
16654
- if (options.replyTo) {
16655
- lines.push(`Reply-To: ${options.replyTo}`);
16656
- }
16657
- if (options.headers) {
16658
- for (const [key, value] of Object.entries(options.headers)) {
16659
- lines.push(`${key}: ${value}`);
16660
- }
16661
- }
16662
- if (hasAttachments) {
16663
- lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
16664
- lines.push("");
16665
- lines.push(`--${boundary}`);
16666
- if (hasTextAlt) {
16667
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16668
- lines.push("");
16669
- lines.push(`--${altBoundary}`);
16670
- lines.push("Content-Type: text/plain; charset=UTF-8");
16671
- lines.push("Content-Transfer-Encoding: 7bit");
16672
- lines.push("");
16673
- lines.push(options.text);
16674
- lines.push("");
16675
- lines.push(`--${altBoundary}`);
16676
- lines.push("Content-Type: text/html; charset=UTF-8");
16677
- lines.push("Content-Transfer-Encoding: 7bit");
16678
- lines.push("");
16679
- lines.push(options.body);
16680
- lines.push("");
16681
- lines.push(`--${altBoundary}--`);
16682
- } else {
16683
- const contentType = options.html ? "text/html" : "text/plain";
16684
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16685
- lines.push("Content-Transfer-Encoding: 7bit");
16686
- lines.push("");
16687
- lines.push(options.body);
16688
- }
16689
- for (const filePath of options.attachments) {
16690
- const fileName = basename3(filePath);
16691
- const fileData = readFileSync8(filePath);
16692
- const base64Data = fileData.toString("base64");
16693
- lines.push("");
16694
- lines.push(`--${boundary}`);
16695
- lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
16696
- lines.push("Content-Transfer-Encoding: base64");
16697
- lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
16698
- lines.push("");
16699
- for (let i = 0; i < base64Data.length; i += 76) {
16700
- lines.push(base64Data.substring(i, i + 76));
16701
- }
16702
- }
16703
- lines.push("");
16704
- lines.push(`--${boundary}--`);
16705
- } else if (hasTextAlt) {
16706
- lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
16707
- lines.push("");
16708
- lines.push(`--${altBoundary}`);
16709
- lines.push("Content-Type: text/plain; charset=UTF-8");
16710
- lines.push("Content-Transfer-Encoding: 7bit");
16711
- lines.push("");
16712
- lines.push(options.text);
16713
- lines.push("");
16714
- lines.push(`--${altBoundary}`);
16715
- lines.push("Content-Type: text/html; charset=UTF-8");
16716
- lines.push("Content-Transfer-Encoding: 7bit");
16717
- lines.push("");
16718
- lines.push(options.body);
16719
- lines.push("");
16720
- lines.push(`--${altBoundary}--`);
16721
- } else {
16722
- const contentType = options.html ? "text/html" : "text/plain";
16723
- lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
16724
- lines.push("");
16725
- lines.push(options.body);
16726
- }
16727
- return lines.join("\r\n");
16728
- }
16729
- function imapQuote(s) {
16730
- if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
16731
- return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
16732
- }
16733
- function imapReadLine(socket) {
16734
- return new Promise((resolve31, reject) => {
16735
- let buffer = "";
16736
- const onData = (chunk) => {
16737
- buffer += chunk.toString("utf-8");
16738
- const nlIndex = buffer.indexOf("\r\n");
16739
- if (nlIndex !== -1) {
16740
- socket.removeListener("data", onData);
16741
- socket.removeListener("error", onError);
16742
- resolve31(buffer);
16730
+ var DevMailbox;
16731
+ var init_devMailbox = __esm({
16732
+ "../core/src/devMailbox.ts"() {
16733
+ "use strict";
16734
+ DevMailbox = class {
16735
+ mailboxDir;
16736
+ constructor(mailboxDir) {
16737
+ this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
16743
16738
  }
16744
- };
16745
- const onError = (err) => {
16746
- socket.removeListener("data", onData);
16747
- reject(err);
16748
- };
16749
- socket.on("data", onData);
16750
- socket.on("error", onError);
16751
- });
16752
- }
16753
- function imapCommand(socket, command) {
16754
- return new Promise((resolve31, reject) => {
16755
- imapTagCounter++;
16756
- const tag = `T${imapTagCounter}`;
16757
- const fullCommand = `${tag} ${command}\r
16758
- `;
16759
- let buffer = "";
16760
- const onData = (chunk) => {
16761
- buffer += chunk.toString("utf-8");
16762
- if (buffer.includes(`${tag} OK`)) {
16763
- socket.removeListener("data", onData);
16764
- socket.removeListener("error", onError);
16765
- resolve31(buffer);
16766
- return;
16739
+ /**
16740
+ * Ensure a folder directory exists.
16741
+ */
16742
+ ensureFolder(folder) {
16743
+ const dir = join13(this.mailboxDir, folder);
16744
+ mkdirSync9(dir, { recursive: true });
16745
+ return dir;
16767
16746
  }
16768
- if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
16769
- socket.removeListener("data", onData);
16770
- socket.removeListener("error", onError);
16771
- reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
16747
+ /**
16748
+ * Capture an email to the dev mailbox instead of sending it.
16749
+ *
16750
+ * The parameter order MATCHES Messenger.send() on purpose. It did not before:
16751
+ * send()'s 5th positional was `text` and capture()'s was `cc`, so the same call
16752
+ * meant different things depending on which door it came through -- that mismatch
16753
+ * IS nodejs#42.
16754
+ *
16755
+ * BREAKING: `text` is now the 5th positional. A caller passing cc positionally
16756
+ * must move it. Aligning the two signatures is the fix; leaving them apart would
16757
+ * preserve the bug.
16758
+ */
16759
+ capture(to, subject, body, html = false, text, cc = [], bcc = [], replyTo, attachments = [], from) {
16760
+ const id = randomUUID2();
16761
+ const toList = Array.isArray(to) ? to : [to];
16762
+ const ccList = Array.isArray(cc) ? cc : cc ? [cc] : [];
16763
+ const bccList = Array.isArray(bcc) ? bcc : bcc ? [bcc] : [];
16764
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16765
+ const message = {
16766
+ id,
16767
+ type: "outbox",
16768
+ from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
16769
+ to: toList,
16770
+ cc: ccList,
16771
+ bcc: bccList,
16772
+ reply_to: replyTo,
16773
+ subject,
16774
+ body,
16775
+ text,
16776
+ html,
16777
+ attachments,
16778
+ date: now,
16779
+ read: false
16780
+ };
16781
+ const outboxDir = this.ensureFolder("outbox");
16782
+ writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
16783
+ const inboxDir = this.ensureFolder("inbox");
16784
+ const inboxMessage = { ...message, type: "inbox" };
16785
+ writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
16786
+ return { success: true, message: "Email captured to dev mailbox", id };
16772
16787
  }
16773
- };
16774
- const onError = (err) => {
16775
- socket.removeListener("data", onData);
16776
- reject(err);
16777
- };
16778
- socket.on("data", onData);
16779
- socket.on("error", onError);
16780
- socket.write(fullCommand, "utf-8");
16781
- });
16782
- }
16783
- function imapFail(method, err) {
16784
- const e = err instanceof Error ? err : new Error(String(err));
16785
- Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
16786
- if (e instanceof MessengerConnectionError) return e;
16787
- return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
16788
- }
16789
- function parseSearchResponse(response) {
16790
- const match = response.match(/\* SEARCH (.+)/);
16791
- if (!match) return [];
16792
- return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
16793
- }
16794
- function parseHeaderResponse(uid, response) {
16795
- const headers = {};
16796
- const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
16797
- if (headerBlock) {
16798
- const lines = headerBlock[1].split(/\r\n/);
16799
- let currentKey = "";
16800
- for (const line of lines) {
16801
- if (/^\s/.test(line) && currentKey) {
16802
- headers[currentKey] += " " + line.trim();
16803
- } else {
16804
- const colonIdx = line.indexOf(":");
16805
- if (colonIdx > 0) {
16806
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16807
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16788
+ /**
16789
+ * List messages from a folder (default: inbox).
16790
+ */
16791
+ inbox(limit = 50, offset = 0, folder = "inbox") {
16792
+ const dir = this.ensureFolder(folder);
16793
+ const results = [];
16794
+ let files;
16795
+ try {
16796
+ files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
16797
+ } catch {
16798
+ return [];
16808
16799
  }
16809
- }
16810
- }
16811
- }
16812
- const seen = /\\Seen/i.test(response);
16813
- return {
16814
- uid,
16815
- subject: headers["subject"] ?? "",
16816
- from: headers["from"] ?? "",
16817
- to: headers["to"] ?? "",
16818
- date: headers["date"] ?? "",
16819
- snippet: "",
16820
- seen
16821
- };
16822
- }
16823
- function emptyFullMessage(uid) {
16824
- return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
16825
- }
16826
- function parseFullMessage(uid, response) {
16827
- const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
16828
- const rawMessage = bodyMatch ? bodyMatch[2] : response;
16829
- const headerEnd = rawMessage.indexOf("\r\n\r\n");
16830
- const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
16831
- const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
16832
- const headers = {};
16833
- const headerLines = headerSection.split(/\r\n/);
16834
- let currentKey = "";
16835
- for (const line of headerLines) {
16836
- if (/^\s/.test(line) && currentKey) {
16837
- headers[currentKey] += " " + line.trim();
16838
- } else {
16839
- const colonIdx = line.indexOf(":");
16840
- if (colonIdx > 0) {
16841
- currentKey = line.substring(0, colonIdx).trim().toLowerCase();
16842
- headers[currentKey] = line.substring(colonIdx + 1).trim();
16843
- }
16844
- }
16845
- }
16846
- const contentType = headers["content-type"] ?? "text/plain";
16847
- let bodyText = "";
16848
- let bodyHtml = "";
16849
- if (contentType.includes("multipart")) {
16850
- const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
16851
- if (boundaryMatch) {
16852
- const boundary = boundaryMatch[1];
16853
- const parts = bodySection.split("--" + boundary);
16854
- for (const part of parts) {
16855
- if (part.trim() === "" || part.trim() === "--") continue;
16856
- const partHeaderEnd = part.indexOf("\r\n\r\n");
16857
- const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
16858
- const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
16859
- if (partHeaders.includes("text/html")) {
16860
- bodyHtml = partBody;
16861
- } else if (partHeaders.includes("text/plain")) {
16862
- bodyText = partBody;
16800
+ const sliced = files.slice(offset, offset + limit);
16801
+ for (const file of sliced) {
16802
+ try {
16803
+ const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
16804
+ results.push(msg);
16805
+ } catch {
16806
+ }
16863
16807
  }
16808
+ return results;
16864
16809
  }
16865
- }
16866
- } else if (contentType.includes("text/html")) {
16867
- bodyHtml = bodySection;
16868
- } else {
16869
- bodyText = bodySection;
16870
- }
16871
- bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16872
- bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
16873
- return {
16874
- uid,
16875
- subject: headers["subject"] ?? "",
16876
- from: headers["from"] ?? "",
16877
- to: headers["to"] ?? "",
16878
- cc: headers["cc"] ?? "",
16879
- date: headers["date"] ?? "",
16880
- bodyText,
16881
- bodyHtml,
16882
- headers
16883
- };
16884
- }
16885
- var MessengerConnectionError, Messenger, imapTagCounter;
16886
- var init_messenger = __esm({
16887
- "../core/src/messenger.ts"() {
16888
- "use strict";
16889
- init_dotenv();
16890
- init_logger();
16891
- MessengerConnectionError = class extends Error {
16892
- constructor(message) {
16893
- super(message);
16894
- this.name = "MessengerConnectionError";
16895
- }
16896
- };
16897
- Messenger = class {
16898
- host;
16899
- port;
16900
- username;
16901
- password;
16902
- fromAddress;
16903
- fromName;
16904
- encryption;
16905
- useTls;
16906
- imapHost;
16907
- imapPort;
16908
- imapUser;
16909
- imapPass;
16910
- imapEncryption;
16911
- constructor(options) {
16912
- this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
16913
- this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
16914
- this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
16915
- this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
16916
- this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
16917
- this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
16918
- const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
16919
- if (envEncryption) {
16920
- this.encryption = envEncryption.toLowerCase();
16921
- } else if (options?.useTls !== void 0) {
16922
- this.encryption = options.useTls ? "tls" : "none";
16923
- } else {
16924
- this.encryption = "tls";
16810
+ /**
16811
+ * Read a single message by ID. Searches all folders.
16812
+ */
16813
+ read(msgId) {
16814
+ const folders = ["inbox", "outbox"];
16815
+ for (const folder of folders) {
16816
+ const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
16817
+ if (existsSync11(filePath)) {
16818
+ try {
16819
+ const msg = JSON.parse(readFileSync8(filePath, "utf-8"));
16820
+ msg.read = true;
16821
+ writeFileSync7(filePath, JSON.stringify(msg, null, 2));
16822
+ return msg;
16823
+ } catch {
16824
+ return null;
16825
+ }
16826
+ }
16925
16827
  }
16926
- this.useTls = ["tls", "starttls"].includes(this.encryption);
16927
- this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
16928
- this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
16929
- this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
16930
- this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
16931
- this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
16828
+ return null;
16932
16829
  }
16933
16830
  /**
16934
- * Read-only IMAP encryption mode for inspection / tests.
16935
- * Returns one of "tls", "starttls", "none", "ssl".
16831
+ * Count unread messages in the inbox.
16936
16832
  */
16937
- getImapEncryption() {
16938
- return this.imapEncryption;
16833
+ unreadCount() {
16834
+ const dir = this.ensureFolder("inbox");
16835
+ let count = 0;
16836
+ try {
16837
+ const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
16838
+ for (const file of files) {
16839
+ try {
16840
+ const msg = JSON.parse(readFileSync8(join13(dir, file), "utf-8"));
16841
+ if (!msg.read) count++;
16842
+ } catch {
16843
+ }
16844
+ }
16845
+ } catch {
16846
+ }
16847
+ return count;
16939
16848
  }
16940
16849
  /**
16941
- * Send an email via SMTP.
16942
- */
16943
- async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
16944
- const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
16945
- const toList = Array.isArray(options.to) ? options.to : [options.to];
16946
- const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
16947
- const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
16948
- const allRecipients = [...toList, ...ccList, ...bccList];
16949
- const messageId = `${randomUUID2()}@${this.host}`;
16950
- if (allRecipients.length === 0) {
16951
- return { success: false, message: "No recipients specified" };
16952
- }
16953
- if (!this.fromAddress) {
16954
- return { success: false, message: "No from address configured" };
16955
- }
16956
- try {
16957
- let socket;
16958
- if (this.port === 465) {
16959
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
16960
- await new Promise((resolve31, reject) => {
16961
- socket.once("secureConnect", resolve31);
16962
- socket.once("error", reject);
16963
- });
16964
- } else {
16965
- socket = net2.createConnection({ host: this.host, port: this.port });
16966
- await new Promise((resolve31, reject) => {
16967
- socket.once("connect", resolve31);
16968
- socket.once("error", reject);
16969
- });
16970
- }
16971
- const greeting = await readResponse(socket);
16972
- if (greeting.code !== 220) {
16973
- socket.destroy();
16974
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
16975
- }
16976
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
16977
- if (ehlo.code !== 250) {
16978
- socket.destroy();
16979
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
16980
- }
16981
- if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
16982
- const starttls = await sendCommand(socket, "STARTTLS");
16983
- if (starttls.code !== 220) {
16984
- socket.destroy();
16985
- return { success: false, message: `STARTTLS failed: ${starttls.text}` };
16986
- }
16987
- const plainSocket = socket;
16988
- socket = tls.connect(
16989
- { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
16990
- );
16991
- await new Promise((resolve31, reject) => {
16992
- socket.once("secureConnect", resolve31);
16993
- socket.once("error", reject);
16994
- });
16995
- const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
16996
- if (ehlo2.code !== 250) {
16997
- socket.destroy();
16998
- return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
16999
- }
17000
- }
17001
- if (this.username && this.password) {
17002
- const auth = await sendCommand(socket, "AUTH LOGIN");
17003
- if (auth.code !== 334) {
17004
- socket.destroy();
17005
- return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
17006
- }
17007
- const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
17008
- if (userResp.code !== 334) {
17009
- socket.destroy();
17010
- return { success: false, message: `AUTH username failed: ${userResp.text}` };
17011
- }
17012
- const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
17013
- if (passResp.code !== 235) {
17014
- socket.destroy();
17015
- return { success: false, message: `AUTH password failed: ${passResp.text}` };
17016
- }
17017
- }
17018
- const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
17019
- if (mailFrom.code !== 250) {
17020
- socket.destroy();
17021
- return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
17022
- }
17023
- for (const recipient of allRecipients) {
17024
- const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
17025
- if (rcpt.code !== 250 && rcpt.code !== 251) {
17026
- socket.destroy();
17027
- return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
17028
- }
17029
- }
17030
- const dataCmd = await sendCommand(socket, "DATA");
17031
- if (dataCmd.code !== 354) {
17032
- socket.destroy();
17033
- return { success: false, message: `DATA failed: ${dataCmd.text}` };
17034
- }
17035
- const mimeMessage = buildMimeMessage({
17036
- from: this.fromAddress,
17037
- fromName: this.fromName,
17038
- to: toList,
17039
- cc: ccList,
17040
- subject: options.subject,
17041
- body: options.body,
17042
- html: options.html ?? false,
17043
- text: options.text,
17044
- replyTo: options.replyTo,
17045
- attachments: options.attachments,
17046
- headers: options.headers,
17047
- messageId
17048
- });
17049
- const endData = await sendCommand(socket, mimeMessage + "\r\n.");
17050
- if (endData.code !== 250) {
17051
- socket.destroy();
17052
- return { success: false, message: `Message delivery failed: ${endData.text}` };
17053
- }
17054
- await sendCommand(socket, "QUIT");
17055
- socket.destroy();
17056
- return { success: true, message: "Email sent successfully", id: messageId };
17057
- } catch (err) {
17058
- const errMsg = err instanceof Error ? err.message : String(err);
17059
- return { success: false, message: `SMTP error: ${errMsg}` };
17060
- }
17061
- }
17062
- /**
17063
- * Test the SMTP connection without sending an email.
17064
- */
17065
- async testConnection() {
17066
- try {
17067
- let socket;
17068
- if (this.port === 465) {
17069
- socket = tls.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
17070
- await new Promise((resolve31, reject) => {
17071
- socket.once("secureConnect", resolve31);
17072
- socket.once("error", reject);
17073
- });
17074
- } else {
17075
- socket = net2.createConnection({ host: this.host, port: this.port });
17076
- await new Promise((resolve31, reject) => {
17077
- socket.once("connect", resolve31);
17078
- socket.once("error", reject);
17079
- });
17080
- }
17081
- const greeting = await readResponse(socket);
17082
- if (greeting.code !== 220) {
17083
- socket.destroy();
17084
- return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
17085
- }
17086
- const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
17087
- if (ehlo.code !== 250) {
17088
- socket.destroy();
17089
- return { success: false, message: `EHLO failed: ${ehlo.text}` };
17090
- }
17091
- await sendCommand(socket, "QUIT");
17092
- socket.destroy();
17093
- return { success: true, message: `Connected to ${this.host}:${this.port}` };
17094
- } catch (err) {
17095
- const errMsg = err instanceof Error ? err.message : String(err);
17096
- return { success: false, message: `Connection failed: ${errMsg}` };
17097
- }
17098
- }
17099
- // ── IMAP (Read) ────────────────────────────────────────────
17100
- /**
17101
- * Connect to the IMAP server via raw TCP/TLS.
17102
- * Returns the socket and reads the greeting.
17103
- */
17104
- async imapConnect() {
17105
- if (!this.imapHost) {
17106
- throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
17107
- }
17108
- let socket;
17109
- const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
17110
- if (useTls) {
17111
- socket = tls.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
17112
- await new Promise((resolve31, reject) => {
17113
- socket.once("secureConnect", resolve31);
17114
- socket.once("error", reject);
17115
- });
17116
- } else {
17117
- socket = net2.createConnection({ host: this.imapHost, port: this.imapPort });
17118
- await new Promise((resolve31, reject) => {
17119
- socket.once("connect", resolve31);
17120
- socket.once("error", reject);
17121
- });
17122
- }
17123
- await imapReadLine(socket);
17124
- if (this.imapUser && this.imapPass) {
17125
- const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
17126
- if (!loginResp.includes("OK")) {
17127
- socket.destroy();
17128
- throw new Error(`IMAP login failed: ${loginResp}`);
17129
- }
17130
- }
17131
- return socket;
17132
- }
17133
- /**
17134
- * Disconnect from IMAP cleanly.
17135
- */
17136
- async imapDisconnect(socket) {
17137
- try {
17138
- await imapCommand(socket, "LOGOUT");
17139
- } catch {
17140
- }
17141
- socket.destroy();
17142
- }
17143
- /**
17144
- * Fetch latest messages from a folder.
17145
- * Returns list of message summaries.
17146
- */
17147
- async inbox(limit = 20, offset = 0, folder = "INBOX") {
17148
- let socket;
17149
- try {
17150
- socket = await this.imapConnect();
17151
- } catch (err) {
17152
- throw imapFail("inbox", err);
17153
- }
17154
- try {
17155
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17156
- const searchResp = await imapCommand(socket, "SEARCH ALL");
17157
- const uids = parseSearchResponse(searchResp);
17158
- if (uids.length === 0) return [];
17159
- uids.reverse();
17160
- const selected = uids.slice(offset, offset + limit);
17161
- if (selected.length === 0) return [];
17162
- const messages = [];
17163
- for (const uid of selected) {
17164
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17165
- messages.push(parseHeaderResponse(uid, fetchResp));
17166
- }
17167
- return messages;
17168
- } catch (err) {
17169
- throw imapFail("inbox", err);
17170
- } finally {
17171
- await this.imapDisconnect(socket);
17172
- }
17173
- }
17174
- /**
17175
- * Read a single message by sequence number or UID.
17176
- */
17177
- async read(uid, folder = "INBOX") {
17178
- let socket;
17179
- try {
17180
- socket = await this.imapConnect();
17181
- } catch (err) {
17182
- throw imapFail("read", err);
17183
- }
17184
- try {
17185
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17186
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
17187
- if (!/\{\d+\}/.test(fetchResp)) {
17188
- return emptyFullMessage(uid);
17189
- }
17190
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17191
- return parseFullMessage(uid, fetchResp);
17192
- } catch (err) {
17193
- throw imapFail("read", err);
17194
- } finally {
17195
- await this.imapDisconnect(socket);
17196
- }
17197
- }
17198
- /**
17199
- * Search messages using IMAP search criteria.
17200
- */
17201
- async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
17202
- const criteria = ["ALL"];
17203
- if (subject) criteria.push(`SUBJECT "${subject}"`);
17204
- if (sender) criteria.push(`FROM "${sender}"`);
17205
- if (since) criteria.push(`SINCE ${since}`);
17206
- if (before) criteria.push(`BEFORE ${before}`);
17207
- if (unseenOnly) criteria.push("UNSEEN");
17208
- const query = criteria.join(" ");
17209
- let socket;
17210
- try {
17211
- socket = await this.imapConnect();
17212
- } catch (err) {
17213
- throw imapFail("search", err);
17214
- }
17215
- try {
17216
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17217
- const searchResp = await imapCommand(socket, `SEARCH ${query}`);
17218
- const uids = parseSearchResponse(searchResp);
17219
- if (uids.length === 0) return [];
17220
- uids.reverse();
17221
- const messages = [];
17222
- for (const uid of uids.slice(0, limit)) {
17223
- const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
17224
- messages.push(parseHeaderResponse(uid, fetchResp));
17225
- }
17226
- return messages;
17227
- } catch (err) {
17228
- throw imapFail("search", err);
17229
- } finally {
17230
- await this.imapDisconnect(socket);
17231
- }
17232
- }
17233
- /**
17234
- * Delete a message by UID.
17235
- */
17236
- async deleteMessage(uid, folder = "INBOX") {
17237
- const socket = await this.imapConnect();
17238
- try {
17239
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17240
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
17241
- await imapCommand(socket, "EXPUNGE");
17242
- } finally {
17243
- await this.imapDisconnect(socket);
17244
- }
17245
- }
17246
- /**
17247
- * Mark a message as read.
17248
- */
17249
- async markRead(uid, folder = "INBOX") {
17250
- const socket = await this.imapConnect();
17251
- try {
17252
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17253
- await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
17254
- } finally {
17255
- await this.imapDisconnect(socket);
17256
- }
17257
- }
17258
- /**
17259
- * Count unseen messages in a folder.
17260
- */
17261
- async unread(folder = "INBOX") {
17262
- let socket;
17263
- try {
17264
- socket = await this.imapConnect();
17265
- } catch (err) {
17266
- throw imapFail("unread", err);
17267
- }
17268
- try {
17269
- await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
17270
- const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
17271
- return parseSearchResponse(searchResp).length;
17272
- } catch (err) {
17273
- throw imapFail("unread", err);
17274
- } finally {
17275
- await this.imapDisconnect(socket);
17276
- }
17277
- }
17278
- /**
17279
- * List available IMAP folders/mailboxes.
17280
- */
17281
- async folders() {
17282
- let socket;
17283
- try {
17284
- socket = await this.imapConnect();
17285
- } catch (err) {
17286
- throw imapFail("folders", err);
17287
- }
17288
- try {
17289
- const resp = await imapCommand(socket, 'LIST "" "*"');
17290
- const result = [];
17291
- for (const line of resp.split("\r\n")) {
17292
- const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
17293
- if (m) result.push(m[1]);
17294
- }
17295
- return result;
17296
- } catch (err) {
17297
- throw imapFail("folders", err);
17298
- } finally {
17299
- await this.imapDisconnect(socket);
17300
- }
17301
- }
17302
- /**
17303
- * Test IMAP connectivity without reading.
17304
- */
17305
- async testImapConnection() {
17306
- try {
17307
- const socket = await this.imapConnect();
17308
- await this.imapDisconnect(socket);
17309
- return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
17310
- } catch (err) {
17311
- const errMsg = err instanceof Error ? err.message : String(err);
17312
- return { success: false, message: `IMAP connection failed: ${errMsg}` };
17313
- }
17314
- }
17315
- };
17316
- imapTagCounter = 0;
17317
- }
17318
- });
17319
-
17320
- // ../core/src/devMailbox.ts
17321
- import { mkdirSync as mkdirSync9, readdirSync as readdirSync6, readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync4, existsSync as existsSync11 } from "node:fs";
17322
- import { join as join13 } from "node:path";
17323
- import { randomUUID as randomUUID3 } from "node:crypto";
17324
- function createMessenger() {
17325
- const debug = process.env.TINA4_DEBUG;
17326
- const smtpHost = process.env.TINA4_MAIL_HOST;
17327
- const isProd = !isTruthy(debug) && process.env.NODE_ENV === "production";
17328
- if (isTruthy(debug)) {
17329
- return new DevMailbox();
17330
- }
17331
- if (!smtpHost) {
17332
- return new DevMailbox();
17333
- }
17334
- if (!isProd) {
17335
- return new DevMailbox();
17336
- }
17337
- return new Messenger();
17338
- }
17339
- var DevMailbox;
17340
- var init_devMailbox = __esm({
17341
- "../core/src/devMailbox.ts"() {
17342
- "use strict";
17343
- init_messenger();
17344
- init_dotenv();
17345
- DevMailbox = class {
17346
- mailboxDir;
17347
- constructor(mailboxDir) {
17348
- this.mailboxDir = mailboxDir ?? process.env.TINA4_MAILBOX_DIR ?? "data/mailbox";
17349
- }
17350
- /**
17351
- * Ensure a folder directory exists.
17352
- */
17353
- ensureFolder(folder) {
17354
- const dir = join13(this.mailboxDir, folder);
17355
- mkdirSync9(dir, { recursive: true });
17356
- return dir;
17357
- }
17358
- /**
17359
- * Capture an email to the dev mailbox instead of sending it.
17360
- */
17361
- capture(to, subject, body, html = false, cc = [], bcc = [], replyTo, attachments = [], from) {
17362
- const id = randomUUID3();
17363
- const toList = Array.isArray(to) ? to : [to];
17364
- const now = (/* @__PURE__ */ new Date()).toISOString();
17365
- const message = {
17366
- id,
17367
- type: "outbox",
17368
- from: from ?? process.env.TINA4_MAIL_FROM ?? "dev@localhost",
17369
- to: toList,
17370
- cc,
17371
- bcc,
17372
- reply_to: replyTo,
17373
- subject,
17374
- body,
17375
- html,
17376
- attachments,
17377
- date: now,
17378
- read: false
17379
- };
17380
- const outboxDir = this.ensureFolder("outbox");
17381
- writeFileSync7(join13(outboxDir, `${id}.json`), JSON.stringify(message, null, 2));
17382
- const inboxDir = this.ensureFolder("inbox");
17383
- const inboxMessage = { ...message, type: "inbox" };
17384
- writeFileSync7(join13(inboxDir, `${id}.json`), JSON.stringify(inboxMessage, null, 2));
17385
- return { success: true, message: "Email captured to dev mailbox", id };
17386
- }
17387
- /**
17388
- * List messages from a folder (default: inbox).
17389
- */
17390
- inbox(limit = 50, offset = 0, folder = "inbox") {
17391
- const dir = this.ensureFolder(folder);
17392
- const results = [];
17393
- let files;
17394
- try {
17395
- files = readdirSync6(dir).filter((f) => f.endsWith(".json")).sort().reverse();
17396
- } catch {
17397
- return [];
17398
- }
17399
- const sliced = files.slice(offset, offset + limit);
17400
- for (const file of sliced) {
17401
- try {
17402
- const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
17403
- results.push(msg);
17404
- } catch {
17405
- }
17406
- }
17407
- return results;
17408
- }
17409
- /**
17410
- * Read a single message by ID. Searches all folders.
17411
- */
17412
- read(msgId) {
17413
- const folders = ["inbox", "outbox"];
17414
- for (const folder of folders) {
17415
- const filePath = join13(this.mailboxDir, folder, `${msgId}.json`);
17416
- if (existsSync11(filePath)) {
17417
- try {
17418
- const msg = JSON.parse(readFileSync9(filePath, "utf-8"));
17419
- msg.read = true;
17420
- writeFileSync7(filePath, JSON.stringify(msg, null, 2));
17421
- return msg;
17422
- } catch {
17423
- return null;
17424
- }
17425
- }
17426
- }
17427
- return null;
17428
- }
17429
- /**
17430
- * Count unread messages in the inbox.
17431
- */
17432
- unreadCount() {
17433
- const dir = this.ensureFolder("inbox");
17434
- let count = 0;
17435
- try {
17436
- const files = readdirSync6(dir).filter((f) => f.endsWith(".json"));
17437
- for (const file of files) {
17438
- try {
17439
- const msg = JSON.parse(readFileSync9(join13(dir, file), "utf-8"));
17440
- if (!msg.read) count++;
17441
- } catch {
17442
- }
17443
- }
17444
- } catch {
17445
- }
17446
- return count;
17447
- }
17448
- /**
17449
- * Delete a message by ID. Removes from all folders.
16850
+ * Delete a message by ID. Removes from all folders.
17450
16851
  */
17451
16852
  delete(msgId) {
17452
16853
  let deleted = false;
@@ -17508,7 +16909,7 @@ var init_devMailbox = __esm({
17508
16909
  const subject = subjects[i % subjects.length];
17509
16910
  const from = senders[i % senders.length];
17510
16911
  const date = new Date(Date.now() - i * 36e5).toISOString();
17511
- const id = randomUUID3();
16912
+ const id = randomUUID2();
17512
16913
  const message = {
17513
16914
  id,
17514
16915
  type: "inbox",
@@ -18629,7 +18030,7 @@ var init_metrics = __esm({
18629
18030
  });
18630
18031
 
18631
18032
  // ../core/src/feedback.ts
18632
- import { readFileSync as readFileSync11, existsSync as existsSync13 } from "node:fs";
18033
+ import { readFileSync as readFileSync10, existsSync as existsSync13 } from "node:fs";
18633
18034
  import { dirname as dirname6, join as join15, resolve as resolve9 } from "node:path";
18634
18035
  import { fileURLToPath as fileURLToPath2 } from "node:url";
18635
18036
  function feedbackEnabled() {
@@ -18770,7 +18171,7 @@ var init_feedback = __esm({
18770
18171
  handleFeedbackWidgetJs = (_req, res) => {
18771
18172
  let body;
18772
18173
  if (existsSync13(WIDGET_BUNDLE_PATH)) {
18773
- body = readFileSync11(WIDGET_BUNDLE_PATH);
18174
+ body = readFileSync10(WIDGET_BUNDLE_PATH);
18774
18175
  } else {
18775
18176
  body = "console.warn('tina4-feedback-widget bundle not built yet');";
18776
18177
  }
@@ -20478,7 +19879,7 @@ __export(errorOverlay_exports, {
20478
19879
  renderErrorOverlay: () => renderErrorOverlay,
20479
19880
  renderProductionError: () => renderProductionError
20480
19881
  });
20481
- import { readFileSync as readFileSync13, statSync as statSync10 } from "node:fs";
19882
+ import { readFileSync as readFileSync12, statSync as statSync10 } from "node:fs";
20482
19883
  import { resolve as resolve11 } from "node:path";
20483
19884
  function esc(text) {
20484
19885
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -20502,7 +19903,7 @@ function parseStack(stack) {
20502
19903
  function readSourceLines(filename, lineno) {
20503
19904
  try {
20504
19905
  const absPath = resolve11(filename);
20505
- const content = readFileSync13(absPath, "utf-8");
19906
+ const content = readFileSync12(absPath, "utf-8");
20506
19907
  const allLines = content.split("\n");
20507
19908
  const start2 = Math.max(0, lineno - CONTEXT_LINES - 1);
20508
19909
  const end = Math.min(allLines.length, lineno + CONTEXT_LINES);
@@ -21766,8 +21167,8 @@ __export(context_exports, {
21766
21167
  fts5Supported: () => fts5Supported
21767
21168
  });
21768
21169
  import { DatabaseSync as DatabaseSync3 } from "node:sqlite";
21769
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync14, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21770
- import { basename as basename5, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
21170
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync13, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
21171
+ import { basename as basename4, dirname as dirname8, extname as extname6, isAbsolute as isAbsolute5, join as join17, relative as relative4, resolve as resolve12 } from "node:path";
21771
21172
  function fts5Supported() {
21772
21173
  try {
21773
21174
  const conn = new DatabaseSync3(":memory:");
@@ -21790,7 +21191,7 @@ function realResolve(abs) {
21790
21191
  } catch {
21791
21192
  }
21792
21193
  try {
21793
- return join17(realpathSync2(dirname8(abs)), basename5(abs));
21194
+ return join17(realpathSync2(dirname8(abs)), basename4(abs));
21794
21195
  } catch {
21795
21196
  return abs;
21796
21197
  }
@@ -21903,7 +21304,7 @@ var init_context = __esm({
21903
21304
  // ── indexing ───────────────────────────────────────────────
21904
21305
  static chunksFor(label, text) {
21905
21306
  const ext = extname6(label).toLowerCase();
21906
- const special = SPECIAL_FILES.has(basename5(label).toLowerCase());
21307
+ const special = SPECIAL_FILES.has(basename4(label).toLowerCase());
21907
21308
  if (CODE_EXTS.has(ext) || CONFIG_EXTS.has(ext) || special) {
21908
21309
  return chunkCode(text, label);
21909
21310
  }
@@ -21920,7 +21321,7 @@ var init_context = __esm({
21920
21321
  const stored = label != null ? String(label) : String(file);
21921
21322
  let text;
21922
21323
  try {
21923
- text = readFileSync14(file, "utf-8");
21324
+ text = readFileSync13(file, "utf-8");
21924
21325
  } catch {
21925
21326
  return 0;
21926
21327
  }
@@ -22000,7 +21401,7 @@ var init_context = __esm({
22000
21401
  if (parts.some((seg) => SKIP_DIRS.has(seg)) || dirParts.some((seg) => seg.startsWith("."))) {
22001
21402
  return -1;
22002
21403
  }
22003
- if (!_Context.eligible(basename5(rel))) return -1;
21404
+ if (!_Context.eligible(basename4(rel))) return -1;
22004
21405
  const stored = rel;
22005
21406
  if (!existsSync15(abs)) {
22006
21407
  this.conn.prepare("DELETE FROM chunks WHERE path = ?").run(stored);
@@ -22092,7 +21493,7 @@ var init_context = __esm({
22092
21493
  });
22093
21494
 
22094
21495
  // ../core/src/websocketBackplane.ts
22095
- import { randomUUID as randomUUID4 } from "node:crypto";
21496
+ import { randomUUID as randomUUID3 } from "node:crypto";
22096
21497
  function createBackplane(url) {
22097
21498
  const backend = (process.env.TINA4_WS_BACKPLANE ?? "").trim().toLowerCase();
22098
21499
  switch (backend) {
@@ -22122,7 +21523,7 @@ function buildEnvelope(src, kind, message, opts = {}) {
22122
21523
  return envelope;
22123
21524
  }
22124
21525
  function randomInstanceId() {
22125
- return randomUUID4().replace(/-/g, "").slice(0, 16);
21526
+ return randomUUID3().replace(/-/g, "").slice(0, 16);
22126
21527
  }
22127
21528
  var RedisBackplane, NATSBackplane, WS_BACKPLANE_CHANNEL, WsBackplaneManager;
22128
21529
  var init_websocketBackplane = __esm({
@@ -22350,7 +21751,7 @@ __export(websocket_exports, {
22350
21751
  });
22351
21752
  import { createServer } from "node:http";
22352
21753
  import { createHash as createHash4 } from "node:crypto";
22353
- import { randomUUID as randomUUID5 } from "node:crypto";
21754
+ import { randomUUID as randomUUID4 } from "node:crypto";
22354
21755
  function computeAcceptKey(key) {
22355
21756
  return createHash4("sha1").update(key + MAGIC_STRING).digest("base64");
22356
21757
  }
@@ -22478,7 +21879,7 @@ function parseFrame(data) {
22478
21879
  return { fin: !!fin, opcode, payload: Buffer.from(payload), bytesConsumed: offset + payloadLen };
22479
21880
  }
22480
21881
  function createRouteConnection(socket, path8, headers, params, auth) {
22481
- const id = randomUUID5().slice(0, 8);
21882
+ const id = randomUUID4().slice(0, 8);
22482
21883
  const send = (message) => {
22483
21884
  try {
22484
21885
  socket.write(buildFrame(OP_TEXT, Buffer.from(message, "utf-8")));
@@ -23035,7 +22436,7 @@ var init_websocket = __esm({
23035
22436
  }
23036
22437
  responseLines.push("", "");
23037
22438
  socket.write(responseLines.join("\r\n"));
23038
- const clientId = randomUUID5().slice(0, 8);
22439
+ const clientId = randomUUID4().slice(0, 8);
23039
22440
  const client = {
23040
22441
  id: clientId,
23041
22442
  socket,
@@ -23323,7 +22724,7 @@ var init_websocket = __esm({
23323
22724
 
23324
22725
  // ../core/src/queueBackends/rabbitmqBackend.ts
23325
22726
  import { execFileSync } from "node:child_process";
23326
- import { randomUUID as randomUUID6 } from "node:crypto";
22727
+ import { randomUUID as randomUUID5 } from "node:crypto";
23327
22728
  function parseAmqpUrl(url) {
23328
22729
  const config = {};
23329
22730
  let rest = url.replace(/^amqps:\/\//, "").replace(/^amqp:\/\//, "");
@@ -23767,7 +23168,7 @@ var init_rabbitmqBackend = __esm({
23767
23168
  }
23768
23169
  }
23769
23170
  push(queue, payload, _delay) {
23770
- const id = randomUUID6();
23171
+ const id = randomUUID5();
23771
23172
  const now = (/* @__PURE__ */ new Date()).toISOString();
23772
23173
  const job = {
23773
23174
  id,
@@ -23806,7 +23207,7 @@ var init_rabbitmqBackend = __esm({
23806
23207
 
23807
23208
  // ../core/src/queueBackends/kafkaBackend.ts
23808
23209
  import { execFileSync as execFileSync2 } from "node:child_process";
23809
- import { randomUUID as randomUUID7 } from "node:crypto";
23210
+ import { randomUUID as randomUUID6 } from "node:crypto";
23810
23211
  function kafkaSecurityConfig(env = process.env) {
23811
23212
  const mapping = [
23812
23213
  ["security.protocol", "SECURITY_PROTOCOL"],
@@ -23830,7 +23231,7 @@ var init_kafkaBackend = __esm({
23830
23231
  "use strict";
23831
23232
  API_PRODUCE = 0;
23832
23233
  API_FETCH = 1;
23833
- KafkaBackend = class {
23234
+ KafkaBackend = class _KafkaBackend {
23834
23235
  brokers;
23835
23236
  groupId;
23836
23237
  constructor(config) {
@@ -24192,12 +23593,15 @@ var init_kafkaBackend = __esm({
24192
23593
  if (errCode === 0) {
24193
23594
  finish("__PUBLISHED__", 0);
24194
23595
  } else {
23596
+ // Report the CODE, not just "it failed" \u2014 the caller decides
23597
+ // whether it is retriable (3/5, the async topic-creation race)
23598
+ // or fatal (e.g. 29 TOPIC_AUTHORIZATION_FAILED).
24195
23599
  process.stderr.write("Produce error code " + errCode);
24196
- finish("__ERROR__" + errCode, 0);
23600
+ finish("__PRODUCEERROR__" + errCode, 0);
24197
23601
  }
24198
23602
  } catch (e) {
24199
23603
  process.stderr.write("produce parse: " + e.message);
24200
- finish("__ERROR__", 0);
23604
+ finish("__PARSEERROR__produce: " + e.message, 0);
24201
23605
  }
24202
23606
  return;
24203
23607
  } else if (operation === "get") {
@@ -24211,6 +23615,7 @@ var init_kafkaBackend = __esm({
24211
23615
  pos += 4; // throttleTimeMs (v1+)
24212
23616
  const topicCount = buffer.readInt32BE(pos); pos += 4;
24213
23617
  let out = "__EMPTY__";
23618
+ let fatalCode = 0;
24214
23619
  for (let t = 0; t < topicCount; t++) {
24215
23620
  const tl = buffer.readInt16BE(pos); pos += 2 + tl;
24216
23621
  const pc = buffer.readInt32BE(pos); pos += 4;
@@ -24222,6 +23627,14 @@ var init_kafkaBackend = __esm({
24222
23627
  const abortedCount = buffer.readInt32BE(pos); pos += 4;
24223
23628
  if (abortedCount > 0) pos += abortedCount * 16; // (-1 => none, skip)
24224
23629
  const recSetSize = buffer.readInt32BE(pos); pos += 4;
23630
+ // 3 = UNKNOWN_TOPIC_OR_PARTITION, 5 = LEADER_NOT_AVAILABLE:
23631
+ // "nothing to read here yet", which a consumer that starts
23632
+ // before its producer hits on every cold start. Any OTHER code
23633
+ // (29 TOPIC_AUTHORIZATION_FAILED, 13 STALE_CONTROLLER_EPOCH, \u2026)
23634
+ // is a real failure and must NOT be reported as an empty queue.
23635
+ if (errCode !== 0 && errCode !== 3 && errCode !== 5) {
23636
+ fatalCode = errCode;
23637
+ }
24225
23638
  if (errCode === 0 && recSetSize > 0) {
24226
23639
  const val = firstRecordValue(buffer, pos, pos + recSetSize);
24227
23640
  if (val !== null) out = val;
@@ -24229,21 +23642,33 @@ var init_kafkaBackend = __esm({
24229
23642
  pos += recSetSize > 0 ? recSetSize : 0;
24230
23643
  }
24231
23644
  }
23645
+ if (fatalCode !== 0) {
23646
+ process.stderr.write("Fetch error code " + fatalCode);
23647
+ finish("__FETCHERROR__" + fatalCode, 0);
23648
+ return;
23649
+ }
24232
23650
  finish(out, 0);
24233
23651
  } catch (e) {
23652
+ // A parse failure is NOT an empty queue either \u2014 say so.
24234
23653
  process.stderr.write("fetch parse: " + e.message);
24235
- finish("__EMPTY__", 0);
23654
+ finish("__PARSEERROR__fetch: " + e.message, 0);
24236
23655
  }
24237
23656
  return;
24238
23657
  }
24239
23658
  });
24240
23659
 
23660
+ // Report the reason on STDOUT and exit 0. Writing it to stderr and
23661
+ // exiting non-zero LOST it: stderr to a pipe is an async write and
23662
+ // process.exit() truncates it, so the parent saw an empty stderr and fell
23663
+ // back to execFileSync's message -- which embeds this entire script.
23664
+ // stdout is flushed by finish()'s write callback, so it survives.
24241
23665
  sock.on("error", (err) => {
24242
- process.stderr.write(err.message);
24243
- finish("", 1);
23666
+ finish("__TRANSPORTERROR__" + err.message, 0);
24244
23667
  });
24245
23668
 
24246
- var timer = setTimeout(() => { finish("", 1); }, 10000);
23669
+ var timer = setTimeout(() => {
23670
+ finish("__TRANSPORTERROR__timed out after 10s talking to " + host + ":" + port, 0);
23671
+ }, 10000);
24247
23672
  `;
24248
23673
  try {
24249
23674
  const result = execFileSync2(process.execPath, ["-e", script], {
@@ -24252,12 +23677,51 @@ var init_kafkaBackend = __esm({
24252
23677
  stdio: ["pipe", "pipe", "pipe"]
24253
23678
  });
24254
23679
  return result;
24255
- } catch {
24256
- return "";
23680
+ } catch (err) {
23681
+ const e = err;
23682
+ const reason = String(e.stderr ?? "").trim() || e.message || "unknown error";
23683
+ const firstLine2 = reason.split("\n", 1)[0].slice(0, 200);
23684
+ return "__TRANSPORTERROR__" + firstLine2;
23685
+ }
23686
+ }
23687
+ /**
23688
+ * Sleep synchronously between produce retries.
23689
+ *
23690
+ * `push()` is synchronous (the whole backend drives its socket through a child
23691
+ * process), so there is no event loop to await on. `Atomics.wait` on a
23692
+ * SharedArrayBuffer is the stdlib way to block a thread for a fixed time --
23693
+ * no dependency, no busy-wait burning CPU.
23694
+ */
23695
+ static sleepSync(ms) {
23696
+ const shared = new Int32Array(new SharedArrayBuffer(4));
23697
+ Atomics.wait(shared, 0, 0, ms);
23698
+ }
23699
+ /**
23700
+ * Turn a sentinel from the protocol child into a thrown error, or return.
23701
+ *
23702
+ * The wording matches the Python and PHP backends exactly -- the parity rule
23703
+ * covers user-visible error messages, not just behaviour.
23704
+ */
23705
+ static assertNoError(result, operation, topic) {
23706
+ const fatal = /^__(PRODUCEERROR|FETCHERROR)__(\d+)/.exec(result);
23707
+ if (fatal) {
23708
+ throw new Error(
23709
+ `Kafka rejected the ${operation} for topic ${topic}: error code ${fatal[2]}`
23710
+ );
23711
+ }
23712
+ if (result.startsWith("__TRANSPORTERROR__")) {
23713
+ throw new Error(
23714
+ `Kafka ${operation} for topic ${topic} failed: ` + result.slice("__TRANSPORTERROR__".length)
23715
+ );
23716
+ }
23717
+ if (result.startsWith("__PARSEERROR__")) {
23718
+ throw new Error(
23719
+ `Kafka ${operation} for topic ${topic} returned an unreadable response: ` + result.slice("__PARSEERROR__".length)
23720
+ );
24257
23721
  }
24258
23722
  }
24259
23723
  push(queue, payload, _delay) {
24260
- const id = randomUUID7();
23724
+ const id = randomUUID6();
24261
23725
  const now = (/* @__PURE__ */ new Date()).toISOString();
24262
23726
  const job = {
24263
23727
  id,
@@ -24267,14 +23731,25 @@ var init_kafkaBackend = __esm({
24267
23731
  attempts: 0,
24268
23732
  delayUntil: null
24269
23733
  };
24270
- const result = this.execSync("publish", queue, JSON.stringify(job));
24271
- if (!result.includes("__PUBLISHED__")) {
24272
- throw new Error("Kafka publish failed");
23734
+ const body = JSON.stringify(job);
23735
+ let result = "";
23736
+ for (let attempt = 1; attempt <= 10; attempt++) {
23737
+ result = this.execSync("publish", queue, body);
23738
+ if (result.includes("__PUBLISHED__")) {
23739
+ return id;
23740
+ }
23741
+ const retriable = /^__PRODUCEERROR__(3|5)\b/.test(result);
23742
+ if (!retriable || attempt === 10) {
23743
+ break;
23744
+ }
23745
+ _KafkaBackend.sleepSync(200);
24273
23746
  }
24274
- return id;
23747
+ _KafkaBackend.assertNoError(result, "produce", queue);
23748
+ throw new Error(`Kafka publish failed for topic ${queue}: ${result || "no response"}`);
24275
23749
  }
24276
23750
  pop(queue) {
24277
23751
  const result = this.execSync("get", queue);
23752
+ _KafkaBackend.assertNoError(result, "fetch", queue);
24278
23753
  if (!result || result === "__EMPTY__" || result === "__UNSUPPORTED__") return null;
24279
23754
  try {
24280
23755
  return JSON.parse(result);
@@ -24292,7 +23767,7 @@ var init_kafkaBackend = __esm({
24292
23767
  });
24293
23768
 
24294
23769
  // ../core/src/queueBackends/mongoBackend.ts
24295
- import { randomUUID as randomUUID8 } from "node:crypto";
23770
+ import { randomUUID as randomUUID7 } from "node:crypto";
24296
23771
  import { execFileSync as execFileSync3 } from "node:child_process";
24297
23772
  var MongoBackend2;
24298
23773
  var init_mongoBackend = __esm({
@@ -24605,7 +24080,7 @@ var init_mongoBackend = __esm({
24605
24080
  }
24606
24081
  }
24607
24082
  push(queue, payload, delay) {
24608
- const id = randomUUID8();
24083
+ const id = randomUUID7();
24609
24084
  const now = (/* @__PURE__ */ new Date()).toISOString();
24610
24085
  const job = {
24611
24086
  id,
@@ -24740,9 +24215,9 @@ var init_job = __esm({
24740
24215
  });
24741
24216
 
24742
24217
  // ../core/src/queueBackends/liteBackend.ts
24743
- import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
24218
+ import { mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, existsSync as existsSync16 } from "node:fs";
24744
24219
  import { join as join18 } from "node:path";
24745
- import { randomUUID as randomUUID9 } from "node:crypto";
24220
+ import { randomUUID as randomUUID8 } from "node:crypto";
24746
24221
  var LiteBackend;
24747
24222
  var init_liteBackend = __esm({
24748
24223
  "../core/src/queueBackends/liteBackend.ts"() {
@@ -24794,7 +24269,7 @@ var init_liteBackend = __esm({
24794
24269
  }
24795
24270
  push(queue, payload, delay, priority) {
24796
24271
  const dir = this.ensureDir(queue);
24797
- const id = randomUUID9();
24272
+ const id = randomUUID8();
24798
24273
  const now = (/* @__PURE__ */ new Date()).toISOString();
24799
24274
  const job = {
24800
24275
  id,
@@ -24830,7 +24305,7 @@ var init_liteBackend = __esm({
24830
24305
  const filePath = join18(dir, filename);
24831
24306
  let job;
24832
24307
  try {
24833
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24308
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
24834
24309
  } catch {
24835
24310
  continue;
24836
24311
  }
@@ -24894,7 +24369,7 @@ var init_liteBackend = __esm({
24894
24369
  const filePath = join18(reservedDir, filename);
24895
24370
  let record;
24896
24371
  try {
24897
- record = JSON.parse(readFileSync15(filePath, "utf-8"));
24372
+ record = JSON.parse(readFileSync14(filePath, "utf-8"));
24898
24373
  } catch {
24899
24374
  continue;
24900
24375
  }
@@ -25007,7 +24482,7 @@ var init_liteBackend = __esm({
25007
24482
  let count = 0;
25008
24483
  for (const file of files) {
25009
24484
  try {
25010
- const job = JSON.parse(readFileSync15(join18(scanDir, file), "utf-8"));
24485
+ const job = JSON.parse(readFileSync14(join18(scanDir, file), "utf-8"));
25011
24486
  if (job.status === status2) count++;
25012
24487
  } catch {
25013
24488
  }
@@ -25064,7 +24539,7 @@ var init_liteBackend = __esm({
25064
24539
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data")).sort();
25065
24540
  for (const file of files) {
25066
24541
  try {
25067
- const job = JSON.parse(readFileSync15(join18(dir, file), "utf-8"));
24542
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
25068
24543
  const attempts = job.attempts || 0;
25069
24544
  if (attempts > 0 && attempts < maxRetries) {
25070
24545
  results.push(job);
@@ -25090,7 +24565,7 @@ var init_liteBackend = __esm({
25090
24565
  const failedDir = join18(this.basePath, q, "failed");
25091
24566
  const filePath = join18(failedDir, `${jobId}.queue-data`);
25092
24567
  if (existsSync16(filePath)) {
25093
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24568
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25094
24569
  job.status = "pending";
25095
24570
  job.attempts = (job.attempts || 0) + 1;
25096
24571
  job.error = void 0;
@@ -25114,7 +24589,7 @@ var init_liteBackend = __esm({
25114
24589
  const files = readdirSync10(failedDir).filter((f) => f.endsWith(".queue-data")).sort();
25115
24590
  for (const file of files) {
25116
24591
  try {
25117
- const job = JSON.parse(readFileSync15(join18(failedDir, file), "utf-8"));
24592
+ const job = JSON.parse(readFileSync14(join18(failedDir, file), "utf-8"));
25118
24593
  if ((job.attempts || 0) >= maxRetries) {
25119
24594
  job.status = "dead";
25120
24595
  results.push(job);
@@ -25148,7 +24623,7 @@ var init_liteBackend = __esm({
25148
24623
  const files = readdirSync10(dir).filter((f) => f.endsWith(".queue-data"));
25149
24624
  for (const file of files) {
25150
24625
  try {
25151
- const job = JSON.parse(readFileSync15(join18(dir, file), "utf-8"));
24626
+ const job = JSON.parse(readFileSync14(join18(dir, file), "utf-8"));
25152
24627
  if (job.status === status2) {
25153
24628
  unlinkSync5(join18(dir, file));
25154
24629
  count++;
@@ -25175,7 +24650,7 @@ var init_liteBackend = __esm({
25175
24650
  for (const file of files) {
25176
24651
  try {
25177
24652
  const filePath = join18(failedDir, file);
25178
- const job = JSON.parse(readFileSync15(filePath, "utf-8"));
24653
+ const job = JSON.parse(readFileSync14(filePath, "utf-8"));
25179
24654
  if ((job.attempts || 0) >= maxRetries) {
25180
24655
  continue;
25181
24656
  }
@@ -25207,7 +24682,7 @@ var init_liteBackend = __esm({
25207
24682
  const filePath = join18(dir, file);
25208
24683
  let job;
25209
24684
  try {
25210
- job = JSON.parse(readFileSync15(filePath, "utf-8"));
24685
+ job = JSON.parse(readFileSync14(filePath, "utf-8"));
25211
24686
  } catch {
25212
24687
  continue;
25213
24688
  }
@@ -27697,7 +27172,7 @@ ${end}
27697
27172
 
27698
27173
  // ../core/src/devAdmin.ts
27699
27174
  import { cpus as osCpus } from "node:os";
27700
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
27175
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13, existsSync as existsSync20, readdirSync as readdirSync14, mkdirSync as mkdirSync15, copyFileSync as copyFileSync2, statSync as statSync15 } from "node:fs";
27701
27176
  import { join as join22, dirname as dirname10, resolve as resolve16, relative as relative8 } from "node:path";
27702
27177
  import { fileURLToPath as fileURLToPath4 } from "node:url";
27703
27178
  import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
@@ -27883,7 +27358,7 @@ function resolveDevEnvVar(key) {
27883
27358
  if (live !== void 0 && live !== "") return live;
27884
27359
  const envPath = join22(process.cwd(), ".env");
27885
27360
  if (!existsSync20(envPath)) return "";
27886
- for (const line of readFileSync19(envPath, "utf-8").split("\n")) {
27361
+ for (const line of readFileSync18(envPath, "utf-8").split("\n")) {
27887
27362
  const t = line.trim();
27888
27363
  if (!t || t.startsWith("#") || !t.includes("=")) continue;
27889
27364
  const eq = t.indexOf("=");
@@ -27893,7 +27368,7 @@ function resolveDevEnvVar(key) {
27893
27368
  }
27894
27369
  function upsertDevEnvVar(key, value) {
27895
27370
  const envPath = join22(process.cwd(), ".env");
27896
- const lines = existsSync20(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
27371
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
27897
27372
  let found = false;
27898
27373
  const out = [];
27899
27374
  for (const line of lines) {
@@ -27926,7 +27401,7 @@ function parseEnvFile() {
27926
27401
  const envPath = join22(process.cwd(), ".env");
27927
27402
  const result = {};
27928
27403
  if (!existsSync20(envPath)) return result;
27929
- const lines = readFileSync19(envPath, "utf-8").split("\n");
27404
+ const lines = readFileSync18(envPath, "utf-8").split("\n");
27930
27405
  for (const line of lines) {
27931
27406
  const trimmed = line.trim();
27932
27407
  if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) continue;
@@ -28283,7 +27758,7 @@ var init_devAdmin = __esm({
28283
27758
  for (const rel of ["../../../package.json", "../../package.json"]) {
28284
27759
  const p = resolve16(__dirname2, rel);
28285
27760
  if (existsSync20(p)) {
28286
- const pkg = JSON.parse(readFileSync19(p, "utf-8"));
27761
+ const pkg = JSON.parse(readFileSync18(p, "utf-8"));
28287
27762
  if (pkg.version) return pkg.version;
28288
27763
  }
28289
27764
  }
@@ -28941,7 +28416,7 @@ var init_devAdmin = __esm({
28941
28416
  for (const filename of readdirSync14(queueDir).sort()) {
28942
28417
  if (!filename.endsWith(".queue-data")) continue;
28943
28418
  try {
28944
- const job = JSON.parse(readFileSync19(join22(queueDir, filename), "utf-8"));
28419
+ const job = JSON.parse(readFileSync18(join22(queueDir, filename), "utf-8"));
28945
28420
  jobs.push(mapQueueJob(job, topic, "pending"));
28946
28421
  } catch {
28947
28422
  }
@@ -29427,7 +28902,7 @@ var init_devAdmin = __esm({
29427
28902
  }
29428
28903
  try {
29429
28904
  const envPath = join22(process.cwd(), ".env");
29430
- const lines = existsSync20(envPath) ? readFileSync19(envPath, "utf-8").split("\n") : [];
28905
+ const lines = existsSync20(envPath) ? readFileSync18(envPath, "utf-8").split("\n") : [];
29431
28906
  const keysFound = { TINA4_DATABASE_URL: false, TINA4_DATABASE_USERNAME: false, TINA4_DATABASE_PASSWORD: false };
29432
28907
  const newLines = [];
29433
28908
  for (const line of lines) {
@@ -29473,7 +28948,7 @@ var init_devAdmin = __esm({
29473
28948
  const metaFile = join22(entryPath, "meta.json");
29474
28949
  if (statSync15(entryPath).isDirectory() && existsSync20(metaFile)) {
29475
28950
  try {
29476
- const meta = JSON.parse(readFileSync19(metaFile, "utf-8"));
28951
+ const meta = JSON.parse(readFileSync18(metaFile, "utf-8"));
29477
28952
  meta.id = entry;
29478
28953
  const srcDir = join22(entryPath, "src");
29479
28954
  if (existsSync20(srcDir)) {
@@ -29688,7 +29163,7 @@ var init_devAdmin = __esm({
29688
29163
  return;
29689
29164
  }
29690
29165
  try {
29691
- const content = readFileSync19(target, "utf-8");
29166
+ const content = readFileSync18(target, "utf-8");
29692
29167
  const path8 = relative8(root, target);
29693
29168
  res.json({ path: path8, content, language: devAdminLanguage(path8), bytes: Buffer.byteLength(content, "utf-8") });
29694
29169
  } catch (e) {
@@ -29730,7 +29205,7 @@ var init_devAdmin = __esm({
29730
29205
  return;
29731
29206
  }
29732
29207
  try {
29733
- const buf = readFileSync19(target);
29208
+ const buf = readFileSync18(target);
29734
29209
  const ext = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
29735
29210
  const mime = {
29736
29211
  js: "application/javascript",
@@ -30158,7 +29633,7 @@ var init_devAdmin = __esm({
30158
29633
  });
30159
29634
 
30160
29635
  // ../core/src/i18n.ts
30161
- import { readFileSync as readFileSync20, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
29636
+ import { readFileSync as readFileSync19, readdirSync as readdirSync15, existsSync as existsSync21 } from "node:fs";
30162
29637
  import { join as join23, resolve as resolve17 } from "node:path";
30163
29638
  var I18n;
30164
29639
  var init_i18n = __esm({
@@ -30255,7 +29730,7 @@ var init_i18n = __esm({
30255
29730
  const filePath = join23(this._localeDir, `${locale}.json`);
30256
29731
  if (existsSync21(filePath)) {
30257
29732
  try {
30258
- const raw = readFileSync20(filePath, "utf-8");
29733
+ const raw = readFileSync19(filePath, "utf-8");
30259
29734
  const data = JSON.parse(raw);
30260
29735
  this._translations.set(locale, _I18n._flatten(data));
30261
29736
  return;
@@ -30268,7 +29743,7 @@ var init_i18n = __esm({
30268
29743
  const yamlPath = join23(this._localeDir, `${locale}${ext}`);
30269
29744
  if (existsSync21(yamlPath)) {
30270
29745
  try {
30271
- const raw = readFileSync20(yamlPath, "utf-8");
29746
+ const raw = readFileSync19(yamlPath, "utf-8");
30272
29747
  const data = _I18n._parseSimpleYaml(raw);
30273
29748
  this._translations.set(locale, _I18n._flatten(data));
30274
29749
  return;
@@ -30936,6 +30411,36 @@ var init_docsAutoDiscovery = __esm({
30936
30411
  }
30937
30412
  });
30938
30413
 
30414
+ // ../core/src/sessionHandlers/childError.ts
30415
+ function childFailureReason(err) {
30416
+ const e = err ?? {};
30417
+ const stderr = String(e.stderr ?? "").trim();
30418
+ if (stderr !== "") {
30419
+ return firstLine(stderr);
30420
+ }
30421
+ if (e.code === "ETIMEDOUT" || e.signal) {
30422
+ return `timed out or was killed (${e.code ?? e.signal})`;
30423
+ }
30424
+ if (typeof e.status === "number" && e.status !== 0) {
30425
+ return `child exited with code ${e.status} and no output`;
30426
+ }
30427
+ return firstLine(String(e.message ?? "unknown error"));
30428
+ }
30429
+ function firstLine(text) {
30430
+ const line = text.split("\n", 1)[0] ?? "";
30431
+ return line.length > MAX_FALLBACK ? `${line.slice(0, MAX_FALLBACK)}...` : line;
30432
+ }
30433
+ function childFailureError(label, err) {
30434
+ return new Error(`${label} command failed: ${childFailureReason(err)}`);
30435
+ }
30436
+ var MAX_FALLBACK;
30437
+ var init_childError = __esm({
30438
+ "../core/src/sessionHandlers/childError.ts"() {
30439
+ "use strict";
30440
+ MAX_FALLBACK = 200;
30441
+ }
30442
+ });
30443
+
30939
30444
  // ../core/src/sessionHandlers/respClient.ts
30940
30445
  import { execFileSync as execFileSync4 } from "node:child_process";
30941
30446
  function respCommandSync(target, args, label = "Redis") {
@@ -31055,7 +30560,7 @@ function respCommandSync(target, args, label = "Redis") {
31055
30560
  stdio: ["pipe", "pipe", "pipe"]
31056
30561
  });
31057
30562
  } catch (err) {
31058
- throw new Error(`${label} command failed: ${err.message}`);
30563
+ throw childFailureError(label, err);
31059
30564
  }
31060
30565
  if (result === "__NULL__") return "";
31061
30566
  if (result.startsWith("__ERR__")) {
@@ -31066,6 +30571,7 @@ function respCommandSync(target, args, label = "Redis") {
31066
30571
  var init_respClient = __esm({
31067
30572
  "../core/src/sessionHandlers/respClient.ts"() {
31068
30573
  "use strict";
30574
+ init_childError();
31069
30575
  }
31070
30576
  });
31071
30577
 
@@ -31084,6 +30590,7 @@ var moduleRequire, RedisNpmSessionHandler;
31084
30590
  var init_redisHandler = __esm({
31085
30591
  "../core/src/sessionHandlers/redisHandler.ts"() {
31086
30592
  "use strict";
30593
+ init_childError();
31087
30594
  init_respClient();
31088
30595
  moduleRequire = createRequire8(import.meta.url);
31089
30596
  RedisNpmSessionHandler = class {
@@ -31144,9 +30651,17 @@ var init_redisHandler = __esm({
31144
30651
  (async () => {
31145
30652
  try {
31146
30653
  const redis = require("redis");
30654
+ // reconnectStrategy: false \u2014 this child runs ONE command and exits, so
30655
+ // retrying inside it is pointless: the handler is called again on the
30656
+ // next request anyway. With the driver's default strategy a refused
30657
+ // connection never rejects, the child hangs until execFileSync's 5s
30658
+ // timeout kills it, and the caller is told "timed out" when the truth
30659
+ // is "connection refused". Off, connect() rejects in ~5ms with the real
30660
+ // reason -- a better message AND no 5s stall per request when Redis is
30661
+ // down.
31147
30662
  const clientOpts = useUrl
31148
- ? { url }
31149
- : { socket: { host, port }, password: password || undefined, database: db };
30663
+ ? { url, socket: { reconnectStrategy: false } }
30664
+ : { socket: { host, port, reconnectStrategy: false }, password: password || undefined, database: db };
31150
30665
  const client = redis.createClient(clientOpts);
31151
30666
  client.on("error", () => {});
31152
30667
  await client.connect();
@@ -31160,8 +30675,10 @@ var init_redisHandler = __esm({
31160
30675
  const out = (result === null || result === undefined) ? "__NULL__" : String(result);
31161
30676
  process.stdout.write(out, () => process.exit(0));
31162
30677
  } catch (err) {
31163
- process.stderr.write(String((err && err.message) || err));
31164
- process.exit(1);
30678
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30679
+ // a bare process.exit() truncates it, which left the parent with an
30680
+ // empty stderr and nothing but execFileSync's script-dump message.
30681
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31165
30682
  }
31166
30683
  })();
31167
30684
  `;
@@ -31173,7 +30690,7 @@ var init_redisHandler = __esm({
31173
30690
  stdio: ["pipe", "pipe", "pipe"]
31174
30691
  });
31175
30692
  } catch (err) {
31176
- throw new Error(`Redis command failed: ${err.message}`);
30693
+ throw childFailureError("Redis", err);
31177
30694
  }
31178
30695
  if (result === "__NULL__") return "";
31179
30696
  return result;
@@ -31305,8 +30822,10 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31305
30822
  process.stdout.write(out, () => process.exit(0));
31306
30823
  } catch (err) {
31307
30824
  try { if (client) await client.close(); } catch (e) {}
31308
- process.stderr.write(String((err && err.message) || err));
31309
- process.exit(1);
30825
+ // Exit from the write CALLBACK: stderr to a pipe is an async write and
30826
+ // a bare process.exit() truncates it, which left the parent with an
30827
+ // empty stderr and nothing but execFileSync's script-dump message.
30828
+ process.stderr.write(String((err && err.message) || err), () => process.exit(1));
31310
30829
  }
31311
30830
  })();
31312
30831
  } else {
@@ -31447,12 +30966,13 @@ function mongoCommandSync(target, command, args, label = "MongoDB") {
31447
30966
  stdio: ["pipe", "pipe", "pipe"]
31448
30967
  });
31449
30968
  } catch (err) {
31450
- throw new Error(`${label} command failed: ${err.message}`);
30969
+ throw childFailureError(label, err);
31451
30970
  }
31452
30971
  }
31453
30972
  var init_mongoClient = __esm({
31454
30973
  "../core/src/sessionHandlers/mongoClient.ts"() {
31455
30974
  "use strict";
30975
+ init_childError();
31456
30976
  }
31457
30977
  });
31458
30978
 
@@ -31605,7 +31125,7 @@ __export(session_exports, {
31605
31125
  sessionCookieName: () => sessionCookieName
31606
31126
  });
31607
31127
  import { randomBytes as randomBytes6 } from "node:crypto";
31608
- import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31128
+ import { existsSync as existsSync23, mkdirSync as mkdirSync18, readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync as unlinkSync7, readdirSync as readdirSync16 } from "node:fs";
31609
31129
  import { join as join25 } from "node:path";
31610
31130
  function isSecureScheme(forwardedProto, socketEncrypted) {
31611
31131
  const forwarded = (forwardedProto ?? "").trim();
@@ -31658,7 +31178,7 @@ var init_session = __esm({
31658
31178
  const filePath = this.filePath(sessionId);
31659
31179
  try {
31660
31180
  if (!existsSync23(filePath)) return null;
31661
- const raw = readFileSync22(filePath, "utf-8");
31181
+ const raw = readFileSync21(filePath, "utf-8");
31662
31182
  const wrapper = JSON.parse(raw);
31663
31183
  if (wrapper._expires && wrapper._expires > 0 && Date.now() / 1e3 > wrapper._expires) {
31664
31184
  try {
@@ -31694,7 +31214,7 @@ var init_session = __esm({
31694
31214
  if (!file.endsWith(".json")) continue;
31695
31215
  const fullPath = join25(this.storagePath, file);
31696
31216
  try {
31697
- const raw = readFileSync22(fullPath, "utf-8");
31217
+ const raw = readFileSync21(fullPath, "utf-8");
31698
31218
  const wrapper = JSON.parse(raw);
31699
31219
  if (wrapper._expires && wrapper._expires > 0 && now > wrapper._expires) {
31700
31220
  unlinkSync7(fullPath);
@@ -32211,7 +31731,7 @@ var init_events = __esm({
32211
31731
  // ../core/src/server.ts
32212
31732
  import { createServer as createServer2 } from "node:http";
32213
31733
  import { resolve as resolve19, dirname as dirname11, join as join26, relative as relative9 } from "node:path";
32214
- import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync16 } from "node:fs";
31734
+ import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync22, statSync as statSync16 } from "node:fs";
32215
31735
  import { isatty } from "node:tty";
32216
31736
  import { fileURLToPath as fileURLToPath5 } from "node:url";
32217
31737
  import { execFileSync as execFileSync7, exec } from "node:child_process";
@@ -32273,7 +31793,7 @@ async function autoMigrateOnStartup(migrationDir = "migrations", base = process.
32273
31793
  function readPackageVersion() {
32274
31794
  try {
32275
31795
  const pkgPath = resolve19(dirname11(fileURLToPath5(import.meta.url)), "..", "..", "..", "package.json");
32276
- const pkg = JSON.parse(readFileSync23(pkgPath, "utf-8"));
31796
+ const pkg = JSON.parse(readFileSync22(pkgPath, "utf-8"));
32277
31797
  return pkg.version ?? "0.0.0";
32278
31798
  } catch {
32279
31799
  return "0.0.0";
@@ -33103,7 +32623,7 @@ ${reset2}
33103
32623
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33104
32624
  res.raw.end(html);
33105
32625
  } else {
33106
- const html = readFileSync23(resolve19(templatesDir, tplFile), "utf-8");
32626
+ const html = readFileSync22(resolve19(templatesDir, tplFile), "utf-8");
33107
32627
  res.raw.writeHead(200, void 0, { "Content-Type": "text/html; charset=utf-8" });
33108
32628
  res.raw.end(html);
33109
32629
  }
@@ -33494,7 +33014,7 @@ var init_constants = __esm({
33494
33014
  });
33495
33015
 
33496
33016
  // ../core/src/scss.ts
33497
- import { readFileSync as readFileSync24, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
33017
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync17, existsSync as existsSync25, mkdirSync as mkdirSync19, readdirSync as readdirSync18 } from "node:fs";
33498
33018
  import { join as join27, resolve as resolve20, dirname as dirname12 } from "node:path";
33499
33019
  function compileString(scss, importPaths, variables) {
33500
33020
  const imported = /* @__PURE__ */ new Set();
@@ -33525,7 +33045,7 @@ function resolveImports(content, paths, imported) {
33525
33045
  for (const candidate of candidates) {
33526
33046
  if (existsSync25(candidate) && !imported.has(candidate)) {
33527
33047
  imported.add(candidate);
33528
- const fileContent = readFileSync24(candidate, "utf-8");
33048
+ const fileContent = readFileSync23(candidate, "utf-8");
33529
33049
  return resolveImports(fileContent, [dirname12(candidate), ...paths], imported);
33530
33050
  }
33531
33051
  }
@@ -33866,7 +33386,7 @@ var init_scss = __esm({
33866
33386
  /** Compile an SCSS file to CSS. */
33867
33387
  compileFile(filePath) {
33868
33388
  const absPath = resolve20(filePath);
33869
- const content = readFileSync24(absPath, "utf-8");
33389
+ const content = readFileSync23(absPath, "utf-8");
33870
33390
  const paths = [dirname12(absPath), ...this._importPaths];
33871
33391
  return compileString(content, paths, { ...this._variables });
33872
33392
  }
@@ -33889,7 +33409,7 @@ var init_scss = __esm({
33889
33409
  const imported = /* @__PURE__ */ new Set();
33890
33410
  let merged = "";
33891
33411
  for (const file of files) {
33892
- const content = readFileSync24(file, "utf-8");
33412
+ const content = readFileSync23(file, "utf-8");
33893
33413
  imported.add(file);
33894
33414
  merged += resolveImports(content, paths, imported) + "\n";
33895
33415
  }
@@ -33906,7 +33426,7 @@ var init_scss = __esm({
33906
33426
  if (!existsSync25(outDir)) mkdirSync19(outDir, { recursive: true });
33907
33427
  let existing = null;
33908
33428
  try {
33909
- existing = existsSync25(absOutput) ? readFileSync24(absOutput, "utf-8") : null;
33429
+ existing = existsSync25(absOutput) ? readFileSync23(absOutput, "utf-8") : null;
33910
33430
  } catch {
33911
33431
  existing = null;
33912
33432
  }
@@ -33990,10 +33510,10 @@ var init_mqttMessage = __esm({
33990
33510
  });
33991
33511
 
33992
33512
  // ../core/src/mqtt.ts
33993
- import net3 from "node:net";
33994
- import tls2 from "node:tls";
33513
+ import net2 from "node:net";
33514
+ import tls from "node:tls";
33995
33515
  import { randomBytes as randomBytes7 } from "node:crypto";
33996
- import { existsSync as existsSync26, readFileSync as readFileSync25 } from "node:fs";
33516
+ import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
33997
33517
  var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
33998
33518
  var init_mqtt = __esm({
33999
33519
  "../core/src/mqtt.ts"() {
@@ -34458,10 +33978,10 @@ var init_mqtt = __esm({
34458
33978
  servername: this.host,
34459
33979
  rejectUnauthorized: this.tlsVerify
34460
33980
  };
34461
- if (this.tlsVerify && this.caFile) opts.ca = readFileSync25(this.caFile);
34462
- sock = tls2.connect(opts, () => settle(() => resolve31(sock)));
33981
+ if (this.tlsVerify && this.caFile) opts.ca = readFileSync24(this.caFile);
33982
+ sock = tls.connect(opts, () => settle(() => resolve31(sock)));
34463
33983
  } else {
34464
- sock = net3.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
33984
+ sock = net2.createConnection({ host: this.host, port: this.port }, () => settle(() => resolve31(sock)));
34465
33985
  }
34466
33986
  sock.once("error", (err) => {
34467
33987
  settle(() => {
@@ -35012,7 +34532,7 @@ import https from "node:https";
35012
34532
  import { URL as URL2 } from "node:url";
35013
34533
  import { randomBytes as randomBytes8 } from "node:crypto";
35014
34534
  import { promises as fsp, createWriteStream } from "node:fs";
35015
- import { basename as basename7 } from "node:path";
34535
+ import { basename as basename6 } from "node:path";
35016
34536
  import { pipeline } from "node:stream/promises";
35017
34537
  function sameOrigin(urlA, urlB) {
35018
34538
  try {
@@ -35302,7 +34822,7 @@ var init_api = __esm({
35302
34822
  error: err instanceof Error ? err.message : String(err)
35303
34823
  };
35304
34824
  }
35305
- uploadName = filename || basename7(filePath);
34825
+ uploadName = filename || basename6(filePath);
35306
34826
  } else {
35307
34827
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
35308
34828
  }
@@ -35658,6 +35178,777 @@ var init_api = __esm({
35658
35178
  }
35659
35179
  });
35660
35180
 
35181
+ // ../core/src/messenger.ts
35182
+ import net3 from "node:net";
35183
+ import tls2 from "node:tls";
35184
+ import { readFileSync as readFileSync25 } from "node:fs";
35185
+ import { basename as basename7 } from "node:path";
35186
+ import { randomUUID as randomUUID9 } from "node:crypto";
35187
+ function tlsRejectUnauthorized() {
35188
+ return !isTruthy(process.env.TINA4_MAIL_TLS_INSECURE);
35189
+ }
35190
+ function readResponse(socket) {
35191
+ return new Promise((resolve31, reject) => {
35192
+ let buffer = "";
35193
+ const onData = (chunk) => {
35194
+ buffer += chunk.toString("utf-8");
35195
+ const lines = buffer.split("\r\n");
35196
+ for (let i = 0; i < lines.length; i++) {
35197
+ const line = lines[i];
35198
+ if (line.length < 3) continue;
35199
+ const code = parseInt(line.substring(0, 3), 10);
35200
+ if (line.length >= 4 && line[3] === " ") {
35201
+ socket.removeListener("data", onData);
35202
+ socket.removeListener("error", onError);
35203
+ resolve31({ code, text: buffer.trim() });
35204
+ return;
35205
+ }
35206
+ }
35207
+ };
35208
+ const onError = (err) => {
35209
+ socket.removeListener("data", onData);
35210
+ reject(err);
35211
+ };
35212
+ socket.on("data", onData);
35213
+ socket.on("error", onError);
35214
+ });
35215
+ }
35216
+ function sendCommand(socket, command) {
35217
+ return new Promise((resolve31, reject) => {
35218
+ socket.write(command + "\r\n", "utf-8", (err) => {
35219
+ if (err) return reject(err);
35220
+ readResponse(socket).then(resolve31, reject);
35221
+ });
35222
+ });
35223
+ }
35224
+ function buildMimeMessage(options) {
35225
+ const boundary = `----=_Tina4_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35226
+ const altBoundary = `----=_Tina4Alt_${Date.now()}_${Math.random().toString(36).substring(2)}`;
35227
+ const hasAttachments = options.attachments && options.attachments.length > 0;
35228
+ const hasTextAlt = options.text !== void 0 && options.html;
35229
+ const lines = [];
35230
+ const fromHeader = options.fromName ? `"${options.fromName}" <${options.from}>` : options.from;
35231
+ lines.push(`From: ${fromHeader}`);
35232
+ lines.push(`To: ${options.to.join(", ")}`);
35233
+ if (options.cc.length > 0) {
35234
+ lines.push(`Cc: ${options.cc.join(", ")}`);
35235
+ }
35236
+ lines.push(`Subject: ${options.subject}`);
35237
+ lines.push(`Date: ${(/* @__PURE__ */ new Date()).toUTCString()}`);
35238
+ lines.push(`Message-ID: <${options.messageId}>`);
35239
+ lines.push("MIME-Version: 1.0");
35240
+ if (options.replyTo) {
35241
+ lines.push(`Reply-To: ${options.replyTo}`);
35242
+ }
35243
+ if (options.headers) {
35244
+ for (const [key, value] of Object.entries(options.headers)) {
35245
+ lines.push(`${key}: ${value}`);
35246
+ }
35247
+ }
35248
+ if (hasAttachments) {
35249
+ lines.push(`Content-Type: multipart/mixed; boundary="${boundary}"`);
35250
+ lines.push("");
35251
+ lines.push(`--${boundary}`);
35252
+ if (hasTextAlt) {
35253
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35254
+ lines.push("");
35255
+ lines.push(`--${altBoundary}`);
35256
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35257
+ lines.push("Content-Transfer-Encoding: 7bit");
35258
+ lines.push("");
35259
+ lines.push(options.text);
35260
+ lines.push("");
35261
+ lines.push(`--${altBoundary}`);
35262
+ lines.push("Content-Type: text/html; charset=UTF-8");
35263
+ lines.push("Content-Transfer-Encoding: 7bit");
35264
+ lines.push("");
35265
+ lines.push(options.body);
35266
+ lines.push("");
35267
+ lines.push(`--${altBoundary}--`);
35268
+ } else {
35269
+ const contentType = options.html ? "text/html" : "text/plain";
35270
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35271
+ lines.push("Content-Transfer-Encoding: 7bit");
35272
+ lines.push("");
35273
+ lines.push(options.body);
35274
+ }
35275
+ for (const filePath of options.attachments) {
35276
+ const fileName = basename7(filePath);
35277
+ const fileData = readFileSync25(filePath);
35278
+ const base64Data = fileData.toString("base64");
35279
+ lines.push("");
35280
+ lines.push(`--${boundary}`);
35281
+ lines.push(`Content-Type: application/octet-stream; name="${fileName}"`);
35282
+ lines.push("Content-Transfer-Encoding: base64");
35283
+ lines.push(`Content-Disposition: attachment; filename="${fileName}"`);
35284
+ lines.push("");
35285
+ for (let i = 0; i < base64Data.length; i += 76) {
35286
+ lines.push(base64Data.substring(i, i + 76));
35287
+ }
35288
+ }
35289
+ lines.push("");
35290
+ lines.push(`--${boundary}--`);
35291
+ } else if (hasTextAlt) {
35292
+ lines.push(`Content-Type: multipart/alternative; boundary="${altBoundary}"`);
35293
+ lines.push("");
35294
+ lines.push(`--${altBoundary}`);
35295
+ lines.push("Content-Type: text/plain; charset=UTF-8");
35296
+ lines.push("Content-Transfer-Encoding: 7bit");
35297
+ lines.push("");
35298
+ lines.push(options.text);
35299
+ lines.push("");
35300
+ lines.push(`--${altBoundary}`);
35301
+ lines.push("Content-Type: text/html; charset=UTF-8");
35302
+ lines.push("Content-Transfer-Encoding: 7bit");
35303
+ lines.push("");
35304
+ lines.push(options.body);
35305
+ lines.push("");
35306
+ lines.push(`--${altBoundary}--`);
35307
+ } else {
35308
+ const contentType = options.html ? "text/html" : "text/plain";
35309
+ lines.push(`Content-Type: ${contentType}; charset=UTF-8`);
35310
+ lines.push("");
35311
+ lines.push(options.body);
35312
+ }
35313
+ return lines.join("\r\n");
35314
+ }
35315
+ function imapQuote(s) {
35316
+ if (/^[a-zA-Z0-9_./-]+$/.test(s)) return s;
35317
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
35318
+ }
35319
+ function imapReadLine(socket) {
35320
+ return new Promise((resolve31, reject) => {
35321
+ let buffer = "";
35322
+ const onData = (chunk) => {
35323
+ buffer += chunk.toString("utf-8");
35324
+ const nlIndex = buffer.indexOf("\r\n");
35325
+ if (nlIndex !== -1) {
35326
+ socket.removeListener("data", onData);
35327
+ socket.removeListener("error", onError);
35328
+ resolve31(buffer);
35329
+ }
35330
+ };
35331
+ const onError = (err) => {
35332
+ socket.removeListener("data", onData);
35333
+ reject(err);
35334
+ };
35335
+ socket.on("data", onData);
35336
+ socket.on("error", onError);
35337
+ });
35338
+ }
35339
+ function imapCommand(socket, command) {
35340
+ return new Promise((resolve31, reject) => {
35341
+ imapTagCounter++;
35342
+ const tag = `T${imapTagCounter}`;
35343
+ const fullCommand = `${tag} ${command}\r
35344
+ `;
35345
+ let buffer = "";
35346
+ const onData = (chunk) => {
35347
+ buffer += chunk.toString("utf-8");
35348
+ if (buffer.includes(`${tag} OK`)) {
35349
+ socket.removeListener("data", onData);
35350
+ socket.removeListener("error", onError);
35351
+ resolve31(buffer);
35352
+ return;
35353
+ }
35354
+ if (buffer.includes(`${tag} NO`) || buffer.includes(`${tag} BAD`)) {
35355
+ socket.removeListener("data", onData);
35356
+ socket.removeListener("error", onError);
35357
+ reject(new MessengerConnectionError(`IMAP command failed: ${command.split(" ")[0]} \u2192 ${buffer.trim()}`));
35358
+ }
35359
+ };
35360
+ const onError = (err) => {
35361
+ socket.removeListener("data", onData);
35362
+ reject(err);
35363
+ };
35364
+ socket.on("data", onData);
35365
+ socket.on("error", onError);
35366
+ socket.write(fullCommand, "utf-8");
35367
+ });
35368
+ }
35369
+ function imapFail(method, err) {
35370
+ const e = err instanceof Error ? err : new Error(String(err));
35371
+ Log.error(`Messenger IMAP ${method}() failed: ${e.name}: ${e.message}`);
35372
+ if (e instanceof MessengerConnectionError) return e;
35373
+ return new MessengerConnectionError(`IMAP ${method} failed: ${e.message}`);
35374
+ }
35375
+ function parseSearchResponse(response) {
35376
+ const match = response.match(/\* SEARCH (.+)/);
35377
+ if (!match) return [];
35378
+ return match[1].trim().split(/\s+/).filter((s) => /^\d+$/.test(s));
35379
+ }
35380
+ function parseHeaderResponse(uid, response) {
35381
+ const headers = {};
35382
+ const headerBlock = response.match(/\r\n([\s\S]*?)\r\n\)/);
35383
+ if (headerBlock) {
35384
+ const lines = headerBlock[1].split(/\r\n/);
35385
+ let currentKey = "";
35386
+ for (const line of lines) {
35387
+ if (/^\s/.test(line) && currentKey) {
35388
+ headers[currentKey] += " " + line.trim();
35389
+ } else {
35390
+ const colonIdx = line.indexOf(":");
35391
+ if (colonIdx > 0) {
35392
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35393
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35394
+ }
35395
+ }
35396
+ }
35397
+ }
35398
+ const seen = /\\Seen/i.test(response);
35399
+ return {
35400
+ uid,
35401
+ subject: headers["subject"] ?? "",
35402
+ from: headers["from"] ?? "",
35403
+ to: headers["to"] ?? "",
35404
+ date: headers["date"] ?? "",
35405
+ snippet: "",
35406
+ seen
35407
+ };
35408
+ }
35409
+ function emptyFullMessage(uid) {
35410
+ return { uid, subject: "", from: "", to: "", cc: "", date: "", bodyText: "", bodyHtml: "", headers: {} };
35411
+ }
35412
+ function parseFullMessage(uid, response) {
35413
+ const bodyMatch = response.match(/\{(\d+)\}\r\n([\s\S]*)/);
35414
+ const rawMessage = bodyMatch ? bodyMatch[2] : response;
35415
+ const headerEnd = rawMessage.indexOf("\r\n\r\n");
35416
+ const headerSection = headerEnd > 0 ? rawMessage.substring(0, headerEnd) : rawMessage;
35417
+ const bodySection = headerEnd > 0 ? rawMessage.substring(headerEnd + 4) : "";
35418
+ const headers = {};
35419
+ const headerLines = headerSection.split(/\r\n/);
35420
+ let currentKey = "";
35421
+ for (const line of headerLines) {
35422
+ if (/^\s/.test(line) && currentKey) {
35423
+ headers[currentKey] += " " + line.trim();
35424
+ } else {
35425
+ const colonIdx = line.indexOf(":");
35426
+ if (colonIdx > 0) {
35427
+ currentKey = line.substring(0, colonIdx).trim().toLowerCase();
35428
+ headers[currentKey] = line.substring(colonIdx + 1).trim();
35429
+ }
35430
+ }
35431
+ }
35432
+ const contentType = headers["content-type"] ?? "text/plain";
35433
+ let bodyText = "";
35434
+ let bodyHtml = "";
35435
+ if (contentType.includes("multipart")) {
35436
+ const boundaryMatch = contentType.match(/boundary="?([^";\s]+)"?/);
35437
+ if (boundaryMatch) {
35438
+ const boundary = boundaryMatch[1];
35439
+ const parts = bodySection.split("--" + boundary);
35440
+ for (const part of parts) {
35441
+ if (part.trim() === "" || part.trim() === "--") continue;
35442
+ const partHeaderEnd = part.indexOf("\r\n\r\n");
35443
+ const partHeaders = partHeaderEnd > 0 ? part.substring(0, partHeaderEnd).toLowerCase() : "";
35444
+ const partBody = partHeaderEnd > 0 ? part.substring(partHeaderEnd + 4).trim() : "";
35445
+ if (partHeaders.includes("text/html")) {
35446
+ bodyHtml = partBody;
35447
+ } else if (partHeaders.includes("text/plain")) {
35448
+ bodyText = partBody;
35449
+ }
35450
+ }
35451
+ }
35452
+ } else if (contentType.includes("text/html")) {
35453
+ bodyHtml = bodySection;
35454
+ } else {
35455
+ bodyText = bodySection;
35456
+ }
35457
+ bodyText = bodyText.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35458
+ bodyHtml = bodyHtml.replace(/\)\r\n[A-Z]\d+ OK.*$/s, "").trim();
35459
+ return {
35460
+ uid,
35461
+ subject: headers["subject"] ?? "",
35462
+ from: headers["from"] ?? "",
35463
+ to: headers["to"] ?? "",
35464
+ cc: headers["cc"] ?? "",
35465
+ date: headers["date"] ?? "",
35466
+ bodyText,
35467
+ bodyHtml,
35468
+ headers
35469
+ };
35470
+ }
35471
+ function createMessenger() {
35472
+ return new Messenger();
35473
+ }
35474
+ var MessengerConnectionError, Messenger, imapTagCounter;
35475
+ var init_messenger = __esm({
35476
+ "../core/src/messenger.ts"() {
35477
+ "use strict";
35478
+ init_dotenv();
35479
+ init_devMailbox();
35480
+ init_logger();
35481
+ MessengerConnectionError = class extends Error {
35482
+ constructor(message) {
35483
+ super(message);
35484
+ this.name = "MessengerConnectionError";
35485
+ }
35486
+ };
35487
+ Messenger = class {
35488
+ host;
35489
+ port;
35490
+ username;
35491
+ password;
35492
+ fromAddress;
35493
+ fromName;
35494
+ encryption;
35495
+ useTls;
35496
+ /** Whether an SMTP host was actually configured (see the constructor). */
35497
+ smtpConfigured = false;
35498
+ /** The local mailbox, present only when this messenger captures. */
35499
+ devMailbox = null;
35500
+ imapHost;
35501
+ imapPort;
35502
+ imapUser;
35503
+ imapPass;
35504
+ imapEncryption;
35505
+ constructor(options) {
35506
+ this.smtpConfigured = Boolean(options?.host ?? process.env.TINA4_MAIL_HOST);
35507
+ this.host = options?.host ?? process.env.TINA4_MAIL_HOST ?? "localhost";
35508
+ this.port = options?.port ?? parseInt(process.env.TINA4_MAIL_PORT ?? "587", 10);
35509
+ this.username = options?.username ?? process.env.TINA4_MAIL_USERNAME ?? "";
35510
+ this.password = options?.password ?? process.env.TINA4_MAIL_PASSWORD ?? "";
35511
+ this.fromAddress = options?.fromAddress ?? process.env.TINA4_MAIL_FROM ?? (this.username || "noreply@localhost");
35512
+ this.fromName = options?.fromName ?? process.env.TINA4_MAIL_FROM_NAME ?? "";
35513
+ const envEncryption = options?.encryption ?? process.env.TINA4_MAIL_ENCRYPTION;
35514
+ if (envEncryption) {
35515
+ this.encryption = envEncryption.toLowerCase();
35516
+ } else if (options?.useTls !== void 0) {
35517
+ this.encryption = options.useTls ? "tls" : "none";
35518
+ } else {
35519
+ this.encryption = "tls";
35520
+ }
35521
+ this.useTls = ["tls", "starttls"].includes(this.encryption);
35522
+ this.imapHost = options?.imapHost ?? process.env.TINA4_MAIL_IMAP_HOST ?? "";
35523
+ this.imapPort = options?.imapPort ?? parseInt(process.env.TINA4_MAIL_IMAP_PORT ?? "993", 10);
35524
+ this.imapUser = options?.imapUser ?? process.env.TINA4_MAIL_IMAP_USERNAME ?? this.username;
35525
+ this.imapPass = options?.imapPass ?? process.env.TINA4_MAIL_IMAP_PASSWORD ?? this.password;
35526
+ this.imapEncryption = (options?.imapEncryption ?? process.env.TINA4_MAIL_IMAP_ENCRYPTION ?? "tls").toLowerCase();
35527
+ }
35528
+ /**
35529
+ * Read-only IMAP encryption mode for inspection / tests.
35530
+ * Returns one of "tls", "starttls", "none", "ssl".
35531
+ */
35532
+ getImapEncryption() {
35533
+ return this.imapEncryption;
35534
+ }
35535
+ /**
35536
+ * Send an email via SMTP.
35537
+ */
35538
+ /**
35539
+ * Should send() capture locally instead of talking to SMTP?
35540
+ *
35541
+ * Availability decides, not verbosity. With no SMTP host configured sending is
35542
+ * impossible, so simulate it into a folder rather than failing -- that is what
35543
+ * makes a laptop with no mail server usable. TINA4_MAIL_CAPTURE forces capture
35544
+ * even when a host IS configured.
35545
+ *
35546
+ * TINA4_DEBUG deliberately does NOT gate this, and neither does NODE_ENV. Debug
35547
+ * must still be able to send, and the old `NODE_ENV !== "production"` clause
35548
+ * silently swallowed every staging email.
35549
+ */
35550
+ shouldCapture() {
35551
+ if (isTruthy(process.env.TINA4_MAIL_CAPTURE)) return true;
35552
+ return !this.smtpConfigured;
35553
+ }
35554
+ /** The local mailbox, created on first capture and reused after. */
35555
+ getDevMailbox() {
35556
+ if (this.devMailbox === null) {
35557
+ this.devMailbox = new DevMailbox();
35558
+ }
35559
+ return this.devMailbox;
35560
+ }
35561
+ async send(to, subject, body, html = false, text, cc, bcc, replyTo, attachments, headers) {
35562
+ const options = { to, subject, body, html, text, cc, bcc, replyTo, attachments, headers };
35563
+ const toList = Array.isArray(options.to) ? options.to : [options.to];
35564
+ const ccList = Array.isArray(options.cc) ? options.cc : options.cc ? [options.cc] : [];
35565
+ const bccList = Array.isArray(options.bcc) ? options.bcc : options.bcc ? [options.bcc] : [];
35566
+ const allRecipients = [...toList, ...ccList, ...bccList];
35567
+ if (this.shouldCapture()) {
35568
+ return this.getDevMailbox().capture(
35569
+ to,
35570
+ subject,
35571
+ body,
35572
+ html,
35573
+ text,
35574
+ ccList,
35575
+ bccList,
35576
+ replyTo,
35577
+ attachments,
35578
+ this.fromAddress || void 0
35579
+ );
35580
+ }
35581
+ const messageId = `${randomUUID9()}@${this.host}`;
35582
+ if (allRecipients.length === 0) {
35583
+ return { success: false, message: "No recipients specified" };
35584
+ }
35585
+ if (!this.fromAddress) {
35586
+ return { success: false, message: "No from address configured" };
35587
+ }
35588
+ try {
35589
+ let socket;
35590
+ if (this.port === 465) {
35591
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35592
+ await new Promise((resolve31, reject) => {
35593
+ socket.once("secureConnect", resolve31);
35594
+ socket.once("error", reject);
35595
+ });
35596
+ } else {
35597
+ socket = net3.createConnection({ host: this.host, port: this.port });
35598
+ await new Promise((resolve31, reject) => {
35599
+ socket.once("connect", resolve31);
35600
+ socket.once("error", reject);
35601
+ });
35602
+ }
35603
+ const greeting = await readResponse(socket);
35604
+ if (greeting.code !== 220) {
35605
+ socket.destroy();
35606
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35607
+ }
35608
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35609
+ if (ehlo.code !== 250) {
35610
+ socket.destroy();
35611
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35612
+ }
35613
+ if (this.useTls && this.port !== 465 && ehlo.text.includes("STARTTLS")) {
35614
+ const starttls = await sendCommand(socket, "STARTTLS");
35615
+ if (starttls.code !== 220) {
35616
+ socket.destroy();
35617
+ return { success: false, message: `STARTTLS failed: ${starttls.text}` };
35618
+ }
35619
+ const plainSocket = socket;
35620
+ socket = tls2.connect(
35621
+ { socket: plainSocket, host: this.host, rejectUnauthorized: tlsRejectUnauthorized() }
35622
+ );
35623
+ await new Promise((resolve31, reject) => {
35624
+ socket.once("secureConnect", resolve31);
35625
+ socket.once("error", reject);
35626
+ });
35627
+ const ehlo2 = await sendCommand(socket, `EHLO ${this.host}`);
35628
+ if (ehlo2.code !== 250) {
35629
+ socket.destroy();
35630
+ return { success: false, message: `EHLO after STARTTLS failed: ${ehlo2.text}` };
35631
+ }
35632
+ }
35633
+ if (this.username && this.password) {
35634
+ const auth = await sendCommand(socket, "AUTH LOGIN");
35635
+ if (auth.code !== 334) {
35636
+ socket.destroy();
35637
+ return { success: false, message: `AUTH LOGIN failed: ${auth.text}` };
35638
+ }
35639
+ const userResp = await sendCommand(socket, Buffer.from(this.username).toString("base64"));
35640
+ if (userResp.code !== 334) {
35641
+ socket.destroy();
35642
+ return { success: false, message: `AUTH username failed: ${userResp.text}` };
35643
+ }
35644
+ const passResp = await sendCommand(socket, Buffer.from(this.password).toString("base64"));
35645
+ if (passResp.code !== 235) {
35646
+ socket.destroy();
35647
+ return { success: false, message: `AUTH password failed: ${passResp.text}` };
35648
+ }
35649
+ }
35650
+ const mailFrom = await sendCommand(socket, `MAIL FROM:<${this.fromAddress}>`);
35651
+ if (mailFrom.code !== 250) {
35652
+ socket.destroy();
35653
+ return { success: false, message: `MAIL FROM failed: ${mailFrom.text}` };
35654
+ }
35655
+ for (const recipient of allRecipients) {
35656
+ const rcpt = await sendCommand(socket, `RCPT TO:<${recipient}>`);
35657
+ if (rcpt.code !== 250 && rcpt.code !== 251) {
35658
+ socket.destroy();
35659
+ return { success: false, message: `RCPT TO <${recipient}> failed: ${rcpt.text}` };
35660
+ }
35661
+ }
35662
+ const dataCmd = await sendCommand(socket, "DATA");
35663
+ if (dataCmd.code !== 354) {
35664
+ socket.destroy();
35665
+ return { success: false, message: `DATA failed: ${dataCmd.text}` };
35666
+ }
35667
+ const mimeMessage = buildMimeMessage({
35668
+ from: this.fromAddress,
35669
+ fromName: this.fromName,
35670
+ to: toList,
35671
+ cc: ccList,
35672
+ subject: options.subject,
35673
+ body: options.body,
35674
+ html: options.html ?? false,
35675
+ text: options.text,
35676
+ replyTo: options.replyTo,
35677
+ attachments: options.attachments,
35678
+ headers: options.headers,
35679
+ messageId
35680
+ });
35681
+ const endData = await sendCommand(socket, mimeMessage + "\r\n.");
35682
+ if (endData.code !== 250) {
35683
+ socket.destroy();
35684
+ return { success: false, message: `Message delivery failed: ${endData.text}` };
35685
+ }
35686
+ await sendCommand(socket, "QUIT");
35687
+ socket.destroy();
35688
+ return { success: true, message: "Email sent successfully", id: messageId };
35689
+ } catch (err) {
35690
+ const errMsg = err instanceof Error ? err.message : String(err);
35691
+ return { success: false, message: `SMTP error: ${errMsg}` };
35692
+ }
35693
+ }
35694
+ /**
35695
+ * Test the SMTP connection without sending an email.
35696
+ */
35697
+ async testConnection() {
35698
+ try {
35699
+ let socket;
35700
+ if (this.port === 465) {
35701
+ socket = tls2.connect({ host: this.host, port: this.port, rejectUnauthorized: tlsRejectUnauthorized() });
35702
+ await new Promise((resolve31, reject) => {
35703
+ socket.once("secureConnect", resolve31);
35704
+ socket.once("error", reject);
35705
+ });
35706
+ } else {
35707
+ socket = net3.createConnection({ host: this.host, port: this.port });
35708
+ await new Promise((resolve31, reject) => {
35709
+ socket.once("connect", resolve31);
35710
+ socket.once("error", reject);
35711
+ });
35712
+ }
35713
+ const greeting = await readResponse(socket);
35714
+ if (greeting.code !== 220) {
35715
+ socket.destroy();
35716
+ return { success: false, message: `SMTP greeting failed: ${greeting.text}` };
35717
+ }
35718
+ const ehlo = await sendCommand(socket, `EHLO ${this.host}`);
35719
+ if (ehlo.code !== 250) {
35720
+ socket.destroy();
35721
+ return { success: false, message: `EHLO failed: ${ehlo.text}` };
35722
+ }
35723
+ await sendCommand(socket, "QUIT");
35724
+ socket.destroy();
35725
+ return { success: true, message: `Connected to ${this.host}:${this.port}` };
35726
+ } catch (err) {
35727
+ const errMsg = err instanceof Error ? err.message : String(err);
35728
+ return { success: false, message: `Connection failed: ${errMsg}` };
35729
+ }
35730
+ }
35731
+ // ── IMAP (Read) ────────────────────────────────────────────
35732
+ /**
35733
+ * Connect to the IMAP server via raw TCP/TLS.
35734
+ * Returns the socket and reads the greeting.
35735
+ */
35736
+ async imapConnect() {
35737
+ if (!this.imapHost) {
35738
+ throw new Error("IMAP host not configured (set imapHost or IMAP_HOST env)");
35739
+ }
35740
+ let socket;
35741
+ const useTls = this.imapEncryption === "tls" || this.imapEncryption === "ssl" || this.imapEncryption === "" && this.imapPort === 993;
35742
+ if (useTls) {
35743
+ socket = tls2.connect({ host: this.imapHost, port: this.imapPort, rejectUnauthorized: tlsRejectUnauthorized() });
35744
+ await new Promise((resolve31, reject) => {
35745
+ socket.once("secureConnect", resolve31);
35746
+ socket.once("error", reject);
35747
+ });
35748
+ } else {
35749
+ socket = net3.createConnection({ host: this.imapHost, port: this.imapPort });
35750
+ await new Promise((resolve31, reject) => {
35751
+ socket.once("connect", resolve31);
35752
+ socket.once("error", reject);
35753
+ });
35754
+ }
35755
+ await imapReadLine(socket);
35756
+ if (this.imapUser && this.imapPass) {
35757
+ const loginResp = await imapCommand(socket, `LOGIN ${imapQuote(this.imapUser)} ${imapQuote(this.imapPass)}`);
35758
+ if (!loginResp.includes("OK")) {
35759
+ socket.destroy();
35760
+ throw new Error(`IMAP login failed: ${loginResp}`);
35761
+ }
35762
+ }
35763
+ return socket;
35764
+ }
35765
+ /**
35766
+ * Disconnect from IMAP cleanly.
35767
+ */
35768
+ async imapDisconnect(socket) {
35769
+ try {
35770
+ await imapCommand(socket, "LOGOUT");
35771
+ } catch {
35772
+ }
35773
+ socket.destroy();
35774
+ }
35775
+ /**
35776
+ * Fetch latest messages from a folder.
35777
+ * Returns list of message summaries.
35778
+ */
35779
+ async inbox(limit = 20, offset = 0, folder = "INBOX") {
35780
+ let socket;
35781
+ try {
35782
+ socket = await this.imapConnect();
35783
+ } catch (err) {
35784
+ throw imapFail("inbox", err);
35785
+ }
35786
+ try {
35787
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35788
+ const searchResp = await imapCommand(socket, "SEARCH ALL");
35789
+ const uids = parseSearchResponse(searchResp);
35790
+ if (uids.length === 0) return [];
35791
+ uids.reverse();
35792
+ const selected = uids.slice(offset, offset + limit);
35793
+ if (selected.length === 0) return [];
35794
+ const messages = [];
35795
+ for (const uid of selected) {
35796
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35797
+ messages.push(parseHeaderResponse(uid, fetchResp));
35798
+ }
35799
+ return messages;
35800
+ } catch (err) {
35801
+ throw imapFail("inbox", err);
35802
+ } finally {
35803
+ await this.imapDisconnect(socket);
35804
+ }
35805
+ }
35806
+ /**
35807
+ * Read a single message by sequence number or UID.
35808
+ */
35809
+ async read(uid, folder = "INBOX") {
35810
+ let socket;
35811
+ try {
35812
+ socket = await this.imapConnect();
35813
+ } catch (err) {
35814
+ throw imapFail("read", err);
35815
+ }
35816
+ try {
35817
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35818
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY[])`);
35819
+ if (!/\{\d+\}/.test(fetchResp)) {
35820
+ return emptyFullMessage(uid);
35821
+ }
35822
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35823
+ return parseFullMessage(uid, fetchResp);
35824
+ } catch (err) {
35825
+ throw imapFail("read", err);
35826
+ } finally {
35827
+ await this.imapDisconnect(socket);
35828
+ }
35829
+ }
35830
+ /**
35831
+ * Search messages using IMAP search criteria.
35832
+ */
35833
+ async search(folder = "INBOX", subject, sender, since, before, unseenOnly = false, limit = 50) {
35834
+ const criteria = ["ALL"];
35835
+ if (subject) criteria.push(`SUBJECT "${subject}"`);
35836
+ if (sender) criteria.push(`FROM "${sender}"`);
35837
+ if (since) criteria.push(`SINCE ${since}`);
35838
+ if (before) criteria.push(`BEFORE ${before}`);
35839
+ if (unseenOnly) criteria.push("UNSEEN");
35840
+ const query = criteria.join(" ");
35841
+ let socket;
35842
+ try {
35843
+ socket = await this.imapConnect();
35844
+ } catch (err) {
35845
+ throw imapFail("search", err);
35846
+ }
35847
+ try {
35848
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35849
+ const searchResp = await imapCommand(socket, `SEARCH ${query}`);
35850
+ const uids = parseSearchResponse(searchResp);
35851
+ if (uids.length === 0) return [];
35852
+ uids.reverse();
35853
+ const messages = [];
35854
+ for (const uid of uids.slice(0, limit)) {
35855
+ const fetchResp = await imapCommand(socket, `FETCH ${uid} (FLAGS BODY.PEEK[HEADER.FIELDS (FROM TO SUBJECT DATE)])`);
35856
+ messages.push(parseHeaderResponse(uid, fetchResp));
35857
+ }
35858
+ return messages;
35859
+ } catch (err) {
35860
+ throw imapFail("search", err);
35861
+ } finally {
35862
+ await this.imapDisconnect(socket);
35863
+ }
35864
+ }
35865
+ /**
35866
+ * Delete a message by UID.
35867
+ */
35868
+ async deleteMessage(uid, folder = "INBOX") {
35869
+ const socket = await this.imapConnect();
35870
+ try {
35871
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35872
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Deleted)`);
35873
+ await imapCommand(socket, "EXPUNGE");
35874
+ } finally {
35875
+ await this.imapDisconnect(socket);
35876
+ }
35877
+ }
35878
+ /**
35879
+ * Mark a message as read.
35880
+ */
35881
+ async markRead(uid, folder = "INBOX") {
35882
+ const socket = await this.imapConnect();
35883
+ try {
35884
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35885
+ await imapCommand(socket, `STORE ${uid} +FLAGS (\\Seen)`);
35886
+ } finally {
35887
+ await this.imapDisconnect(socket);
35888
+ }
35889
+ }
35890
+ /**
35891
+ * Count unseen messages in a folder.
35892
+ */
35893
+ async unread(folder = "INBOX") {
35894
+ let socket;
35895
+ try {
35896
+ socket = await this.imapConnect();
35897
+ } catch (err) {
35898
+ throw imapFail("unread", err);
35899
+ }
35900
+ try {
35901
+ await imapCommand(socket, `SELECT ${imapQuote(folder)}`);
35902
+ const searchResp = await imapCommand(socket, "SEARCH UNSEEN");
35903
+ return parseSearchResponse(searchResp).length;
35904
+ } catch (err) {
35905
+ throw imapFail("unread", err);
35906
+ } finally {
35907
+ await this.imapDisconnect(socket);
35908
+ }
35909
+ }
35910
+ /**
35911
+ * List available IMAP folders/mailboxes.
35912
+ */
35913
+ async folders() {
35914
+ let socket;
35915
+ try {
35916
+ socket = await this.imapConnect();
35917
+ } catch (err) {
35918
+ throw imapFail("folders", err);
35919
+ }
35920
+ try {
35921
+ const resp = await imapCommand(socket, 'LIST "" "*"');
35922
+ const result = [];
35923
+ for (const line of resp.split("\r\n")) {
35924
+ const m = line.match(/\* LIST \([^)]*\) "[^"]*" "?([^"\r\n]+)"?/i);
35925
+ if (m) result.push(m[1]);
35926
+ }
35927
+ return result;
35928
+ } catch (err) {
35929
+ throw imapFail("folders", err);
35930
+ } finally {
35931
+ await this.imapDisconnect(socket);
35932
+ }
35933
+ }
35934
+ /**
35935
+ * Test IMAP connectivity without reading.
35936
+ */
35937
+ async testImapConnection() {
35938
+ try {
35939
+ const socket = await this.imapConnect();
35940
+ await this.imapDisconnect(socket);
35941
+ return { success: true, message: `Connected to ${this.imapHost}:${this.imapPort}` };
35942
+ } catch (err) {
35943
+ const errMsg = err instanceof Error ? err.message : String(err);
35944
+ return { success: false, message: `IMAP connection failed: ${errMsg}` };
35945
+ }
35946
+ }
35947
+ };
35948
+ imapTagCounter = 0;
35949
+ }
35950
+ });
35951
+
35661
35952
  // ../core/src/wsdl.ts
35662
35953
  function escapeXml(value) {
35663
35954
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");