@remit/backend 0.0.147 → 0.0.149

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/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,8 +1,10 @@
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";
4
- import { ownedAccount, resolveConnectionCredentials, testImapConnection, testSmtpConnection, } from "@remit/mailbox-service";
5
+ import { ownedAccount, resolveConnectionCredentials, testImapConnection, } from "@remit/mailbox-service";
5
6
  import { deserializeEncryptedPayload, serializeEncryptedPayload, } from "@remit/secrets-service";
7
+ import { verifySmtpConnection } from "@remit/smtp-service";
6
8
  import { loadMsOAuth } from "../config/msoauth.js";
7
9
  import { deleteAccount } from "../service/account-deletion.js";
8
10
  import { fireAndForget } from "../service/fire-and-forget.js";
@@ -11,7 +13,6 @@ import { triggerAccountSync } from "../service/trigger-sync.js";
11
13
  import { assertNoDuplicateMailbox, assertNotOAuthCreate, assertPasswordProvided, resolveSyncedServices, toAccountResponse, } from "./account-guards.js";
12
14
  import { loadAccountOverrides, upsertAccountDisplayName, writeAccountComposeLanguages, writeAccountMuted, } from "./account-overrides.js";
13
15
  import { loadSignatureForAccount, upsertAccountSignature, } from "./account-signature.js";
14
- import { ensureAccountConfig } from "./ensure-account-config.js";
15
16
  import { resolveAccountFolderAppointments } from "./folder-role-appointments.js";
16
17
  export { assertNotOAuthCreate, assertPasswordProvided, toAccountResponse };
17
18
  export const triggerAccountSyncSafe = async (client, accountId) => {
@@ -44,7 +45,7 @@ export const triggerAccountSyncSafe = async (client, accountId) => {
44
45
  * reauth-required case is mapped to the distinct `reauth_required` error code
45
46
  * so the UI can route the user into the OAuth re-auth flow.
46
47
  */
47
- const testOAuthConnection = async (msOAuth, existingAccount, account, secrets) => {
48
+ const testOAuthConnection = async (msOAuth, existingAccount, account, secrets, mailTransports) => {
48
49
  const { config, credentials: app } = await loadMsOAuth(msOAuth);
49
50
  const tokenService = createMailOAuthService(microsoftProviderConfig({
50
51
  clientId: app.clientId,
@@ -93,19 +94,20 @@ const testOAuthConnection = async (msOAuth, existingAccount, account, secrets) =
93
94
  secure: existingAccount.imapTls,
94
95
  user: existingAccount.username,
95
96
  credentials,
96
- });
97
+ }, mailTransports.imap(existingAccount.imapHost, existingAccount.imapTls));
97
98
  result.imapSuccess = imapResult.success;
98
99
  if (!imapResult.success) {
99
100
  result.imapError = imapResult.error;
100
101
  }
101
102
  // OAuth accounts share the same access token for IMAP and SMTP.
102
103
  if (existingAccount.smtpEnabled) {
103
- const smtpResult = await testSmtpConnection({
104
+ const smtpResult = await verifySmtpConnection({
104
105
  host: existingAccount.smtpHost ?? "",
105
106
  port: existingAccount.smtpPort ?? 587,
106
107
  secure: existingAccount.smtpTls ?? false,
107
108
  user: existingAccount.smtpUsername || existingAccount.username,
108
109
  credentials,
110
+ getSocket: mailTransports.smtp,
109
111
  });
110
112
  result.smtpSuccess = smtpResult.success;
111
113
  if (!smtpResult.success) {
@@ -181,7 +183,7 @@ export const AccountOperations = {
181
183
  AccountOperations_testConnection: async (context, ctx) => {
182
184
  const accountConfigId = ctx.identity.accountConfigId;
183
185
  const input = context.request.requestBody;
184
- const { account, secrets } = ctx.client;
186
+ const { account, secrets, mailTransports } = ctx.client;
185
187
  // Resolve password - use provided password or decrypt stored password
186
188
  let imapPassword = input.password;
187
189
  let smtpPassword = input.smtpPassword;
@@ -190,7 +192,7 @@ export const AccountOperations = {
190
192
  unwrap(ownedAccount(existingAccount, accountConfigId, "read"));
191
193
  const authType = existingAccount.authType ?? "password";
192
194
  if (authType === "oauthMicrosoft") {
193
- return testOAuthConnection(ctx.config.msOAuth, existingAccount, account, secrets);
195
+ return testOAuthConnection(ctx.config.msOAuth, existingAccount, account, secrets, mailTransports);
194
196
  }
195
197
  if (existingAccount.passwordHash) {
196
198
  const payload = deserializeEncryptedPayload(JSON.parse(existingAccount.passwordHash));
@@ -218,17 +220,16 @@ export const AccountOperations = {
218
220
  host: input.imapHost,
219
221
  port: input.imapPort,
220
222
  secure: input.imapTls,
221
- startTls: input.imapStartTls,
222
223
  user: input.username,
223
224
  credentials: { kind: "password", password: imapPassword },
224
- });
225
+ }, mailTransports.imap(input.imapHost, input.imapTls));
225
226
  result.imapSuccess = imapResult.success;
226
227
  if (!imapResult.success) {
227
228
  result.imapError = imapResult.error;
228
229
  }
229
230
  // Test SMTP connection if configured
230
231
  if (input.smtpHost) {
231
- const smtpResult = await testSmtpConnection({
232
+ const smtpResult = await verifySmtpConnection({
232
233
  host: input.smtpHost,
233
234
  port: input.smtpPort ?? 587,
234
235
  secure: input.smtpTls ?? false,
@@ -237,6 +238,7 @@ export const AccountOperations = {
237
238
  kind: "password",
238
239
  password: smtpPassword ?? imapPassword,
239
240
  },
241
+ getSocket: mailTransports.smtp,
240
242
  });
241
243
  result.smtpSuccess = smtpResult.success;
242
244
  if (!smtpResult.success) {
@@ -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, userId, commit) => {
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.identity.sub, ctx.config.commit);
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.identity.sub, ctx.config.commit);
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,11 +1,14 @@
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 IImapConnection, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
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
+ import type { SmtpGetSocket } from "@remit/smtp-service";
4
5
  import type { StorageService } from "@remit/storage-service";
5
6
  import type { QueueName, RemitClientQueues } from "./queues.js";
6
- export interface ConnectionScope {
7
- getConnection: () => Promise<IImapConnection>;
8
- disconnect: () => Promise<void>;
7
+ export type { ConnectionScope };
8
+ export type ConnectionScopeFactory = (accountService: IAccountRepository, secretsService: SecretsService) => (accountId: string) => Promise<ConnectionScope>;
9
+ export interface MailTransports {
10
+ imap: (host: string, secure: boolean) => ImapTransport;
11
+ smtp: SmtpGetSocket;
9
12
  }
10
13
  export interface RemitClientCore {
11
14
  accountConfig: IAccountConfigRepository;
@@ -41,6 +44,7 @@ export interface RemitClientCore {
41
44
  flagPush: IMessageFlagPushRepository;
42
45
  outboxAttachment: OutboxAttachmentService;
43
46
  createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
47
+ mailTransports: MailTransports;
44
48
  }
45
49
  export interface SyncQueueServices {
46
50
  flagQueue: FlagQueueService;
@@ -95,11 +99,14 @@ export interface RemitClientRepositories {
95
99
  export interface RemitClientSharedDeps {
96
100
  storage: StorageService;
97
101
  secrets: SecretsService;
102
+ connectionScope: ConnectionScopeFactory;
103
+ mailTransports: MailTransports;
98
104
  queues: RemitClientQueues;
99
105
  }
100
106
  export interface RemitClientDeps extends RemitClientSharedDeps {
101
107
  repositories: RemitClientRepositories;
102
108
  }
109
+ export declare const passwordConnectionScope: (transport: ImapTransport) => ConnectionScopeFactory;
103
110
  export declare const buildReadPathFilterConfig: (repositories: Pick<RemitClientRepositories, "filter" | "messageLabel" | "mailbox" | "message" | "threadMessage" | "placementMove" | "address" | "mailboxSpecialUse">, queues: Pick<RemitClientQueues, "messageManagement">) => FilterConfig;
104
111
  export declare const createRemitCore: (deps: Omit<RemitClientDeps, "queues">) => RemitClientCore;
105
112
  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, createConnection, FlagPushService, FlagQueueService, fifoMailboxManagement, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, PlacementMoveService, SpamReportService, SyncWarningService, } from "@remit/mailbox-service";
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 buildConnectionScope = (accountService, secretsService) => async (accountId) => {
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
- let connection = null;
12
- let connectPromise = null;
13
- const getConnection = async () => {
14
- if (connectPromise) {
15
- return connectPromise;
16
- }
17
- const conn = createConnection({
18
- user: account.username,
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, mailTransports } = deps;
66
49
  return {
67
50
  accountConfig: repositories.accountConfig,
68
51
  account: repositories.account,
@@ -103,7 +86,8 @@ export const createRemitCore = (deps) => {
103
86
  outboxAttachmentService: repositories.outboxAttachment,
104
87
  storage,
105
88
  }),
106
- createConnectionScope: buildConnectionScope(repositories.account, secrets),
89
+ createConnectionScope: connectionScope(repositories.account, secrets),
90
+ mailTransports,
107
91
  };
108
92
  };
109
93
  export const composeSyncServices = (core, queues) => {
@@ -1,4 +1,4 @@
1
- import type { RemitClientSharedDeps } from "./create-remit-client.js";
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,11 @@
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";
4
+ import { directSmtpSocket } from "@remit/smtp-service";
3
5
  import { createFifoQueueProducer, createStandardQueueProducer, } from "@remit/sqs-client/producer";
4
6
  import { createStorageService } from "@remit/storage-service/s3";
5
7
  import { env } from "expect-env";
8
+ import { passwordConnectionScope, } from "./create-remit-client.js";
6
9
  export const sqsQueueUrlsFromEnv = () => ({
7
10
  sync: env.SQS_QUEUE_URL,
8
11
  accountFanout: env.SQS_QUEUE_URL_ACCOUNT_FANOUT,
@@ -19,8 +22,13 @@ export const sqsQueues = (urls) => ({
19
22
  }),
20
23
  body: createStandardQueueProducer({ queueUrl: urls.body }),
21
24
  });
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
- });
25
+ export const buildSharedDeps = (queues) => {
26
+ const imapTransport = nodeTransport();
27
+ return {
28
+ storage: createStorageService(),
29
+ secrets: createSecretsService(createCachedDataKeyProvider(createKmsDataKeyProvider(process.env.KMS_KEY_ID ?? FAKE_KMS_KEY_ID))),
30
+ connectionScope: passwordConnectionScope(imapTransport),
31
+ mailTransports: { imap: () => imapTransport, smtp: directSmtpSocket },
32
+ queues,
33
+ };
34
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.147",
3
+ "version": "0.0.149",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -101,6 +101,7 @@
101
101
  "@remit/mail-oauth-service": "*",
102
102
  "@remit/secrets-service": "*",
103
103
  "@remit/mailbox-service": "*",
104
+ "@remit/smtp-service": "*",
104
105
  "@remit/storage-service": "*",
105
106
  "@remit/auth-service": "*",
106
107
  "@remit/drizzle-service": "*",
@@ -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
- };