@remit/backend 0.0.143 → 0.0.144

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 (269) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -522
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. package/tsconfig.json +0 -7
@@ -1,205 +0,0 @@
1
- /**
2
- * Unit tests for sync-status aggregation math and phase derivation.
3
- *
4
- * Imports the pure helpers from sync-progress.ts directly so the tests pin
5
- * the shipped implementation (no DynamoDB or AWS infrastructure required).
6
- */
7
- import assert from "node:assert/strict";
8
- import { describe, it } from "node:test";
9
- import {
10
- computeMessagesSynced,
11
- deriveMailboxPhase,
12
- type MailboxProgressFields,
13
- type MailboxSyncFields,
14
- toMailboxSyncProgress,
15
- } from "./sync-progress.js";
16
-
17
- // ─── deriveMailboxPhase tests ───────────────────────────────────────────────
18
-
19
- describe("deriveMailboxPhase", () => {
20
- it("returns pending when lastMessageSyncAt is 0", () => {
21
- const mailbox: MailboxSyncFields = {
22
- lastMessageSyncAt: 0,
23
- messageCount: 100,
24
- lastSyncUid: 0,
25
- highWaterMarkUid: 0,
26
- };
27
- assert.equal(deriveMailboxPhase(mailbox), "pending");
28
- });
29
-
30
- it("returns pending even when watermarks are set but lastMessageSyncAt is 0", () => {
31
- const mailbox: MailboxSyncFields = {
32
- lastMessageSyncAt: 0,
33
- messageCount: 50,
34
- lastSyncUid: 50,
35
- highWaterMarkUid: 100,
36
- };
37
- assert.equal(deriveMailboxPhase(mailbox), "pending");
38
- });
39
-
40
- it("returns complete when the worker stamped initialSyncCompletedAt after the last batch", () => {
41
- const now = Date.now();
42
- const mailbox: MailboxSyncFields = {
43
- lastMessageSyncAt: now - 1000,
44
- initialSyncCompletedAt: now,
45
- messageCount: 200,
46
- // Backfill settles at the smallest real UID (sparse UIDs) — the
47
- // marker, not the watermark, decides completion.
48
- lastSyncUid: 42,
49
- highWaterMarkUid: 200,
50
- };
51
- assert.equal(deriveMailboxPhase(mailbox), "complete");
52
- });
53
-
54
- it("returns complete when marker equals lastMessageSyncAt (same-millisecond writes)", () => {
55
- const now = Date.now();
56
- const mailbox: MailboxSyncFields = {
57
- lastMessageSyncAt: now,
58
- initialSyncCompletedAt: now,
59
- messageCount: 10,
60
- lastSyncUid: 5,
61
- highWaterMarkUid: 20,
62
- };
63
- assert.equal(deriveMailboxPhase(mailbox), "complete");
64
- });
65
-
66
- it("returns complete for empty mailbox (messageCount 0) without a marker", () => {
67
- const mailbox: MailboxSyncFields = {
68
- lastMessageSyncAt: Date.now(),
69
- messageCount: 0,
70
- lastSyncUid: 0,
71
- highWaterMarkUid: 0,
72
- };
73
- assert.equal(deriveMailboxPhase(mailbox), "complete");
74
- });
75
-
76
- it("returns syncing when a batch was written after the completion marker", () => {
77
- const now = Date.now();
78
- const mailbox: MailboxSyncFields = {
79
- lastMessageSyncAt: now,
80
- initialSyncCompletedAt: now - 60_000,
81
- messageCount: 500,
82
- lastSyncUid: 150,
83
- highWaterMarkUid: 500,
84
- };
85
- assert.equal(deriveMailboxPhase(mailbox), "syncing");
86
- });
87
-
88
- it("returns syncing when backfill is in progress and no marker is set", () => {
89
- const mailbox: MailboxSyncFields = {
90
- lastMessageSyncAt: Date.now(),
91
- messageCount: 500,
92
- lastSyncUid: 150,
93
- highWaterMarkUid: 500,
94
- };
95
- assert.equal(deriveMailboxPhase(mailbox), "syncing");
96
- });
97
- });
98
-
99
- // ─── computeMessagesSynced tests ───────────────────────────────────────────
100
-
101
- describe("computeMessagesSynced", () => {
102
- it("returns 0 when no sync has started (highWaterMarkUid = 0)", () => {
103
- const mailbox: MailboxSyncFields = {
104
- lastMessageSyncAt: 0,
105
- messageCount: 100,
106
- lastSyncUid: 0,
107
- highWaterMarkUid: 0,
108
- };
109
- assert.equal(computeMessagesSynced(mailbox), 0);
110
- });
111
-
112
- it("computes UID range approximation", () => {
113
- // highWaterMarkUid - lastSyncUid + 1 = 200 - 150 + 1 = 51
114
- const mailbox: MailboxSyncFields = {
115
- lastMessageSyncAt: Date.now(),
116
- messageCount: 200,
117
- lastSyncUid: 150,
118
- highWaterMarkUid: 200,
119
- };
120
- assert.equal(computeMessagesSynced(mailbox), 51);
121
- });
122
-
123
- it("clamps result to messagesTotal", () => {
124
- // UID range would be 200 - 1 + 1 = 200; total is 180 (sparse UIDs)
125
- const mailbox: MailboxSyncFields = {
126
- lastMessageSyncAt: Date.now(),
127
- messageCount: 180,
128
- lastSyncUid: 1,
129
- highWaterMarkUid: 200,
130
- };
131
- assert.equal(
132
- computeMessagesSynced(mailbox),
133
- 180,
134
- "should clamp to messagesTotal",
135
- );
136
- });
137
-
138
- it("never returns negative value", () => {
139
- const mailbox: MailboxSyncFields = {
140
- lastMessageSyncAt: Date.now(),
141
- messageCount: 0,
142
- lastSyncUid: 100,
143
- highWaterMarkUid: 50,
144
- };
145
- assert.ok(computeMessagesSynced(mailbox) >= 0, "should never be negative");
146
- });
147
-
148
- it("returns messagesTotal when fully synced (backfill reached the oldest UID)", () => {
149
- // Fully synced: lastSyncUid settled at the smallest real UID (42).
150
- // Range 100 - 42 + 1 = 59, clamped to the 50 actual messages.
151
- const mailbox: MailboxSyncFields = {
152
- lastMessageSyncAt: Date.now(),
153
- initialSyncCompletedAt: Date.now(),
154
- messageCount: 50,
155
- lastSyncUid: 42,
156
- highWaterMarkUid: 100,
157
- };
158
- assert.equal(computeMessagesSynced(mailbox), 50);
159
- });
160
- });
161
-
162
- // ─── toMailboxSyncProgress tests ────────────────────────────────────────────
163
-
164
- const mailboxRow = (
165
- overrides: Partial<MailboxProgressFields> = {},
166
- ): MailboxProgressFields => ({
167
- mailboxId: "mb-1",
168
- fullPath: "INBOX",
169
- lastMessageSyncAt: 1_700_000_000_000,
170
- initialSyncCompletedAt: 1_700_000_000_001,
171
- messageCount: 10,
172
- lastSyncUid: 1,
173
- highWaterMarkUid: 100,
174
- ...overrides,
175
- });
176
-
177
- describe("toMailboxSyncProgress", () => {
178
- // Issue #771: the client cannot tell an arrival from a departure by count —
179
- // one message in and one out nets to zero. The highest UID only ever moves
180
- // up, so the projection has to carry it or the arrival stays invisible.
181
- it("carries the mailbox's highest UID", () => {
182
- assert.equal(
183
- toMailboxSyncProgress(mailboxRow({ highWaterMarkUid: 4321 }))
184
- .highWaterMarkUid,
185
- 4321,
186
- );
187
- });
188
-
189
- it("reports the sentinel 0 for a mailbox that has never taken a message", () => {
190
- assert.equal(
191
- toMailboxSyncProgress(mailboxRow({ highWaterMarkUid: 0 }))
192
- .highWaterMarkUid,
193
- 0,
194
- );
195
- });
196
-
197
- it("projects identity, phase and totals alongside it", () => {
198
- const progress = toMailboxSyncProgress(mailboxRow());
199
- assert.equal(progress.mailboxId, "mb-1");
200
- assert.equal(progress.fullPath, "INBOX");
201
- assert.equal(progress.phase, "complete");
202
- assert.equal(progress.messagesTotal, 10);
203
- assert.equal(progress.lastSyncedAt, 1_700_000_000_000);
204
- });
205
- });
@@ -1,99 +0,0 @@
1
- import type { MailboxItem } from "@remit/data-ports";
2
-
3
- /** The subset of mailbox fields needed to derive sync progress. */
4
- export type MailboxSyncFields = Pick<
5
- MailboxItem,
6
- | "lastMessageSyncAt"
7
- | "initialSyncCompletedAt"
8
- | "messageCount"
9
- | "lastSyncUid"
10
- | "highWaterMarkUid"
11
- >;
12
-
13
- /** A mailbox row, as far as the sync-status projection reads it. */
14
- export type MailboxProgressFields = MailboxSyncFields &
15
- Pick<MailboxItem, "mailboxId" | "fullPath">;
16
-
17
- /** The per-mailbox entry `getSyncStatus` returns. */
18
- export interface MailboxSyncProgress {
19
- mailboxId: string;
20
- fullPath: string;
21
- phase: string;
22
- messagesTotal: number;
23
- messagesSynced: number;
24
- highWaterMarkUid: number;
25
- lastSyncedAt: number | undefined;
26
- }
27
-
28
- /**
29
- * Derive a per-mailbox sync phase from worker-written sync state.
30
- *
31
- * - pending: never synced (lastMessageSyncAt is 0 / unset)
32
- * - complete: the imap-worker drained the mailbox and stamped
33
- * initialSyncCompletedAt after the last batch write
34
- * (initialSyncCompletedAt >= lastMessageSyncAt), or the
35
- * mailbox is empty
36
- * - syncing: sync has started but is not yet complete
37
- *
38
- * Note: UID watermarks alone cannot express completion — backfill settles
39
- * at the mailbox's smallest real UID (UIDs are sparse), so the completion
40
- * marker written by the worker is the source of truth.
41
- */
42
- export const deriveMailboxPhase = (mailbox: MailboxSyncFields): string => {
43
- if (!mailbox.lastMessageSyncAt) return "pending";
44
- const messageCount = mailbox.messageCount ?? 0;
45
- const initialSyncCompletedAt = mailbox.initialSyncCompletedAt ?? 0;
46
-
47
- // Completion marker is fresh: no batch was written after the worker
48
- // recorded the mailbox as drained.
49
- if (
50
- initialSyncCompletedAt > 0 &&
51
- initialSyncCompletedAt >= mailbox.lastMessageSyncAt
52
- ) {
53
- return "complete";
54
- }
55
-
56
- // Also complete if messageCount is 0 (empty mailbox)
57
- if (messageCount === 0) return "complete";
58
-
59
- return "syncing";
60
- };
61
-
62
- /**
63
- * Compute approximate messagesSynced from UID watermarks.
64
- *
65
- * UIDs are sparse (not necessarily contiguous), so this is an approximation.
66
- * We clamp the result to [0, messagesTotal].
67
- */
68
- export const computeMessagesSynced = (mailbox: MailboxSyncFields): number => {
69
- const messagesTotal = mailbox.messageCount ?? 0;
70
- const lastSyncUid = mailbox.lastSyncUid ?? 0;
71
- const highWaterMarkUid = mailbox.highWaterMarkUid ?? 0;
72
-
73
- if (highWaterMarkUid === 0) return 0;
74
-
75
- // UID range approximation: number of UIDs in the synced range
76
- const synced = highWaterMarkUid - lastSyncUid + 1;
77
-
78
- return Math.min(Math.max(synced, 0), messagesTotal);
79
- };
80
-
81
- /**
82
- * Project a mailbox row into its sync-status entry.
83
- *
84
- * `highWaterMarkUid` is carried through because a total cannot tell an arrival
85
- * from a departure: a message landing in the same sync round that another one
86
- * leaves nets to zero, and the client's freshness check saw nothing (#771). The
87
- * highest UID only ever moves up, so it names the arrival on its own.
88
- */
89
- export const toMailboxSyncProgress = (
90
- mailbox: MailboxProgressFields,
91
- ): MailboxSyncProgress => ({
92
- mailboxId: mailbox.mailboxId,
93
- fullPath: mailbox.fullPath,
94
- phase: deriveMailboxPhase(mailbox),
95
- messagesTotal: mailbox.messageCount ?? 0,
96
- messagesSynced: computeMessagesSynced(mailbox),
97
- highWaterMarkUid: mailbox.highWaterMarkUid ?? 0,
98
- lastSyncedAt: mailbox.lastMessageSyncAt || undefined,
99
- });
@@ -1,168 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
4
- import { triggerSyncSafe } from "./sync.js";
5
-
6
- const QUEUE_URL = "http://localhost:9324/000000000000/remit-dev-mailboxes";
7
-
8
- interface LogCall {
9
- fields: Record<string, unknown>;
10
- message: string;
11
- }
12
-
13
- const createLoggerSpy = () => {
14
- const info: LogCall[] = [];
15
- const error: LogCall[] = [];
16
- const logger = {
17
- info: (fields: Record<string, unknown>, message: string): void => {
18
- info.push({ fields, message });
19
- },
20
- error: (fields: Record<string, unknown>, message: string): void => {
21
- error.push({ fields, message });
22
- },
23
- };
24
- return { info, error, logger };
25
- };
26
-
27
- const okSqsClient = (sent: SendMessageCommand[]): SQSClient =>
28
- ({
29
- send: async (cmd: SendMessageCommand) => {
30
- sent.push(cmd);
31
- return {};
32
- },
33
- }) as unknown as SQSClient;
34
-
35
- /**
36
- * Reproduces the real smoke failure shape: the SQS send does NOT reject inline —
37
- * it rejects on a later microtask/timer, the way a socket ECONNREFUSED settles
38
- * after the AWS SDK's connect + retries. A try/catch that only guards the
39
- * synchronous call would miss it; only awaiting the actual promise contains it.
40
- */
41
- const deferredFailingSqsClient = (error: Error): SQSClient =>
42
- ({
43
- send: () =>
44
- new Promise((_resolve, reject) => {
45
- setImmediate(() => reject(error));
46
- }),
47
- }) as unknown as SQSClient;
48
-
49
- describe("triggerSyncSafe", () => {
50
- it("enqueues a SYNC_MAILBOXES event and logs success", async () => {
51
- const sent: SendMessageCommand[] = [];
52
- const { logger, info, error } = createLoggerSpy();
53
-
54
- await triggerSyncSafe("acc-a", "config-1", {
55
- sqsClient: okSqsClient(sent),
56
- queueUrl: QUEUE_URL,
57
- logger,
58
- });
59
-
60
- assert.equal(sent.length, 1);
61
- assert.equal(info.length, 1);
62
- assert.equal(error.length, 0);
63
- });
64
-
65
- // The guarantee behind issue #37: POST /sync asks for a sync of this
66
- // account by name, so the event it enqueues has to carry the mark that
67
- // makes the worker's fan-out sync every mailbox rather than skip the fresh
68
- // ones. Its callers are the refresh control, pull-to-refresh and the
69
- // client's automatic poll — the poll is kept from abusing this by the
70
- // interval floor in useStaleAccountSync, not by the server.
71
- it("marks the event as an explicit request", async () => {
72
- const sent: SendMessageCommand[] = [];
73
- const { logger } = createLoggerSpy();
74
-
75
- await triggerSyncSafe("acc-a", "config-1", {
76
- sqsClient: okSqsClient(sent),
77
- queueUrl: QUEUE_URL,
78
- logger,
79
- });
80
-
81
- const body = JSON.parse(sent[0]?.input.MessageBody ?? "{}") as {
82
- explicitRequest?: boolean;
83
- };
84
- assert.equal(body.explicitRequest, true);
85
- });
86
-
87
- it("does NOT reject when the SQS enqueue fails (POST /sync stays resilient)", async () => {
88
- const econnrefused = Object.assign(new Error(""), {
89
- name: "AggregateError",
90
- code: "ECONNREFUSED",
91
- });
92
- const { logger } = createLoggerSpy();
93
-
94
- await assert.doesNotReject(
95
- triggerSyncSafe("acc-a", "config-1", {
96
- sqsClient: deferredFailingSqsClient(econnrefused),
97
- queueUrl: QUEUE_URL,
98
- logger,
99
- }),
100
- );
101
- });
102
-
103
- it("logs the failure loudly with the alertable structured fields", async () => {
104
- const econnrefused = Object.assign(new Error(""), {
105
- name: "AggregateError",
106
- code: "ECONNREFUSED",
107
- });
108
- const { logger, error } = createLoggerSpy();
109
-
110
- await triggerSyncSafe("acc-a", "config-1", {
111
- sqsClient: deferredFailingSqsClient(econnrefused),
112
- queueUrl: QUEUE_URL,
113
- logger,
114
- });
115
-
116
- assert.equal(error.length, 1);
117
- const call = error[0];
118
- if (!call) throw new Error("expected an error log");
119
- assert.equal(call.fields.alert, "sync_trigger_failed");
120
- assert.equal(call.fields.source, "trigger_sync");
121
- assert.equal(call.fields.accountId, "acc-a");
122
- assert.equal(call.fields.accountConfigId, "config-1");
123
- assert.equal(call.fields.errorCode, "ECONNREFUSED");
124
- });
125
-
126
- // The actual smoke-storm failure mode #746/#753 missed: POST /sync void-fires
127
- // this trigger, the queue is unreachable, and a concurrent read is in flight on
128
- // the same event loop. Before containment the escaped rejection landed on that
129
- // read and 500'd it. The SQS send here rejects on a LATER tick (a real socket
130
- // error never settles inline), which is exactly why a synchronous-only guard
131
- // did not reproduce the failure. Assert nothing leaks onto the concurrent read.
132
- it("void-fired with an unreachable queue leaks NO unhandled rejection onto a concurrent read", async () => {
133
- const leaked: unknown[] = [];
134
- const onUnhandled = (reason: unknown): void => {
135
- leaked.push(reason);
136
- };
137
- process.on("unhandledRejection", onUnhandled);
138
-
139
- const econnrefused = Object.assign(new Error(""), {
140
- name: "AggregateError",
141
- code: "ECONNREFUSED",
142
- });
143
- const { logger } = createLoggerSpy();
144
-
145
- let readResolved = false;
146
- try {
147
- // Fire-and-forget exactly as SyncOperations_triggerSync does.
148
- void triggerSyncSafe("acc-a", "config-1", {
149
- sqsClient: deferredFailingSqsClient(econnrefused),
150
- queueUrl: QUEUE_URL,
151
- logger,
152
- });
153
-
154
- // A concurrent "read" sharing the event loop, like /mailboxes or /outbox.
155
- await new Promise((resolve) => setImmediate(resolve)).then(() => {
156
- readResolved = true;
157
- });
158
- // Drain remaining microtasks so any escaped rejection would surface.
159
- await new Promise((resolve) => setImmediate(resolve));
160
- await new Promise((resolve) => setImmediate(resolve));
161
- } finally {
162
- process.off("unhandledRejection", onUnhandled);
163
- }
164
-
165
- assert.equal(readResolved, true);
166
- assert.equal(leaked.length, 0);
167
- });
168
- });
@@ -1,118 +0,0 @@
1
- import type { SQSClient } from "@aws-sdk/client-sqs";
2
- import { logger } from "@remit/logger-lambda";
3
- import type { APIGatewayProxyEvent } from "aws-lambda";
4
- import { env } from "expect-env";
5
- import { getAccountConfigIdFromEvent } from "../auth.js";
6
- import { getClient } from "../service/data-client.js";
7
- import {
8
- type FireAndForgetLogger,
9
- fireAndForget,
10
- } from "../service/fire-and-forget.js";
11
- import { sqsClient } from "../service/sqs.js";
12
- import { triggerAccountSync } from "../service/trigger-sync.js";
13
- import type { OperationHandler, SyncOperationIds } from "../types.js";
14
- import { assertAccountOwnership } from "./account-ownership.js";
15
- import { toMailboxSyncProgress } from "./sync-progress.js";
16
-
17
- interface SyncTriggerDeps {
18
- sqsClient: SQSClient;
19
- queueUrl: string;
20
- logger: FireAndForgetLogger & {
21
- info: (fields: Record<string, unknown>, message: string) => void;
22
- };
23
- }
24
-
25
- const defaultSyncTriggerDeps = (): SyncTriggerDeps => ({
26
- sqsClient,
27
- queueUrl: env.SQS_QUEUE_URL,
28
- logger,
29
- });
30
-
31
- /**
32
- * Fire-and-forget enqueue for the POST /sync kick.
33
- *
34
- * POST /sync is a best-effort nudge: the web client polls it to wake a sync and
35
- * acts on no enqueue result in the response. So the enqueue must never fail —
36
- * nor leak onto — this request or any concurrent read when the queue is
37
- * unreachable. Previously this handler `await`ed the SQS send directly; with the
38
- * queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
39
- * the handler's promise and landed on whatever read was in flight on the shared
40
- * event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
41
- *
42
- * Routing it through fireAndForget catches the rejection at the source, logs it
43
- * loudly with the alertable structured fields, and resolves to void. A genuine
44
- * account-lookup / ownership failure still propagates and 500s as normal.
45
- */
46
- export const triggerSyncSafe = async (
47
- accountId: string,
48
- accountConfigId: string,
49
- deps: SyncTriggerDeps = defaultSyncTriggerDeps(),
50
- ): Promise<void> => {
51
- await fireAndForget(
52
- async () => {
53
- const { eventId } = await triggerAccountSync({
54
- sqsClient: deps.sqsClient,
55
- queueUrl: deps.queueUrl,
56
- accountId,
57
- // POST /sync asks for a sync of this account by name — the refresh
58
- // control, pull-to-refresh, and the client's automatic poll all land
59
- // here — so it syncs every mailbox regardless of how recently one
60
- // ran.
61
- explicitRequest: true,
62
- });
63
- deps.logger.info(
64
- { accountId, eventId },
65
- "Sync triggered - enqueued SYNC_MAILBOXES event",
66
- );
67
- },
68
- {
69
- source: "trigger_sync",
70
- message: "Failed to enqueue SYNC_MAILBOXES on POST /sync (best-effort)",
71
- ids: { accountId, accountConfigId },
72
- logger: deps.logger,
73
- },
74
- );
75
- };
76
-
77
- export const SyncOperations: Record<
78
- SyncOperationIds,
79
- OperationHandler<SyncOperationIds>
80
- > = {
81
- SyncOperations_triggerSync: async (context, ...args: unknown[]) => {
82
- const event = args[0] as APIGatewayProxyEvent;
83
- const accountConfigId = getAccountConfigIdFromEvent(event);
84
- const { accountId } = context.request.params as { accountId: string };
85
-
86
- const account = await (await getClient()).account.get(accountId);
87
- assertAccountOwnership(account, accountConfigId, "act");
88
-
89
- void triggerSyncSafe(account.accountId, accountConfigId);
90
-
91
- return {
92
- triggered: true,
93
- message: `Sync triggered for account ${accountId}`,
94
- };
95
- },
96
-
97
- SyncOperations_getSyncStatus: async (context, ...args: unknown[]) => {
98
- const event = args[0] as APIGatewayProxyEvent;
99
- const accountConfigId = getAccountConfigIdFromEvent(event);
100
- const { accountId } = context.request.params as { accountId: string };
101
-
102
- const client = await getClient();
103
- const account = await client.account.get(accountId);
104
- assertAccountOwnership(account, accountConfigId, "read");
105
-
106
- // Collect all mailboxes (read-only — does not write or contend with sync lock)
107
- const mailboxes = await client.mailbox.listAllByAccount(accountId);
108
-
109
- return {
110
- accountId: account.accountId,
111
- syncPhase: account.syncPhase,
112
- mailboxCountTotal: account.mailboxCountTotal,
113
- mailboxCountSynced: account.mailboxCountSynced,
114
- lastSyncAt: account.lastSyncAt,
115
- mailboxes: mailboxes.map(toMailboxSyncProgress),
116
- };
117
- },
118
- };