@remit/backend 0.0.144 → 0.0.146

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 (91) hide show
  1. package/dist/src/api.d.ts +0 -2
  2. package/dist/src/api.js +9 -80
  3. package/dist/src/auth.d.ts +13 -19
  4. package/dist/src/auth.js +51 -43
  5. package/dist/src/config/api-config.d.ts +20 -0
  6. package/dist/src/config/api-config.js +1 -0
  7. package/dist/src/config/from-env.d.ts +2 -0
  8. package/dist/src/config/from-env.js +65 -0
  9. package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
  10. package/dist/src/config/msoauth-secrets-manager.js +28 -0
  11. package/dist/src/config/msoauth.d.ts +18 -29
  12. package/dist/src/config/msoauth.js +4 -28
  13. package/dist/src/derive/contentSignature.d.ts +3 -4
  14. package/dist/src/derive/contentSignature.js +4 -13
  15. package/dist/src/derive/contentUrl.d.ts +0 -13
  16. package/dist/src/derive/contentUrl.js +0 -19
  17. package/dist/src/handlers/account-oauth.d.ts +9 -3
  18. package/dist/src/handlers/account-oauth.js +197 -250
  19. package/dist/src/handlers/account.d.ts +13 -4
  20. package/dist/src/handlers/account.js +26 -39
  21. package/dist/src/handlers/address.d.ts +11 -3
  22. package/dist/src/handlers/address.js +11 -19
  23. package/dist/src/handlers/calendar-event.js +18 -26
  24. package/dist/src/handlers/calendar-feed.d.ts +2 -2
  25. package/dist/src/handlers/calendar-feed.js +24 -32
  26. package/dist/src/handlers/calendar-suggestion.js +18 -26
  27. package/dist/src/handlers/calendar.d.ts +1 -1
  28. package/dist/src/handlers/calendar.js +15 -22
  29. package/dist/src/handlers/config.d.ts +16 -6
  30. package/dist/src/handlers/config.js +23 -35
  31. package/dist/src/handlers/filter.d.ts +16 -3
  32. package/dist/src/handlers/filter.js +17 -32
  33. package/dist/src/handlers/folder-role.js +3 -6
  34. package/dist/src/handlers/index.d.ts +187 -2
  35. package/dist/src/handlers/index.js +6 -4
  36. package/dist/src/handlers/label.js +15 -22
  37. package/dist/src/handlers/mailbox.d.ts +20 -4
  38. package/dist/src/handlers/mailbox.js +18 -26
  39. package/dist/src/handlers/me.d.ts +10 -2
  40. package/dist/src/handlers/me.js +26 -47
  41. package/dist/src/handlers/message.d.ts +78 -6
  42. package/dist/src/handlers/message.js +34 -46
  43. package/dist/src/handlers/organize.d.ts +10 -4
  44. package/dist/src/handlers/organize.js +11 -20
  45. package/dist/src/handlers/outbox-attachment.d.ts +6 -5
  46. package/dist/src/handlers/outbox-attachment.js +11 -15
  47. package/dist/src/handlers/outbox.d.ts +25 -4
  48. package/dist/src/handlers/outbox.js +27 -35
  49. package/dist/src/handlers/sync.d.ts +19 -6
  50. package/dist/src/handlers/sync.js +13 -22
  51. package/dist/src/handlers/system-update.js +14 -27
  52. package/dist/src/handlers/thread.js +9 -14
  53. package/dist/src/handlers/unified-threads.js +3 -6
  54. package/dist/src/index.d.ts +4 -1
  55. package/dist/src/index.js +18 -2
  56. package/dist/src/jwt-auth.d.ts +9 -12
  57. package/dist/src/jwt-auth.js +19 -71
  58. package/dist/src/lambda.d.ts +13 -0
  59. package/dist/src/lambda.js +33 -0
  60. package/dist/src/request.d.ts +8 -2
  61. package/dist/src/request.js +16 -6
  62. package/dist/src/response.d.ts +20 -3
  63. package/dist/src/response.js +15 -22
  64. package/dist/src/serve.d.ts +31 -0
  65. package/dist/src/serve.js +117 -0
  66. package/dist/src/service/account-deletion.d.ts +2 -4
  67. package/dist/src/service/account-deletion.js +3 -9
  68. package/dist/src/service/compose-sqlite.d.ts +2 -1
  69. package/dist/src/service/compose-sqlite.js +5 -3
  70. package/dist/src/service/create-remit-client.d.ts +25 -11
  71. package/dist/src/service/create-remit-client.js +97 -116
  72. package/dist/src/service/data-client.js +3 -5
  73. package/dist/src/service/export-identity.d.ts +2 -1
  74. package/dist/src/service/export-identity.js +4 -6
  75. package/dist/src/service/fire-and-forget.js +1 -2
  76. package/dist/src/service/organize.d.ts +7 -10
  77. package/dist/src/service/organize.js +10 -27
  78. package/dist/src/service/queues.d.ts +12 -0
  79. package/dist/src/service/queues.js +1 -0
  80. package/dist/src/service/shared-deps.d.ts +6 -0
  81. package/dist/src/service/shared-deps.js +26 -0
  82. package/dist/src/service/trigger-sync.d.ts +11 -5
  83. package/dist/src/service/trigger-sync.js +10 -18
  84. package/dist/src/types.d.ts +24 -1
  85. package/package.json +24 -8
  86. package/dist/src/data-backend.d.ts +0 -12
  87. package/dist/src/data-backend.js +0 -12
  88. package/dist/src/request-context.d.ts +0 -9
  89. package/dist/src/request-context.js +0 -22
  90. package/dist/src/service/sqs.d.ts +0 -1
  91. package/dist/src/service/sqs.js +0 -3
@@ -1,9 +1,7 @@
1
1
  import { logger } from "@remit/logger";
2
- import { BodySyncQueueService, BodySyncService, createConnection, FlagPushService, FlagQueueService, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, PlacementMoveService, SpamReportService, SyncWarningService, } from "@remit/mailbox-service";
2
+ import { BodySyncQueueService, BodySyncService, createConnection, FlagPushService, FlagQueueService, fifoMailboxManagement, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, PlacementMoveService, SpamReportService, SyncWarningService, } from "@remit/mailbox-service";
3
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";
4
+ import { deserializeEncryptedPayload, } from "@remit/secrets-service";
7
5
  const buildConnectionScope = (accountService, secretsService) => async (accountId) => {
8
6
  const account = await accountService.get(accountId);
9
7
  if (!account.passwordHash) {
@@ -36,34 +34,6 @@ const buildConnectionScope = (accountService, secretsService) => async (accountI
36
34
  };
37
35
  return { getConnection, disconnect };
38
36
  };
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
37
  // The read-path body backfill (describeMessage / getRawMessage →
68
38
  // fetchAndGetBody) materializes a body that was never sync-synced, and must run
69
39
  // the account's standing filters the same way the imap-worker's sync path does.
@@ -73,77 +43,26 @@ export const buildSharedDeps = () => {
73
43
  // #223). Only the explicit user-rule half is wired here; heuristic classifier
74
44
  // placement stays on the sync path. It shares `buildFilterConfig` with
75
45
  // `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;
46
+ // message-management queue's local-first PlacementMoveService.
47
+ export const buildReadPathFilterConfig = (repositories, queues) => {
48
+ const { messageManagement } = queues;
80
49
  return buildFilterConfig({
81
50
  filterService: repositories.filter,
82
51
  messageLabelService: repositories.messageLabel,
83
52
  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,
53
+ placementMoveService: new PlacementMoveService({
54
+ messageService: repositories.message,
55
+ threadMessageService: repositories.threadMessage,
56
+ markerService: repositories.placementMove,
57
+ addressService: repositories.address,
58
+ mailboxSpecialUseService: repositories.mailboxSpecialUse,
59
+ mailboxService: repositories.mailbox,
60
+ queue: messageManagement,
61
+ }),
95
62
  });
96
63
  };
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
- });
64
+ export const createRemitCore = (deps) => {
65
+ const { repositories, storage, secrets } = deps;
147
66
  return {
148
67
  accountConfig: repositories.accountConfig,
149
68
  account: repositories.account,
@@ -174,34 +93,96 @@ export const createRemitClient = (deps) => {
174
93
  calendarStore: repositories.calendarStore,
175
94
  storage,
176
95
  secrets,
177
- bodySync,
178
- bodySyncQueue,
179
96
  placementMove: repositories.placementMove,
180
97
  flagPush: repositories.flagPush,
181
- flagQueue: flagQueueService,
182
- mailboxQueue: new MailboxQueueService({
183
- mailboxService: repositories.mailbox,
184
- sqsQueueUrl,
98
+ // One instance, shared with the outbox queue service: the per-draft chain
99
+ // that makes the attachment cap safe under concurrency lives on it, and a
100
+ // discard racing an upload has to take the same lock the upload does.
101
+ outboxAttachment: new OutboxAttachmentService({
102
+ outboxMessageService: repositories.outboxMessage,
103
+ outboxAttachmentService: repositories.outboxAttachment,
104
+ storage,
105
+ }),
106
+ createConnectionScope: buildConnectionScope(repositories.account, secrets),
107
+ };
108
+ };
109
+ export const composeSyncServices = (core, queues) => {
110
+ const flagPushService = new FlagPushService({
111
+ markerService: core.flagPush,
112
+ queue: queues.sync,
113
+ logger,
114
+ });
115
+ const messageMove = new MessageMoveService({
116
+ messageService: core.message,
117
+ mailboxService: core.mailbox,
118
+ mailboxSpecialUseService: core.mailboxSpecialUse,
119
+ threadMessageService: core.threadMessage,
120
+ addressService: core.address,
121
+ queue: queues.sync,
122
+ logger,
123
+ });
124
+ return {
125
+ flagQueue: new FlagQueueService({
126
+ messageFlagService: core.messageFlag,
127
+ messageService: core.message,
128
+ threadMessageService: core.threadMessage,
129
+ flagPushService,
185
130
  logger,
186
131
  }),
187
- messageMove: messageMoveService,
188
- outboxAttachment: outboxAttachmentService,
189
- outboxQueue: new OutboxQueueService({
190
- outboxMessageService: repositories.outboxMessage,
191
- outboxAttachmentService,
192
- accountService: repositories.account,
193
- sqsSmtpQueueUrl,
132
+ mailboxQueue: new MailboxQueueService({
133
+ mailboxService: core.mailbox,
134
+ enqueue: fifoMailboxManagement(queues.sync),
194
135
  logger,
195
136
  }),
137
+ messageMove,
196
138
  spamReport: new SpamReportService({
197
- messageService: repositories.message,
198
- addressService: repositories.address,
199
- accountService: repositories.account,
200
- mailboxSpecialUseService: repositories.mailboxSpecialUse,
201
- messageMoveService,
139
+ messageService: core.message,
140
+ addressService: core.address,
141
+ accountService: core.account,
142
+ mailboxSpecialUseService: core.mailboxSpecialUse,
143
+ messageMoveService: messageMove,
202
144
  flagPushService,
203
145
  logger,
204
146
  }),
205
- createConnectionScope: buildConnectionScope(repositories.account, secrets),
147
+ };
148
+ };
149
+ export const composeSmtpServices = (core, queues) => ({
150
+ outboxQueue: new OutboxQueueService({
151
+ outboxMessageService: core.outboxMessage,
152
+ outboxAttachmentService: core.outboxAttachment,
153
+ accountService: core.account,
154
+ queue: queues.smtp,
155
+ logger,
156
+ }),
157
+ });
158
+ export const composeBodyServices = (queues) => ({
159
+ bodySyncQueue: new BodySyncQueueService({ queue: queues.body, logger }),
160
+ });
161
+ export const composeReadPathServices = (core, queues, sync) => ({
162
+ // The flag service is the one `flagQueue` is (issue #302): body sync
163
+ // auto-marks-read a message from an unsubscribed sender through the exact
164
+ // same `FlagQueueService.markAsRead` a manual mark-as-read goes through.
165
+ bodySync: new BodySyncService(core.message, core.storage, core.threadMessage, core.address, core.envelope, new SyncWarningService(core.syncWarning), logger, undefined, buildReadPathFilterConfig(core, queues), undefined, { flagQueueService: sync.flagQueue },
166
+ // The read-path backfill materializes a body the sync path never got to,
167
+ // so it is a message's first sight as much as the sync pass is — and an
168
+ // invitation the user opens before background sync must still get its
169
+ // card (issue #1033, the same argument as the filter wiring above).
170
+ {
171
+ calendarSuggestionService: core.calendarSuggestion,
172
+ calendarStore: core.calendarStore,
173
+ filterService: core.filter,
174
+ }),
175
+ });
176
+ export const createRemitClient = (deps) => {
177
+ const { queues } = deps;
178
+ const core = createRemitCore(deps);
179
+ const sync = composeSyncServices(core, queues);
180
+ return {
181
+ ...core,
182
+ queues,
183
+ ...sync,
184
+ ...composeSmtpServices(core, queues),
185
+ ...composeBodyServices(queues),
186
+ ...composeReadPathServices(core, queues, sync),
206
187
  };
207
188
  };
@@ -30,11 +30,9 @@ const buildRelationalClient = async () => {
30
30
  return composition.buildSqliteClient();
31
31
  };
32
32
  export const getClient = () => {
33
- if (!clientPromise) {
34
- clientPromise = injected
35
- ? Promise.resolve(injected)
36
- : buildRelationalClient();
37
- }
33
+ clientPromise ??= injected
34
+ ? Promise.resolve(injected)
35
+ : buildRelationalClient();
38
36
  return clientPromise;
39
37
  };
40
38
  /** Reset the singleton and any injected client — test use only. */
@@ -1,3 +1,4 @@
1
1
  import type { ConfigExportIdentity } from "@remit/config-transfer";
2
+ import type { ApiConfig } from "../config/api-config.js";
2
3
  /** Who wrote a configuration file, and from where. Provenance only. */
3
- export declare const exportIdentity: (now?: Date) => ConfigExportIdentity;
4
+ export declare const exportIdentity: (config: Pick<ApiConfig, "updateControlDir" | "publicOrigin">, now?: Date) => ConfigExportIdentity;
@@ -1,6 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- const DEFAULT_CONTROL_DIR = "/data/control";
4
3
  /**
5
4
  * The version a file says it was written by. The updater owns this fact: it
6
5
  * reads the tag from `.env` and writes it into `state.json`, which is the only
@@ -9,8 +8,7 @@ const DEFAULT_CONTROL_DIR = "/data/control";
9
8
  * stamp, and an export says so rather than inventing a version an importing
10
9
  * reader would then trust.
11
10
  */
12
- const runningVersion = () => {
13
- const dir = process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
11
+ const runningVersion = (dir) => {
14
12
  let raw;
15
13
  try {
16
14
  raw = readFileSync(join(dir, "state.json"), "utf8");
@@ -27,9 +25,9 @@ const runningVersion = () => {
27
25
  : "unknown";
28
26
  };
29
27
  /** Who wrote a configuration file, and from where. Provenance only. */
30
- export const exportIdentity = (now = new Date()) => ({
28
+ export const exportIdentity = (config, now = new Date()) => ({
31
29
  app: "reader",
32
- version: runningVersion(),
30
+ version: runningVersion(config.updateControlDir),
33
31
  exportedAt: now.toISOString(),
34
- instance: process.env.PUBLIC_ORIGIN ?? "",
32
+ instance: config.publicOrigin,
35
33
  });
@@ -1,4 +1,3 @@
1
- import { inspect } from "node:util";
2
1
  import { logger } from "@remit/logger";
3
2
  /**
4
3
  * Run unawaited background work that MUST NOT be able to fail the request that
@@ -32,7 +31,7 @@ export const fireAndForget = async (work, context) => {
32
31
  errorName: error?.name,
33
32
  errorCode: error?.Code ??
34
33
  error?.code,
35
- error: inspect(error),
34
+ error,
36
35
  }, context.message);
37
36
  });
38
37
  };
@@ -117,22 +117,19 @@ export declare const organizePredicateRejection: (predicate: OrganizePredicate)
117
117
  */
118
118
  export declare const matchOrganize: (deps: OrganizeMatchDeps, accountConfigId: string, predicate: OrganizePredicate, limit?: number, scope?: OrganizeMatchScope) => Promise<OrganizeMatchResult>;
119
119
  /** The matcher deps for the running backend/worker. */
120
- export declare const buildOrganizeMatchDeps: (client: RemitClient) => OrganizeMatchDeps;
120
+ export declare const buildOrganizeMatchDeps: (client: RemitClient<never>) => OrganizeMatchDeps;
121
121
  /**
122
122
  * The exclusive-move arm of a back-apply, wired exactly like the body-sync
123
123
  * placement mover (`sync-message-body.ts`): the local-first
124
124
  * {@link PlacementMoveService} over the shared message-management queue. A move
125
125
  * commits locally (marker + ThreadMessage + Message row) and enqueues a
126
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}).
127
+ * redelivered job idempotent (mirrors #1297).
131
128
  */
132
- export declare const buildOrganizeMoveService: (client: RemitClient) => PlacementMoveService | undefined;
129
+ export declare const buildOrganizeMoveService: (client: RemitClient<"messageManagement">) => PlacementMoveService;
133
130
  export interface ApplyOrganizeDeps {
134
- client: RemitClient;
135
- moveService?: PlacementMoveService;
131
+ client: RemitClient<never>;
132
+ moveService: PlacementMoveService;
136
133
  }
137
134
  export interface ApplyOrganizeResult {
138
135
  applied: number;
@@ -165,9 +162,9 @@ export interface FilterBackApplyResult {
165
162
  failures: string[];
166
163
  }
167
164
  export interface FilterBackApplyDeps {
168
- client: RemitClient;
165
+ client: RemitClient<never>;
169
166
  matchDeps: OrganizeMatchDeps;
170
- moveService?: PlacementMoveService;
167
+ moveService: PlacementMoveService;
171
168
  }
172
169
  /**
173
170
  * Run one standing filter over the inbox as if each matching message had just
@@ -154,25 +154,17 @@ export const buildOrganizeMatchDeps = (client) => ({
154
154
  * {@link PlacementMoveService} over the shared message-management queue. A move
155
155
  * commits locally (marker + ThreadMessage + Message row) and enqueues a
156
156
  * `PLACEMENT_MOVE_PUSH` for the reconciler; its marker state engine makes a
157
- * redelivered job idempotent (mirrors #1297). Returns `undefined` when no
158
- * message-management queue is wired — the same gate body sync uses to keep the
159
- * move path off — so a move requested in that environment is counted as failed
160
- * rather than silently dropped (see {@link applyOrganize}).
157
+ * redelivered job idempotent (mirrors #1297).
161
158
  */
162
- export const buildOrganizeMoveService = (client) => {
163
- const queueUrl = process.env.SQS_QUEUE_URL_MESSAGE_MGMT;
164
- if (!queueUrl)
165
- return undefined;
166
- return new PlacementMoveService({
167
- messageService: client.message,
168
- threadMessageService: client.threadMessage,
169
- markerService: client.placementMove,
170
- addressService: client.address,
171
- mailboxSpecialUseService: client.mailboxSpecialUse,
172
- mailboxService: client.mailbox,
173
- sqsQueueUrl: queueUrl,
174
- });
175
- };
159
+ export const buildOrganizeMoveService = (client) => new PlacementMoveService({
160
+ messageService: client.message,
161
+ threadMessageService: client.threadMessage,
162
+ markerService: client.placementMove,
163
+ addressService: client.address,
164
+ mailboxSpecialUseService: client.mailboxSpecialUse,
165
+ mailboxService: client.mailbox,
166
+ queue: client.queues.messageManagement,
167
+ });
176
168
  /**
177
169
  * Apply the back-apply action to every matched message, reusing the index-time
178
170
  * apply plumbing: an idempotent MessageLabel upsert (additive) with
@@ -195,12 +187,6 @@ export const applyOrganize = async (deps, accountConfigId, messageIds, predicate
195
187
  const applyMove = predicate.actionMailboxId !== NO_ACTION && predicate.actionMailboxId !== "";
196
188
  const applyToMessage = async (messageId) => {
197
189
  if (applyMove) {
198
- if (!moveService) {
199
- // An exclusive move was requested but this caller wired no move
200
- // service. Never silently pretend it applied — surface it as a
201
- // failed message so the job's failedCount reflects reality.
202
- throw new Error("Organize move action requested but no move service is wired");
203
- }
204
190
  const message = await client.message.get(messageId);
205
191
  const accountId = await client.mailbox.resolveAccountId(message.mailboxId);
206
192
  if (!accountId) {
@@ -324,9 +310,6 @@ export const backApplyFilter = async (deps, accountConfigId, filterId) => {
324
310
  mailboxService: client.mailbox,
325
311
  placementMoveService: moveService,
326
312
  });
327
- if (!config) {
328
- throw new Error("Filter back-apply needs the message-management queue to move mail");
329
- }
330
313
  const pipeline = new FilterPipeline(config, logger);
331
314
  const mailboxIds = await inboxMailboxIds(client, accountConfigId);
332
315
  const nothing = {
@@ -0,0 +1,12 @@
1
+ import type { AccountFanoutEvent, FifoQueueProducer, QueueProducer } from "@remit/data-ports";
2
+ import type { FlagPushEvent, MailboxManagementEvent, MessageMoveQueueEvent, PlacementMovePushEvent, SendMessageEvent, SyncMessageBodyEvent } from "@remit/mailbox-service";
3
+ import type { SyncMailboxesEvent } from "./trigger-sync.js";
4
+ export type SyncQueueEvent = SyncMailboxesEvent | FlagPushEvent | MailboxManagementEvent | MessageMoveQueueEvent;
5
+ export interface RemitClientQueues {
6
+ sync: FifoQueueProducer<SyncQueueEvent>;
7
+ accountFanout: QueueProducer<AccountFanoutEvent>;
8
+ smtp: QueueProducer<SendMessageEvent>;
9
+ messageManagement: QueueProducer<PlacementMovePushEvent>;
10
+ body: QueueProducer<SyncMessageBodyEvent>;
11
+ }
12
+ export type QueueName = keyof RemitClientQueues;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { RemitClientSharedDeps } from "./create-remit-client.js";
2
+ import type { QueueName, RemitClientQueues } from "./queues.js";
3
+ export type SqsQueueUrls = Record<QueueName, string>;
4
+ export declare const sqsQueueUrlsFromEnv: () => SqsQueueUrls;
5
+ export declare const sqsQueues: (urls: SqsQueueUrls) => RemitClientQueues;
6
+ export declare const buildSharedDeps: (queues: RemitClientQueues) => RemitClientSharedDeps;
@@ -0,0 +1,26 @@
1
+ import { createCachedDataKeyProvider, createSecretsService, } from "@remit/secrets-service";
2
+ import { createKmsDataKeyProvider, FAKE_KMS_KEY_ID, } from "@remit/secrets-service/kms";
3
+ import { createFifoQueueProducer, createStandardQueueProducer, } from "@remit/sqs-client/producer";
4
+ import { createStorageService } from "@remit/storage-service/s3";
5
+ import { env } from "expect-env";
6
+ export const sqsQueueUrlsFromEnv = () => ({
7
+ sync: env.SQS_QUEUE_URL,
8
+ accountFanout: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
9
+ smtp: env.SQS_QUEUE_URL_SMTP,
10
+ messageManagement: env.SQS_QUEUE_URL_MESSAGE_MGMT,
11
+ body: env.SQS_QUEUE_URL_BODY,
12
+ });
13
+ export const sqsQueues = (urls) => ({
14
+ sync: createFifoQueueProducer({ queueUrl: urls.sync }),
15
+ accountFanout: createStandardQueueProducer({ queueUrl: urls.accountFanout }),
16
+ smtp: createStandardQueueProducer({ queueUrl: urls.smtp }),
17
+ messageManagement: createStandardQueueProducer({
18
+ queueUrl: urls.messageManagement,
19
+ }),
20
+ body: createStandardQueueProducer({ queueUrl: urls.body }),
21
+ });
22
+ export const buildSharedDeps = (queues) => ({
23
+ storage: createStorageService(),
24
+ secrets: createSecretsService(createCachedDataKeyProvider(createKmsDataKeyProvider(process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID))),
25
+ queues,
26
+ });
@@ -1,7 +1,13 @@
1
- import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
1
+ import type { FifoQueueMessage, FifoQueueProducer } from "@remit/data-ports";
2
+ export interface SyncMailboxesEvent {
3
+ type: "SYNC_MAILBOXES";
4
+ eventId: string;
5
+ timestamp: number;
6
+ accountId: string;
7
+ explicitRequest?: boolean;
8
+ }
2
9
  interface TriggerAccountSyncInput {
3
- sqsClient: SQSClient;
4
- queueUrl: string;
10
+ queue: FifoQueueProducer<SyncMailboxesEvent>;
5
11
  accountId: string;
6
12
  /**
7
13
  * Set by POST /sync, whose callers ask for a sync of one named account: the
@@ -21,7 +27,7 @@ interface TriggerAccountSyncInput {
21
27
  */
22
28
  explicitRequest?: boolean;
23
29
  /**
24
- * Override the FIFO `MessageDeduplicationId`. Defaults to the event's own
30
+ * Override the queue deduplication id. Defaults to the event's own
25
31
  * id, so the queue suppresses a re-send of one event and nothing else —
26
32
  * every manual call site (POST /sync, OAuth connect, config load,
27
33
  * pull-to-refresh, the client's online-poll) always enqueues. A shared,
@@ -44,7 +50,7 @@ interface TriggerAccountSyncInput {
44
50
  * retried Lambda) — see `dedupId` above.
45
51
  */
46
52
  export declare const buildScheduledSyncDedupId: (accountId: string, now: number, bucketMs: number) => string;
47
- export declare const buildSyncMailboxesCommand: (input: Omit<TriggerAccountSyncInput, "sqsClient">) => SendMessageCommand;
53
+ export declare const buildSyncMailboxesMessage: (input: Omit<TriggerAccountSyncInput, "queue">) => FifoQueueMessage<SyncMailboxesEvent>;
48
54
  export declare const triggerAccountSync: (input: TriggerAccountSyncInput) => Promise<{
49
55
  eventId: string;
50
56
  }>;
@@ -1,6 +1,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { SendMessageCommand } from "@aws-sdk/client-sqs";
3
- const isFifoQueue = (queueUrl) => queueUrl.endsWith(".fifo");
4
2
  /**
5
3
  * Build the scheduler's own dedup namespace, bucketed by tick interval so
6
4
  * consecutive ticks each get a fresh id (never colliding with each other)
@@ -11,8 +9,8 @@ export const buildScheduledSyncDedupId = (accountId, now, bucketMs) => {
11
9
  const bucket = Math.floor(now / bucketMs);
12
10
  return `SYNC_MAILBOXES:scheduled:${accountId}:${bucket}`;
13
11
  };
14
- export const buildSyncMailboxesCommand = (input) => {
15
- const { queueUrl, accountId, dedupId, explicitRequest } = input;
12
+ export const buildSyncMailboxesMessage = (input) => {
13
+ const { accountId, dedupId, explicitRequest } = input;
16
14
  const event = {
17
15
  type: "SYNC_MAILBOXES",
18
16
  eventId: randomUUID(),
@@ -20,20 +18,14 @@ export const buildSyncMailboxesCommand = (input) => {
20
18
  accountId,
21
19
  ...(explicitRequest && { explicitRequest }),
22
20
  };
23
- const useFifo = isFifoQueue(queueUrl);
24
- return new SendMessageCommand({
25
- QueueUrl: queueUrl,
26
- MessageBody: JSON.stringify(event),
27
- ...(useFifo && {
28
- MessageGroupId: accountId,
29
- MessageDeduplicationId: dedupId ?? event.eventId,
30
- }),
31
- });
21
+ return {
22
+ body: event,
23
+ groupId: accountId,
24
+ deduplicationId: dedupId ?? event.eventId,
25
+ };
32
26
  };
33
27
  export const triggerAccountSync = async (input) => {
34
- const command = buildSyncMailboxesCommand(input);
35
- await input.sqsClient.send(command);
36
- const body = command.input.MessageBody ?? "{}";
37
- const parsed = JSON.parse(body);
38
- return { eventId: parsed.eventId };
28
+ const message = buildSyncMailboxesMessage(input);
29
+ await input.queue.send(message);
30
+ return { eventId: message.body.eventId };
39
31
  };
@@ -1,4 +1,7 @@
1
1
  import type { Context } from "openapi-backend";
2
+ import type { ApiConfig } from "./config/api-config.js";
3
+ import type { RemitClient } from "./service/create-remit-client.js";
4
+ import type { QueueName } from "./service/queues.js";
2
5
  type MatchPrefix<Prefix extends string, S extends string> = S extends `${Prefix}${infer _}` ? S : never;
3
6
  export type OperationIds = "MeOperations_deleteMe" | "MeOperations_listVipSuggestions" | "MeOperations_createExport" | "MeOperations_getExport" | "MeOperations_listQuarantine" | "ConfigOperations_getConfig" | "ConfigOperations_exportConfig" | "ConfigOperations_importConfig" | "SystemOperations_getSystemUpdate" | "SystemOperations_applySystemUpdate" | "AccountOperations_createAccount" | "AccountOperations_testConnection" | "AccountDetailOperations_updateAccount" | "AccountDetailOperations_deleteAccount" | "MicrosoftOAuthOperations_microsoftOAuthStart" | "MicrosoftOAuthOperations_microsoftOAuthCallback" | "MailboxOperations_listMailboxes" | "MailboxOperations_createMailbox" | "MailboxDetailOperations_getMailbox" | "MailboxDetailOperations_renameMailbox" | "MailboxDetailOperations_deleteMailbox" | "FolderRoleOperations_appointFolderRole" | "FilterOperations_listFilters" | "FilterOperations_createFilter" | "FilterDetailOperations_getFilter" | "FilterDetailOperations_updateFilter" | "FilterDetailOperations_deleteFilter" | "LabelOperations_listLabels" | "LabelOperations_createLabel" | "LabelDetailOperations_getLabel" | "LabelDetailOperations_updateLabel" | "LabelDetailOperations_deleteLabel" | "OrganizeOperations_createOrganizeJob" | "OrganizeOperations_previewOrganize" | "OrganizeJobDetailOperations_getOrganizeJob" | "SyncOperations_triggerSync" | "SyncOperations_getSyncStatus" | "ThreadDetailOperations_listThreadMessages" | "UnifiedThreadOperations_listAllThreads" | "ThreadOperations_listThreads" | "ThreadOperations_searchThreads" | "MessageOperations_describeMessage" | "MessageOperations_getRawMessage" | "MessageOperations_updateMessageFlags" | "MessageBulkOperations_deleteMessages" | "MessageBulkOperations_moveMessages" | "MessageBulkOperations_updateFlags" | "MessageBulkOperations_copyMessages" | "MessageBulkOperations_updateMessageLabels" | "MessageBulkOperations_reportSpam" | "MessageBulkOperations_notSpam" | "CalendarOperations_listCalendars" | "CalendarOperations_createCalendar" | "CalendarDetailOperations_getCalendar" | "CalendarDetailOperations_updateCalendar" | "CalendarDetailOperations_deleteCalendar" | "CalendarDetailOperations_getCalendarFeed" | "CalendarDetailOperations_putCalendarFeed" | "CalendarDetailOperations_revokeCalendarFeed" | "CalendarFeedOperations_getCalendarFeedIcal" | "CalendarEventOperations_listCalendarEvents" | "CalendarEventOperations_createCalendarEvent" | "CalendarEventDetailOperations_getCalendarEvent" | "CalendarEventDetailOperations_updateCalendarEvent" | "CalendarEventDetailOperations_deleteCalendarEvent" | "CalendarFreeBusyOperations_listCalendarFreeBusy" | "TrashOperations_emptyTrash" | "OutboxOperations_createOutboxMessage" | "OutboxOperations_listOutboxMessages" | "OutboxDetailOperations_getOutboxMessage" | "OutboxDetailOperations_updateOutboxMessage" | "OutboxDetailOperations_deleteOutboxMessage" | "OutboxDetailOperations_sendOutboxMessage" | "OutboxDetailOperations_mintOutboxAttachment" | "OutboxAttachmentOperations_completeOutboxAttachment" | "AddressOperations_searchAddresses" | "AddressDetailOperations_updateAddress" | "CalendarSuggestionOperations_listCalendarSuggestions" | "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions" | "CalendarSuggestionActionOperations_acceptCalendarSuggestion" | "CalendarSuggestionActionOperations_declineCalendarSuggestion" | "CalendarSuggestionActionOperations_dismissCalendarSuggestion" | "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
4
7
  export type MeOperationIds = MatchPrefix<"MeOperations_", OperationIds>;
@@ -45,5 +48,25 @@ export type CalendarSuggestionOperationIds = MatchPrefix<"CalendarSuggestionOper
45
48
  export type MessageCalendarSuggestionOperationIds = MatchPrefix<"MessageCalendarSuggestionOperations_", OperationIds>;
46
49
  export type CalendarSuggestionActionOperationIds = MatchPrefix<"CalendarSuggestionActionOperations_", OperationIds>;
47
50
  type HandlerResponse = Record<string, any>;
48
- export type OperationHandler<_T extends OperationIds = OperationIds> = (context: Context) => Promise<HandlerResponse>;
51
+ export interface UserIdentity {
52
+ kind: "user";
53
+ accountConfigId: string;
54
+ sub: string;
55
+ email: string;
56
+ }
57
+ export type HandlerIdentity = {
58
+ kind: "anonymous";
59
+ } | UserIdentity;
60
+ export interface HandlerContext<K extends QueueName = never> {
61
+ client: RemitClient<K>;
62
+ identity: UserIdentity;
63
+ config: ApiConfig;
64
+ }
65
+ export interface PublicHandlerContext<K extends QueueName = never> {
66
+ client: RemitClient<K>;
67
+ config: ApiConfig;
68
+ }
69
+ export type PublicOperationIds = "MicrosoftOAuthOperations_microsoftOAuthCallback" | "CalendarFeedOperations_getCalendarFeedIcal";
70
+ export type OperationHandler<_T extends OperationIds = OperationIds, K extends QueueName = never> = (context: Context, ctx: HandlerContext<K>) => Promise<HandlerResponse>;
71
+ export type PublicOperationHandler<K extends QueueName = never> = (context: Context, ctx: PublicHandlerContext<K>) => Promise<HandlerResponse>;
49
72
  export {};