@remit/backend 0.0.142 → 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 -521
  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,207 @@
1
+ import { logger } from "@remit/logger";
2
+ import { BodySyncQueueService, BodySyncService, createConnection, FlagPushService, FlagQueueService, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, PlacementMoveService, SpamReportService, SyncWarningService, } from "@remit/mailbox-service";
3
+ import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
4
+ import { createCachedDataKeyProvider, createKmsDataKeyProvider, createSecretsService, deserializeEncryptedPayload, FAKE_KMS_KEY_ID, } from "@remit/secrets-service";
5
+ import { createStorageService } from "@remit/storage-service/s3";
6
+ import { env } from "expect-env";
7
+ const buildConnectionScope = (accountService, secretsService) => async (accountId) => {
8
+ const account = await accountService.get(accountId);
9
+ if (!account.passwordHash) {
10
+ throw new Error(`Account ${accountId} has no passwordHash — OAuth accounts cannot use this connection path`);
11
+ }
12
+ const password = await secretsService.decrypt(deserializeEncryptedPayload(JSON.parse(account.passwordHash)));
13
+ let connection = null;
14
+ let connectPromise = null;
15
+ const getConnection = async () => {
16
+ if (connectPromise) {
17
+ return connectPromise;
18
+ }
19
+ const conn = createConnection({
20
+ user: account.username,
21
+ credentials: { kind: "password", password },
22
+ host: account.imapHost,
23
+ port: account.imapPort,
24
+ tls: account.imapTls,
25
+ });
26
+ connection = conn;
27
+ connectPromise = conn.connect().then(() => conn);
28
+ return connectPromise;
29
+ };
30
+ const disconnect = async () => {
31
+ if (connection) {
32
+ await connection.disconnect();
33
+ connection = null;
34
+ connectPromise = null;
35
+ }
36
+ };
37
+ return { getConnection, disconnect };
38
+ };
39
+ // The body queue lives on the worker infra; the API only knows it locally
40
+ // (dev / e2e set SQS_QUEUE_URL_BODY). When unset the read path still returns a
41
+ // retryable 202 — it just cannot re-arm the cue itself.
42
+ const buildBodySyncQueue = () => {
43
+ const bodyQueueUrl = process.env.SQS_QUEUE_URL_BODY;
44
+ if (!bodyQueueUrl)
45
+ return undefined;
46
+ return new BodySyncQueueService({ sqsQueueUrl: bodyQueueUrl, logger });
47
+ };
48
+ // Everything a RemitClient needs that is chosen from the environment and does
49
+ // not vary by data backend: storage, secrets, the SQS queue URLs, and
50
+ // the optional body-sync queue. Each composition root builds its backend's
51
+ // repositories and merges them with this.
52
+ export const buildSharedDeps = () => {
53
+ const storage = createStorageService();
54
+ const kmsKeyId = process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID;
55
+ const dataKeyProvider = createCachedDataKeyProvider(createKmsDataKeyProvider(kmsKeyId));
56
+ const secrets = createSecretsService(dataKeyProvider);
57
+ const sqsQueueUrl = env.SQS_QUEUE_URL;
58
+ const sqsSmtpQueueUrl = process.env.SQS_QUEUE_URL_SMTP ?? sqsQueueUrl;
59
+ return {
60
+ storage,
61
+ secrets,
62
+ sqsQueueUrl,
63
+ sqsSmtpQueueUrl,
64
+ bodySyncQueue: buildBodySyncQueue(),
65
+ };
66
+ };
67
+ // The read-path body backfill (describeMessage / getRawMessage →
68
+ // fetchAndGetBody) materializes a body that was never sync-synced, and must run
69
+ // the account's standing filters the same way the imap-worker's sync path does.
70
+ // Without this, a message a user opens before background body-sync is classified
71
+ // but never filter-evaluated — and once its body is stored the filter-capable
72
+ // sync path skips it, so an explicit standing filter silently never fires (issue
73
+ // #223). Only the explicit user-rule half is wired here; heuristic classifier
74
+ // placement stays on the sync path. It shares `buildFilterConfig` with
75
+ // `sync-message-body.ts`, so a filter fires on either (issue #298). Over the
76
+ // message-management queue's
77
+ // local-first PlacementMoveService; filters stay off when that queue is unset.
78
+ export const buildReadPathFilterConfig = (repositories) => {
79
+ const placementMoveQueueUrl = process.env.SQS_QUEUE_URL_MESSAGE_MGMT;
80
+ return buildFilterConfig({
81
+ filterService: repositories.filter,
82
+ messageLabelService: repositories.messageLabel,
83
+ mailboxService: repositories.mailbox,
84
+ placementMoveService: placementMoveQueueUrl
85
+ ? new PlacementMoveService({
86
+ messageService: repositories.message,
87
+ threadMessageService: repositories.threadMessage,
88
+ markerService: repositories.placementMove,
89
+ addressService: repositories.address,
90
+ mailboxSpecialUseService: repositories.mailboxSpecialUse,
91
+ mailboxService: repositories.mailbox,
92
+ sqsQueueUrl: placementMoveQueueUrl,
93
+ })
94
+ : undefined,
95
+ });
96
+ };
97
+ // Backend-neutral composition root: given repositories (from any data-ports
98
+ // implementation) and the shared services, wire the domain and queue services
99
+ // and assemble a RemitClient. Imports neither ElectroDB nor Drizzle — the
100
+ // caller chooses the backend.
101
+ export const createRemitClient = (deps) => {
102
+ const { repositories, storage, secrets, sqsQueueUrl, sqsSmtpQueueUrl, bodySyncQueue, } = deps;
103
+ const filterConfig = buildReadPathFilterConfig(repositories);
104
+ const flagPushService = new FlagPushService({
105
+ markerService: repositories.flagPush,
106
+ sqsQueueUrl,
107
+ logger,
108
+ });
109
+ // Shared with the returned `flagQueue` below (issue #302): body sync
110
+ // auto-marks-read a message from an unsubscribed sender through the exact
111
+ // same `FlagQueueService.markAsRead` a manual mark-as-read goes through, not
112
+ // a second instance or a second primitive.
113
+ const flagQueueService = new FlagQueueService({
114
+ messageFlagService: repositories.messageFlag,
115
+ messageService: repositories.message,
116
+ threadMessageService: repositories.threadMessage,
117
+ flagPushService,
118
+ logger,
119
+ });
120
+ const messageMoveService = new MessageMoveService({
121
+ messageService: repositories.message,
122
+ mailboxService: repositories.mailbox,
123
+ mailboxSpecialUseService: repositories.mailboxSpecialUse,
124
+ threadMessageService: repositories.threadMessage,
125
+ addressService: repositories.address,
126
+ sqsQueueUrl,
127
+ logger,
128
+ });
129
+ // One instance, shared with the queue service below: the per-draft chain that
130
+ // makes the attachment cap safe under concurrency lives on it, and a discard
131
+ // racing an upload has to take the same lock the upload does.
132
+ const outboxAttachmentService = new OutboxAttachmentService({
133
+ outboxMessageService: repositories.outboxMessage,
134
+ outboxAttachmentService: repositories.outboxAttachment,
135
+ storage,
136
+ });
137
+ const bodySync = new BodySyncService(repositories.message, storage, repositories.threadMessage, repositories.address, repositories.envelope, new SyncWarningService(repositories.syncWarning), logger, undefined, filterConfig, undefined, { flagQueueService },
138
+ // The read-path backfill materializes a body the sync path never got to,
139
+ // so it is a message's first sight as much as the sync pass is — and an
140
+ // invitation the user opens before background sync must still get its
141
+ // card (issue #1033, the same argument as the filter wiring above).
142
+ {
143
+ calendarSuggestionService: repositories.calendarSuggestion,
144
+ calendarStore: repositories.calendarStore,
145
+ filterService: repositories.filter,
146
+ });
147
+ return {
148
+ accountConfig: repositories.accountConfig,
149
+ account: repositories.account,
150
+ accountSetting: repositories.accountSetting,
151
+ address: repositories.address,
152
+ mailbox: repositories.mailbox,
153
+ mailboxSpecialUse: repositories.mailboxSpecialUse,
154
+ mailboxLock: repositories.mailboxLock,
155
+ message: repositories.message,
156
+ messageFlag: repositories.messageFlag,
157
+ outboxMessage: repositories.outboxMessage,
158
+ threadMessage: repositories.threadMessage,
159
+ envelope: repositories.envelope,
160
+ accountExportRequest: repositories.accountExportRequest,
161
+ configImport: repositories.configImport,
162
+ quarantine: repositories.quarantine,
163
+ syncWarning: repositories.syncWarning,
164
+ organizeJobRequest: repositories.organizeJobRequest,
165
+ filter: repositories.filter,
166
+ label: repositories.label,
167
+ messageLabel: repositories.messageLabel,
168
+ senderSignerStanding: repositories.senderSignerStanding,
169
+ calendarCollection: repositories.calendarCollection,
170
+ calendarObject: repositories.calendarObject,
171
+ calendarEventIndex: repositories.calendarEventIndex,
172
+ calendarFeedToken: repositories.calendarFeedToken,
173
+ calendarSuggestion: repositories.calendarSuggestion,
174
+ calendarStore: repositories.calendarStore,
175
+ storage,
176
+ secrets,
177
+ bodySync,
178
+ bodySyncQueue,
179
+ placementMove: repositories.placementMove,
180
+ flagPush: repositories.flagPush,
181
+ flagQueue: flagQueueService,
182
+ mailboxQueue: new MailboxQueueService({
183
+ mailboxService: repositories.mailbox,
184
+ sqsQueueUrl,
185
+ logger,
186
+ }),
187
+ messageMove: messageMoveService,
188
+ outboxAttachment: outboxAttachmentService,
189
+ outboxQueue: new OutboxQueueService({
190
+ outboxMessageService: repositories.outboxMessage,
191
+ outboxAttachmentService,
192
+ accountService: repositories.account,
193
+ sqsSmtpQueueUrl,
194
+ logger,
195
+ }),
196
+ spamReport: new SpamReportService({
197
+ messageService: repositories.message,
198
+ addressService: repositories.address,
199
+ accountService: repositories.account,
200
+ mailboxSpecialUseService: repositories.mailboxSpecialUse,
201
+ messageMoveService,
202
+ flagPushService,
203
+ logger,
204
+ }),
205
+ createConnectionScope: buildConnectionScope(repositories.account, secrets),
206
+ };
207
+ };
@@ -0,0 +1,12 @@
1
+ import type { RemitClient } from "./create-remit-client.js";
2
+ export type { ConnectionScope, RemitClient, } from "./create-remit-client.js";
3
+ /**
4
+ * Register the client from the composition root. An adapter that lives outside
5
+ * this shared, open-core module — DynamoDB, or a Postgres adapter in the
6
+ * repository that deploys it — is never imported here; its composition root
7
+ * calls this before handling a request.
8
+ */
9
+ export declare const setClient: (client: RemitClient) => void;
10
+ export declare const getClient: () => Promise<RemitClient>;
11
+ /** Reset the singleton and any injected client — test use only. */
12
+ export declare const _resetForTest: () => void;
@@ -0,0 +1,44 @@
1
+ let clientPromise = null;
2
+ let injected = null;
3
+ /**
4
+ * Register the client from the composition root. An adapter that lives outside
5
+ * this shared, open-core module — DynamoDB, or a Postgres adapter in the
6
+ * repository that deploys it — is never imported here; its composition root
7
+ * calls this before handling a request.
8
+ */
9
+ export const setClient = (client) => {
10
+ injected = client;
11
+ clientPromise = null;
12
+ };
13
+ /**
14
+ * The SQLite composition this build contains, reached only by a process that
15
+ * registered no client of its own. The `import()` is dynamic so a bundler
16
+ * following the entry graph of a process that injects its client never reaches
17
+ * `@remit/drizzle-service` or drizzle-orm (both `external` for the Lambda
18
+ * esbuild build).
19
+ *
20
+ * `SQLITE_APP_DB_PATH` is the precondition the composition needs, checked ahead of
21
+ * the import so its absence names `setClient` instead of failing on module
22
+ * resolution inside a bundle, or — worse — opening an empty database and
23
+ * serving an empty mailbox. It is a precondition, not a backend selection.
24
+ */
25
+ const buildRelationalClient = async () => {
26
+ if (!process.env.SQLITE_APP_DB_PATH) {
27
+ throw new Error("no client registered — register one with setClient() from your composition root");
28
+ }
29
+ const composition = await import("./compose-sqlite.js");
30
+ return composition.buildSqliteClient();
31
+ };
32
+ export const getClient = () => {
33
+ if (!clientPromise) {
34
+ clientPromise = injected
35
+ ? Promise.resolve(injected)
36
+ : buildRelationalClient();
37
+ }
38
+ return clientPromise;
39
+ };
40
+ /** Reset the singleton and any injected client — test use only. */
41
+ export const _resetForTest = () => {
42
+ clientPromise = null;
43
+ injected = null;
44
+ };
@@ -0,0 +1,3 @@
1
+ import type { ConfigExportIdentity } from "@remit/config-transfer";
2
+ /** Who wrote a configuration file, and from where. Provenance only. */
3
+ export declare const exportIdentity: (now?: Date) => ConfigExportIdentity;
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const DEFAULT_CONTROL_DIR = "/data/control";
4
+ /**
5
+ * The version a file says it was written by. The updater owns this fact: it
6
+ * reads the tag from `.env` and writes it into `state.json`, which is the only
7
+ * honest source across an update that rewrites that tag underneath a
8
+ * long-lived process. With no state file there is nothing authoritative to
9
+ * stamp, and an export says so rather than inventing a version an importing
10
+ * reader would then trust.
11
+ */
12
+ const runningVersion = () => {
13
+ const dir = process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
14
+ let raw;
15
+ try {
16
+ raw = readFileSync(join(dir, "state.json"), "utf8");
17
+ }
18
+ catch {
19
+ return "unknown";
20
+ }
21
+ const state = JSON.parse(raw);
22
+ if (typeof state !== "object" || state === null)
23
+ return "unknown";
24
+ const version = state.currentVersion;
25
+ return typeof version === "string" && version.length > 0
26
+ ? version
27
+ : "unknown";
28
+ };
29
+ /** Who wrote a configuration file, and from where. Provenance only. */
30
+ export const exportIdentity = (now = new Date()) => ({
31
+ app: "reader",
32
+ version: runningVersion(),
33
+ exportedAt: now.toISOString(),
34
+ instance: process.env.PUBLIC_ORIGIN ?? "",
35
+ });
@@ -0,0 +1,54 @@
1
+ type StructuredLog = (fields: Record<string, unknown>, message: string) => void;
2
+ export interface FireAndForgetLogger {
3
+ error: StructuredLog;
4
+ }
5
+ export interface FireAndForgetContext {
6
+ /**
7
+ * Stable discriminator for the originating call site (e.g. `"config_load"`,
8
+ * `"account_create"`). Surfaces on the log line as `source` so a CloudWatch
9
+ * metric filter / alarm can attribute a leak to its origin.
10
+ */
11
+ source: string;
12
+ /**
13
+ * Message logged when the background work rejects. Should describe the work
14
+ * and reassure that the failure is contained (e.g. "...best-effort").
15
+ */
16
+ message: string;
17
+ /**
18
+ * Identifiers carried onto the structured error line (accountId,
19
+ * accountConfigId, …) so a failure is traceable without a stack dive.
20
+ */
21
+ ids?: Record<string, unknown>;
22
+ /**
23
+ * Alertable discriminator on the structured error line, the field CloudWatch
24
+ * metric filters key on. Defaults to `"sync_trigger_failed"` for the original
25
+ * sync-kick sites; a different fire-and-forget surface (e.g. a flag enqueue)
26
+ * must pass its own so it is not mis-attributed to the sync-trigger alarm.
27
+ */
28
+ alert?: string;
29
+ logger?: FireAndForgetLogger;
30
+ }
31
+ /**
32
+ * Run unawaited background work that MUST NOT be able to fail the request that
33
+ * spawned it.
34
+ *
35
+ * The dev/Lambda runtime shares one event loop across concurrent requests, so a
36
+ * rejection escaping an unawaited background promise (a `void trigger()`) lands
37
+ * on whatever request happens to be in flight when the deferred work finally
38
+ * fails — turning an unrelated READ into a spurious 500. That is exactly how an
39
+ * unreachable SQS queue 500'd `/mailboxes`, `/threads`, `/outbox` and `/config`.
40
+ *
41
+ * This helper is the single containment point: it awaits the work inside a
42
+ * try/catch, logs every rejection LOUDLY with the alertable structured fields
43
+ * (`alert` — defaulting to `"sync_trigger_failed"`, `source`, the SDK error
44
+ * name/code, the caller ids), and ALWAYS resolves to void. Callers `void
45
+ * fireAndForget(...)` with no
46
+ * chance of an unhandled rejection escaping. Routing all fire-and-forget sites
47
+ * through here keeps the containment from regressing one handler at a time.
48
+ *
49
+ * Use this ONLY for genuine side effects whose failure must not change the
50
+ * response. A failure the caller is responsible for (the real DynamoDB read, an
51
+ * enqueue that IS the request's purpose) must still propagate and 500.
52
+ */
53
+ export declare const fireAndForget: (work: () => Promise<unknown>, context: FireAndForgetContext) => Promise<void>;
54
+ export {};
@@ -0,0 +1,38 @@
1
+ import { inspect } from "node:util";
2
+ import { logger } from "@remit/logger";
3
+ /**
4
+ * Run unawaited background work that MUST NOT be able to fail the request that
5
+ * spawned it.
6
+ *
7
+ * The dev/Lambda runtime shares one event loop across concurrent requests, so a
8
+ * rejection escaping an unawaited background promise (a `void trigger()`) lands
9
+ * on whatever request happens to be in flight when the deferred work finally
10
+ * fails — turning an unrelated READ into a spurious 500. That is exactly how an
11
+ * unreachable SQS queue 500'd `/mailboxes`, `/threads`, `/outbox` and `/config`.
12
+ *
13
+ * This helper is the single containment point: it awaits the work inside a
14
+ * try/catch, logs every rejection LOUDLY with the alertable structured fields
15
+ * (`alert` — defaulting to `"sync_trigger_failed"`, `source`, the SDK error
16
+ * name/code, the caller ids), and ALWAYS resolves to void. Callers `void
17
+ * fireAndForget(...)` with no
18
+ * chance of an unhandled rejection escaping. Routing all fire-and-forget sites
19
+ * through here keeps the containment from regressing one handler at a time.
20
+ *
21
+ * Use this ONLY for genuine side effects whose failure must not change the
22
+ * response. A failure the caller is responsible for (the real DynamoDB read, an
23
+ * enqueue that IS the request's purpose) must still propagate and 500.
24
+ */
25
+ export const fireAndForget = async (work, context) => {
26
+ await work().catch((error) => {
27
+ const log = context.logger ?? logger;
28
+ log.error({
29
+ alert: context.alert ?? "sync_trigger_failed",
30
+ source: context.source,
31
+ ...context.ids,
32
+ errorName: error?.name,
33
+ errorCode: error?.Code ??
34
+ error?.code,
35
+ error: inspect(error),
36
+ }, context.message);
37
+ });
38
+ };
@@ -0,0 +1,184 @@
1
+ import type { FilterItem, OrganizeJobRequestItem } from "@remit/data-ports";
2
+ import { type FilterMessage, type LiteralClauseNarrowing, PlacementMoveService } from "@remit/mailbox-service";
3
+ import type { RemitClient } from "./data-client.js";
4
+ /**
5
+ * Hard cap on both the previewed and the applied set. A back-apply is a
6
+ * data-heavy corpus pass (remit-data-heavy: frugal), bounded by counting
7
+ * MATCHES as it pages a store-side narrowed listing, and the final match set
8
+ * never exceeds this. It bounds the answer, never the corpus the question is
9
+ * asked of (#459).
10
+ */
11
+ export declare const ORGANIZE_MATCH_LIMIT = 500;
12
+ /**
13
+ * The predicate + action for one back-apply pass — the OrganizeInput fields,
14
+ * flattened. The action fields carry absence as the `"None"` sentinel (RFC 034
15
+ * Decision 3.1).
16
+ */
17
+ export interface OrganizePredicate {
18
+ matchOperator: FilterItem["matchOperator"];
19
+ literalClauses: FilterItem["literalClauses"];
20
+ actionLabelId: string;
21
+ actionMailboxId: string;
22
+ }
23
+ /**
24
+ * The predicate a back-apply job snapshotted onto its row, read back verbatim so
25
+ * the worker runs exactly what the request asked for.
26
+ */
27
+ export declare const predicateFromJob: (job: OrganizeJobRequestItem) => OrganizePredicate;
28
+ /**
29
+ * One corpus message projected onto the fields a literal clause matches
30
+ * against, paired with its id. Sourced from the core thread rows.
31
+ */
32
+ export interface OrganizeCandidate {
33
+ messageId: string;
34
+ message: FilterMessage;
35
+ }
36
+ /**
37
+ * One page of the narrowed corpus, cursored exactly as the underlying listing
38
+ * is: an absent `continuationToken` means the store has no more rows matching
39
+ * the terms, never that a read window ran out.
40
+ */
41
+ export interface OrganizeCandidatePage {
42
+ items: OrganizeCandidate[];
43
+ continuationToken?: string;
44
+ }
45
+ /** What to ask the store for, and where to resume. */
46
+ export interface OrganizeCandidateQuery extends LiteralClauseNarrowing {
47
+ limit: number;
48
+ continuationToken?: string;
49
+ mailboxIds?: readonly string[];
50
+ }
51
+ /**
52
+ * Where a pass looks and which standing filter it runs for. A filter
53
+ * back-apply narrows the corpus to the inbox and matches a body-content clause
54
+ * on the stored preview; an "all like these" pass sets neither.
55
+ */
56
+ export interface OrganizeMatchScope {
57
+ filterId?: string;
58
+ mailboxIds?: readonly string[];
59
+ }
60
+ export interface OrganizeMatchDeps {
61
+ /**
62
+ * One page of the account's messages as literal-match candidates, with the
63
+ * clause terms evaluated by the store. Paged rather than capped, so the
64
+ * caller's bound falls on matches instead of on how far back the read
65
+ * reached (#459).
66
+ */
67
+ listAccountFilterMessages: (accountConfigId: string, query: OrganizeCandidateQuery) => Promise<OrganizeCandidatePage>;
68
+ }
69
+ /**
70
+ * Why the matcher refused a predicate. A refusal is an expected outcome of
71
+ * asking for a rule this deployment cannot evaluate, not a fault: the HTTP
72
+ * boundary words it as a 400 and the back-apply worker fails the job row on it,
73
+ * neither of which may treat it as the infrastructure failure a throw would
74
+ * make it (reader #463).
75
+ */
76
+ export interface OrganizeRejection {
77
+ reason: "BodyContentUnsupported" | "FilterNotActive" | "FilterMissing" | "FolderMissing";
78
+ message: string;
79
+ }
80
+ /** The matched ids. */
81
+ export interface OrganizeMatched {
82
+ rejected: null;
83
+ messageIds: string[];
84
+ }
85
+ /** The predicate was refused: nothing matched, and nothing may be applied. */
86
+ export interface OrganizeMatchRejected {
87
+ rejected: OrganizeRejection;
88
+ }
89
+ export type OrganizeMatchResult = OrganizeMatched | OrganizeMatchRejected;
90
+ /**
91
+ * A `HasWords` clause matches the message body (RFC 031, `clauseMatches`). The
92
+ * live index-time filter evaluates it against the full parsed body
93
+ * (`body-sync.ts` `toFilterMessage`); the corpus slice carries only
94
+ * `From`/`Subject` at full fidelity and no faithful body, so it cannot serve a
95
+ * body-content clause without silently narrowing the match to whatever preview
96
+ * happened to be stored. A body-content clause on an "all like these" pass is
97
+ * refused instead of returning a wrong set.
98
+ */
99
+ export declare const BODY_CONTENT_REJECTION_MESSAGE = "Organize cannot match a body-content (HasWords) clause over the mailbox: it reads only each message's preview, not its body";
100
+ /**
101
+ * The refusal a predicate carries on its own, decidable without reading the
102
+ * corpus: a body-content clause is refused up front. `createOrganizeJob` runs
103
+ * this before enqueueing and {@link matchOrganize} runs it too, so preview, the
104
+ * worker and the request boundary all refuse the same rule (reader #463).
105
+ */
106
+ export declare const organizePredicateRejection: (predicate: OrganizePredicate) => OrganizeRejection | null;
107
+ /**
108
+ * The matcher shared by preview and apply — the previewed set equals the applied
109
+ * set for the same input. Read-only. Returns the matching message ids, bounded.
110
+ *
111
+ * - An ad-hoc predicate with no clause matches nothing: a run-now request
112
+ * carries none, and an empty predicate must never sweep the whole mailbox.
113
+ * - A predicate the corpus read cannot evaluate (a body-content clause on an
114
+ * "all like these" pass, see {@link bodyContentRejection}) comes back as a
115
+ * rejection, so a caller can tell a refused rule from a broken deployment
116
+ * (reader #463).
117
+ */
118
+ export declare const matchOrganize: (deps: OrganizeMatchDeps, accountConfigId: string, predicate: OrganizePredicate, limit?: number, scope?: OrganizeMatchScope) => Promise<OrganizeMatchResult>;
119
+ /** The matcher deps for the running backend/worker. */
120
+ export declare const buildOrganizeMatchDeps: (client: RemitClient) => OrganizeMatchDeps;
121
+ /**
122
+ * The exclusive-move arm of a back-apply, wired exactly like the body-sync
123
+ * placement mover (`sync-message-body.ts`): the local-first
124
+ * {@link PlacementMoveService} over the shared message-management queue. A move
125
+ * commits locally (marker + ThreadMessage + Message row) and enqueues a
126
+ * `PLACEMENT_MOVE_PUSH` for the reconciler; its marker state engine makes a
127
+ * redelivered job idempotent (mirrors #1297). Returns `undefined` when no
128
+ * message-management queue is wired — the same gate body sync uses to keep the
129
+ * move path off — so a move requested in that environment is counted as failed
130
+ * rather than silently dropped (see {@link applyOrganize}).
131
+ */
132
+ export declare const buildOrganizeMoveService: (client: RemitClient) => PlacementMoveService | undefined;
133
+ export interface ApplyOrganizeDeps {
134
+ client: RemitClient;
135
+ moveService?: PlacementMoveService;
136
+ }
137
+ export interface ApplyOrganizeResult {
138
+ applied: number;
139
+ failed: number;
140
+ /** Why each failed message failed, as the job reports it. */
141
+ failures: string[];
142
+ }
143
+ /**
144
+ * Apply the back-apply action to every matched message, reusing the index-time
145
+ * apply plumbing: an idempotent MessageLabel upsert (additive) with
146
+ * `appliedByFilterId` deliberately ABSENT — this path attributes to no filter,
147
+ * exactly like a hand-applied label (RFC 034 Decision 3.3) — and an idempotent
148
+ * folder move (exclusive). One poisoned message never fails the batch; it is
149
+ * counted as failed and the pass continues.
150
+ *
151
+ * Every requested move happens. A standing filter acts at exactly two moments —
152
+ * when a message is first seen, and when the user presses "run rules now" — and
153
+ * holds no claim on that message afterwards, so it never arbitrates against a
154
+ * later user-initiated apply (reader #497). Cross-filter precedence stays where
155
+ * two filters collide over the same message at the same trigger
156
+ * (`FilterPipeline`/`selectMoveWinner`), and `applied` therefore counts moves
157
+ * that actually happened.
158
+ */
159
+ export declare const applyOrganize: (deps: ApplyOrganizeDeps, accountConfigId: string, messageIds: readonly string[], predicate: OrganizePredicate) => Promise<ApplyOrganizeResult>;
160
+ export interface FilterBackApplyResult {
161
+ rejected: null;
162
+ matched: number;
163
+ applied: number;
164
+ failed: number;
165
+ failures: string[];
166
+ }
167
+ export interface FilterBackApplyDeps {
168
+ client: RemitClient;
169
+ matchDeps: OrganizeMatchDeps;
170
+ moveService?: PlacementMoveService;
171
+ }
172
+ /**
173
+ * Run one standing filter over the inbox as if each matching message had just
174
+ * arrived. Every message the matcher returns is a match: its label always
175
+ * applies, being additive. The index-time {@link FilterPipeline} only orders
176
+ * the exclusive move, so the move is skipped where it names another active
177
+ * filter. The pipeline sees the stored preview rather than the full body, so
178
+ * its verdict on whether this filter matches is not asked; and an evaluate it
179
+ * could not finish names no other filter, so the move goes ahead rather than
180
+ * silently dropping out.
181
+ */
182
+ export declare const backApplyFilter: (deps: FilterBackApplyDeps, accountConfigId: string, filterId: string) => Promise<FilterBackApplyResult | OrganizeMatchRejected>;
183
+ /** Whether a job row runs a standing filter rather than its own snapshot. */
184
+ export declare const isFilterJob: (job: Pick<OrganizeJobRequestItem, "filterId">) => boolean;