@remit/backend 0.0.144 → 0.0.145

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 +4 -3
  18. package/dist/src/handlers/account-oauth.js +197 -250
  19. package/dist/src/handlers/account.d.ts +4 -3
  20. package/dist/src/handlers/account.js +26 -39
  21. package/dist/src/handlers/address.d.ts +2 -2
  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 +5 -5
  30. package/dist/src/handlers/config.js +23 -35
  31. package/dist/src/handlers/filter.d.ts +2 -2
  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 +88 -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 +3 -3
  38. package/dist/src/handlers/mailbox.js +18 -26
  39. package/dist/src/handlers/me.d.ts +1 -1
  40. package/dist/src/handlers/me.js +26 -47
  41. package/dist/src/handlers/message.d.ts +3 -5
  42. package/dist/src/handlers/message.js +34 -46
  43. package/dist/src/handlers/organize.d.ts +2 -2
  44. package/dist/src/handlers/organize.js +11 -20
  45. package/dist/src/handlers/outbox-attachment.d.ts +5 -3
  46. package/dist/src/handlers/outbox-attachment.js +11 -15
  47. package/dist/src/handlers/outbox.d.ts +3 -3
  48. package/dist/src/handlers/outbox.js +27 -35
  49. package/dist/src/handlers/sync.d.ts +5 -5
  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 +96 -115
  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 +24 -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 +20 -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
@@ -0,0 +1,117 @@
1
+ import { redactCalendarFeedPath } from "@remit/calendar-service";
2
+ import { logger, withLogContext } from "@remit/logger";
3
+ import { api } from "./api.js";
4
+ import { defaultErrorCode, handleError } from "./error.js";
5
+ import { normalizeRequest } from "./request.js";
6
+ import { envelopeHeaders } from "./response.js";
7
+ const unauthenticated = () => ({
8
+ statusCode: 401,
9
+ body: { code: defaultErrorCode(401), message: "Unauthorized" },
10
+ });
11
+ const isDefined = (value) => value !== undefined;
12
+ const entriesOf = (record) => Object.entries(record).flatMap(([key, value]) => isDefined(value) ? [[key, value]] : []);
13
+ /**
14
+ * Register a host's operations and return the two ways in: a normalised
15
+ * request, or a Fetch `Request`. Each request's host supplies the client, and
16
+ * the client's queues must cover every operation in the registry.
17
+ */
18
+ export const serveApi = (registry) => {
19
+ api.register({
20
+ ...Object.fromEntries(entriesOf(registry.handlers).map(([operationId, handler]) => [
21
+ operationId,
22
+ async (c, host) => {
23
+ const identity = await host.identity();
24
+ if (identity.kind === "anonymous")
25
+ return unauthenticated();
26
+ return handler(c, {
27
+ client: await host.client(),
28
+ identity,
29
+ config: host.config,
30
+ });
31
+ },
32
+ ])),
33
+ ...Object.fromEntries(entriesOf(registry.publicHandlers).map(([operationId, handler]) => [
34
+ operationId,
35
+ async (c, host) => handler(c, { client: await host.client(), config: host.config }),
36
+ ])),
37
+ });
38
+ return { dispatch, handleFetch };
39
+ };
40
+ const readHeader = (headers, name) => {
41
+ for (const [key, value] of Object.entries(headers ?? {})) {
42
+ if (key.toLowerCase() === name && typeof value === "string")
43
+ return value;
44
+ }
45
+ return undefined;
46
+ };
47
+ const dispatch = async (request, host, correlationId) => {
48
+ // A calendar feed carries its credential in the path, so the raw path is a
49
+ // secret and cannot be a log field (issue #1065). Redacted once, here, where
50
+ // every line inside the request picks it up from the scope.
51
+ const path = redactCalendarFeedPath(request.path);
52
+ const { method } = request;
53
+ return withLogContext({ path, method }, async () => {
54
+ logger.debug({ method, path }, "Request received");
55
+ const startedAt = Date.now();
56
+ const answered = await api
57
+ .handleRequest(request, host)
58
+ .catch(handleError);
59
+ const result = {
60
+ ...answered,
61
+ headers: {
62
+ ...envelopeHeaders({
63
+ origin: readHeader(request.headers, "origin"),
64
+ allowedOrigins: host.config.corsAllowedOrigins,
65
+ correlationId,
66
+ }),
67
+ ...answered.headers,
68
+ },
69
+ };
70
+ // One structured access line per request: method, path, status, and
71
+ // duration. The correlation id is inlined as a top-level field so a bug
72
+ // report quoting the id can grep logs by it directly, and emitted at
73
+ // info so it is on by default in production (the debug "Request
74
+ // received" above already filters out under normal log levels).
75
+ // biome-ignore lint/plugin/no-logger-info: one access line per request is an audit-grade signal (issue #1054)
76
+ logger.info({
77
+ method,
78
+ path,
79
+ statusCode: result.statusCode,
80
+ durationMs: Date.now() - startedAt,
81
+ correlationId,
82
+ }, "Request completed");
83
+ return result;
84
+ });
85
+ };
86
+ const queryOf = (params) => {
87
+ const query = {};
88
+ for (const [key, value] of params) {
89
+ query[key] = [...(query[key] ?? []), value];
90
+ }
91
+ return query;
92
+ };
93
+ const NULL_BODY_STATUSES = new Set([101, 103, 204, 205, 304]);
94
+ const toFetchResponse = (result) => {
95
+ const headers = new Headers();
96
+ for (const [name, value] of Object.entries(result.headers ?? {})) {
97
+ headers.set(name, String(value));
98
+ }
99
+ for (const [name, values] of Object.entries(result.multiValueHeaders ?? {})) {
100
+ for (const value of values)
101
+ headers.append(name, String(value));
102
+ }
103
+ const body = NULL_BODY_STATUSES.has(result.statusCode) ? null : result.body;
104
+ return new Response(body, { status: result.statusCode, headers });
105
+ };
106
+ const handleFetch = async (request, host, correlationId) => {
107
+ const url = new URL(request.url);
108
+ const body = request.body === null ? null : await request.text();
109
+ const result = await dispatch(normalizeRequest({
110
+ method: request.method,
111
+ path: url.pathname,
112
+ query: queryOf(url.searchParams),
113
+ headers: Object.fromEntries(request.headers),
114
+ body,
115
+ }), host, correlationId);
116
+ return toFetchResponse(result);
117
+ };
@@ -1,10 +1,8 @@
1
- import { type SQSClient } from "@aws-sdk/client-sqs";
2
- import type { AccountItem, IAccountRepository, IConfigImportRepository } from "@remit/data-ports";
1
+ import type { AccountDataPurgeEvent, AccountItem, IAccountRepository, IConfigImportRepository, QueueProducer } from "@remit/data-ports";
3
2
  import type { Logger } from "@remit/logger";
4
3
  export interface AccountDeletionDeps {
5
4
  account: Pick<IAccountRepository, "update">;
6
- sqsClient: SQSClient;
7
- accountFanoutQueueUrl: string;
5
+ queue: QueueProducer<AccountDataPurgeEvent>;
8
6
  log: Logger;
9
7
  }
10
8
  export declare const deleteAccount: (deps: AccountDeletionDeps, account: Pick<AccountItem, "accountId" | "accountConfigId">, deletedAt: number) => Promise<void>;
@@ -1,16 +1,10 @@
1
- import { SendMessageCommand } from "@aws-sdk/client-sqs";
2
1
  import { readConfigDocument } from "@remit/config-format";
3
2
  export const deleteAccount = async (deps, account, deletedAt) => {
4
3
  const { accountId, accountConfigId } = account;
5
4
  // Purge first: an account whose request never left still reads as not deleted, so it is found and asked for again.
6
- await deps.sqsClient.send(new SendMessageCommand({
7
- QueueUrl: deps.accountFanoutQueueUrl,
8
- MessageBody: JSON.stringify({
9
- type: "AccountDataPurge",
10
- accountId,
11
- accountConfigId,
12
- }),
13
- }));
5
+ await deps.queue.send({
6
+ body: { type: "AccountDataPurge", accountId, accountConfigId },
7
+ });
14
8
  await deps.account.update(accountId, { deletedAt, isActive: false });
15
9
  deps.log.info({ accountId, accountConfigId }, "Account data purge initiated");
16
10
  };
@@ -1,2 +1,3 @@
1
- import { type RemitClient } from "./create-remit-client.js";
1
+ import { type RemitClient, type RemitClientSharedDeps } from "./create-remit-client.js";
2
+ export declare const composeSqliteClient: (shared: RemitClientSharedDeps) => Promise<RemitClient>;
2
3
  export declare const buildSqliteClient: () => Promise<RemitClient>;
@@ -1,11 +1,13 @@
1
1
  import { createSqliteRepositories, openSqliteDatabases, sqliteDatabasePathsFromEnv, } from "@remit/drizzle-service";
2
- import { buildSharedDeps, createRemitClient, } from "./create-remit-client.js";
2
+ import { createRemitClient, } from "./create-remit-client.js";
3
+ import { buildSharedDeps, sqsQueues, sqsQueueUrlsFromEnv, } from "./shared-deps.js";
3
4
  // The SQLite adapter composition (RFC 036). Every repo runs on the database
4
5
  // that holds its tables: app, tenant mail or tenant search (#1420).
5
- export const buildSqliteClient = async () => {
6
+ export const composeSqliteClient = async (shared) => {
6
7
  const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
7
8
  return createRemitClient({
8
9
  repositories: createSqliteRepositories(databases),
9
- ...buildSharedDeps(),
10
+ ...shared,
10
11
  });
11
12
  };
13
+ export const buildSqliteClient = () => composeSqliteClient(buildSharedDeps(sqsQueues(sqsQueueUrlsFromEnv())));
@@ -2,11 +2,12 @@ import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccoun
2
2
  import { BodySyncQueueService, BodySyncService, type FilterConfig, FlagQueueService, type IImapConnection, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
3
3
  import { type SecretsService } from "@remit/secrets-service";
4
4
  import type { StorageService } from "@remit/storage-service";
5
+ import type { QueueName, RemitClientQueues } from "./queues.js";
5
6
  export interface ConnectionScope {
6
7
  getConnection: () => Promise<IImapConnection>;
7
8
  disconnect: () => Promise<void>;
8
9
  }
9
- export interface RemitClient {
10
+ export interface RemitClientCore {
10
11
  accountConfig: IAccountConfigRepository;
11
12
  account: IAccountRepository;
12
13
  accountSetting: IAccountSettingRepository;
@@ -36,18 +37,29 @@ export interface RemitClient {
36
37
  calendarStore: ICalendarStore;
37
38
  storage: StorageService;
38
39
  secrets: SecretsService;
39
- bodySync: BodySyncService;
40
- bodySyncQueue?: BodySyncQueueService;
41
40
  placementMove: IMessagePlacementMoveRepository;
42
41
  flagPush: IMessageFlagPushRepository;
42
+ outboxAttachment: OutboxAttachmentService;
43
+ createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
44
+ }
45
+ export interface SyncQueueServices {
43
46
  flagQueue: FlagQueueService;
44
47
  mailboxQueue: MailboxQueueService;
45
48
  messageMove: MessageMoveService;
46
- outboxAttachment: OutboxAttachmentService;
47
- outboxQueue: OutboxQueueService;
48
49
  spamReport: SpamReportService;
49
- createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
50
50
  }
51
+ export interface SmtpQueueServices {
52
+ outboxQueue: OutboxQueueService;
53
+ }
54
+ export interface BodyQueueServices {
55
+ bodySyncQueue: BodySyncQueueService;
56
+ }
57
+ export interface ReadPathServices {
58
+ bodySync: BodySyncService;
59
+ }
60
+ export type RemitClient<K extends QueueName = QueueName> = RemitClientCore & {
61
+ queues: Pick<RemitClientQueues, K>;
62
+ } & ("sync" extends K ? SyncQueueServices : unknown) & ("smtp" extends K ? SmtpQueueServices : unknown) & ("body" extends K ? BodyQueueServices : unknown) & ("sync" | "messageManagement" extends K ? ReadPathServices : unknown);
51
63
  export interface RemitClientRepositories {
52
64
  accountConfig: IAccountConfigRepository;
53
65
  account: IAccountRepository;
@@ -83,13 +95,15 @@ export interface RemitClientRepositories {
83
95
  export interface RemitClientSharedDeps {
84
96
  storage: StorageService;
85
97
  secrets: SecretsService;
86
- sqsQueueUrl: string;
87
- sqsSmtpQueueUrl: string;
88
- bodySyncQueue?: BodySyncQueueService;
98
+ queues: RemitClientQueues;
89
99
  }
90
100
  export interface RemitClientDeps extends RemitClientSharedDeps {
91
101
  repositories: RemitClientRepositories;
92
102
  }
93
- export declare const buildSharedDeps: () => RemitClientSharedDeps;
94
- export declare const buildReadPathFilterConfig: (repositories: RemitClientRepositories) => FilterConfig | undefined;
103
+ export declare const buildReadPathFilterConfig: (repositories: Pick<RemitClientRepositories, "filter" | "messageLabel" | "mailbox" | "message" | "threadMessage" | "placementMove" | "address" | "mailboxSpecialUse">, queues: Pick<RemitClientQueues, "messageManagement">) => FilterConfig;
104
+ export declare const createRemitCore: (deps: Omit<RemitClientDeps, "queues">) => RemitClientCore;
105
+ export declare const composeSyncServices: (core: RemitClientCore, queues: Pick<RemitClientQueues, "sync">) => SyncQueueServices;
106
+ export declare const composeSmtpServices: (core: RemitClientCore, queues: Pick<RemitClientQueues, "smtp">) => SmtpQueueServices;
107
+ export declare const composeBodyServices: (queues: Pick<RemitClientQueues, "body">) => BodyQueueServices;
108
+ export declare const composeReadPathServices: (core: RemitClientCore, queues: Pick<RemitClientQueues, "messageManagement">, sync: Pick<SyncQueueServices, "flagQueue">) => ReadPathServices;
95
109
  export declare const createRemitClient: (deps: RemitClientDeps) => RemitClient;
@@ -1,9 +1,7 @@
1
1
  import { logger } from "@remit/logger";
2
2
  import { BodySyncQueueService, BodySyncService, createConnection, FlagPushService, FlagQueueService, 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
+ queue: 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