@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,245 +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 { AccountAuthType } from "@remit/domain-enums";
5
- import { triggerConfigLoadSyncs } from "./config.js";
6
-
7
- /**
8
- * The credential a password account stores. Its content is never read here —
9
- * only its presence decides whether a sync is worth enqueuing.
10
- */
11
- const withPassword = (accountId: string) => ({
12
- accountId,
13
- passwordHash: '{"ciphertext":"x"}',
14
- });
15
-
16
- const QUEUE_URL = "http://localhost:9324/000000000000/remit-dev-mailboxes";
17
-
18
- interface LogCall {
19
- fields: Record<string, unknown>;
20
- message: string;
21
- }
22
-
23
- const createLoggerSpy = () => {
24
- const info: LogCall[] = [];
25
- const error: LogCall[] = [];
26
- const logger = {
27
- info: (fields: Record<string, unknown>, message: string): void => {
28
- info.push({ fields, message });
29
- },
30
- error: (fields: Record<string, unknown>, message: string): void => {
31
- error.push({ fields, message });
32
- },
33
- };
34
- return { info, error, logger };
35
- };
36
-
37
- const okSqsClient = (sent: SendMessageCommand[]): SQSClient =>
38
- ({
39
- send: async (cmd: SendMessageCommand) => {
40
- sent.push(cmd);
41
- return {};
42
- },
43
- }) as unknown as SQSClient;
44
-
45
- const failingSqsClient = (error: Error): SQSClient =>
46
- ({
47
- send: async () => {
48
- throw error;
49
- },
50
- }) as unknown as SQSClient;
51
-
52
- describe("triggerConfigLoadSyncs", () => {
53
- it("enqueues a sync for each account and logs success", async () => {
54
- const sent: SendMessageCommand[] = [];
55
- const { logger, info, error } = createLoggerSpy();
56
-
57
- await triggerConfigLoadSyncs(
58
- "config-1",
59
- [withPassword("acc-a"), withPassword("acc-b")],
60
- {
61
- sqsClient: okSqsClient(sent),
62
- queueUrl: QUEUE_URL,
63
- logger,
64
- },
65
- );
66
-
67
- assert.equal(sent.length, 2);
68
- assert.equal(info.length, 2);
69
- assert.equal(error.length, 0);
70
- });
71
-
72
- it("skips an account that stores no credential (issue #1120)", async () => {
73
- const sent: SendMessageCommand[] = [];
74
- const { logger, error } = createLoggerSpy();
75
-
76
- await triggerConfigLoadSyncs(
77
- "config-1",
78
- [
79
- { accountId: "imported-no-password" },
80
- withPassword("acc-with-password"),
81
- ],
82
- { sqsClient: okSqsClient(sent), queueUrl: QUEUE_URL, logger },
83
- );
84
-
85
- assert.equal(sent.length, 1);
86
- assert.equal(error.length, 0);
87
- const body = JSON.parse(String(sent[0]?.input.MessageBody)) as {
88
- accountId: string;
89
- };
90
- assert.equal(body.accountId, "acc-with-password");
91
- });
92
-
93
- it("skips an OAuth account that stores no refresh token", async () => {
94
- const sent: SendMessageCommand[] = [];
95
- const { logger } = createLoggerSpy();
96
-
97
- await triggerConfigLoadSyncs(
98
- "config-1",
99
- [
100
- {
101
- accountId: "oauth-unconnected",
102
- authType: AccountAuthType.OauthMicrosoft,
103
- },
104
- ],
105
- { sqsClient: okSqsClient(sent), queueUrl: QUEUE_URL, logger },
106
- );
107
-
108
- assert.equal(sent.length, 0);
109
- });
110
-
111
- it("does NOT reject when the SQS enqueue fails (read stays resilient)", async () => {
112
- const econnrefused = Object.assign(new Error(""), {
113
- name: "AggregateError",
114
- code: "ECONNREFUSED",
115
- });
116
- const { logger } = createLoggerSpy();
117
-
118
- await assert.doesNotReject(
119
- triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
120
- sqsClient: failingSqsClient(econnrefused),
121
- queueUrl: QUEUE_URL,
122
- logger,
123
- }),
124
- );
125
- });
126
-
127
- it("logs the failure loudly with the alertable structured fields", async () => {
128
- const econnrefused = Object.assign(new Error(""), {
129
- name: "AggregateError",
130
- code: "ECONNREFUSED",
131
- });
132
- const { logger, error } = createLoggerSpy();
133
-
134
- await triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
135
- sqsClient: failingSqsClient(econnrefused),
136
- queueUrl: QUEUE_URL,
137
- logger,
138
- });
139
-
140
- assert.equal(error.length, 1);
141
- const call = error[0];
142
- if (!call) throw new Error("expected an error log");
143
- assert.equal(call.fields.alert, "sync_trigger_failed");
144
- assert.equal(call.fields.source, "config_load");
145
- assert.equal(call.fields.accountId, "acc-a");
146
- assert.equal(call.fields.accountConfigId, "config-1");
147
- assert.equal(call.fields.errorName, "AggregateError");
148
- assert.equal(call.fields.errorCode, "ECONNREFUSED");
149
- });
150
-
151
- it("isolates failures per account — one bad enqueue does not block the others", async () => {
152
- const sent: SendMessageCommand[] = [];
153
- let calls = 0;
154
- const sqsClient = {
155
- send: async (cmd: SendMessageCommand) => {
156
- calls += 1;
157
- if (calls === 1)
158
- throw Object.assign(new Error(""), { code: "ECONNREFUSED" });
159
- sent.push(cmd);
160
- return {};
161
- },
162
- } as unknown as SQSClient;
163
- const { logger, info, error } = createLoggerSpy();
164
-
165
- await triggerConfigLoadSyncs(
166
- "config-1",
167
- [withPassword("acc-a"), withPassword("acc-b")],
168
- {
169
- sqsClient,
170
- queueUrl: QUEUE_URL,
171
- logger,
172
- },
173
- );
174
-
175
- assert.equal(error.length, 1);
176
- assert.equal(info.length, 1);
177
- assert.equal(sent.length, 1);
178
- });
179
-
180
- it("reads SDK error name from the .Code field when present", async () => {
181
- const sdkError = Object.assign(new Error("boom"), {
182
- name: "QueueDoesNotExist",
183
- Code: "AWS.SimpleQueueService.NonExistentQueue",
184
- });
185
- const { logger, error } = createLoggerSpy();
186
-
187
- await triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
188
- sqsClient: failingSqsClient(sdkError),
189
- queueUrl: QUEUE_URL,
190
- logger,
191
- });
192
-
193
- const call = error[0];
194
- if (!call) throw new Error("expected an error log");
195
- assert.equal(
196
- call.fields.errorCode,
197
- "AWS.SimpleQueueService.NonExistentQueue",
198
- );
199
- });
200
-
201
- // The actual smoke-storm failure mode: GET /config void-fires this trigger
202
- // (it does not await it), the SQS queue is unreachable, and a concurrent read
203
- // is in flight on the same event loop. The escaped rejection used to land on
204
- // that read and 500 it. Assert nothing leaks even when fired exactly that way.
205
- it("void-fired with an unreachable queue leaks NO unhandled rejection onto a concurrent read", async () => {
206
- const leaked: unknown[] = [];
207
- const onUnhandled = (reason: unknown): void => {
208
- leaked.push(reason);
209
- };
210
- process.on("unhandledRejection", onUnhandled);
211
-
212
- const econnrefused = Object.assign(new Error(""), {
213
- name: "AggregateError",
214
- code: "ECONNREFUSED",
215
- });
216
- const { logger } = createLoggerSpy();
217
-
218
- let readResolved = false;
219
- try {
220
- // Fire-and-forget exactly as the getConfig handler does.
221
- void triggerConfigLoadSyncs(
222
- "config-1",
223
- [withPassword("acc-a"), withPassword("acc-b")],
224
- {
225
- sqsClient: failingSqsClient(econnrefused),
226
- queueUrl: QUEUE_URL,
227
- logger,
228
- },
229
- );
230
-
231
- // A concurrent "read" sharing the event loop, like /mailboxes or /outbox.
232
- await new Promise((resolve) => setImmediate(resolve)).then(() => {
233
- readResolved = true;
234
- });
235
- // Drain remaining microtasks so any escaped rejection would surface.
236
- await new Promise((resolve) => setImmediate(resolve));
237
- await new Promise((resolve) => setImmediate(resolve));
238
- } finally {
239
- process.off("unhandledRejection", onUnhandled);
240
- }
241
-
242
- assert.equal(readResolved, true);
243
- assert.equal(leaked.length, 0);
244
- });
245
- });
@@ -1,332 +0,0 @@
1
- import type { SQSClient } from "@aws-sdk/client-sqs";
2
- import type {
3
- AccountConfigResponse,
4
- ConfigDescriptionResponse,
5
- ConfigImportReport,
6
- } from "@remit/api-openapi-types";
7
- import type { ReaderConfigDocument } from "@remit/config-format";
8
- import {
9
- importConfig,
10
- pendingImportOf,
11
- readConfigForExport,
12
- } from "@remit/config-transfer";
13
- import type { AccountConfigItem, MailboxItem } from "@remit/data-ports";
14
- import { ConfigNotEmptyError, NotFoundError } from "@remit/data-ports/errors";
15
- import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
16
- import { logger } from "@remit/logger-lambda";
17
- import {
18
- hasStoredCredential,
19
- type StoredCredentialFields,
20
- } from "@remit/mailbox-service/account-credentials";
21
- import {
22
- EMBEDDING_PROVIDER_OFF,
23
- readEmbeddingProviderFromEnv,
24
- } from "@remit/search-service/from-env";
25
- import type { APIGatewayProxyEvent } from "aws-lambda";
26
- import { env } from "expect-env";
27
- import type { Context } from "openapi-backend";
28
- import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
29
- import { embedAnchorText } from "../service/config-import.js";
30
- import { getClient } from "../service/data-client.js";
31
- import { exportIdentity } from "../service/export-identity.js";
32
- import { fireAndForget } from "../service/fire-and-forget.js";
33
- import { sqsClient } from "../service/sqs.js";
34
- import { triggerAccountSync } from "../service/trigger-sync.js";
35
- import type { ConfigOperationIds, OperationHandler } from "../types.js";
36
- import { toAccountResponse } from "./account-guards.js";
37
- import {
38
- type AccountOverrides,
39
- groupAccountOverrides,
40
- } from "./account-overrides.js";
41
- import {
42
- type AccountSignature,
43
- groupSignaturesByAccount,
44
- } from "./account-signature.js";
45
- import {
46
- groupFolderAppointmentsByAccount,
47
- resolveFolderAppointments,
48
- writeFolderRoleAppointment,
49
- } from "./folder-role-appointments.js";
50
-
51
- type StructuredLog = (fields: Record<string, unknown>, message: string) => void;
52
-
53
- interface ConfigSyncTriggerDeps {
54
- sqsClient: SQSClient;
55
- queueUrl: string;
56
- logger: { info: StructuredLog; error: StructuredLog };
57
- }
58
-
59
- const defaultConfigSyncTriggerDeps = (): ConfigSyncTriggerDeps => ({
60
- sqsClient,
61
- queueUrl: env.SQS_QUEUE_URL,
62
- logger,
63
- });
64
-
65
- /**
66
- * Fire-and-forget sync trigger for the accounts surfaced by GET /config.
67
- *
68
- * GET /config is a READ: its response is already computed and does not depend
69
- * on the trigger succeeding. A failure here (the SQS queue unreachable in
70
- * smoke/e2e, an IAM/SQS misconfig in prod) must therefore never reach the read
71
- * response — but it also silently stops ALL sync, so it has to be loud and
72
- * alertable rather than swallowed.
73
- *
74
- * This helper is fully self-contained: it catches every per-account rejection
75
- * internally and resolves to void, so the caller can `void`-fire it with no
76
- * chance of an unhandled rejection escaping. The dev/Lambda runtime shares one
77
- * event loop, so an escaped rejection lands on whatever request is active when
78
- * the deferred SQS retry finally fails — which is how a `/config` enqueue
79
- * failure ended up 500-ing unrelated reads like `/outbox` and `/threads`.
80
- *
81
- * Each failure is logged as a distinct structured error carrying the SDK error
82
- * name/code on dedicated fields plus a stable `alert: "sync_trigger_failed"`
83
- * discriminator a CloudWatch metric filter / alarm can key off.
84
- *
85
- * An account storing no credential is not enqueued at all (issue #1120): the
86
- * import wizard lands accounts without a password by design, and the client
87
- * polls this read while the user is still typing one. The guard is the stored
88
- * credential rather than `connectionState`, which stays `credentials_missing`
89
- * until a connect succeeds — see `hasStoredCredential`.
90
- */
91
- export const triggerConfigLoadSyncs = async (
92
- accountConfigId: string,
93
- accounts: ReadonlyArray<{ accountId: string } & StoredCredentialFields>,
94
- deps: ConfigSyncTriggerDeps = defaultConfigSyncTriggerDeps(),
95
- ): Promise<void> => {
96
- await Promise.all(
97
- accounts.filter(hasStoredCredential).map(({ accountId }) =>
98
- fireAndForget(
99
- async () => {
100
- const { eventId } = await triggerAccountSync({
101
- sqsClient: deps.sqsClient,
102
- queueUrl: deps.queueUrl,
103
- accountId,
104
- });
105
- deps.logger.info(
106
- { accountId, eventId },
107
- "Sync triggered on config load",
108
- );
109
- },
110
- {
111
- source: "config_load",
112
- message: "Failed to trigger account sync on config load",
113
- ids: { accountId, accountConfigId },
114
- logger: deps.logger,
115
- },
116
- ),
117
- ),
118
- );
119
- };
120
-
121
- const toAccountConfigResponse = (
122
- config: AccountConfigItem,
123
- ): AccountConfigResponse => ({
124
- accountConfigId: config.accountConfigId,
125
- userId: config.userId,
126
- name: config.name,
127
- state: ((config as unknown as { state?: string }).state ??
128
- "active") as AccountConfigResponse["state"],
129
- createdAt: config.createdAt,
130
- updatedAt: config.updatedAt,
131
- });
132
-
133
- /**
134
- * Returned by GET /config when no AccountConfig row exists yet for this user.
135
- * GET must never mutate state, so instead of creating a row we synthesize a
136
- * stable, empty shape the frontend can render as a "no accounts yet" state.
137
- * The POST account-creation flow will materialize the real row when the user
138
- * adds their first account.
139
- */
140
- const emptyConfigResponse = (
141
- accountConfigId: string,
142
- event: APIGatewayProxyEvent,
143
- ): ConfigDescriptionResponse => {
144
- const now = Date.now();
145
- const userId = getSubFromEvent(event) ?? accountConfigId;
146
- return {
147
- accountConfig: {
148
- accountConfigId,
149
- userId,
150
- state: "active",
151
- createdAt: now,
152
- updatedAt: now,
153
- },
154
- accounts: [],
155
- semanticSearchEnabled: semanticSearchEnabled(),
156
- };
157
- };
158
-
159
- /**
160
- * Whether this instance embeds anything, read from the same
161
- * `SEARCH_EMBEDDING_PROVIDER` the search-index worker and the `remit` wrapper
162
- * read. It rides GET /config because the semantic surfaces need it before they
163
- * have a query to send: an off instance stores no vectors, so the Organize
164
- * widen and semantic filters have nothing to read, and a client that learns
165
- * that only from an empty result cannot tell it from a mailbox with nothing
166
- * similar in it (#1068).
167
- */
168
- const semanticSearchEnabled = (): boolean =>
169
- readEmbeddingProviderFromEnv() !== EMBEDDING_PROVIDER_OFF;
170
-
171
- export const ConfigOperations: Record<
172
- ConfigOperationIds,
173
- OperationHandler<ConfigOperationIds>
174
- > = {
175
- ConfigOperations_getConfig: async (
176
- _context: Context,
177
- ...args: unknown[]
178
- ): Promise<ConfigDescriptionResponse> => {
179
- const event = args[0] as APIGatewayProxyEvent;
180
- const accountConfigId = getAccountConfigIdFromEvent(event);
181
- const client = await getClient();
182
- const description = await client.accountConfig
183
- .describe(accountConfigId)
184
- .catch((err: unknown) => {
185
- if (err instanceof NotFoundError) return undefined;
186
- throw err;
187
- });
188
-
189
- if (!description || description.accountConfig.length === 0) {
190
- return emptyConfigResponse(accountConfigId, event);
191
- }
192
-
193
- const accountConfig = description.accountConfig[0];
194
- if (!accountConfig) {
195
- return emptyConfigResponse(accountConfigId, event);
196
- }
197
-
198
- // Filter out deleted accounts (tombstone pattern)
199
- const activeAccounts = description.account.filter(
200
- (acc) => acc.deletedAt === undefined,
201
- );
202
-
203
- // Signatures and the display-name/mute overrides are stored per-account in
204
- // AccountSetting rows (RFC 032). Load the whole set for this config once and
205
- // key both by accountId so the account mapping below surfaces each without
206
- // an N+1. The per-account folder-role map (RFC 032
207
- // exclusive-folder-appointment, #976) rides the same settings rows, grouped
208
- // the same way.
209
- const allSettings =
210
- await client.accountSetting.listByAccountConfig(accountConfigId);
211
- const signaturesByAccount = groupSignaturesByAccount(allSettings);
212
- const overridesByAccount = groupAccountOverrides(allSettings);
213
- const appointmentsByAccount = groupFolderAppointmentsByAccount(allSettings);
214
- const signatureOf = (accountId: string): AccountSignature =>
215
- signaturesByAccount.get(accountId) ?? {};
216
- const overridesOf = (accountId: string): AccountOverrides =>
217
- overridesByAccount.get(accountId) ?? {};
218
-
219
- // One mailbox list per account, fetched in parallel: `findFolderForRole`
220
- // needs each account's folders to fill any role the user hasn't appointed
221
- // yet (RFC 032: "the map is never empty for a normal provider").
222
- const mailboxesByAccount = new Map<string, MailboxItem[]>(
223
- await Promise.all(
224
- activeAccounts.map(
225
- async (acc): Promise<[string, MailboxItem[]]> => [
226
- acc.accountId,
227
- await client.mailbox.listAllByAccount(acc.accountId),
228
- ],
229
- ),
230
- ),
231
- );
232
-
233
- // Fire-and-forget: a failed sync enqueue must never fail this read.
234
- // triggerConfigLoadSyncs swallows nothing — it logs each failure loudly
235
- // with an alertable structured field — but it also never rejects, so the
236
- // `void` here cannot leak an unhandled rejection into a later request.
237
- void triggerConfigLoadSyncs(accountConfigId, activeAccounts);
238
-
239
- // An import that named folders IMAP had not produced yet rides the config
240
- // read rather than a route of its own, so nothing has to poll for it.
241
- const pendingImport = pendingImportOf(
242
- await client.configImport.listByAccountConfig(accountConfigId),
243
- );
244
-
245
- return {
246
- accountConfig: toAccountConfigResponse(accountConfig),
247
- semanticSearchEnabled: semanticSearchEnabled(),
248
- ...(pendingImport ? { pendingImport } : {}),
249
- accounts: activeAccounts.map((acc) =>
250
- toAccountResponse(
251
- acc,
252
- signatureOf(acc.accountId),
253
- overridesOf(acc.accountId),
254
- resolveFolderAppointments(
255
- appointmentsByAccount.get(acc.accountId) ?? new Map(),
256
- mailboxesByAccount.get(acc.accountId) ?? [],
257
- ),
258
- ),
259
- ),
260
- };
261
- },
262
-
263
- ConfigOperations_exportConfig: async (
264
- _context: Context,
265
- ...args: unknown[]
266
- ): Promise<{ schemaVersion: number; document: ReaderConfigDocument }> => {
267
- const event = args[0] as APIGatewayProxyEvent;
268
- const accountConfigId = getAccountConfigIdFromEvent(event);
269
- const client = await getClient();
270
- const document = await readConfigForExport(
271
- client,
272
- accountConfigId,
273
- exportIdentity(),
274
- );
275
- return { schemaVersion: document.schemaVersion, document };
276
- },
277
-
278
- ConfigOperations_importConfig: async (
279
- context: Context,
280
- ...args: unknown[]
281
- ): Promise<ConfigImportReport> => {
282
- const event = args[0] as APIGatewayProxyEvent;
283
- const accountConfigId = getAccountConfigIdFromEvent(event);
284
- const body = (context.request.requestBody ?? {}) as {
285
- mode?: "validate" | "apply";
286
- onExisting?: "abort" | "merge";
287
- document?: unknown;
288
- };
289
- const client = await getClient();
290
-
291
- const outcome = await importConfig(
292
- {
293
- repositories: client,
294
- // Passed through as-is, undefined included: a backend with no
295
- // cross-entity transaction writes without one, and the report words
296
- // what survived a failure from whether this is here.
297
- transaction: client.writeSet,
298
- appointFolderRole: (
299
- configId,
300
- accountId,
301
- role,
302
- mailboxId,
303
- lastKnownPath,
304
- ) =>
305
- writeFolderRoleAppointment(
306
- client.accountSetting,
307
- configId,
308
- accountId,
309
- role as CanonicalMailboxRoleValue,
310
- mailboxId,
311
- lastKnownPath,
312
- ),
313
- embedAnchor: embedAnchorText,
314
- },
315
- {
316
- accountConfigId,
317
- userId: getSubFromEvent(event) ?? accountConfigId,
318
- document: body.document,
319
- mode: body.mode ?? "validate",
320
- onExisting: body.onExisting ?? "abort",
321
- },
322
- );
323
-
324
- if (outcome.outcome === "conflict") {
325
- throw new ConfigNotEmptyError(
326
- outcome.conflict.message,
327
- outcome.conflict.details,
328
- );
329
- }
330
- return outcome.report;
331
- },
332
- };
@@ -1,30 +0,0 @@
1
- import type { IAccountConfigRepository } from "@remit/data-ports";
2
- import { ConflictError, NotFoundError } from "@remit/data-ports/errors";
3
-
4
- /**
5
- * Ensure an AccountConfig row exists for the given id. A first-time caller
6
- * coming from Cognito will not yet have one; subsequent calls are idempotent.
7
- *
8
- * We attempt a read first to avoid relying on a race on `create()` — the
9
- * deterministic id (derived from Cognito `sub`) makes both paths safe.
10
- */
11
- export const ensureAccountConfig = async (
12
- accountConfig: IAccountConfigRepository,
13
- accountConfigId: string,
14
- ): Promise<void> => {
15
- const existing = await accountConfig.get(accountConfigId).catch((err) => {
16
- if (err instanceof NotFoundError) return undefined;
17
- throw err;
18
- });
19
- if (existing) return;
20
-
21
- await accountConfig
22
- .create({
23
- accountConfigId,
24
- userId: accountConfigId,
25
- })
26
- .catch((err) => {
27
- if (err instanceof ConflictError) return;
28
- throw err;
29
- });
30
- };