@remit/backend 0.0.146 → 0.0.148
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.js +19 -1
- package/dist/src/error.js +10 -1
- package/dist/src/handlers/account-oauth.js +1 -1
- package/dist/src/handlers/account.js +1 -1
- package/dist/src/handlers/config.js +4 -5
- package/dist/src/service/create-remit-client.d.ts +5 -5
- package/dist/src/service/create-remit-client.js +12 -29
- package/dist/src/service/shared-deps.d.ts +1 -1
- package/dist/src/service/shared-deps.js +3 -0
- package/package.json +1 -1
- package/dist/src/handlers/ensure-account-config.d.ts +0 -9
- package/dist/src/handlers/ensure-account-config.js +0 -27
package/dist/src/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import spec from "@remit/api-openapi-spec/openapi.json" with { type: "json" };
|
|
2
|
+
import { requestValidators } from "@remit/api-openapi-spec/request-validators";
|
|
2
3
|
import { redactCalendarFeedPath } from "@remit/calendar-service";
|
|
3
4
|
import { logger } from "@remit/logger";
|
|
4
5
|
import { OpenAPIBackend, } from "openapi-backend";
|
|
@@ -16,7 +17,24 @@ if (!isDocument(spec)) {
|
|
|
16
17
|
}
|
|
17
18
|
// Typed as a Document so importers do not see the JSON literal type.
|
|
18
19
|
export const OpenAPISpec = spec;
|
|
19
|
-
|
|
20
|
+
// ajv compiles a validator with `new Function`, which a Cloudflare Worker
|
|
21
|
+
// refuses, so every init hands the router the validators codegen compiled ahead
|
|
22
|
+
// of time. A host may call init again; in quick mode it builds the validator
|
|
23
|
+
// before its first await, so the validators land before any request does.
|
|
24
|
+
class PrecompiledOpenAPIBackend extends OpenAPIBackend {
|
|
25
|
+
init() {
|
|
26
|
+
const initialized = super.init();
|
|
27
|
+
for (const { operationId } of this.router.getOperations()) {
|
|
28
|
+
if (!operationId || !Object.hasOwn(requestValidators, operationId)) {
|
|
29
|
+
throw new Error(`No precompiled request validator for ${operationId}; run npm run codegen`);
|
|
30
|
+
}
|
|
31
|
+
this.validator.requestValidators[operationId] =
|
|
32
|
+
requestValidators[operationId];
|
|
33
|
+
}
|
|
34
|
+
return initialized;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const api = new PrecompiledOpenAPIBackend({
|
|
20
38
|
definition: OpenAPISpec,
|
|
21
39
|
quick: true,
|
|
22
40
|
// Coerce query/path params to their schema types. API Gateway delivers every
|
package/dist/src/error.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPublicApiError } from "@remit/data-ports/errors";
|
|
1
|
+
import { HostRefusedError, isPublicApiError } from "@remit/data-ports/errors";
|
|
2
2
|
import { logger } from "@remit/logger";
|
|
3
3
|
import { formatResponse } from "./response.js";
|
|
4
4
|
/**
|
|
@@ -35,6 +35,15 @@ export const defaultErrorCode = (statusCode) => {
|
|
|
35
35
|
return CODE_BY_STATUS[statusCode] ?? "request_refused";
|
|
36
36
|
};
|
|
37
37
|
export const handleError = async (error) => {
|
|
38
|
+
if (error instanceof HostRefusedError) {
|
|
39
|
+
logger.error({
|
|
40
|
+
error: error.message,
|
|
41
|
+
errorKind: "quota",
|
|
42
|
+
statusCode: error.statusCode,
|
|
43
|
+
stack: error.stack,
|
|
44
|
+
}, "Host refused the request");
|
|
45
|
+
return formatResponse({ code: "request_refused", message: error.message }, error.statusCode);
|
|
46
|
+
}
|
|
38
47
|
if (error instanceof Error) {
|
|
39
48
|
if ("statusCode" in error) {
|
|
40
49
|
logger.error({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { ensureAccountConfig } from "@remit/data-ports/account-config";
|
|
2
3
|
import { AccountAuthType, AccountService, ConnectionState, } from "@remit/domain-enums";
|
|
3
4
|
import { logger } from "@remit/logger";
|
|
4
5
|
import { createMailOAuthService, microsoftProviderConfig, microsoftServicesGranted, orderServices, RefreshTokenError, } from "@remit/mail-oauth-service";
|
|
@@ -8,7 +9,6 @@ import { safeJsonParse } from "../json.js";
|
|
|
8
9
|
import { rawApiResponse } from "../response.js";
|
|
9
10
|
import { triggerAccountSyncSafe } from "./account.js";
|
|
10
11
|
import { findActiveDuplicateMailbox } from "./account-guards.js";
|
|
11
|
-
import { ensureAccountConfig } from "./ensure-account-config.js";
|
|
12
12
|
const OUTLOOK_IMAP_HOST = "outlook.office365.com";
|
|
13
13
|
const ACCOUNT_SERVICES = Object.values(AccountService);
|
|
14
14
|
const isServiceList = (value) => Array.isArray(value) &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ensureAccountConfig } from "@remit/data-ports/account-config";
|
|
1
2
|
import { AccountAuthType, AccountService, ConnectionState, } from "@remit/domain-enums";
|
|
2
3
|
import { logger } from "@remit/logger";
|
|
3
4
|
import { createMailOAuthService, microsoftProviderConfig, RefreshTokenError, } from "@remit/mail-oauth-service";
|
|
@@ -11,7 +12,6 @@ import { triggerAccountSync } from "../service/trigger-sync.js";
|
|
|
11
12
|
import { assertNoDuplicateMailbox, assertNotOAuthCreate, assertPasswordProvided, resolveSyncedServices, toAccountResponse, } from "./account-guards.js";
|
|
12
13
|
import { loadAccountOverrides, upsertAccountDisplayName, writeAccountComposeLanguages, writeAccountMuted, } from "./account-overrides.js";
|
|
13
14
|
import { loadSignatureForAccount, upsertAccountSignature, } from "./account-signature.js";
|
|
14
|
-
import { ensureAccountConfig } from "./ensure-account-config.js";
|
|
15
15
|
import { resolveAccountFolderAppointments } from "./folder-role-appointments.js";
|
|
16
16
|
export { assertNotOAuthCreate, assertPasswordProvided, toAccountResponse };
|
|
17
17
|
export const triggerAccountSyncSafe = async (client, accountId) => {
|
|
@@ -65,12 +65,12 @@ const toAccountConfigResponse = (config) => ({
|
|
|
65
65
|
* The POST account-creation flow will materialize the real row when the user
|
|
66
66
|
* adds their first account.
|
|
67
67
|
*/
|
|
68
|
-
const emptyConfigResponse = (accountConfigId,
|
|
68
|
+
const emptyConfigResponse = (accountConfigId, commit) => {
|
|
69
69
|
const now = Date.now();
|
|
70
70
|
return {
|
|
71
71
|
accountConfig: {
|
|
72
72
|
accountConfigId,
|
|
73
|
-
userId,
|
|
73
|
+
userId: accountConfigId,
|
|
74
74
|
state: "active",
|
|
75
75
|
createdAt: now,
|
|
76
76
|
updatedAt: now,
|
|
@@ -91,11 +91,11 @@ export const ConfigOperations = {
|
|
|
91
91
|
throw err;
|
|
92
92
|
});
|
|
93
93
|
if (!description || description.accountConfig.length === 0) {
|
|
94
|
-
return emptyConfigResponse(accountConfigId, ctx.
|
|
94
|
+
return emptyConfigResponse(accountConfigId, ctx.config.commit);
|
|
95
95
|
}
|
|
96
96
|
const accountConfig = description.accountConfig[0];
|
|
97
97
|
if (!accountConfig) {
|
|
98
|
-
return emptyConfigResponse(accountConfigId, ctx.
|
|
98
|
+
return emptyConfigResponse(accountConfigId, ctx.config.commit);
|
|
99
99
|
}
|
|
100
100
|
// Filter out deleted accounts (tombstone pattern)
|
|
101
101
|
const activeAccounts = description.account.filter((acc) => acc.deletedAt === undefined);
|
|
@@ -151,7 +151,6 @@ export const ConfigOperations = {
|
|
|
151
151
|
appointFolderRole: (configId, accountId, role, mailboxId, lastKnownPath) => writeFolderRoleAppointment(client.accountSetting, configId, accountId, role, mailboxId, lastKnownPath),
|
|
152
152
|
}, {
|
|
153
153
|
accountConfigId,
|
|
154
|
-
userId: ctx.identity.sub,
|
|
155
154
|
document: body.document,
|
|
156
155
|
mode: body.mode ?? "validate",
|
|
157
156
|
onExisting: body.onExisting ?? "abort",
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
2
|
-
import { BodySyncQueueService, BodySyncService, type FilterConfig, FlagQueueService, type
|
|
2
|
+
import { BodySyncQueueService, BodySyncService, type ConnectionScope, type FilterConfig, FlagQueueService, type ImapTransport, 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
5
|
import type { QueueName, RemitClientQueues } from "./queues.js";
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
disconnect: () => Promise<void>;
|
|
9
|
-
}
|
|
6
|
+
export type { ConnectionScope };
|
|
7
|
+
export type ConnectionScopeFactory = (accountService: IAccountRepository, secretsService: SecretsService) => (accountId: string) => Promise<ConnectionScope>;
|
|
10
8
|
export interface RemitClientCore {
|
|
11
9
|
accountConfig: IAccountConfigRepository;
|
|
12
10
|
account: IAccountRepository;
|
|
@@ -95,11 +93,13 @@ export interface RemitClientRepositories {
|
|
|
95
93
|
export interface RemitClientSharedDeps {
|
|
96
94
|
storage: StorageService;
|
|
97
95
|
secrets: SecretsService;
|
|
96
|
+
connectionScope: ConnectionScopeFactory;
|
|
98
97
|
queues: RemitClientQueues;
|
|
99
98
|
}
|
|
100
99
|
export interface RemitClientDeps extends RemitClientSharedDeps {
|
|
101
100
|
repositories: RemitClientRepositories;
|
|
102
101
|
}
|
|
102
|
+
export declare const passwordConnectionScope: (transport: ImapTransport) => ConnectionScopeFactory;
|
|
103
103
|
export declare const buildReadPathFilterConfig: (repositories: Pick<RemitClientRepositories, "filter" | "messageLabel" | "mailbox" | "message" | "threadMessage" | "placementMove" | "address" | "mailboxSpecialUse">, queues: Pick<RemitClientQueues, "messageManagement">) => FilterConfig;
|
|
104
104
|
export declare const createRemitCore: (deps: Omit<RemitClientDeps, "queues">) => RemitClientCore;
|
|
105
105
|
export declare const composeSyncServices: (core: RemitClientCore, queues: Pick<RemitClientQueues, "sync">) => SyncQueueServices;
|
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
import { logger } from "@remit/logger";
|
|
2
|
-
import { BodySyncQueueService, BodySyncService,
|
|
2
|
+
import { BodySyncQueueService, BodySyncService, connectionScope, 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
4
|
import { deserializeEncryptedPayload, } from "@remit/secrets-service";
|
|
5
|
-
const
|
|
5
|
+
export const passwordConnectionScope = (transport) => (accountService, secretsService) => async (accountId) => {
|
|
6
6
|
const account = await accountService.get(accountId);
|
|
7
7
|
if (!account.passwordHash) {
|
|
8
8
|
throw new Error(`Account ${accountId} has no passwordHash — OAuth accounts cannot use this connection path`);
|
|
9
9
|
}
|
|
10
10
|
const password = await secretsService.decrypt(deserializeEncryptedPayload(JSON.parse(account.passwordHash)));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
credentials: { kind: "password", password },
|
|
20
|
-
host: account.imapHost,
|
|
21
|
-
port: account.imapPort,
|
|
22
|
-
tls: account.imapTls,
|
|
23
|
-
});
|
|
24
|
-
connection = conn;
|
|
25
|
-
connectPromise = conn.connect().then(() => conn);
|
|
26
|
-
return connectPromise;
|
|
27
|
-
};
|
|
28
|
-
const disconnect = async () => {
|
|
29
|
-
if (connection) {
|
|
30
|
-
await connection.disconnect();
|
|
31
|
-
connection = null;
|
|
32
|
-
connectPromise = null;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
return { getConnection, disconnect };
|
|
11
|
+
return connectionScope({
|
|
12
|
+
user: account.username,
|
|
13
|
+
credentials: { kind: "password", password },
|
|
14
|
+
host: account.imapHost,
|
|
15
|
+
port: account.imapPort,
|
|
16
|
+
tls: account.imapTls,
|
|
17
|
+
transport,
|
|
18
|
+
});
|
|
36
19
|
};
|
|
37
20
|
// The read-path body backfill (describeMessage / getRawMessage →
|
|
38
21
|
// fetchAndGetBody) materializes a body that was never sync-synced, and must run
|
|
@@ -62,7 +45,7 @@ export const buildReadPathFilterConfig = (repositories, queues) => {
|
|
|
62
45
|
});
|
|
63
46
|
};
|
|
64
47
|
export const createRemitCore = (deps) => {
|
|
65
|
-
const { repositories, storage, secrets } = deps;
|
|
48
|
+
const { repositories, storage, secrets, connectionScope } = deps;
|
|
66
49
|
return {
|
|
67
50
|
accountConfig: repositories.accountConfig,
|
|
68
51
|
account: repositories.account,
|
|
@@ -103,7 +86,7 @@ export const createRemitCore = (deps) => {
|
|
|
103
86
|
outboxAttachmentService: repositories.outboxAttachment,
|
|
104
87
|
storage,
|
|
105
88
|
}),
|
|
106
|
-
createConnectionScope:
|
|
89
|
+
createConnectionScope: connectionScope(repositories.account, secrets),
|
|
107
90
|
};
|
|
108
91
|
};
|
|
109
92
|
export const composeSyncServices = (core, queues) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RemitClientSharedDeps } from "./create-remit-client.js";
|
|
2
2
|
import type { QueueName, RemitClientQueues } from "./queues.js";
|
|
3
3
|
export type SqsQueueUrls = Record<QueueName, string>;
|
|
4
4
|
export declare const sqsQueueUrlsFromEnv: () => SqsQueueUrls;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { nodeTransport } from "@remit/mailbox-service/node-transport";
|
|
1
2
|
import { createCachedDataKeyProvider, createSecretsService, } from "@remit/secrets-service";
|
|
2
3
|
import { createKmsDataKeyProvider, FAKE_KMS_KEY_ID, } from "@remit/secrets-service/kms";
|
|
3
4
|
import { createFifoQueueProducer, createStandardQueueProducer, } from "@remit/sqs-client/producer";
|
|
4
5
|
import { createStorageService } from "@remit/storage-service/s3";
|
|
5
6
|
import { env } from "expect-env";
|
|
7
|
+
import { passwordConnectionScope, } from "./create-remit-client.js";
|
|
6
8
|
export const sqsQueueUrlsFromEnv = () => ({
|
|
7
9
|
sync: env.SQS_QUEUE_URL,
|
|
8
10
|
accountFanout: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
|
|
@@ -22,5 +24,6 @@ export const sqsQueues = (urls) => ({
|
|
|
22
24
|
export const buildSharedDeps = (queues) => ({
|
|
23
25
|
storage: createStorageService(),
|
|
24
26
|
secrets: createSecretsService(createCachedDataKeyProvider(createKmsDataKeyProvider(process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID))),
|
|
27
|
+
connectionScope: passwordConnectionScope(nodeTransport()),
|
|
25
28
|
queues,
|
|
26
29
|
});
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { IAccountConfigRepository } from "@remit/data-ports";
|
|
2
|
-
/**
|
|
3
|
-
* Ensure an AccountConfig row exists for the given id. A first-time caller
|
|
4
|
-
* coming from Cognito will not yet have one; subsequent calls are idempotent.
|
|
5
|
-
*
|
|
6
|
-
* We attempt a read first to avoid relying on a race on `create()` — the
|
|
7
|
-
* deterministic id (derived from Cognito `sub`) makes both paths safe.
|
|
8
|
-
*/
|
|
9
|
-
export declare const ensureAccountConfig: (accountConfig: IAccountConfigRepository, accountConfigId: string) => Promise<void>;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ConflictError, NotFoundError } from "@remit/data-ports/errors";
|
|
2
|
-
/**
|
|
3
|
-
* Ensure an AccountConfig row exists for the given id. A first-time caller
|
|
4
|
-
* coming from Cognito will not yet have one; subsequent calls are idempotent.
|
|
5
|
-
*
|
|
6
|
-
* We attempt a read first to avoid relying on a race on `create()` — the
|
|
7
|
-
* deterministic id (derived from Cognito `sub`) makes both paths safe.
|
|
8
|
-
*/
|
|
9
|
-
export const ensureAccountConfig = async (accountConfig, accountConfigId) => {
|
|
10
|
-
const existing = await accountConfig.get(accountConfigId).catch((err) => {
|
|
11
|
-
if (err instanceof NotFoundError)
|
|
12
|
-
return undefined;
|
|
13
|
-
throw err;
|
|
14
|
-
});
|
|
15
|
-
if (existing)
|
|
16
|
-
return;
|
|
17
|
-
await accountConfig
|
|
18
|
-
.create({
|
|
19
|
-
accountConfigId,
|
|
20
|
-
userId: accountConfigId,
|
|
21
|
-
})
|
|
22
|
-
.catch((err) => {
|
|
23
|
-
if (err instanceof ConflictError)
|
|
24
|
-
return;
|
|
25
|
-
throw err;
|
|
26
|
-
});
|
|
27
|
-
};
|