@rubytech/create-maxy-code 0.1.477 → 0.1.479

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 (204) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1704-data-portal-standing-audit.md +673 -0
  3. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1789-reseat-channel-row-fork.md +1244 -0
  4. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1818-loop-gate-app-routes.md +462 -0
  5. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1819-top-level-label-allowlist.md +321 -0
  6. package/payload/platform/docs/superpowers/plans/2026-07-20-task-1831-data-portal-class-derived-allowlist.md +2177 -0
  7. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1704-data-portal-standing-audit-design.md +177 -0
  8. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1789-reseat-channel-row-fork-design.md +201 -0
  9. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1818-loop-gate-app-routes-design.md +217 -0
  10. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1819-top-level-label-allowlist-design.md +80 -0
  11. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1820-cpu-triage-admin-tools-design.md +97 -0
  12. package/payload/platform/docs/superpowers/specs/2026-07-20-task-1831-data-portal-class-derived-allowlist-design.md +163 -0
  13. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  14. package/payload/platform/lib/graph-style/dist/index.js +22 -0
  15. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  16. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +6 -0
  17. package/payload/platform/lib/graph-style/src/index.ts +20 -0
  18. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +44 -0
  19. package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
  20. package/payload/platform/lib/storage-broker/dist/audit.d.ts +56 -0
  21. package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
  22. package/payload/platform/lib/storage-broker/dist/audit.js +34 -0
  23. package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
  24. package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +70 -1
  25. package/payload/platform/lib/storage-broker/src/audit.ts +111 -0
  26. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  27. package/payload/platform/plugins/admin/PLUGIN.md +7 -1
  28. package/payload/platform/plugins/admin/mcp/dist/index.js +72 -0
  29. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  30. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts +94 -0
  31. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.d.ts.map +1 -0
  32. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js +229 -0
  33. package/payload/platform/plugins/admin/mcp/dist/tools/cpu-triage.js.map +1 -0
  34. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +14 -2
  35. package/payload/platform/plugins/cloudflare/PLUGIN.md +26 -1
  36. package/payload/platform/plugins/cloudflare/bin/d1-http.mjs +80 -0
  37. package/payload/platform/plugins/cloudflare/bin/portal-enrol.mjs +17 -2
  38. package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +357 -0
  39. package/payload/platform/plugins/cloudflare/bin/schema-exposed-dirs.mjs +103 -0
  40. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +2 -2
  41. package/payload/platform/plugins/cloudflare/mcp/__tests__/d1-http.test.ts +96 -0
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +120 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-enrol.test.ts +59 -4
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +176 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-state-contract.test.ts +75 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +131 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-sign.test.ts +68 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/schema-exposed-dirs.test.ts +88 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +39 -0
  50. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +68 -1
  51. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +30 -1
  52. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/portal-sign.mjs +87 -0
  53. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +8 -4
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +10 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +107 -1
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +82 -2
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +9 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.css +56 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +98 -2
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +12 -0
  61. package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
  62. package/payload/platform/plugins/docs/references/cloudflare.md +12 -0
  63. package/payload/platform/plugins/email/PLUGIN.md +2 -1
  64. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts +2 -0
  65. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.d.ts.map +1 -0
  66. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js +92 -0
  67. package/payload/platform/plugins/email/mcp/dist/__tests__/confirm-sent-copy.test.js.map +1 -0
  68. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js +112 -12
  69. package/payload/platform/plugins/email/mcp/dist/__tests__/email-draft-send.test.js.map +1 -1
  70. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts +2 -0
  71. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.d.ts.map +1 -0
  72. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js +54 -0
  73. package/payload/platform/plugins/email/mcp/dist/__tests__/email-send-sent-copy.test.js.map +1 -0
  74. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js +170 -1
  75. package/payload/platform/plugins/email/mcp/dist/__tests__/imap-drafts.test.js.map +1 -1
  76. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts +2 -0
  77. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js +86 -0
  79. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-note.test.js.map +1 -0
  80. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts +2 -0
  81. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js +164 -0
  83. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-copy-sweep.test.js.map +1 -0
  84. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts +2 -0
  85. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.d.ts.map +1 -0
  86. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js +82 -0
  87. package/payload/platform/plugins/email/mcp/dist/__tests__/sent-ledger.test.js.map +1 -0
  88. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +95 -0
  89. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  90. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +223 -0
  91. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  92. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts +18 -0
  93. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.d.ts.map +1 -0
  94. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js +59 -0
  95. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-note.js.map +1 -0
  96. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts +48 -0
  97. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.d.ts.map +1 -0
  98. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js +79 -0
  99. package/payload/platform/plugins/email/mcp/dist/lib/sent-copy-sweep.js.map +1 -0
  100. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts +44 -0
  101. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.d.ts.map +1 -0
  102. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js +63 -0
  103. package/payload/platform/plugins/email/mcp/dist/lib/sent-ledger.js.map +1 -0
  104. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts +2 -0
  105. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.d.ts.map +1 -0
  106. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js +55 -0
  107. package/payload/platform/plugins/email/mcp/dist/scripts/sent-copy-sweep.js.map +1 -0
  108. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.d.ts.map +1 -1
  109. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js +64 -13
  110. package/payload/platform/plugins/email/mcp/dist/tools/email-draft-send.js.map +1 -1
  111. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  112. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +11 -1
  113. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  114. package/payload/platform/plugins/email/mcp/dist/tools/email-send.d.ts.map +1 -1
  115. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js +16 -3
  116. package/payload/platform/plugins/email/mcp/dist/tools/email-send.js.map +1 -1
  117. package/payload/platform/plugins/email/references/email-reference.md +36 -0
  118. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts +2 -0
  119. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.d.ts.map +1 -0
  120. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js +66 -0
  121. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/portal-index-audit.test.js.map +1 -0
  122. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts +2 -0
  123. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.d.ts.map +1 -0
  124. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js +18 -0
  125. package/payload/platform/plugins/scheduling/mcp/dist/__tests__/sent-sweep-throttle.test.js.map +1 -0
  126. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts +41 -0
  127. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.d.ts.map +1 -0
  128. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js +75 -0
  129. package/payload/platform/plugins/scheduling/mcp/dist/lib/portal-index-audit.js.map +1 -0
  130. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts +13 -0
  131. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.d.ts.map +1 -0
  132. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js +10 -0
  133. package/payload/platform/plugins/scheduling/mcp/dist/lib/sent-sweep-throttle.js.map +1 -0
  134. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +175 -0
  135. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  136. package/payload/platform/scripts/cpu-triage-run.sh +117 -0
  137. package/payload/platform/scripts/cpu-triage.sh +23 -4
  138. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts +40 -0
  139. package/payload/platform/services/claude-session-manager/dist/activity-range.d.ts.map +1 -0
  140. package/payload/platform/services/claude-session-manager/dist/activity-range.js +68 -0
  141. package/payload/platform/services/claude-session-manager/dist/activity-range.js.map +1 -0
  142. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts +40 -8
  143. package/payload/platform/services/claude-session-manager/dist/activity-rows.d.ts.map +1 -1
  144. package/payload/platform/services/claude-session-manager/dist/activity-rows.js +74 -7
  145. package/payload/platform/services/claude-session-manager/dist/activity-rows.js.map +1 -1
  146. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  147. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +2 -0
  148. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  149. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  150. package/payload/platform/services/claude-session-manager/dist/http-server.js +9 -0
  151. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  152. package/payload/server/{chunk-56WJMBQQ.js → chunk-3XLLTG6R.js} +14 -0
  153. package/payload/server/{chunk-JECAP3Z2.js → chunk-XGNRSM57.js} +3 -0
  154. package/payload/server/maxy-edge.js +1 -1
  155. package/payload/server/public/activity.html +8 -6
  156. package/payload/server/public/assets/{AdminLoginScreens-mEKP4pDi.js → AdminLoginScreens-DkxQZnvB.js} +1 -1
  157. package/payload/server/public/assets/AdminShell-Cv_nIFGe.js +2 -0
  158. package/payload/server/public/assets/Checkbox-CsTWK8mm.js +1 -0
  159. package/payload/server/public/assets/activity-BbnTiVBT.js +1 -0
  160. package/payload/server/public/assets/{admin-BVhgfgs1.js → admin-BZY3i_Ff.js} +1 -1
  161. package/payload/server/public/assets/{browser-D-HMmHcM.js → browser-BbIiSFKr.js} +1 -1
  162. package/payload/server/public/assets/{calendar-tGWvFyFn.js → calendar-BvxeTHvQ.js} +1 -1
  163. package/payload/server/public/assets/chat-DUKxufRk.js +1 -0
  164. package/payload/server/public/assets/chevron-left-BtfJD6LL.js +1 -0
  165. package/payload/server/public/assets/data-hl1wa-Ax.js +1 -0
  166. package/payload/server/public/assets/{graph-zEw610xK.js → graph-CDSGRoeb.js} +1 -1
  167. package/payload/server/public/assets/graph-labels-C5qLwnWA.js +1 -0
  168. package/payload/server/public/assets/{maximize-2-BlTjXT_Y.js → maximize-2-CAzH8dED.js} +1 -1
  169. package/payload/server/public/assets/{operator-C7oIw2PG.js → operator-BK7r8DAB.js} +1 -1
  170. package/payload/server/public/assets/{page-CTP7OFZa.js → page-B5YngOrK.js} +1 -1
  171. package/payload/server/public/assets/{page-DL6Zsdvk.js → page-BsCS0Dmc.js} +1 -1
  172. package/payload/server/public/assets/{public-r1A9dqR_.js → public-C1Pz-IBs.js} +1 -1
  173. package/payload/server/public/assets/{rotate-ccw-CzkkKx4-.js → rotate-ccw-UDln4Tou.js} +1 -1
  174. package/payload/server/public/assets/tasks-B-cLsn3p.js +1 -0
  175. package/payload/server/public/assets/{time-entry-format-j669DgXj.js → time-entry-format-DPOBIE18.js} +1 -1
  176. package/payload/server/public/assets/{triangle-alert-BPgCgIef.js → triangle-alert-Dz-CZKo5.js} +1 -1
  177. package/payload/server/public/assets/{useCopyFeedback-BmLa2aes.js → useCopyFeedback-wtUHGohk.js} +1 -1
  178. package/payload/server/public/assets/{useSelectionMode-DnnM7A8-.js → useSelectionMode-sph7AQT5.js} +1 -1
  179. package/payload/server/public/assets/useSubAccountSwitcher-BRIluIKf.css +1 -0
  180. package/payload/server/public/assets/{useVoiceRecorder-u2dwZfen.js → useVoiceRecorder-DN4jYQ0A.js} +1 -1
  181. package/payload/server/public/brand-defaults.css +1 -0
  182. package/payload/server/public/browser.html +5 -5
  183. package/payload/server/public/calendar.html +6 -6
  184. package/payload/server/public/chat.html +12 -12
  185. package/payload/server/public/data.html +11 -11
  186. package/payload/server/public/graph.html +11 -11
  187. package/payload/server/public/index.html +14 -14
  188. package/payload/server/public/operator.html +14 -14
  189. package/payload/server/public/privacy.html +1 -0
  190. package/payload/server/public/public.html +12 -12
  191. package/payload/server/public/tasks.html +6 -5
  192. package/payload/server/public/vnc-popout.html +1 -0
  193. package/payload/server/server.js +894 -384
  194. package/payload/server/{src-JYPKMWJR.js → src-XQ63FPRE.js} +3 -1
  195. package/payload/server/public/assets/AdminShell-BwSaZ88d.js +0 -2
  196. package/payload/server/public/assets/Checkbox-CN4uQ80w.js +0 -1
  197. package/payload/server/public/assets/activity-CIQ8ozNZ.js +0 -1
  198. package/payload/server/public/assets/chat-DkttVxAz.js +0 -1
  199. package/payload/server/public/assets/chevron-left-CZ4ez9G5.js +0 -1
  200. package/payload/server/public/assets/data-P-mcmnNi.js +0 -1
  201. package/payload/server/public/assets/graph-labels-qnBleOE6.js +0 -1
  202. package/payload/server/public/assets/tasks-fIJwYFWG.js +0 -1
  203. package/payload/server/public/assets/useSubAccountSwitcher-C_E8h07P.css +0 -1
  204. /package/payload/server/public/assets/{useSubAccountSwitcher-CFa6ZAT1.js → useSubAccountSwitcher-v8LyBzZN.js} +0 -0
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Standing sent-copy reconciliation, one pass over every local account.
3
+ *
4
+ * Spawned by the scheduling heartbeat on an hourly throttle rather than
5
+ * imported by it: the scheduling plugin builds with its own rootDir and does
6
+ * not carry imapflow, so the IMAP work stays on this side of the boundary
7
+ * (Task 1822).
8
+ *
9
+ * Exit code is 0 whenever the sweep ran, including when it found a shortfall —
10
+ * a shortfall is a reported finding, not a failure of the check. A non-zero
11
+ * exit means the sweep itself could not run, which is what keeps "failing" and
12
+ * "did not run" distinguishable (Task 1817).
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { enumerateValidAccountIds } from "../../../../../lib/account-enumeration/dist/index.js";
17
+ import { listConfigs, readPassword } from "../lib/credentials.js";
18
+ import { readSentLedger, pruneSentLedger } from "../lib/sent-ledger.js";
19
+ import { findMissingInSent } from "../lib/imap.js";
20
+ import { sweepAccount, formatSweepLine } from "../lib/sent-copy-sweep.js";
21
+ async function main() {
22
+ const platformRoot = process.env.PLATFORM_ROOT;
23
+ if (!platformRoot)
24
+ throw new Error("PLATFORM_ROOT is not set");
25
+ const accountsDir = resolve(platformRoot, "..", "data/accounts");
26
+ const accountIds = existsSync(accountsDir) ? enumerateValidAccountIds(accountsDir) : [];
27
+ const now = Date.now();
28
+ let mailboxes = 0;
29
+ for (const accountId of accountIds) {
30
+ const results = await sweepAccount(accountId, now, {
31
+ listConfigs,
32
+ // The password lives in the on-disk store, so this needs no second graph
33
+ // round-trip on top of the one listConfigs already made.
34
+ loadPassword: async (email) => readPassword(email),
35
+ readLedger: readSentLedger,
36
+ findMissingInSent,
37
+ });
38
+ for (const r of results) {
39
+ console.error(formatSweepLine(r));
40
+ mailboxes++;
41
+ }
42
+ }
43
+ // Trim here rather than on the send path: rewriting the ledger is the one
44
+ // operation that can lose a concurrent append, so it runs once an hour in
45
+ // this single process instead of on every send.
46
+ pruneSentLedger();
47
+ console.error(`[email-sent-sweep] done: ${accountIds.length} accounts, ${mailboxes} mailboxes checked`);
48
+ }
49
+ main()
50
+ .then(() => process.exit(0))
51
+ .catch((err) => {
52
+ console.error(`[email-sent-sweep] FAILED ${err instanceof Error ? err.message : String(err)}`);
53
+ process.exit(1);
54
+ });
55
+ //# sourceMappingURL=sent-copy-sweep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sent-copy-sweep.js","sourceRoot":"","sources":["../../src/scripts/sent-copy-sweep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,wBAAwB,EAAE,MAAM,sDAAsD,CAAC;AAChG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAE1E,KAAK,UAAU,IAAI;IACjB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;IAC/C,IAAI,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAE/D,MAAM,WAAW,GAAG,OAAO,CAAC,YAAY,EAAE,IAAI,EAAE,eAAe,CAAC,CAAC;IACjE,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,SAAS,EAAE,GAAG,EAAE;YACjD,WAAW;YACX,yEAAyE;YACzE,yDAAyD;YACzD,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC;YAClD,UAAU,EAAE,cAAc;YAC1B,iBAAiB;SAClB,CAAC,CAAC;QACH,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;YAClC,SAAS,EAAE,CAAC;QACd,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,0EAA0E;IAC1E,gDAAgD;IAChD,eAAe,EAAE,CAAC;IAElB,OAAO,CAAC,KAAK,CACX,4BAA4B,UAAU,CAAC,MAAM,cAAc,SAAS,oBAAoB,CACzF,CAAC;AACJ,CAAC;AAED,IAAI,EAAE;KACH,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;KAC3B,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACb,OAAO,CAAC,KAAK,CAAC,6BAA6B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/F,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"email-draft-send.d.ts","sourceRoot":"","sources":["../../src/tools/email-draft-send.ts"],"names":[],"mappings":"AAIA,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAqD7E"}
1
+ {"version":3,"file":"email-draft-send.d.ts","sourceRoot":"","sources":["../../src/tools/email-draft-send.ts"],"names":[],"mappings":"AAOA,UAAU,eAAe;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAiG7E"}
@@ -1,6 +1,7 @@
1
1
  import { loadCredentials } from "../lib/credentials.js";
2
2
  import { sendRaw } from "../lib/smtp.js";
3
- import { fetchDraftSource, deleteDraft } from "../lib/imap.js";
3
+ import { fetchDraftSource, fileDraftToSent } from "../lib/imap.js";
4
+ import { appendSentLedger } from "../lib/sent-ledger.js";
4
5
  export async function emailDraftSend(params) {
5
6
  const result = await loadCredentials(params.accountId, params.mailbox);
6
7
  if ("error" in result)
@@ -32,21 +33,71 @@ export async function emailDraftSend(params) {
32
33
  }
33
34
  throw err;
34
35
  }
35
- // Only after SMTP accepts: remove the draft. A failure here means the message
36
- // already went out but the draft remains — report a qualified success rather
37
- // than letting the removal error masquerade as a send failure (which would
38
- // invite a duplicate re-send).
39
- let deleted = false;
36
+ // Only after SMTP accepts: relocate the draft into \Sent. A failure here means
37
+ // the message already went out, so report a qualified success rather than
38
+ // letting the filing error masquerade as a send failure (which would invite a
39
+ // duplicate re-send). The draft is never expunged — on every failure path it
40
+ // survives where it is, and the note below says so (Task 1821).
41
+ let filing = null;
42
+ let filingError = null;
40
43
  try {
41
- ({ deleted } = await deleteDraft(config, password, params.uid));
44
+ filing = await fileDraftToSent(config, password, params.uid, messageId);
42
45
  }
43
- catch {
44
- deleted = false;
46
+ catch (err) {
47
+ filingError = err instanceof Error ? err.message : String(err);
45
48
  }
46
49
  const idNote = messageId ? ` (Message-ID: ${messageId})` : "";
47
- const cleanupNote = deleted
48
- ? `removed from ${draft.folder}`
49
- : `sent, but draft UID ${params.uid} could not be confirmed removed from ${draft.folder}`;
50
- return `Draft UID ${params.uid} sent to ${recipients.join(", ")}${idNote}; ${cleanupNote}.`;
50
+ let filedNote;
51
+ if (filing === null) {
52
+ filedNote = `the message went out but filing the copy failed, so it is still in ${draft.folder} as UID ${params.uid} (${filingError})`;
53
+ }
54
+ else if (filing.filed) {
55
+ filedNote = `copy confirmed in ${filing.sentFolder} (UID ${filing.sentUid})`;
56
+ }
57
+ else if (filing.reason === "no-sent-folder") {
58
+ filedNote = `no copy was filed — this server advertises no \\Sent folder, so the copy is still in ${filing.draftFolder} as UID ${params.uid}`;
59
+ }
60
+ else if (filing.reason === "move-did-nothing") {
61
+ filedNote = `no copy was filed — the server moved nothing, so the copy is still in ${filing.draftFolder} as UID ${params.uid}`;
62
+ }
63
+ else if (filing.reason === "moved-but-not-in-sent") {
64
+ filedNote = `the message was moved out of ${filing.draftFolder} but no copy was found in the Sent folder`;
65
+ }
66
+ else {
67
+ filedNote = `no copy was filed — draft UID ${params.uid} was no longer in ${filing.draftFolder}`;
68
+ }
69
+ // No Message-ID means there is no key to reconcile against later, so the row
70
+ // would be uncountable. Skip it rather than writing an empty key.
71
+ if (messageId) {
72
+ // Each filing outcome keeps its own ledger value. Recording a thrown
73
+ // filing or a server with no \Sent folder as "not-in-sent" would assert
74
+ // the folder was searched and found empty — the same conflation this task
75
+ // removes from the tool result, relocated into the ledger the sweep reads.
76
+ let copy;
77
+ if (filing === null)
78
+ copy = "check-failed";
79
+ else if (filing.filed)
80
+ copy = "found";
81
+ else if (filing.reason === "no-sent-folder")
82
+ copy = "no-sent-folder";
83
+ else
84
+ copy = "not-in-sent";
85
+ try {
86
+ appendSentLedger({
87
+ at: new Date().toISOString(),
88
+ accountId: params.accountId,
89
+ mailbox: config.email,
90
+ messageId,
91
+ copy,
92
+ ...(filing !== null && filing.filed
93
+ ? { sentFolder: filing.sentFolder, sentUid: filing.sentUid }
94
+ : {}),
95
+ });
96
+ }
97
+ catch {
98
+ // Observability only — never turn a completed send into an error.
99
+ }
100
+ }
101
+ return `Draft UID ${params.uid} sent to ${recipients.join(", ")}${idNote}; ${filedNote}.`;
51
102
  }
52
103
  //# sourceMappingURL=email-draft-send.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"email-draft-send.js","sourceRoot":"","sources":["../../src/tools/email-draft-send.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAS/D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAuB;IAC1D,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACvE,IAAI,OAAO,IAAI,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;IAEhD,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACnE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,CAAC,GAAG,wBAAwB,CAAC,CAAC;IAC3E,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,oCAAoC;IACpC,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,WAAW,EAAE,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACrG,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,aAAa,MAAM,CAAC,GAAG,sEAAsE,CAC9F,CAAC;IACJ,CAAC;IAED,IAAI,SAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE;YAC7D,IAAI,EAAE,MAAM,CAAC,YAAY;YACzB,EAAE,EAAE,UAAU;SACf,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,yEAAyE;QACzE,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,uBAAuB,MAAM,CAAC,YAAY,sBAAsB,MAAM,CAAC,KAAK,iGAAiG,CACpL,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,8EAA8E;IAC9E,6EAA6E;IAC7E,2EAA2E;IAC3E,+BAA+B;IAC/B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,CAAC;QACH,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,KAAK,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,iBAAiB,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9D,MAAM,WAAW,GAAG,OAAO;QACzB,CAAC,CAAC,gBAAgB,KAAK,CAAC,MAAM,EAAE;QAChC,CAAC,CAAC,uBAAuB,MAAM,CAAC,GAAG,wCAAwC,KAAK,CAAC,MAAM,EAAE,CAAC;IAC5F,OAAO,aAAa,MAAM,CAAC,GAAG,YAAY,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,KAAK,WAAW,GAAG,CAAC;AAC9F,CAAC"}
1
+ {"version":3,"file":"email-draft-send.js","sourceRoot":"","sources":["../../src/tools/email-draft-send.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"email-reply.d.ts","sourceRoot":"","sources":["../../src/tools/email-reply.ts"],"names":[],"mappings":"AAKA,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CA2ErE"}
1
+ {"version":3,"file":"email-reply.d.ts","sourceRoot":"","sources":["../../src/tools/email-reply.ts"],"names":[],"mappings":"AAMA,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAoFrE"}
@@ -2,6 +2,7 @@ import { loadCredentials } from "../lib/credentials.js";
2
2
  import { sendEmail, formatCopyNote, replySubject } from "../lib/smtp.js";
3
3
  import { fetchEnvelopeByMessageId } from "../lib/imap.js";
4
4
  import { resolveOutboundAttachments, formatAttachNote, accountDirFor } from "../lib/attachment-resolve.js";
5
+ import { verifyAndRecordSentCopy } from "../lib/sent-copy-note.js";
5
6
  export async function emailReply(params) {
6
7
  const result = await loadCredentials(params.accountId, params.mailbox);
7
8
  if ("error" in result)
@@ -66,6 +67,15 @@ export async function emailReply(params) {
66
67
  throw smtpErr;
67
68
  }
68
69
  const replyType = params.replyAll ? "Reply-all" : "Reply";
69
- return `${replyType} sent to ${to}${formatCopyNote(params.cc, params.bcc)}${formatAttachNote(attachments)} (Message-ID: ${sentMessageId})`;
70
+ // SMTP acceptance is not evidence the provider filed a copy. Read \Sent back
71
+ // and state what is actually there, as a fact separate from the send itself
72
+ // (Task 1822).
73
+ const sentCopyNote = await verifyAndRecordSentCopy({
74
+ config,
75
+ password,
76
+ messageId: sentMessageId,
77
+ accountId: params.accountId,
78
+ });
79
+ return `${replyType} sent to ${to}${formatCopyNote(params.cc, params.bcc)}${formatAttachNote(attachments)} (Message-ID: ${sentMessageId}); ${sentCopyNote}.`;
70
80
  }
71
81
  //# sourceMappingURL=email-reply.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"email-reply.js","sourceRoot":"","sources":["../../src/tools/email-reply.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"email-reply.js","sourceRoot":"","sources":["../../src/tools/email-reply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC3G,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAcnE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAmB;IAClD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACvE,IAAI,OAAO,IAAI,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;IAEhD,0EAA0E;IAC1E,6EAA6E;IAC7E,kEAAkE;IAClE,8CAA8C;IAC9C,MAAM,WAAW,GAAG,0BAA0B,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAEpG,oEAAoE;IACpE,yCAAyC;IACzC,MAAM,QAAQ,GAAG,MAAM,wBAAwB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACpF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,kCAAkC,MAAM,CAAC,SAAS,sGAAsG,CACzJ,CAAC;IACJ,CAAC;IAED,MAAM,iBAAiB,GAAG,QAAQ,CAAC,SAAS,CAAC;IAC7C,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC;IAC1C,MAAM,UAAU,GAAG,QAAQ,CAAC,WAAW,CAAC;IACxC,MAAM,UAAU,GAAG,QAAQ,CAAC,WAAW,CAAC;IACxC,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,IAAI,cAAc,CAAC;IAE3D,qCAAqC;IACrC,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC;IACpD,IAAI,YAAY,CAAC,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IAED,uBAAuB;IACvB,IAAI,EAAU,CAAC;IACf,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,uFAAuF;QACvF,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,GAAG,UAAU,EAAE,GAAG,UAAU,CAAC,CAAC;QACzD,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,SAAS,CAAC,CAAC;QAChF,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,EAAE,GAAG,YAAY,CAAC;IACpB,CAAC;IAED,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAC;IACjG,CAAC;IAED,wCAAwC;IACxC,IAAI,aAAqB,CAAC;IAC1B,IAAI,CAAC;QACH,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE;YAChE,EAAE;YACF,OAAO,EAAE,YAAY,CAAC,eAAe,CAAC;YACtC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,SAAS,EAAE,iBAAiB;YAC5B,UAAU,EAAE,iBAAiB;YAC7B,WAAW;SACZ,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,OAAO,EAAE,CAAC;QACjB,MAAM,OAAO,GAAG,OAAO,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC7E,8CAA8C;QAC9C,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,GAAG,OAAO,uBAAuB,MAAM,CAAC,YAAY,sBAAsB,MAAM,CAAC,KAAK,iGAAiG,CACxL,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC;IAC1D,6EAA6E;IAC7E,4EAA4E;IAC5E,eAAe;IACf,MAAM,YAAY,GAAG,MAAM,uBAAuB,CAAC;QACjD,MAAM;QACN,QAAQ;QACR,SAAS,EAAE,aAAa;QACxB,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IACH,OAAO,GAAG,SAAS,YAAY,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,WAAW,CAAC,iBAAiB,aAAa,MAAM,YAAY,GAAG,CAAC;AAC/J,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"email-send.d.ts","sourceRoot":"","sources":["../../src/tools/email-send.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCnE"}
1
+ {"version":3,"file":"email-send.d.ts","sourceRoot":"","sources":["../../src/tools/email-send.ts"],"names":[],"mappings":"AAKA,UAAU,UAAU;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+CnE"}
@@ -1,6 +1,7 @@
1
1
  import { loadCredentials } from "../lib/credentials.js";
2
2
  import { sendEmail, formatCopyNote } from "../lib/smtp.js";
3
3
  import { resolveOutboundAttachments, formatAttachNote, accountDirFor } from "../lib/attachment-resolve.js";
4
+ import { verifyAndRecordSentCopy } from "../lib/sent-copy-note.js";
4
5
  export async function emailSend(params) {
5
6
  const result = await loadCredentials(params.accountId, params.mailbox);
6
7
  if ("error" in result)
@@ -13,8 +14,11 @@ export async function emailSend(params) {
13
14
  // just loaded), not an ambient ACCOUNT_DIR. Falls back to the resolver's env
14
15
  // path when undefined.
15
16
  const attachments = resolveOutboundAttachments(accountDirFor(params.accountId), params.attachments);
17
+ // The catch wraps the SMTP call alone. Once SMTP has accepted, the mail is
18
+ // out, and nothing after this point may surface as a send failure.
19
+ let messageId;
16
20
  try {
17
- const { messageId } = await sendEmail(config, password, {
21
+ ({ messageId } = await sendEmail(config, password, {
18
22
  to: params.to,
19
23
  subject: params.subject,
20
24
  body: params.body,
@@ -22,8 +26,7 @@ export async function emailSend(params) {
22
26
  cc: params.cc,
23
27
  bcc: params.bcc,
24
28
  attachments,
25
- });
26
- return `Email sent to ${params.to}${formatCopyNote(params.cc, params.bcc)}${formatAttachNote(attachments)} (Message-ID: ${messageId})`;
29
+ }));
27
30
  }
28
31
  catch (err) {
29
32
  const msg = err instanceof Error ? err.message : String(err);
@@ -32,5 +35,15 @@ export async function emailSend(params) {
32
35
  }
33
36
  throw err;
34
37
  }
38
+ // SMTP acceptance is not evidence the provider filed a copy. Read \Sent back
39
+ // and state what is actually there, as a fact separate from the send itself
40
+ // (Task 1822).
41
+ const sentCopyNote = await verifyAndRecordSentCopy({
42
+ config,
43
+ password,
44
+ messageId,
45
+ accountId: params.accountId,
46
+ });
47
+ return `Email sent to ${params.to}${formatCopyNote(params.cc, params.bcc)}${formatAttachNote(attachments)} (Message-ID: ${messageId}); ${sentCopyNote}.`;
35
48
  }
36
49
  //# sourceMappingURL=email-send.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"email-send.js","sourceRoot":"","sources":["../../src/tools/email-send.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAc3G,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAkB;IAChD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACvE,IAAI,OAAO,IAAI,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;IAEhD,4EAA4E;IAC5E,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,uBAAuB;IACvB,MAAM,WAAW,GAAG,0BAA0B,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAEpG,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE;YACtD,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,WAAW;SACZ,CAAC,CAAC;QACH,OAAO,iBAAiB,MAAM,CAAC,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,WAAW,CAAC,iBAAiB,SAAS,GAAG,CAAC;IACzI,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,uBAAuB,MAAM,CAAC,YAAY,sBAAsB,MAAM,CAAC,KAAK,iGAAiG,CACpL,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"email-send.js","sourceRoot":"","sources":["../../src/tools/email-send.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC3G,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAcnE,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAkB;IAChD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACvE,IAAI,OAAO,IAAI,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAErD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC;IAEhD,4EAA4E;IAC5E,0EAA0E;IAC1E,0EAA0E;IAC1E,2EAA2E;IAC3E,6EAA6E;IAC7E,uBAAuB;IACvB,MAAM,WAAW,GAAG,0BAA0B,CAAC,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IAEpG,2EAA2E;IAC3E,mEAAmE;IACnE,IAAI,SAAiB,CAAC;IACtB,IAAI,CAAC;QACH,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE;YACjD,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,WAAW;SACZ,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7D,IAAI,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACzC,MAAM,IAAI,KAAK,CACb,GAAG,GAAG,uBAAuB,MAAM,CAAC,YAAY,sBAAsB,MAAM,CAAC,KAAK,iGAAiG,CACpL,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,6EAA6E;IAC7E,4EAA4E;IAC5E,eAAe;IACf,MAAM,YAAY,GAAG,MAAM,uBAAuB,CAAC;QACjD,MAAM;QACN,QAAQ;QACR,SAAS;QACT,SAAS,EAAE,MAAM,CAAC,SAAS;KAC5B,CAAC,CAAC;IACH,OAAO,iBAAiB,MAAM,CAAC,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,WAAW,CAAC,iBAAiB,SAAS,MAAM,YAAY,GAAG,CAAC;AAC3J,CAAC"}
@@ -103,6 +103,42 @@ Use `email-reply` when responding to a received email. Use `email-send` for new
103
103
 
104
104
  Both `email-send` and `email-reply` accept optional `cc` and `bcc` fields. Each is an array of email addresses (one element or many); every address is validated individually. CC recipients appear in the message header and are visible to all recipients. BCC recipients receive the message but do not appear in any header — the other recipients cannot see them. Omitting both sends exactly as before, with no CC/BCC headers. The success message names the CC recipients so the operator can confirm the copy went out, and reports the BCC count without naming them, preserving the blind semantics.
105
105
 
106
+ ## The Sent copy is verified, not assumed
107
+
108
+ SMTP accepting a message is not evidence that the provider filed a copy anywhere. `email-send`, `email-reply` and `email-draft-send` therefore each read the mailbox's `\Sent` folder back after the send and report what is actually there, as a fact separate from the send itself. The search is by Message-ID, retried briefly because providers do not file the copy at the instant SMTP returns.
109
+
110
+ Four outcomes are reported, and they are deliberately distinct:
111
+
112
+ | Result clause | Means |
113
+ |---|---|
114
+ | `copy confirmed in <folder> (UID n)` | The copy was found by search. A folder and UID are only ever named when they were read back. |
115
+ | `no copy was found in <folder>` | The folder was searched and the message is not in it. |
116
+ | `no copy was filed — this server advertises no \Sent folder` | There is nowhere to file to. Not the same as a missing copy. |
117
+ | `the Sent folder could not be checked (<error>), so the copy is unconfirmed` | The check itself failed. Not evidence either way. |
118
+
119
+ A send whose copy cannot be located is still a successful send, and the result says both things plainly. Collapsing them into one word is what allowed a sent-copy claim to go unchallenged for four days when no copy was being filed at all.
120
+
121
+ Every send also appends one row to `config/email-sent-ledger.jsonl` recording the observed outcome. That ledger is the left-hand side of the hourly sweep below; it is written after the read-back, so it records what happened, not what was intended.
122
+
123
+ ## The hourly sent-copy sweep
124
+
125
+ Per-tool reporting only covers sends the agent was present for. A mailbox that silently stops filing copies emits nothing at all, so nothing surfaces until someone checks by hand.
126
+
127
+ The install heartbeat therefore spawns `email/mcp/dist/scripts/sent-copy-sweep.js` once an hour. Per account, per mailbox, it takes the Message-IDs this plugin sent in the trailing 24 hours and asks `\Sent` whether each one is actually there, then reports:
128
+
129
+ ```
130
+ [email-sent-sweep] op=reconcile account=<id> mailbox=<email> sends=N copies=M shortfall=K status=clean|shortfall|check-failed
131
+ ```
132
+
133
+ It reports on mailboxes with zero sends too, so a silent check is distinguishable from an absent one. A mailbox that could not be read is `check-failed`, never a shortfall — an unreadable mailbox and a mailbox losing copies are different findings.
134
+
135
+ Two properties of that design are load-bearing:
136
+
137
+ - **Presence is asked per Message-ID, never by comparing totals.** `\Sent` holds everything the mailbox owner sends from any client. A bulk count is dominated by messages this plugin never sent, so a mailbox filing none of our sends would still look healthy the moment its owner sends a few from their phone — which is exactly the incident this check exists to catch.
138
+ - **The answer comes from the folder, not from the ledger's own recorded outcome.** Re-reading is what makes the sweep independent evidence rather than a replay of what the tool already said, and it is the only way a copy that was filed and later removed shows up.
139
+
140
+ The sweep also trims the ledger past its 48-hour retention. That rewrite happens here, once an hour in a single process, rather than on the send path where it could lose a concurrent row.
141
+
106
142
  ## Attachments
107
143
 
108
144
  Both `email-send` and `email-reply` accept an optional `attachments` array of absolute file paths. Each path must point to a regular file inside the account directory and be 25 MB or smaller; the path is validated before the message is sent, so an invalid, missing, oversized, or out-of-account path fails the call with a clear error and no email dispatched. Validated files ride the message as real SMTP attachments, and the success message names each attached file.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=portal-index-audit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-index-audit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/portal-index-audit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { auditPortalIndex, formatPortalIndexAudit, STALE_AFTER_INTERVALS, } from "../lib/portal-index-audit.js";
3
+ const HOUR = 3_600_000;
4
+ const NOW = 1_800_000_000_000;
5
+ const fresh = (ms) => ({
6
+ "acc-a": { lastPushMs: NOW - ms, rows: 41, exposed: ["output", "quotes"] },
7
+ });
8
+ describe("auditPortalIndex", () => {
9
+ it("reports a recently pushed account as healthy", () => {
10
+ const r = auditPortalIndex(["acc-a"], fresh(HOUR - 1), NOW, HOUR);
11
+ expect(r.stale).toBe(0);
12
+ expect(r.neverRan).toBe(0);
13
+ expect(r.accounts[0].ageMin).toBe(59);
14
+ expect(r.accounts[0].rows).toBe(41);
15
+ });
16
+ it("tolerates one missed cycle, flags two", () => {
17
+ // One miss is ordinary (a slow push, a device asleep at the tick). The
18
+ // threshold has to be loose enough that firing means something.
19
+ expect(auditPortalIndex(["acc-a"], fresh(HOUR * 1.5), NOW, HOUR).stale).toBe(0);
20
+ expect(auditPortalIndex(["acc-a"], fresh(HOUR * STALE_AFTER_INTERVALS + 1), NOW, HOUR).stale).toBe(1);
21
+ });
22
+ // The signature of the failure this module exists for: the spawn never fires,
23
+ // so no state is ever written and no other line is ever emitted.
24
+ it("reports an enrolled account with no state as never-ran, not as stale", () => {
25
+ const r = auditPortalIndex(["acc-a"], {}, NOW, HOUR);
26
+ expect(r.neverRan).toBe(1);
27
+ expect(r.stale).toBe(0);
28
+ expect(r.accounts[0].ageMin).toBeNull();
29
+ });
30
+ it("ignores accounts that have no portal", () => {
31
+ const r = auditPortalIndex([], fresh(HOUR * 99), NOW, HOUR);
32
+ expect(r.accounts).toEqual([]);
33
+ expect(r.stale).toBe(0);
34
+ });
35
+ it("survives a malformed state entry rather than throwing", () => {
36
+ const bad = { "acc-a": { rows: 1, exposed: [] } };
37
+ expect(auditPortalIndex(["acc-a"], bad, NOW, HOUR).neverRan).toBe(1);
38
+ });
39
+ it("never reports a negative age when a clock moves backwards", () => {
40
+ const r = auditPortalIndex(["acc-a"], fresh(-HOUR), NOW, HOUR);
41
+ expect(r.accounts[0].ageMin).toBe(0);
42
+ expect(r.accounts[0].stale).toBe(false);
43
+ });
44
+ });
45
+ describe("formatPortalIndexAudit", () => {
46
+ it("emits a summary line even when everything is healthy", () => {
47
+ // An audit that goes quiet on success is indistinguishable from an audit
48
+ // that stopped running — the exact defect this module closes.
49
+ const lines = formatPortalIndexAudit(auditPortalIndex(["acc-a"], fresh(60_000), NOW, HOUR));
50
+ expect(lines).toHaveLength(1);
51
+ expect(lines[0]).toBe("[portal-index] op=audit accounts=1 stale=0 never-ran=0");
52
+ });
53
+ it("names a stale account with its age and exposed set", () => {
54
+ const lines = formatPortalIndexAudit(auditPortalIndex(["acc-a"], fresh(HOUR * 3), NOW, HOUR));
55
+ expect(lines[0]).toContain("account=acc-a");
56
+ expect(lines[0]).toContain("ageMin=180");
57
+ expect(lines[0]).toContain("exposed=output,quotes");
58
+ expect(lines[0]).toContain("result=stale");
59
+ });
60
+ it("distinguishes never-ran from stale in the line itself", () => {
61
+ const lines = formatPortalIndexAudit(auditPortalIndex(["acc-a"], {}, NOW, HOUR));
62
+ expect(lines[0]).toContain("result=never-ran");
63
+ expect(lines[0]).toContain("rows=0");
64
+ });
65
+ });
66
+ //# sourceMappingURL=portal-index-audit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-index-audit.test.js","sourceRoot":"","sources":["../../src/__tests__/portal-index-audit.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,GAEtB,MAAM,8BAA8B,CAAC;AAEtC,MAAM,IAAI,GAAG,SAAS,CAAC;AACvB,MAAM,GAAG,GAAG,iBAAiB,CAAC;AAE9B,MAAM,KAAK,GAAG,CAAC,EAAU,EAAoB,EAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,EAAE,UAAU,EAAE,GAAG,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE;CAC3E,CAAC,CAAC;AAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAClE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,qBAAqB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxG,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,iEAAiE;IACjE,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,gBAAgB,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAC5D,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAiC,CAAC;QACjF,MAAM,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,GAAG,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;QAC/D,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,KAAK,GAAG,sBAAsB,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAC5F,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,KAAK,GAAG,sBAAsB,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QAC9F,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;QAC5C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;QACpD,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,KAAK,GAAG,sBAAsB,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;QACjF,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QAC/C,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sent-sweep-throttle.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sent-sweep-throttle.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sent-sweep-throttle.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { shouldRunSentSweep } from "../lib/sent-sweep-throttle.js";
3
+ const HOUR = 60 * 60 * 1000;
4
+ describe("shouldRunSentSweep", () => {
5
+ it("runs when there is no prior state", () => {
6
+ expect(shouldRunSentSweep(undefined, 1_000_000, HOUR)).toBe(true);
7
+ });
8
+ it("does not run again inside the interval", () => {
9
+ expect(shouldRunSentSweep({ lastRunMs: 1_000_000 }, 1_000_000 + HOUR / 2, HOUR)).toBe(false);
10
+ });
11
+ it("runs once the interval has elapsed", () => {
12
+ expect(shouldRunSentSweep({ lastRunMs: 1_000_000 }, 1_000_000 + HOUR, HOUR)).toBe(true);
13
+ });
14
+ it("runs when the clock stepped backwards", () => {
15
+ expect(shouldRunSentSweep({ lastRunMs: 5_000_000 }, 1_000_000, HOUR)).toBe(true);
16
+ });
17
+ });
18
+ //# sourceMappingURL=sent-sweep-throttle.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sent-sweep-throttle.test.js","sourceRoot":"","sources":["../../src/__tests__/sent-sweep-throttle.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAEnE,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE5B,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,kBAAkB,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,kBAAkB,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,IAAI,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,kBAAkB,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,kBAAkB,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ export interface PortalIndexEntry {
2
+ lastPushMs: number;
3
+ rows: number;
4
+ exposed: string[];
5
+ }
6
+ export type PortalIndexState = Record<string, PortalIndexEntry>;
7
+ export interface PortalAccountAudit {
8
+ accountId: string;
9
+ /** No state entry at all: enrolled for a portal, never successfully pushed. */
10
+ neverRan: boolean;
11
+ /** Whole minutes since the last verified push; null when it never ran. */
12
+ ageMin: number | null;
13
+ rows: number | null;
14
+ exposed: string[];
15
+ stale: boolean;
16
+ }
17
+ export interface PortalIndexAuditResult {
18
+ accounts: PortalAccountAudit[];
19
+ stale: number;
20
+ neverRan: number;
21
+ }
22
+ /**
23
+ * Two intervals, not one: a single missed cycle is ordinary (a slow push, a
24
+ * device asleep at the tick). Two consecutive misses is a pattern, and the
25
+ * threshold has to be loose enough that the line means something when it fires.
26
+ */
27
+ export declare const STALE_AFTER_INTERVALS = 2;
28
+ /**
29
+ * @param portalAccountIds accounts carrying a data-portal.json — the enrolment
30
+ * signal. An account absent from this list has no portal and is not audited.
31
+ * @param state the push's own success record, keyed by accountId.
32
+ * @param nowMs
33
+ * @param intervalMs the push's cadence.
34
+ */
35
+ export declare function auditPortalIndex(portalAccountIds: string[], state: PortalIndexState, nowMs: number, intervalMs: number): PortalIndexAuditResult;
36
+ /** One line per account needing attention, plus one summary line that fires
37
+ * even when everything is healthy — an audit that goes quiet when it finds
38
+ * nothing is indistinguishable from an audit that stopped running, which is
39
+ * the exact defect this whole module exists to close. */
40
+ export declare function formatPortalIndexAudit(result: PortalIndexAuditResult): string[];
41
+ //# sourceMappingURL=portal-index-audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-index-audit.d.ts","sourceRoot":"","sources":["../../src/lib/portal-index-audit.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;AAE/D,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,OAAO,CAAA;IACjB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,kBAAkB,EAAE,CAAA;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,gBAAgB,EAAE,MAAM,EAAE,EAC1B,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACjB,sBAAsB,CA0BxB;AAED;;;0DAG0D;AAC1D,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAgB/E"}
@@ -0,0 +1,75 @@
1
+ // Standing check: is each portal-serving account's folder index still being
2
+ // refreshed?
3
+ //
4
+ // Every other signal in the index push is event-driven — the script logs when
5
+ // it runs. So the one failure it cannot report is its own absence: if the
6
+ // hourly spawn never fires, there is no line at all, the client's folder tree
7
+ // silently freezes at whatever it last held, and silence is indistinguishable
8
+ // from a clean run.
9
+ //
10
+ // This audit therefore must NOT depend on the thing it audits. It runs
11
+ // in-process inside the heartbeat rather than as a spawned child, and it reads
12
+ // only local files — the state the push writes on success, and the presence of
13
+ // a data-portal.json. No D1, no Cloudflare credential, no second spawn that
14
+ // could fail the same way.
15
+ //
16
+ // The push writes state ONLY after a verified write, so a stale entry means the
17
+ // push stopped succeeding, not merely that it stopped logging.
18
+ /**
19
+ * Two intervals, not one: a single missed cycle is ordinary (a slow push, a
20
+ * device asleep at the tick). Two consecutive misses is a pattern, and the
21
+ * threshold has to be loose enough that the line means something when it fires.
22
+ */
23
+ export const STALE_AFTER_INTERVALS = 2;
24
+ /**
25
+ * @param portalAccountIds accounts carrying a data-portal.json — the enrolment
26
+ * signal. An account absent from this list has no portal and is not audited.
27
+ * @param state the push's own success record, keyed by accountId.
28
+ * @param nowMs
29
+ * @param intervalMs the push's cadence.
30
+ */
31
+ export function auditPortalIndex(portalAccountIds, state, nowMs, intervalMs) {
32
+ const threshold = intervalMs * STALE_AFTER_INTERVALS;
33
+ const accounts = portalAccountIds.map((accountId) => {
34
+ const entry = state[accountId];
35
+ if (!entry || typeof entry.lastPushMs !== "number") {
36
+ // Never-ran is reported as its own condition rather than as infinite
37
+ // staleness: the remedies differ (a first run vs a broken cycle), and a
38
+ // brand-new portal must not look like a regression on its first tick.
39
+ return { accountId, neverRan: true, ageMin: null, rows: null, exposed: [], stale: false };
40
+ }
41
+ const ageMs = Math.max(0, nowMs - entry.lastPushMs);
42
+ return {
43
+ accountId,
44
+ neverRan: false,
45
+ ageMin: Math.floor(ageMs / 60_000),
46
+ rows: typeof entry.rows === "number" ? entry.rows : null,
47
+ exposed: Array.isArray(entry.exposed) ? entry.exposed : [],
48
+ stale: ageMs > threshold,
49
+ };
50
+ });
51
+ return {
52
+ accounts,
53
+ stale: accounts.filter((a) => a.stale).length,
54
+ neverRan: accounts.filter((a) => a.neverRan).length,
55
+ };
56
+ }
57
+ /** One line per account needing attention, plus one summary line that fires
58
+ * even when everything is healthy — an audit that goes quiet when it finds
59
+ * nothing is indistinguishable from an audit that stopped running, which is
60
+ * the exact defect this whole module exists to close. */
61
+ export function formatPortalIndexAudit(result) {
62
+ const lines = [];
63
+ for (const a of result.accounts) {
64
+ if (a.neverRan) {
65
+ lines.push(`[portal-index] op=audit account=${a.accountId} rows=0 result=never-ran`);
66
+ }
67
+ else if (a.stale) {
68
+ lines.push(`[portal-index] op=audit account=${a.accountId} rows=${a.rows} ageMin=${a.ageMin} ` +
69
+ `exposed=${a.exposed.join(",") || "none"} result=stale`);
70
+ }
71
+ }
72
+ lines.push(`[portal-index] op=audit accounts=${result.accounts.length} stale=${result.stale} never-ran=${result.neverRan}`);
73
+ return lines;
74
+ }
75
+ //# sourceMappingURL=portal-index-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal-index-audit.js","sourceRoot":"","sources":["../../src/lib/portal-index-audit.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,aAAa;AACb,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,8EAA8E;AAC9E,8EAA8E;AAC9E,oBAAoB;AACpB,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2BAA2B;AAC3B,EAAE;AACF,gFAAgF;AAChF,+DAA+D;AA2B/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAA;AAEtC;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAC9B,gBAA0B,EAC1B,KAAuB,EACvB,KAAa,EACb,UAAkB;IAElB,MAAM,SAAS,GAAG,UAAU,GAAG,qBAAqB,CAAA;IACpD,MAAM,QAAQ,GAAyB,gBAAgB,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QACxE,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,CAAA;QAC9B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YACnD,qEAAqE;YACrE,wEAAwE;YACxE,sEAAsE;YACtE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QAC5F,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;QACpD,OAAO;YACL,SAAS;YACT,QAAQ,EAAE,KAAK;YACf,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC;YAClC,IAAI,EAAE,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;YACxD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE;YAC1D,KAAK,EAAE,KAAK,GAAG,SAAS;SACzB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,QAAQ;QACR,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM;QAC7C,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM;KACpD,CAAC;AACJ,CAAC;AAED;;;0DAG0D;AAC1D,MAAM,UAAU,sBAAsB,CAAC,MAA8B;IACnE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC,SAAS,0BAA0B,CAAC,CAAC;QACvF,CAAC;aAAM,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CACR,mCAAmC,CAAC,CAAC,SAAS,SAAS,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,MAAM,GAAG;gBACjF,WAAW,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,MAAM,eAAe,CAC1D,CAAC;QACJ,CAAC;IACH,CAAC;IACD,KAAK,CAAC,IAAI,CACR,oCAAoC,MAAM,CAAC,QAAQ,CAAC,MAAM,UAAU,MAAM,CAAC,KAAK,cAAc,MAAM,CAAC,QAAQ,EAAE,CAChH,CAAC;IACF,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hourly gate for the email sent-copy sweep inside the 60-second heartbeat.
3
+ *
4
+ * The sweep opens an IMAP connection per mailbox, which is affordable hourly
5
+ * and not per minute. Mirrors the provisioning-heartbeat gate in
6
+ * connector-sync-provision.ts rather than inventing a second pattern.
7
+ */
8
+ export interface SentSweepState {
9
+ lastRunMs: number;
10
+ }
11
+ export declare const SENT_SWEEP_INTERVAL_MS: number;
12
+ export declare function shouldRunSentSweep(prev: SentSweepState | undefined, nowMs: number, intervalMs: number): boolean;
13
+ //# sourceMappingURL=sent-sweep-throttle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sent-sweep-throttle.d.ts","sourceRoot":"","sources":["../../src/lib/sent-sweep-throttle.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,sBAAsB,QAAiB,CAAC;AAErD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,cAAc,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACjB,OAAO,CAMT"}
@@ -0,0 +1,10 @@
1
+ export const SENT_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
2
+ export function shouldRunSentSweep(prev, nowMs, intervalMs) {
3
+ if (!prev)
4
+ return true;
5
+ const elapsed = nowMs - prev.lastRunMs;
6
+ // A negative elapsed means the clock stepped backwards (an NTP correction on
7
+ // a Pi with no battery-backed RTC). Running is the safe side.
8
+ return elapsed < 0 || elapsed >= intervalMs;
9
+ }
10
+ //# sourceMappingURL=sent-sweep-throttle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sent-sweep-throttle.js","sourceRoot":"","sources":["../../src/lib/sent-sweep-throttle.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAErD,MAAM,UAAU,kBAAkB,CAChC,IAAgC,EAChC,KAAa,EACb,UAAkB;IAElB,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;IACvC,6EAA6E;IAC7E,8DAA8D;IAC9D,OAAO,OAAO,GAAG,CAAC,IAAI,OAAO,IAAI,UAAU,CAAC;AAC9C,CAAC"}