@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
@@ -0,0 +1,112 @@
1
+ import { BadRequestError, NotFoundError } from "@remit/data-ports/errors";
2
+ import { logger } from "@remit/logger";
3
+ import { ownedAccount, queueOrganizeJob, } from "@remit/mailbox-service";
4
+ import { env } from "expect-env";
5
+ import { getAccountConfigIdFromEvent, getSubFromEvent } from "../auth.js";
6
+ import { getClient } from "../service/data-client.js";
7
+ import { buildOrganizeMatchDeps, matchOrganize, ORGANIZE_MATCH_LIMIT, organizePredicateRejection, } from "../service/organize.js";
8
+ import { unwrap } from "../service/refusal.js";
9
+ /**
10
+ * Normalize the request body into the flattened predicate the job row and the
11
+ * matcher share.
12
+ */
13
+ export const predicateFromInput = (input) => ({
14
+ matchOperator: input.matchOperator,
15
+ literalClauses: input.literalClauses,
16
+ actionLabelId: input.actionLabelId,
17
+ actionMailboxId: input.actionMailboxId,
18
+ });
19
+ const assertAccount = async (client, accountId, accountConfigId, mode) => {
20
+ const account = await client.account.get(accountId);
21
+ unwrap(ownedAccount(account, accountConfigId, mode));
22
+ };
23
+ const toOrganizeJobResponse = (job) => ({
24
+ organizeJobId: job.organizeJobId,
25
+ accountConfigId: job.accountConfigId,
26
+ userId: job.userId,
27
+ state: job.state,
28
+ filterId: job.filterId,
29
+ matchOperator: job.matchOperator,
30
+ literalClauses: job.literalClauses,
31
+ actionLabelId: job.actionLabelId,
32
+ actionMailboxId: job.actionMailboxId,
33
+ matchedCount: job.matchedCount,
34
+ appliedCount: job.appliedCount,
35
+ failedCount: job.failedCount,
36
+ errorMessage: job.errorMessage,
37
+ createdAt: job.createdAt,
38
+ updatedAt: job.updatedAt,
39
+ });
40
+ /**
41
+ * What a create asks the worker to run. A saved filter is named and read when
42
+ * the job runs, so Run now and a filter's create share one path (#1354); an
43
+ * "all like these" predicate the matcher can never honour is answered here,
44
+ * not with a 202 the worker has to fail later (reader #463).
45
+ */
46
+ const requestedJob = async (client, owner, input) => {
47
+ if (input.filterId) {
48
+ await client.filter.get(owner.accountConfigId, input.filterId);
49
+ return { ...owner, filterId: input.filterId };
50
+ }
51
+ const predicate = predicateFromInput(input);
52
+ const rejection = organizePredicateRejection(predicate);
53
+ if (rejection)
54
+ throw new BadRequestError(rejection.message);
55
+ return { ...owner, predicate };
56
+ };
57
+ export const OrganizeOperations = {
58
+ OrganizeOperations_createOrganizeJob: async (context, ...args) => {
59
+ const event = args[0];
60
+ const accountConfigId = getAccountConfigIdFromEvent(event);
61
+ const { accountId } = context.request.params;
62
+ const input = context.request.requestBody;
63
+ const userId = getSubFromEvent(event);
64
+ if (!userId) {
65
+ throw new Error("Missing Cognito `sub`: cannot attribute an organize job to a user");
66
+ }
67
+ const client = await getClient();
68
+ await assertAccount(client, accountId, accountConfigId, "act");
69
+ const job = await queueOrganizeJob({
70
+ organizeJobRequest: client.organizeJobRequest,
71
+ sqsQueueUrl: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
72
+ logger,
73
+ }, await requestedJob(client, { accountConfigId, userId }, input));
74
+ return {
75
+ statusCode: 202,
76
+ organizeJobId: job.organizeJobId,
77
+ state: job.state,
78
+ };
79
+ },
80
+ OrganizeOperations_previewOrganize: async (context, ...args) => {
81
+ const event = args[0];
82
+ const accountConfigId = getAccountConfigIdFromEvent(event);
83
+ const { accountId } = context.request.params;
84
+ const input = context.request.requestBody;
85
+ const client = await getClient();
86
+ await assertAccount(client, accountId, accountConfigId, "read");
87
+ if (input.filterId) {
88
+ throw new BadRequestError("Preview takes a rule, not a saved filter. Run the filter to apply it.");
89
+ }
90
+ const result = await matchOrganize(buildOrganizeMatchDeps(client), accountConfigId, predicateFromInput(input), ORGANIZE_MATCH_LIMIT);
91
+ if (result.rejected)
92
+ throw new BadRequestError(result.rejected.message);
93
+ return {
94
+ matchedCount: result.messageIds.length,
95
+ messageIds: result.messageIds,
96
+ };
97
+ },
98
+ };
99
+ export const OrganizeJobDetailOperations = {
100
+ OrganizeJobDetailOperations_getOrganizeJob: async (context, ...args) => {
101
+ const event = args[0];
102
+ const accountConfigId = getAccountConfigIdFromEvent(event);
103
+ const { accountId, organizeJobId } = context.request.params;
104
+ const client = await getClient();
105
+ await assertAccount(client, accountId, accountConfigId, "read");
106
+ const job = await client.organizeJobRequest.get(organizeJobId);
107
+ if (job.accountConfigId !== accountConfigId) {
108
+ throw new NotFoundError(`Organize job not found: ${organizeJobId}`);
109
+ }
110
+ return toOrganizeJobResponse(job);
111
+ },
112
+ };
@@ -0,0 +1,23 @@
1
+ import type { MintOutboxAttachmentResponse, OutboxAttachmentRejection, OutboxAttachmentResponse } from "@remit/api-openapi-types";
2
+ import type { OutboxAttachmentItem } from "@remit/data-ports";
3
+ import type { Context } from "openapi-backend";
4
+ export declare const toOutboxAttachmentResponse: (item: OutboxAttachmentItem) => OutboxAttachmentResponse;
5
+ interface RejectionResponse {
6
+ statusCode: number;
7
+ body: OutboxAttachmentRejection;
8
+ }
9
+ /**
10
+ * Reserve room on a draft and hand back somewhere to put the bytes. Small JSON
11
+ * in, small JSON out — the file itself never passes through here, which is what
12
+ * lets the upload live on its own fleet and keeps this operation under any
13
+ * payload ceiling the REST tier has.
14
+ */
15
+ export declare const mintOutboxAttachment: (context: Context, ...args: unknown[]) => Promise<MintOutboxAttachmentResponse | RejectionResponse>;
16
+ /**
17
+ * Confirm the upload landed. The size in the response is read back from
18
+ * storage — on a deployment where the browser PUT straight to block storage
19
+ * this call is the first the API hears of the bytes, and the client's word for
20
+ * how many there are is not evidence.
21
+ */
22
+ export declare const completeOutboxAttachment: (context: Context, ...args: unknown[]) => Promise<OutboxAttachmentResponse | RejectionResponse>;
23
+ export {};
@@ -0,0 +1,81 @@
1
+ import { OutboxAttachmentRejectionReason } from "@remit/domain-enums";
2
+ import { outboxAttachmentContentId } from "@remit/mailbox-service/outbox-attachment-content";
3
+ import { getAccountConfigIdFromEvent } from "../auth.js";
4
+ import { getContentSigner } from "../derive/contentSignature.js";
5
+ import { buildStorageContentUrl, getContentDeliveryDomain, } from "../derive/contentUrl.js";
6
+ import { getClient } from "../service/data-client.js";
7
+ // Every error body the API emits carries a `code` (issue #371). A refusal here
8
+ // is one kind, so the code is one value and `reason` says which refusal it was.
9
+ const ATTACHMENT_REJECTED_CODE = "attachment_rejected";
10
+ const TOO_LARGE = new Set([
11
+ OutboxAttachmentRejectionReason.FileTooLarge,
12
+ OutboxAttachmentRejectionReason.MessageTooLarge,
13
+ OutboxAttachmentRejectionReason.TooManyAttachments,
14
+ ]);
15
+ export const toOutboxAttachmentResponse = (item) => ({
16
+ outboxAttachmentId: item.outboxAttachmentId,
17
+ outboxMessageId: item.outboxMessageId,
18
+ filename: item.filename,
19
+ contentType: item.contentType,
20
+ sizeBytes: item.sizeBytes,
21
+ state: item.state,
22
+ contentId: outboxAttachmentContentId(item.outboxAttachmentId),
23
+ contentUrl: buildStorageContentUrl({
24
+ domain: getContentDeliveryDomain(),
25
+ storageKey: item.storageKey,
26
+ sign: getContentSigner(),
27
+ }),
28
+ });
29
+ const refuse = (detail) => ({
30
+ statusCode: TOO_LARGE.has(detail.reason) ? 413 : 400,
31
+ body: {
32
+ code: ATTACHMENT_REJECTED_CODE,
33
+ reason: detail.reason,
34
+ message: detail.message,
35
+ limitBytes: detail.limitBytes,
36
+ usedBytes: detail.usedBytes,
37
+ },
38
+ });
39
+ /**
40
+ * Reserve room on a draft and hand back somewhere to put the bytes. Small JSON
41
+ * in, small JSON out — the file itself never passes through here, which is what
42
+ * lets the upload live on its own fleet and keeps this operation under any
43
+ * payload ceiling the REST tier has.
44
+ */
45
+ export const mintOutboxAttachment = async (context, ...args) => {
46
+ const event = args[0];
47
+ const accountConfigId = getAccountConfigIdFromEvent(event);
48
+ const { outboxMessageId } = context.request.params;
49
+ const input = context.request.requestBody;
50
+ const client = await getClient();
51
+ const result = await client.outboxAttachment.mint({
52
+ accountConfigId,
53
+ outboxMessageId,
54
+ filename: input.filename,
55
+ contentType: input.contentType,
56
+ sizeBytes: input.sizeBytes,
57
+ });
58
+ if (result.outcome === "Rejected")
59
+ return refuse(result.rejection);
60
+ return result.reservation;
61
+ };
62
+ /**
63
+ * Confirm the upload landed. The size in the response is read back from
64
+ * storage — on a deployment where the browser PUT straight to block storage
65
+ * this call is the first the API hears of the bytes, and the client's word for
66
+ * how many there are is not evidence.
67
+ */
68
+ export const completeOutboxAttachment = async (context, ...args) => {
69
+ const event = args[0];
70
+ const accountConfigId = getAccountConfigIdFromEvent(event);
71
+ const { outboxMessageId, outboxAttachmentId } = context.request.params;
72
+ const client = await getClient();
73
+ const result = await client.outboxAttachment.complete({
74
+ accountConfigId,
75
+ outboxMessageId,
76
+ outboxAttachmentId,
77
+ });
78
+ if (result.outcome === "Rejected")
79
+ return refuse(result.rejection);
80
+ return toOutboxAttachmentResponse(result.attachment);
81
+ };
@@ -0,0 +1,4 @@
1
+ import type { OperationHandler, OutboxAttachmentOperationIds, OutboxDetailOperationIds, OutboxOperationIds } from "../types.js";
2
+ export declare const OutboxOperations: Record<OutboxOperationIds, OperationHandler<OutboxOperationIds>>;
3
+ export declare const OutboxDetailOperations: Record<OutboxDetailOperationIds, OperationHandler<OutboxDetailOperationIds>>;
4
+ export declare const OutboxAttachmentOperations: Record<OutboxAttachmentOperationIds, OperationHandler<OutboxAttachmentOperationIds>>;
@@ -0,0 +1,144 @@
1
+ import { ForbiddenError } from "@remit/data-ports/errors";
2
+ import { getAccountConfigIdFromEvent } from "../auth.js";
3
+ import { getClient } from "../service/data-client.js";
4
+ import { completeOutboxAttachment, mintOutboxAttachment, toOutboxAttachmentResponse, } from "./outbox-attachment.js";
5
+ const toOutboxMessageResponse = (item, attachments = []) => ({
6
+ attachments: attachments.map(toOutboxAttachmentResponse),
7
+ outboxMessageId: item.outboxMessageId,
8
+ accountId: item.accountId,
9
+ fromAddress: item.fromAddress,
10
+ fromName: item.fromName,
11
+ toAddresses: item.toAddresses,
12
+ ccAddresses: item.ccAddresses ?? [],
13
+ bccAddresses: item.bccAddresses ?? [],
14
+ subject: item.subject,
15
+ textBody: item.textBody,
16
+ htmlBody: item.htmlBody,
17
+ inReplyTo: item.inReplyTo,
18
+ references: item.references ?? [],
19
+ status: item.status,
20
+ lastError: item.lastError,
21
+ sentAt: item.sentAt,
22
+ createdAt: item.createdAt,
23
+ updatedAt: item.updatedAt,
24
+ });
25
+ export const OutboxOperations = {
26
+ OutboxOperations_createOutboxMessage: async (_context, ...args) => {
27
+ const event = args[0];
28
+ const accountConfigId = getAccountConfigIdFromEvent(event);
29
+ const input = JSON.parse(event.body ?? "{}");
30
+ const client = await getClient();
31
+ const account = await client.account.get(input.accountId);
32
+ if (account.accountConfigId !== accountConfigId) {
33
+ throw new ForbiddenError(`Account ${input.accountId} not in account config`);
34
+ }
35
+ const fromAddress = account.email;
36
+ if (input.sendImmediately) {
37
+ const outbox = await client.outboxQueue.createAndSend({
38
+ accountId: input.accountId,
39
+ accountConfigId,
40
+ fromAddress,
41
+ toAddresses: input.toAddresses,
42
+ ccAddresses: input.ccAddresses,
43
+ bccAddresses: input.bccAddresses,
44
+ subject: input.subject,
45
+ textBody: input.textBody,
46
+ htmlBody: input.htmlBody,
47
+ inReplyTo: input.inReplyTo,
48
+ references: input.references,
49
+ });
50
+ return toOutboxMessageResponse(outbox);
51
+ }
52
+ const outbox = await client.outboxQueue.createDraft({
53
+ accountId: input.accountId,
54
+ accountConfigId,
55
+ fromAddress,
56
+ toAddresses: input.toAddresses,
57
+ ccAddresses: input.ccAddresses,
58
+ bccAddresses: input.bccAddresses,
59
+ subject: input.subject,
60
+ textBody: input.textBody,
61
+ htmlBody: input.htmlBody,
62
+ inReplyTo: input.inReplyTo,
63
+ references: input.references,
64
+ });
65
+ return toOutboxMessageResponse(outbox);
66
+ },
67
+ OutboxOperations_listOutboxMessages: async (context, ...args) => {
68
+ const event = args[0];
69
+ const accountConfigId = getAccountConfigIdFromEvent(event);
70
+ const { continuationToken } = context.request.query;
71
+ const client = await getClient();
72
+ const accounts = await client.account.listAllByAccountConfig(accountConfigId);
73
+ if (accounts.length === 0) {
74
+ return { items: [], continuationToken: null };
75
+ }
76
+ const result = await client.outboxMessage.listByAccounts(accounts.map((account) => account.accountId), { continuationToken });
77
+ return {
78
+ items: result.items.map((item) => toOutboxMessageResponse(item)),
79
+ continuationToken: result.continuationToken,
80
+ };
81
+ },
82
+ };
83
+ export const OutboxDetailOperations = {
84
+ OutboxDetailOperations_getOutboxMessage: async (context, ...args) => {
85
+ const event = args[0];
86
+ const accountConfigId = getAccountConfigIdFromEvent(event);
87
+ const { outboxMessageId } = context.request.params;
88
+ const client = await getClient();
89
+ // Default mode "read": the scoped get refuses a foreign message with
90
+ // NotFound (404, no existence leak) — no separate check needed.
91
+ const outbox = await client.outboxMessage.get(accountConfigId, outboxMessageId);
92
+ return toOutboxMessageResponse(outbox, await client.outboxAttachment.listFor(accountConfigId, outboxMessageId));
93
+ },
94
+ OutboxDetailOperations_updateOutboxMessage: async (context, ...args) => {
95
+ const event = args[0];
96
+ const accountConfigId = getAccountConfigIdFromEvent(event);
97
+ const { outboxMessageId } = context.request.params;
98
+ const input = context.request.requestBody;
99
+ const client = await getClient();
100
+ // updateDraft's own scoped get uses mode "act": a foreign message is
101
+ // denied with Forbidden (403), not feigned as NotFound.
102
+ const updated = await client.outboxQueue.updateDraft(accountConfigId, outboxMessageId, {
103
+ toAddresses: input.toAddresses,
104
+ ccAddresses: input.ccAddresses,
105
+ bccAddresses: input.bccAddresses,
106
+ subject: input.subject,
107
+ textBody: input.textBody,
108
+ htmlBody: input.htmlBody,
109
+ inReplyTo: input.inReplyTo,
110
+ references: input.references,
111
+ });
112
+ // Absent means "leave the files alone" — a body that only carries a subject
113
+ // must not strip them. Present states exactly what the draft keeps, so an
114
+ // empty list is a real instruction to remove them all.
115
+ if (input.attachmentIds !== undefined) {
116
+ await client.outboxAttachment.retainOnly(accountConfigId, updated.accountId, outboxMessageId, input.attachmentIds);
117
+ }
118
+ return toOutboxMessageResponse(updated, await client.outboxAttachment.listFor(accountConfigId, outboxMessageId));
119
+ },
120
+ OutboxDetailOperations_deleteOutboxMessage: async (context, ...args) => {
121
+ const event = args[0];
122
+ const accountConfigId = getAccountConfigIdFromEvent(event);
123
+ const { outboxMessageId } = context.request.params;
124
+ const client = await getClient();
125
+ // deleteDraft's own scoped get uses mode "act": a foreign message is
126
+ // denied with Forbidden (403), not feigned as NotFound.
127
+ await client.outboxQueue.deleteDraft(accountConfigId, outboxMessageId);
128
+ return { statusCode: 204 };
129
+ },
130
+ OutboxDetailOperations_sendOutboxMessage: async (context, ...args) => {
131
+ const event = args[0];
132
+ const accountConfigId = getAccountConfigIdFromEvent(event);
133
+ const { outboxMessageId } = context.request.params;
134
+ const client = await getClient();
135
+ // send's own scoped get uses mode "act": a foreign message is denied
136
+ // with Forbidden (403), not feigned as NotFound.
137
+ const sent = await client.outboxQueue.send(accountConfigId, outboxMessageId);
138
+ return toOutboxMessageResponse(sent);
139
+ },
140
+ OutboxDetailOperations_mintOutboxAttachment: mintOutboxAttachment,
141
+ };
142
+ export const OutboxAttachmentOperations = {
143
+ OutboxAttachmentOperations_completeOutboxAttachment: completeOutboxAttachment,
144
+ };
@@ -0,0 +1,46 @@
1
+ import type { MailboxItem } from "@remit/data-ports";
2
+ /** The subset of mailbox fields needed to derive sync progress. */
3
+ export type MailboxSyncFields = Pick<MailboxItem, "lastMessageSyncAt" | "initialSyncCompletedAt" | "messageCount" | "lastSyncUid" | "highWaterMarkUid">;
4
+ /** A mailbox row, as far as the sync-status projection reads it. */
5
+ export type MailboxProgressFields = MailboxSyncFields & Pick<MailboxItem, "mailboxId" | "fullPath">;
6
+ /** The per-mailbox entry `getSyncStatus` returns. */
7
+ export interface MailboxSyncProgress {
8
+ mailboxId: string;
9
+ fullPath: string;
10
+ phase: string;
11
+ messagesTotal: number;
12
+ messagesSynced: number;
13
+ highWaterMarkUid: number;
14
+ lastSyncedAt: number | undefined;
15
+ }
16
+ /**
17
+ * Derive a per-mailbox sync phase from worker-written sync state.
18
+ *
19
+ * - pending: never synced (lastMessageSyncAt is 0 / unset)
20
+ * - complete: the imap-worker drained the mailbox and stamped
21
+ * initialSyncCompletedAt after the last batch write
22
+ * (initialSyncCompletedAt >= lastMessageSyncAt), or the
23
+ * mailbox is empty
24
+ * - syncing: sync has started but is not yet complete
25
+ *
26
+ * Note: UID watermarks alone cannot express completion — backfill settles
27
+ * at the mailbox's smallest real UID (UIDs are sparse), so the completion
28
+ * marker written by the worker is the source of truth.
29
+ */
30
+ export declare const deriveMailboxPhase: (mailbox: MailboxSyncFields) => string;
31
+ /**
32
+ * Compute approximate messagesSynced from UID watermarks.
33
+ *
34
+ * UIDs are sparse (not necessarily contiguous), so this is an approximation.
35
+ * We clamp the result to [0, messagesTotal].
36
+ */
37
+ export declare const computeMessagesSynced: (mailbox: MailboxSyncFields) => number;
38
+ /**
39
+ * Project a mailbox row into its sync-status entry.
40
+ *
41
+ * `highWaterMarkUid` is carried through because a total cannot tell an arrival
42
+ * from a departure: a message landing in the same sync round that another one
43
+ * leaves nets to zero, and the client's freshness check saw nothing (#771). The
44
+ * highest UID only ever moves up, so it names the arrival on its own.
45
+ */
46
+ export declare const toMailboxSyncProgress: (mailbox: MailboxProgressFields) => MailboxSyncProgress;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Derive a per-mailbox sync phase from worker-written sync state.
3
+ *
4
+ * - pending: never synced (lastMessageSyncAt is 0 / unset)
5
+ * - complete: the imap-worker drained the mailbox and stamped
6
+ * initialSyncCompletedAt after the last batch write
7
+ * (initialSyncCompletedAt >= lastMessageSyncAt), or the
8
+ * mailbox is empty
9
+ * - syncing: sync has started but is not yet complete
10
+ *
11
+ * Note: UID watermarks alone cannot express completion — backfill settles
12
+ * at the mailbox's smallest real UID (UIDs are sparse), so the completion
13
+ * marker written by the worker is the source of truth.
14
+ */
15
+ export const deriveMailboxPhase = (mailbox) => {
16
+ if (!mailbox.lastMessageSyncAt)
17
+ return "pending";
18
+ const messageCount = mailbox.messageCount ?? 0;
19
+ const initialSyncCompletedAt = mailbox.initialSyncCompletedAt ?? 0;
20
+ // Completion marker is fresh: no batch was written after the worker
21
+ // recorded the mailbox as drained.
22
+ if (initialSyncCompletedAt > 0 &&
23
+ initialSyncCompletedAt >= mailbox.lastMessageSyncAt) {
24
+ return "complete";
25
+ }
26
+ // Also complete if messageCount is 0 (empty mailbox)
27
+ if (messageCount === 0)
28
+ return "complete";
29
+ return "syncing";
30
+ };
31
+ /**
32
+ * Compute approximate messagesSynced from UID watermarks.
33
+ *
34
+ * UIDs are sparse (not necessarily contiguous), so this is an approximation.
35
+ * We clamp the result to [0, messagesTotal].
36
+ */
37
+ export const computeMessagesSynced = (mailbox) => {
38
+ const messagesTotal = mailbox.messageCount ?? 0;
39
+ const lastSyncUid = mailbox.lastSyncUid ?? 0;
40
+ const highWaterMarkUid = mailbox.highWaterMarkUid ?? 0;
41
+ if (highWaterMarkUid === 0)
42
+ return 0;
43
+ // UID range approximation: number of UIDs in the synced range
44
+ const synced = highWaterMarkUid - lastSyncUid + 1;
45
+ return Math.min(Math.max(synced, 0), messagesTotal);
46
+ };
47
+ /**
48
+ * Project a mailbox row into its sync-status entry.
49
+ *
50
+ * `highWaterMarkUid` is carried through because a total cannot tell an arrival
51
+ * from a departure: a message landing in the same sync round that another one
52
+ * leaves nets to zero, and the client's freshness check saw nothing (#771). The
53
+ * highest UID only ever moves up, so it names the arrival on its own.
54
+ */
55
+ export const toMailboxSyncProgress = (mailbox) => ({
56
+ mailboxId: mailbox.mailboxId,
57
+ fullPath: mailbox.fullPath,
58
+ phase: deriveMailboxPhase(mailbox),
59
+ messagesTotal: mailbox.messageCount ?? 0,
60
+ messagesSynced: computeMessagesSynced(mailbox),
61
+ highWaterMarkUid: mailbox.highWaterMarkUid ?? 0,
62
+ lastSyncedAt: mailbox.lastMessageSyncAt || undefined,
63
+ });
@@ -0,0 +1,28 @@
1
+ import type { SQSClient } from "@aws-sdk/client-sqs";
2
+ import { type FireAndForgetLogger } from "../service/fire-and-forget.js";
3
+ import type { OperationHandler, SyncOperationIds } from "../types.js";
4
+ interface SyncTriggerDeps {
5
+ sqsClient: SQSClient;
6
+ queueUrl: string;
7
+ logger: FireAndForgetLogger & {
8
+ info: (fields: Record<string, unknown>, message: string) => void;
9
+ };
10
+ }
11
+ /**
12
+ * Fire-and-forget enqueue for the POST /sync kick.
13
+ *
14
+ * POST /sync is a best-effort nudge: the web client polls it to wake a sync and
15
+ * acts on no enqueue result in the response. So the enqueue must never fail —
16
+ * nor leak onto — this request or any concurrent read when the queue is
17
+ * unreachable. Previously this handler `await`ed the SQS send directly; with the
18
+ * queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
19
+ * the handler's promise and landed on whatever read was in flight on the shared
20
+ * event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
21
+ *
22
+ * Routing it through fireAndForget catches the rejection at the source, logs it
23
+ * loudly with the alertable structured fields, and resolves to void. A genuine
24
+ * account-lookup / ownership failure still propagates and 500s as normal.
25
+ */
26
+ export declare const triggerSyncSafe: (accountId: string, accountConfigId: string, deps?: SyncTriggerDeps) => Promise<void>;
27
+ export declare const SyncOperations: Record<SyncOperationIds, OperationHandler<SyncOperationIds>>;
28
+ export {};
@@ -0,0 +1,82 @@
1
+ import { logger } from "@remit/logger";
2
+ import { ownedAccount } from "@remit/mailbox-service";
3
+ import { env } from "expect-env";
4
+ import { getAccountConfigIdFromEvent } from "../auth.js";
5
+ import { getClient } from "../service/data-client.js";
6
+ import { fireAndForget, } from "../service/fire-and-forget.js";
7
+ import { unwrap } from "../service/refusal.js";
8
+ import { sqsClient } from "../service/sqs.js";
9
+ import { triggerAccountSync } from "../service/trigger-sync.js";
10
+ import { toMailboxSyncProgress } from "./sync-progress.js";
11
+ const defaultSyncTriggerDeps = () => ({
12
+ sqsClient,
13
+ queueUrl: env.SQS_QUEUE_URL,
14
+ logger,
15
+ });
16
+ /**
17
+ * Fire-and-forget enqueue for the POST /sync kick.
18
+ *
19
+ * POST /sync is a best-effort nudge: the web client polls it to wake a sync and
20
+ * acts on no enqueue result in the response. So the enqueue must never fail —
21
+ * nor leak onto — this request or any concurrent read when the queue is
22
+ * unreachable. Previously this handler `await`ed the SQS send directly; with the
23
+ * queue down (smoke/e2e, or an SQS outage in prod) the awaited rejection escaped
24
+ * the handler's promise and landed on whatever read was in flight on the shared
25
+ * event loop, 500-ing unrelated `/mailboxes`, `/threads` and `/outbox` reads.
26
+ *
27
+ * Routing it through fireAndForget catches the rejection at the source, logs it
28
+ * loudly with the alertable structured fields, and resolves to void. A genuine
29
+ * account-lookup / ownership failure still propagates and 500s as normal.
30
+ */
31
+ export const triggerSyncSafe = async (accountId, accountConfigId, deps = defaultSyncTriggerDeps()) => {
32
+ await fireAndForget(async () => {
33
+ const { eventId } = await triggerAccountSync({
34
+ sqsClient: deps.sqsClient,
35
+ queueUrl: deps.queueUrl,
36
+ accountId,
37
+ // POST /sync asks for a sync of this account by name — the refresh
38
+ // control, pull-to-refresh, and the client's automatic poll all land
39
+ // here — so it syncs every mailbox regardless of how recently one
40
+ // ran.
41
+ explicitRequest: true,
42
+ });
43
+ deps.logger.info({ accountId, eventId }, "Sync triggered - enqueued SYNC_MAILBOXES event");
44
+ }, {
45
+ source: "trigger_sync",
46
+ message: "Failed to enqueue SYNC_MAILBOXES on POST /sync (best-effort)",
47
+ ids: { accountId, accountConfigId },
48
+ logger: deps.logger,
49
+ });
50
+ };
51
+ export const SyncOperations = {
52
+ SyncOperations_triggerSync: async (context, ...args) => {
53
+ const event = args[0];
54
+ const accountConfigId = getAccountConfigIdFromEvent(event);
55
+ const { accountId } = context.request.params;
56
+ const account = await (await getClient()).account.get(accountId);
57
+ unwrap(ownedAccount(account, accountConfigId, "act"));
58
+ void triggerSyncSafe(account.accountId, accountConfigId);
59
+ return {
60
+ triggered: true,
61
+ message: `Sync triggered for account ${accountId}`,
62
+ };
63
+ },
64
+ SyncOperations_getSyncStatus: async (context, ...args) => {
65
+ const event = args[0];
66
+ const accountConfigId = getAccountConfigIdFromEvent(event);
67
+ const { accountId } = context.request.params;
68
+ const client = await getClient();
69
+ const account = await client.account.get(accountId);
70
+ unwrap(ownedAccount(account, accountConfigId, "read"));
71
+ // Collect all mailboxes (read-only — does not write or contend with sync lock)
72
+ const mailboxes = await client.mailbox.listAllByAccount(accountId);
73
+ return {
74
+ accountId: account.accountId,
75
+ syncPhase: account.syncPhase,
76
+ mailboxCountTotal: account.mailboxCountTotal,
77
+ mailboxCountSynced: account.mailboxCountSynced,
78
+ lastSyncAt: account.lastSyncAt,
79
+ mailboxes: mailboxes.map(toMailboxSyncProgress),
80
+ };
81
+ },
82
+ };
@@ -0,0 +1,2 @@
1
+ import type { OperationHandler, SystemOperationIds } from "../types.js";
2
+ export declare const SystemOperations: Record<SystemOperationIds, OperationHandler<SystemOperationIds>>;