@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,522 +0,0 @@
1
- import type {
2
- IAccountConfigRepository,
3
- IAccountExportRequestRepository,
4
- IAccountRepository,
5
- IAccountSettingRepository,
6
- IAddressRepository,
7
- ICalendarCollectionRepository,
8
- ICalendarEventIndexRepository,
9
- ICalendarFeedTokenRepository,
10
- ICalendarObjectRepository,
11
- ICalendarSuggestionRepository,
12
- ICalendarUnitOfWork,
13
- IConfigImportRepository,
14
- IEnvelopeRepository,
15
- IFilterAnchorRepository,
16
- IFilterAnchorTransaction,
17
- IFilterRepository,
18
- ILabelRepository,
19
- IMailboxLockRepository,
20
- IMailboxRepository,
21
- IMailboxSpecialUseRepository,
22
- IMessageFlagPushRepository,
23
- IMessageFlagRepository,
24
- IMessageLabelRepository,
25
- IMessagePlacementMoveRepository,
26
- IMessageRepository,
27
- IOrganizeJobRequestRepository,
28
- IOutboxAttachmentRepository,
29
- IOutboxMessageRepository,
30
- IQuarantineRepository,
31
- ISenderSignerStandingRepository,
32
- IThreadMessageRepository,
33
- IUnitOfWork,
34
- } from "@remit/data-ports";
35
- import { logger } from "@remit/logger-lambda";
36
- import {
37
- BodySyncQueueService,
38
- BodySyncService,
39
- createConnection,
40
- type FilterConfig,
41
- FlagPushService,
42
- FlagQueueService,
43
- type IImapConnection,
44
- MailboxQueueService,
45
- MessageMoveService,
46
- OutboxAttachmentService,
47
- OutboxQueueService,
48
- PlacementMoveService,
49
- SpamReportService,
50
- } from "@remit/mailbox-service";
51
- import { buildFilterConfig } from "@remit/mailbox-service/filter-config";
52
- import { createSearchService, type SearchService } from "@remit/search-service";
53
- import {
54
- buildEmbeddingServiceFromEnv,
55
- buildVectorStoreFromEnv,
56
- } from "@remit/search-service/from-env";
57
- import {
58
- createCachedDataKeyProvider,
59
- createKmsDataKeyProvider,
60
- createSecretsService,
61
- deserializeEncryptedPayload,
62
- FAKE_KMS_KEY_ID,
63
- type SecretsService,
64
- } from "@remit/secrets-service";
65
- import type { StorageService } from "@remit/storage-service";
66
- import { createStorageService } from "@remit/storage-service/s3";
67
- import { env } from "expect-env";
68
-
69
- export interface ConnectionScope {
70
- getConnection: () => Promise<IImapConnection>;
71
- disconnect: () => Promise<void>;
72
- }
73
-
74
- export interface RemitClient {
75
- // Data repositories (satisfied by ElectroDB services or Drizzle repos)
76
- accountConfig: IAccountConfigRepository;
77
- account: IAccountRepository;
78
- accountSetting: IAccountSettingRepository;
79
- address: IAddressRepository;
80
- mailbox: IMailboxRepository;
81
- mailboxSpecialUse: IMailboxSpecialUseRepository;
82
- mailboxLock: IMailboxLockRepository;
83
- message: IMessageRepository;
84
- messageFlag: IMessageFlagRepository;
85
- outboxMessage: IOutboxMessageRepository;
86
- threadMessage: IThreadMessageRepository;
87
- envelope: IEnvelopeRepository;
88
- accountExportRequest: IAccountExportRequestRepository;
89
-
90
- // Applied configuration imports (#1021), and the folder references each is
91
- // still waiting for. Read by GET /config to surface what a file named but
92
- // IMAP has not produced yet, and written by the binder when it does.
93
- configImport: IConfigImportRepository;
94
-
95
- // Messages the sync path could not read (issue #72). Read-only from the API
96
- // process: the sync worker writes the rows, settings lists them.
97
- quarantine: IQuarantineRepository;
98
-
99
- // Smart Organize back-apply job (RFC 034, #1278). Present on both backends,
100
- // modeled on accountExportRequest — a Pending row the account-fanout worker
101
- // picks up and drives to Complete/Failed. Never a Filter.
102
- organizeJobRequest: IOrganizeJobRequestRepository;
103
-
104
- // Smart Organize (RFC 034, epic #1280). Present on both backends
105
- // (`FilterService`/`LabelService`/… on DynamoDB, `FilterRepo`/`LabelRepo`/…
106
- // relationally) so the matching pipeline and filter CRUD run unchanged on
107
- // either. The relational side has no TTL reaper for expired Temporary filters;
108
- // match-time correctness gates on `expiresAt` (RFC 034 Decision 1.1), never
109
- // on the row still existing, so the missing reaper is housekeeping only.
110
- filter: IFilterRepository;
111
- filterAnchor: IFilterAnchorRepository;
112
-
113
- // Atomically creates a Filter and its optional sibling FilterAnchor row in
114
- // one transaction (#351) — a failed anchor write can never leave a Filter
115
- // durably marked hasAnchor: true with no matching FilterAnchor row.
116
- filterAnchorTransaction: IFilterAnchorTransaction;
117
- label: ILabelRepository;
118
- messageLabel: IMessageLabelRepository;
119
-
120
- // How often a sender has been seen arriving under one DKIM signing identity,
121
- // per account. Read by the authenticity derivation to tell a familiar
122
- // third-party signer from a first sighting; written once per message by
123
- // body-sync, which is its only writer.
124
- senderSignerStanding: ISenderSignerStandingRepository;
125
-
126
- // The calendar store (issue #15) and the cards a message offers into it
127
- // (issue #1033). Every write goes through `calendarUnitOfWork`: the object,
128
- // its occurrence rows, the collection's sequence bump and — when the write
129
- // came from accepting a suggestion — that suggestion's own state are one
130
- // fact, and the repositories beside it are the read side. A backend that
131
- // cannot supply them cannot serve the calendar at all, so they are part of
132
- // the client rather than something each handler checks for.
133
- calendarCollection: ICalendarCollectionRepository;
134
- calendarObject: ICalendarObjectRepository;
135
- calendarEventIndex: ICalendarEventIndexRepository;
136
- calendarFeedToken: ICalendarFeedTokenRepository;
137
- calendarSuggestion: ICalendarSuggestionRepository;
138
- calendarUnitOfWork: ICalendarUnitOfWork;
139
-
140
- // Atomic write set for a message save. Present on the relational backend (real
141
- // transaction); absent on DynamoDB, where callers fall back to per-repo
142
- // writes with that backend's own (non-transactional) guarantees.
143
- unitOfWork?: IUnitOfWork;
144
-
145
- // Storage service
146
- storage: StorageService;
147
-
148
- // Search service (semantic vector search)
149
- search: SearchService;
150
-
151
- // Secrets service (KMS encryption)
152
- secrets: SecretsService;
153
-
154
- // Body sync service (on-demand IMAP body fetch)
155
- bodySync: BodySyncService;
156
-
157
- // Re-arms the SYNC_MESSAGE_BODY cue when a read-path body fetch finds the
158
- // storage object missing. Absent when SQS_QUEUE_URL_BODY is not configured
159
- // (the deployed API Lambda does not carry the body queue today).
160
- bodySyncQueue?: BodySyncQueueService;
161
-
162
- // Pending placement-move markers (issue #1271). Present on both backends
163
- // (`MessagePlacementMoveService` on DynamoDB, `MessagePlacementMoveRepo` on
164
- // relationally) so a caller never needs to guess which backend is active. Used
165
- // for read-time count prediction (epic #1281 invariant 4) and by the
166
- // account-worker cascade delete. Written by the imap-worker's bulk
167
- // body-sync path through this `placementMove`, so the placement producer
168
- // runs on whatever backend is active — DynamoDB or SQLite.
169
- placementMove: IMessagePlacementMoveRepository;
170
-
171
- // Pending flag-push markers (issue #1273). Present on both backends
172
- // (`MessageFlagPushService` on DynamoDB, `MessageFlagPushRepo` on
173
- // relationally). Used for read-time unseenCount prediction (epic #1281
174
- // invariant 4), by the account-worker cascade delete, and by the periodic
175
- // per-mailbox sync tick to re-arm a marker stuck `pending`. Unlike
176
- // `placementMove`, this one IS written on both backends — `flagQueue`
177
- // below writes through it regardless of `DATA_BACKEND`.
178
- flagPush: IMessageFlagPushRepository;
179
-
180
- // Queue services (writes with IMAP sync)
181
- flagQueue: FlagQueueService;
182
- mailboxQueue: MailboxQueueService;
183
- messageMove: MessageMoveService;
184
- outboxAttachment: OutboxAttachmentService;
185
- outboxQueue: OutboxQueueService;
186
-
187
- // Report-spam / not-spam (block-and-move, unified from the old separate
188
- // Block + Mark spam actions). Composed over `messageMove` and the same
189
- // `flagPushService` instance `flagQueue` shares below.
190
- spamReport: SpamReportService;
191
-
192
- // Helper to create IMAP connection scope from accountId
193
- createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
194
-
195
- // Runs an arbitrary set of repository writes as one transaction. Absent on a
196
- // backend with no cross-entity transaction, where the caller's contract is
197
- // instead: validate before the first write, fail fast, and report what
198
- // landed. The message-save write set has its own bound repositories and uses
199
- // `unitOfWork`; this one takes the repos as they are.
200
- writeSet?: <T>(run: () => Promise<T>) => Promise<T>;
201
- }
202
-
203
- export interface RemitClientRepositories {
204
- accountConfig: IAccountConfigRepository;
205
- account: IAccountRepository;
206
- accountSetting: IAccountSettingRepository;
207
- address: IAddressRepository;
208
- mailbox: IMailboxRepository;
209
- mailboxSpecialUse: IMailboxSpecialUseRepository;
210
- mailboxLock: IMailboxLockRepository;
211
- message: IMessageRepository;
212
- messageFlag: IMessageFlagRepository;
213
- outboxMessage: IOutboxMessageRepository;
214
- outboxAttachment: IOutboxAttachmentRepository;
215
- threadMessage: IThreadMessageRepository;
216
- envelope: IEnvelopeRepository;
217
- accountExportRequest: IAccountExportRequestRepository;
218
- configImport: IConfigImportRepository;
219
- quarantine: IQuarantineRepository;
220
- organizeJobRequest: IOrganizeJobRequestRepository;
221
- placementMove: IMessagePlacementMoveRepository;
222
- flagPush: IMessageFlagPushRepository;
223
- filter: IFilterRepository;
224
- filterAnchor: IFilterAnchorRepository;
225
- filterAnchorTransaction: IFilterAnchorTransaction;
226
- label: ILabelRepository;
227
- messageLabel: IMessageLabelRepository;
228
- senderSignerStanding: ISenderSignerStandingRepository;
229
- calendarCollection: ICalendarCollectionRepository;
230
- calendarObject: ICalendarObjectRepository;
231
- calendarEventIndex: ICalendarEventIndexRepository;
232
- calendarFeedToken: ICalendarFeedTokenRepository;
233
- calendarSuggestion: ICalendarSuggestionRepository;
234
- calendarUnitOfWork: ICalendarUnitOfWork;
235
- unitOfWork?: IUnitOfWork;
236
- writeSet?: <T>(run: () => Promise<T>) => Promise<T>;
237
- }
238
-
239
- export interface RemitClientSharedDeps {
240
- storage: StorageService;
241
- search: SearchService;
242
- secrets: SecretsService;
243
- sqsQueueUrl: string;
244
- sqsSmtpQueueUrl: string;
245
- bodySyncQueue?: BodySyncQueueService;
246
- }
247
-
248
- export interface RemitClientDeps extends RemitClientSharedDeps {
249
- repositories: RemitClientRepositories;
250
- }
251
-
252
- const buildConnectionScope =
253
- (accountService: IAccountRepository, secretsService: SecretsService) =>
254
- async (accountId: string): Promise<ConnectionScope> => {
255
- const account = await accountService.get(accountId);
256
- if (!account.passwordHash) {
257
- throw new Error(
258
- `Account ${accountId} has no passwordHash — OAuth accounts cannot use this connection path`,
259
- );
260
- }
261
- const password = await secretsService.decrypt(
262
- deserializeEncryptedPayload(JSON.parse(account.passwordHash)),
263
- );
264
-
265
- let connection: IImapConnection | null = null;
266
- let connectPromise: Promise<IImapConnection> | null = null;
267
-
268
- const getConnection = async (): Promise<IImapConnection> => {
269
- if (connectPromise) {
270
- return connectPromise;
271
- }
272
-
273
- const conn = createConnection({
274
- user: account.username,
275
- credentials: { kind: "password", password },
276
- host: account.imapHost,
277
- port: account.imapPort,
278
- tls: account.imapTls,
279
- });
280
- connection = conn;
281
- connectPromise = conn.connect().then(() => conn);
282
-
283
- return connectPromise;
284
- };
285
-
286
- const disconnect = async (): Promise<void> => {
287
- if (connection) {
288
- await connection.disconnect();
289
- connection = null;
290
- connectPromise = null;
291
- }
292
- };
293
-
294
- return { getConnection, disconnect };
295
- };
296
-
297
- // The vector store and embedder are selected from the environment by the shared
298
- // builders: S3 Vectors + Bedrock Titan in production, persistent sqlite-vec +
299
- // Transformers.js when the LOCAL_* / local flags are set (local dev), and the
300
- // in-memory store + deterministic embedder otherwise (tests).
301
- const buildSearchService = (): SearchService => {
302
- // Build the embedder first so we can pass its dimension count to the
303
- // sqlite-vec store — the vec0 table dimension must match the embedder.
304
- const embedder = buildEmbeddingServiceFromEnv();
305
- return createSearchService({
306
- store: buildVectorStoreFromEnv(embedder.dimensions),
307
- embedder,
308
- });
309
- };
310
-
311
- // The body queue lives on the worker infra; the API only knows it locally
312
- // (dev / e2e set SQS_QUEUE_URL_BODY). When unset the read path still returns a
313
- // retryable 202 — it just cannot re-arm the cue itself.
314
- const buildBodySyncQueue = (): BodySyncQueueService | undefined => {
315
- const bodyQueueUrl = process.env.SQS_QUEUE_URL_BODY;
316
- if (!bodyQueueUrl) return undefined;
317
- return new BodySyncQueueService({ sqsQueueUrl: bodyQueueUrl, logger });
318
- };
319
-
320
- // Everything a RemitClient needs that is chosen from the environment and does
321
- // not vary by data backend: storage, search, secrets, the SQS queue URLs, and
322
- // the optional body-sync queue. Each composition root builds its backend's
323
- // repositories and merges them with this.
324
- export const buildSharedDeps = (): RemitClientSharedDeps => {
325
- const storage = createStorageService();
326
- const search = buildSearchService();
327
- const kmsKeyId = process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID;
328
- const dataKeyProvider = createCachedDataKeyProvider(
329
- createKmsDataKeyProvider(kmsKeyId),
330
- );
331
- const secrets = createSecretsService(dataKeyProvider);
332
- const sqsQueueUrl = env.SQS_QUEUE_URL;
333
- const sqsSmtpQueueUrl = process.env.SQS_QUEUE_URL_SMTP ?? sqsQueueUrl;
334
- return {
335
- storage,
336
- search,
337
- secrets,
338
- sqsQueueUrl,
339
- sqsSmtpQueueUrl,
340
- bodySyncQueue: buildBodySyncQueue(),
341
- };
342
- };
343
-
344
- // The read-path body backfill (describeMessage / getRawMessage →
345
- // fetchAndGetBody) materializes a body that was never sync-synced, and must run
346
- // the account's standing filters the same way the imap-worker's sync path does.
347
- // Without this, a message a user opens before background body-sync is classified
348
- // but never filter-evaluated — and once its body is stored the filter-capable
349
- // sync path skips it, so an explicit standing filter silently never fires (issue
350
- // #223). Only the explicit user-rule half is wired here; heuristic classifier
351
- // placement stays on the sync path. It shares `buildFilterConfig` with
352
- // `sync-message-body.ts`, so both paths embed a candidate message under the same
353
- // env-selected model that produced the anchors and a semantic (anchor-only)
354
- // filter fires on either (issue #298). Over the message-management queue's
355
- // local-first PlacementMoveService; filters stay off when that queue is unset.
356
- export const buildReadPathFilterConfig = (
357
- repositories: RemitClientRepositories,
358
- ): FilterConfig | undefined => {
359
- const placementMoveQueueUrl = process.env.SQS_QUEUE_URL_MESSAGE_MGMT;
360
- return buildFilterConfig({
361
- filterService: repositories.filter,
362
- filterAnchorService: repositories.filterAnchor,
363
- messageLabelService: repositories.messageLabel,
364
- placementMoveService: placementMoveQueueUrl
365
- ? new PlacementMoveService({
366
- messageService: repositories.message,
367
- threadMessageService: repositories.threadMessage,
368
- markerService: repositories.placementMove,
369
- addressService: repositories.address,
370
- mailboxSpecialUseService: repositories.mailboxSpecialUse,
371
- sqsQueueUrl: placementMoveQueueUrl,
372
- })
373
- : undefined,
374
- });
375
- };
376
-
377
- // Backend-neutral composition root: given repositories (from any data-ports
378
- // implementation) and the shared services, wire the domain and queue services
379
- // and assemble a RemitClient. Imports neither ElectroDB nor Drizzle — the
380
- // caller chooses the backend.
381
- export const createRemitClient = (deps: RemitClientDeps): RemitClient => {
382
- const {
383
- repositories,
384
- storage,
385
- search,
386
- secrets,
387
- sqsQueueUrl,
388
- sqsSmtpQueueUrl,
389
- bodySyncQueue,
390
- } = deps;
391
-
392
- const filterConfig = buildReadPathFilterConfig(repositories);
393
-
394
- const flagPushService = new FlagPushService({
395
- markerService: repositories.flagPush,
396
- sqsQueueUrl,
397
- logger,
398
- });
399
-
400
- // Shared with the returned `flagQueue` below (issue #302): body sync
401
- // auto-marks-read a message from an unsubscribed sender through the exact
402
- // same `FlagQueueService.markAsRead` a manual mark-as-read goes through, not
403
- // a second instance or a second primitive.
404
- const flagQueueService = new FlagQueueService({
405
- messageFlagService: repositories.messageFlag,
406
- messageService: repositories.message,
407
- threadMessageService: repositories.threadMessage,
408
- flagPushService,
409
- logger,
410
- });
411
-
412
- const messageMoveService = new MessageMoveService({
413
- messageService: repositories.message,
414
- mailboxService: repositories.mailbox,
415
- mailboxSpecialUseService: repositories.mailboxSpecialUse,
416
- threadMessageService: repositories.threadMessage,
417
- addressService: repositories.address,
418
- sqsQueueUrl,
419
- logger,
420
- });
421
-
422
- // One instance, shared with the queue service below: the per-draft chain that
423
- // makes the attachment cap safe under concurrency lives on it, and a discard
424
- // racing an upload has to take the same lock the upload does.
425
- const outboxAttachmentService = new OutboxAttachmentService({
426
- outboxMessageService: repositories.outboxMessage,
427
- outboxAttachmentService: repositories.outboxAttachment,
428
- storage,
429
- });
430
-
431
- const bodySync = new BodySyncService(
432
- repositories.message,
433
- storage,
434
- repositories.threadMessage,
435
- repositories.address,
436
- repositories.envelope,
437
- logger,
438
- undefined,
439
- filterConfig,
440
- undefined,
441
- { flagQueueService },
442
- // The read-path backfill materializes a body the sync path never got to,
443
- // so it is a message's first sight as much as the sync pass is — and an
444
- // invitation the user opens before background sync must still get its
445
- // card (issue #1033, the same argument as the filter wiring above).
446
- {
447
- calendarSuggestionService: repositories.calendarSuggestion,
448
- calendarUnitOfWork: repositories.calendarUnitOfWork,
449
- filterService: repositories.filter,
450
- },
451
- repositories.unitOfWork,
452
- );
453
-
454
- return {
455
- accountConfig: repositories.accountConfig,
456
- account: repositories.account,
457
- accountSetting: repositories.accountSetting,
458
- address: repositories.address,
459
- mailbox: repositories.mailbox,
460
- mailboxSpecialUse: repositories.mailboxSpecialUse,
461
- mailboxLock: repositories.mailboxLock,
462
- message: repositories.message,
463
- messageFlag: repositories.messageFlag,
464
- outboxMessage: repositories.outboxMessage,
465
- threadMessage: repositories.threadMessage,
466
- envelope: repositories.envelope,
467
- accountExportRequest: repositories.accountExportRequest,
468
- configImport: repositories.configImport,
469
- quarantine: repositories.quarantine,
470
- organizeJobRequest: repositories.organizeJobRequest,
471
- filter: repositories.filter,
472
- filterAnchor: repositories.filterAnchor,
473
- filterAnchorTransaction: repositories.filterAnchorTransaction,
474
- label: repositories.label,
475
- messageLabel: repositories.messageLabel,
476
- senderSignerStanding: repositories.senderSignerStanding,
477
- calendarCollection: repositories.calendarCollection,
478
- calendarObject: repositories.calendarObject,
479
- calendarEventIndex: repositories.calendarEventIndex,
480
- calendarFeedToken: repositories.calendarFeedToken,
481
- calendarSuggestion: repositories.calendarSuggestion,
482
- calendarUnitOfWork: repositories.calendarUnitOfWork,
483
- unitOfWork: repositories.unitOfWork,
484
- writeSet: repositories.writeSet,
485
-
486
- storage,
487
- search,
488
- secrets,
489
-
490
- bodySync,
491
- bodySyncQueue,
492
- placementMove: repositories.placementMove,
493
- flagPush: repositories.flagPush,
494
-
495
- flagQueue: flagQueueService,
496
- mailboxQueue: new MailboxQueueService({
497
- mailboxService: repositories.mailbox,
498
- sqsQueueUrl,
499
- logger,
500
- }),
501
- messageMove: messageMoveService,
502
- outboxAttachment: outboxAttachmentService,
503
- outboxQueue: new OutboxQueueService({
504
- outboxMessageService: repositories.outboxMessage,
505
- outboxAttachmentService,
506
- accountService: repositories.account,
507
- sqsSmtpQueueUrl,
508
- logger,
509
- }),
510
- spamReport: new SpamReportService({
511
- messageService: repositories.message,
512
- addressService: repositories.address,
513
- accountService: repositories.account,
514
- mailboxSpecialUseService: repositories.mailboxSpecialUse,
515
- messageMoveService,
516
- flagPushService,
517
- logger,
518
- }),
519
-
520
- createConnectionScope: buildConnectionScope(repositories.account, secrets),
521
- };
522
- };
@@ -1,97 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { afterEach, test } from "node:test";
6
- import {
7
- _resetForTest,
8
- getClient,
9
- type RemitClient,
10
- setClient,
11
- } from "./data-client.js";
12
-
13
- const REQUIRED_KEYS: ReadonlyArray<keyof RemitClient> = [
14
- "accountConfig",
15
- "account",
16
- "accountSetting",
17
- "address",
18
- "mailbox",
19
- "mailboxSpecialUse",
20
- "message",
21
- "messageFlag",
22
- "outboxMessage",
23
- "threadMessage",
24
- "envelope",
25
- "accountExportRequest",
26
- "quarantine",
27
- "storage",
28
- "search",
29
- "secrets",
30
- "bodySync",
31
- "flagQueue",
32
- "mailboxQueue",
33
- "messageMove",
34
- "outboxQueue",
35
- "spamReport",
36
- "createConnectionScope",
37
- ] as const;
38
-
39
- afterEach(() => {
40
- _resetForTest();
41
- });
42
-
43
- const withSqliteDbPath = async (run: () => Promise<void>): Promise<void> => {
44
- const saved = process.env.SQLITE_DB_PATH;
45
- const dir = mkdtempSync(join(tmpdir(), "remit-data-client-"));
46
- process.env.SQLITE_DB_PATH = join(dir, "remit.db");
47
- try {
48
- await run();
49
- } finally {
50
- _resetForTest();
51
- if (saved === undefined) delete process.env.SQLITE_DB_PATH;
52
- else process.env.SQLITE_DB_PATH = saved;
53
- rmSync(dir, { recursive: true, force: true });
54
- }
55
- };
56
-
57
- test("getClient() falls back to the in-package composition and constructs every service", async () => {
58
- await withSqliteDbPath(async () => {
59
- const client = await getClient();
60
-
61
- for (const key of REQUIRED_KEYS) {
62
- assert.ok(
63
- client[key] !== undefined && client[key] !== null,
64
- `RemitClient.${String(key)} must be defined on the fallback path`,
65
- );
66
- }
67
-
68
- assert.equal(typeof client.createConnectionScope, "function");
69
- });
70
- });
71
-
72
- test("getClient() names setClient when nothing is registered and there is no database to open", async () => {
73
- const saved = process.env.SQLITE_DB_PATH;
74
- delete process.env.SQLITE_DB_PATH;
75
-
76
- try {
77
- await assert.rejects(getClient(), /register one with setClient\(\)/);
78
- } finally {
79
- if (saved !== undefined) process.env.SQLITE_DB_PATH = saved;
80
- }
81
- });
82
-
83
- test("getClient() returns the injected client without reading the environment", async () => {
84
- const saved = process.env.SQLITE_DB_PATH;
85
- delete process.env.SQLITE_DB_PATH;
86
-
87
- const injected = { account: {} } as unknown as RemitClient;
88
- setClient(injected);
89
-
90
- try {
91
- const client = await getClient();
92
- assert.equal(client, injected);
93
- } finally {
94
- _resetForTest();
95
- if (saved !== undefined) process.env.SQLITE_DB_PATH = saved;
96
- }
97
- });
@@ -1,58 +0,0 @@
1
- import type { RemitClient } from "./create-remit-client.js";
2
-
3
- export type {
4
- ConnectionScope,
5
- RemitClient,
6
- } from "./create-remit-client.js";
7
-
8
- let clientPromise: Promise<RemitClient> | null = null;
9
- let injected: RemitClient | null = null;
10
-
11
- /**
12
- * Register the client from the composition root. An adapter that lives outside
13
- * this shared, open-core module — DynamoDB, or a Postgres adapter in the
14
- * repository that deploys it — is never imported here; its composition root
15
- * calls this before handling a request.
16
- */
17
- export const setClient = (client: RemitClient): void => {
18
- injected = client;
19
- clientPromise = null;
20
- };
21
-
22
- /**
23
- * The SQLite composition this build contains, reached only by a process that
24
- * registered no client of its own. The `import()` is dynamic so a bundler
25
- * following the entry graph of a process that injects its client never reaches
26
- * `@remit/drizzle-service` or drizzle-orm (both `external` for the Lambda
27
- * esbuild build).
28
- *
29
- * `SQLITE_DB_PATH` is the precondition the composition needs, checked ahead of
30
- * the import so its absence names `setClient` instead of failing on module
31
- * resolution inside a bundle, or — worse — opening an empty database and
32
- * serving an empty mailbox. It is a precondition, not a backend selection.
33
- */
34
- const buildRelationalClient = async (): Promise<RemitClient> => {
35
- if (!process.env.SQLITE_DB_PATH) {
36
- throw new Error(
37
- "no client registered — register one with setClient() from your composition root",
38
- );
39
- }
40
- const composition = await import("./compose-sqlite.js");
41
- return composition.buildSqliteClient();
42
- };
43
-
44
- export const getClient = (): Promise<RemitClient> => {
45
- if (!clientPromise) {
46
- clientPromise = injected
47
- ? Promise.resolve(injected)
48
- : buildRelationalClient();
49
- }
50
-
51
- return clientPromise;
52
- };
53
-
54
- /** Reset the singleton and any injected client — test use only. */
55
- export const _resetForTest = (): void => {
56
- clientPromise = null;
57
- injected = null;
58
- };