@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.
- package/dist/src/api.d.ts +0 -2
- package/dist/src/api.js +9 -80
- package/dist/src/auth.d.ts +13 -19
- package/dist/src/auth.js +51 -43
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +18 -29
- package/dist/src/config/msoauth.js +4 -28
- package/dist/src/derive/contentSignature.d.ts +3 -4
- package/dist/src/derive/contentSignature.js +4 -13
- package/dist/src/derive/contentUrl.d.ts +0 -13
- package/dist/src/derive/contentUrl.js +0 -19
- package/dist/src/handlers/account-oauth.d.ts +4 -3
- package/dist/src/handlers/account-oauth.js +197 -250
- package/dist/src/handlers/account.d.ts +4 -3
- package/dist/src/handlers/account.js +26 -39
- package/dist/src/handlers/address.d.ts +2 -2
- package/dist/src/handlers/address.js +11 -19
- package/dist/src/handlers/calendar-event.js +18 -26
- package/dist/src/handlers/calendar-feed.d.ts +2 -2
- package/dist/src/handlers/calendar-feed.js +24 -32
- package/dist/src/handlers/calendar-suggestion.js +18 -26
- package/dist/src/handlers/calendar.d.ts +1 -1
- package/dist/src/handlers/calendar.js +15 -22
- package/dist/src/handlers/config.d.ts +5 -5
- package/dist/src/handlers/config.js +23 -35
- package/dist/src/handlers/filter.d.ts +2 -2
- package/dist/src/handlers/filter.js +17 -32
- package/dist/src/handlers/folder-role.js +3 -6
- package/dist/src/handlers/index.d.ts +88 -2
- package/dist/src/handlers/index.js +6 -4
- package/dist/src/handlers/label.js +15 -22
- package/dist/src/handlers/mailbox.d.ts +3 -3
- package/dist/src/handlers/mailbox.js +18 -26
- package/dist/src/handlers/me.d.ts +1 -1
- package/dist/src/handlers/me.js +26 -47
- package/dist/src/handlers/message.d.ts +3 -5
- package/dist/src/handlers/message.js +34 -46
- package/dist/src/handlers/organize.d.ts +2 -2
- package/dist/src/handlers/organize.js +11 -20
- package/dist/src/handlers/outbox-attachment.d.ts +5 -3
- package/dist/src/handlers/outbox-attachment.js +11 -15
- package/dist/src/handlers/outbox.d.ts +3 -3
- package/dist/src/handlers/outbox.js +27 -35
- package/dist/src/handlers/sync.d.ts +5 -5
- package/dist/src/handlers/sync.js +13 -22
- package/dist/src/handlers/system-update.js +14 -27
- package/dist/src/handlers/thread.js +9 -14
- package/dist/src/handlers/unified-threads.js +3 -6
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +18 -2
- package/dist/src/jwt-auth.d.ts +9 -12
- package/dist/src/jwt-auth.js +19 -71
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +8 -2
- package/dist/src/request.js +16 -6
- package/dist/src/response.d.ts +20 -3
- package/dist/src/response.js +15 -22
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +2 -4
- package/dist/src/service/account-deletion.js +3 -9
- package/dist/src/service/compose-sqlite.d.ts +2 -1
- package/dist/src/service/compose-sqlite.js +5 -3
- package/dist/src/service/create-remit-client.d.ts +25 -11
- package/dist/src/service/create-remit-client.js +96 -115
- package/dist/src/service/data-client.js +3 -5
- package/dist/src/service/export-identity.d.ts +2 -1
- package/dist/src/service/export-identity.js +4 -6
- package/dist/src/service/fire-and-forget.js +1 -2
- package/dist/src/service/organize.d.ts +7 -10
- package/dist/src/service/organize.js +10 -27
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +11 -5
- package/dist/src/service/trigger-sync.js +10 -18
- package/dist/src/types.d.ts +24 -1
- package/package.json +20 -8
- package/dist/src/data-backend.d.ts +0 -12
- package/dist/src/data-backend.js +0 -12
- package/dist/src/request-context.d.ts +0 -9
- package/dist/src/request-context.js +0 -22
- package/dist/src/service/sqs.d.ts +0 -1
- 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 {
|
|
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
|
-
|
|
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.
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
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
|
|
6
|
+
export const composeSqliteClient = async (shared) => {
|
|
6
7
|
const databases = await openSqliteDatabases(sqliteDatabasePathsFromEnv());
|
|
7
8
|
return createRemitClient({
|
|
8
9
|
repositories: createSqliteRepositories(databases),
|
|
9
|
-
...
|
|
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
|
|
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
|
-
|
|
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
|
|
94
|
-
export declare const
|
|
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 {
|
|
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
|
-
|
|
78
|
-
|
|
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:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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:
|
|
198
|
-
addressService:
|
|
199
|
-
accountService:
|
|
200
|
-
mailboxSpecialUseService:
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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
|
|
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).
|
|
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
|
|
129
|
+
export declare const buildOrganizeMoveService: (client: RemitClient<"messageManagement">) => PlacementMoveService;
|
|
133
130
|
export interface ApplyOrganizeDeps {
|
|
134
|
-
client: RemitClient
|
|
135
|
-
moveService
|
|
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
|
|
167
|
+
moveService: PlacementMoveService;
|
|
171
168
|
}
|
|
172
169
|
/**
|
|
173
170
|
* Run one standing filter over the inbox as if each matching message had just
|