@remit/backend 2.6.2 → 2.7.1

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.
@@ -0,0 +1,2 @@
1
+ import { type ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
2
+ export declare const classificationDefaultFromEnv: (env: NodeJS.ProcessEnv) => ClassificationDeploymentDefault;
@@ -0,0 +1,54 @@
1
+ import { CLASSIFICATION_PRESETS, DEFAULT_CLASSIFICATION_DAILY_LIMIT, isClassificationPresetId, } from "@remit/data-ports/classification-presets";
2
+ const PRESET = "CLASSIFICATION_DEFAULT_PRESET";
3
+ const MODEL = "CLASSIFICATION_DEFAULT_MODEL";
4
+ const BASE_URL = "CLASSIFICATION_DEFAULT_BASE_URL";
5
+ const API_KEY = "CLASSIFICATION_DEFAULT_API_KEY";
6
+ const ENABLED = "CLASSIFICATION_DEFAULT_ENABLED";
7
+ const DAILY_LIMIT = "CLASSIFICATION_DEFAULT_DAILY_LIMIT";
8
+ const DEPENDENT_KEYS = [MODEL, BASE_URL, API_KEY, ENABLED, DAILY_LIMIT];
9
+ const refuse = (message) => {
10
+ throw new Error(`Refusing to start: ${message}`);
11
+ };
12
+ const envValue = (env, key) => (env[key] ?? "").trim();
13
+ const enabledOf = (raw) => {
14
+ if (raw === "" || raw === "false")
15
+ return false;
16
+ if (raw === "true")
17
+ return true;
18
+ return refuse(`${ENABLED}=${raw} must be true or false.`);
19
+ };
20
+ const dailyLimitOf = (raw) => {
21
+ if (raw === "")
22
+ return DEFAULT_CLASSIFICATION_DAILY_LIMIT;
23
+ const limit = Number(raw);
24
+ if (/^\d+$/.test(raw) && Number.isSafeInteger(limit) && limit > 0)
25
+ return limit;
26
+ return refuse(`${DAILY_LIMIT}=${raw} must be a positive integer.`);
27
+ };
28
+ export const classificationDefaultFromEnv = (env) => {
29
+ const presetId = envValue(env, PRESET);
30
+ if (presetId === "") {
31
+ const stray = DEPENDENT_KEYS.filter((key) => envValue(env, key) !== "");
32
+ if (stray.length > 0)
33
+ return refuse(`${stray.join(", ")} set without ${PRESET}.`);
34
+ return { state: "None" };
35
+ }
36
+ if (!isClassificationPresetId(presetId))
37
+ return refuse(`${PRESET}=${presetId} is not a known preset. Use one of ${Object.keys(CLASSIFICATION_PRESETS).join(", ")}.`);
38
+ const preset = CLASSIFICATION_PRESETS[presetId];
39
+ const apiKey = envValue(env, API_KEY);
40
+ if (preset.needsKey && apiKey === "")
41
+ return refuse(`${API_KEY} is not set. ${PRESET}=${presetId} needs a key.`);
42
+ const baseUrl = envValue(env, BASE_URL) || preset.defaultBaseUrl;
43
+ if (!baseUrl)
44
+ return refuse(`${BASE_URL} is not set. ${PRESET}=${presetId} has no default base URL.`);
45
+ return {
46
+ state: "Configured",
47
+ presetId,
48
+ model: envValue(env, MODEL) || preset.defaultModel,
49
+ baseUrl,
50
+ apiKey,
51
+ enabled: enabledOf(envValue(env, ENABLED)),
52
+ dailyLimit: dailyLimitOf(envValue(env, DAILY_LIMIT)),
53
+ };
54
+ };
@@ -52,6 +52,8 @@ type ThreadSearch = {
52
52
  starred?: boolean;
53
53
  attachments?: boolean;
54
54
  category?: MessageCategory[];
55
+ senderTrust?: SenderTrust[];
56
+ dkimMismatch?: boolean;
55
57
  };
56
58
  /**
57
59
  * Minimal client surface `executeThreadSearch` needs. Declared structurally
@@ -84,21 +86,12 @@ export type ThreadSearchParams = {
84
86
  limit?: number;
85
87
  };
86
88
  /**
87
- * Run a per-mailbox thread search: select the index/window read, optionally
88
- * count, and resolve off-row criteria by enriching+filtering the window. The
89
- * `results` toggle omits `items` (count-only) and `count` adds the match count.
89
+ * Run a per-mailbox thread search: select the index/window read and optionally
90
+ * count. The `results` toggle omits `items` (count-only) and `count` adds the
91
+ * match count.
90
92
  *
91
93
  * `count` names the whole match, never the page: a page size bounds the rows a
92
94
  * response carries and has no bearing on how many messages match (#509).
93
- *
94
- * An off-row criterion is the one shape that has no such number. `senderTrust`
95
- * and `dkimMismatch` are resolved by enriching a window, so counting them means
96
- * enriching every candidate row in the mailbox. `count` is therefore absent
97
- * (#305) — a page's post-filter length labelled `count` is a page length
98
- * presented as a total, which is the defect the field exists to avoid. Absence
99
- * is the same answer the response already gives for a count nobody asked for,
100
- * so no caller needs a second shape to read it. A count-only request under an
101
- * off-row criterion therefore reads nothing at all.
102
95
  */
103
96
  export declare const executeThreadSearch: (client: ThreadSearchClient, accountConfigId: string, mailboxId: string, params: ThreadSearchParams) => Promise<ThreadSearchResponse>;
104
97
  /**
@@ -1,7 +1,6 @@
1
1
  import { NotFoundError } from "@remit/data-ports/errors";
2
2
  import { isValidMessageId } from "@remit/data-ports/id";
3
3
  import { enrichThreadRows, } from "../derive/enrichThreadRows.js";
4
- import { filterByOffRowCriteria, hasOffRowCriteria, } from "../derive/filterThreadCriteria.js";
5
4
  const DEFAULT_THREADS_PAGE_SIZE = 50;
6
5
  // Project to fields used by ListThreadsResponse — keep in sync with handler mapping.
7
6
  // Includes ThreadMessage table key fields (accountConfigId, threadMessageId) and
@@ -63,21 +62,12 @@ const toArray = (value) => {
63
62
  return Array.isArray(value) ? value : [value];
64
63
  };
65
64
  /**
66
- * Run a per-mailbox thread search: select the index/window read, optionally
67
- * count, and resolve off-row criteria by enriching+filtering the window. The
68
- * `results` toggle omits `items` (count-only) and `count` adds the match count.
65
+ * Run a per-mailbox thread search: select the index/window read and optionally
66
+ * count. The `results` toggle omits `items` (count-only) and `count` adds the
67
+ * match count.
69
68
  *
70
69
  * `count` names the whole match, never the page: a page size bounds the rows a
71
70
  * response carries and has no bearing on how many messages match (#509).
72
- *
73
- * An off-row criterion is the one shape that has no such number. `senderTrust`
74
- * and `dkimMismatch` are resolved by enriching a window, so counting them means
75
- * enriching every candidate row in the mailbox. `count` is therefore absent
76
- * (#305) — a page's post-filter length labelled `count` is a page length
77
- * presented as a total, which is the defect the field exists to avoid. Absence
78
- * is the same answer the response already gives for a count nobody asked for,
79
- * so no caller needs a second shape to read it. A count-only request under an
80
- * off-row criterion therefore reads nothing at all.
81
71
  */
82
72
  export const executeThreadSearch = async (client, accountConfigId, mailboxId, params) => {
83
73
  const search = {
@@ -88,8 +78,6 @@ export const executeThreadSearch = async (client, accountConfigId, mailboxId, pa
88
78
  starred: params.starred,
89
79
  attachments: params.attachments,
90
80
  category: params.category,
91
- };
92
- const offRow = {
93
81
  senderTrust: params.senderTrust,
94
82
  dkimMismatch: params.dkimMismatch,
95
83
  };
@@ -100,16 +88,6 @@ export const executeThreadSearch = async (client, accountConfigId, mailboxId, pa
100
88
  order: params.order,
101
89
  limit: params.limit,
102
90
  });
103
- if (hasOffRowCriteria(offRow)) {
104
- if (!wantResults)
105
- return {};
106
- const window = await client.threadMessage.searchByMailboxWindow(accountConfigId, mailboxId, search, options);
107
- const enriched = await enrichThreadRows(window.items, client, accountConfigId);
108
- return {
109
- items: filterByOffRowCriteria(enriched, offRow),
110
- continuationToken: window.continuationToken,
111
- };
112
- }
113
91
  const response = {};
114
92
  if (wantResults) {
115
93
  const window = await client.threadMessage.searchByMailboxWindow(accountConfigId, mailboxId, search, options);
@@ -1,3 +1,4 @@
1
+ import type { ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
1
2
  import { type RemitClient, type RemitClientSharedDeps } from "./create-remit-client.js";
2
3
  export declare const composeSqliteClient: (shared: RemitClientSharedDeps) => Promise<RemitClient>;
3
- export declare const buildSqliteClient: () => Promise<RemitClient>;
4
+ export declare const buildSqliteClient: (classificationDefault: ClassificationDeploymentDefault) => Promise<RemitClient>;
@@ -12,4 +12,4 @@ export const composeSqliteClient = async (shared) => {
12
12
  ...shared,
13
13
  });
14
14
  };
15
- export const buildSqliteClient = () => composeSqliteClient(buildSharedDeps(sqsQueues(sqsQueueUrlsFromEnv())));
15
+ export const buildSqliteClient = (classificationDefault) => composeSqliteClient(buildSharedDeps(sqsQueues(sqsQueueUrlsFromEnv()), classificationDefault));
@@ -1,4 +1,5 @@
1
1
  import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, ICalendarTaskRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IOutboxSchedulingPartRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
2
+ import type { ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
2
3
  import { BodySyncQueueService, BodySyncService, type ConnectionScope, type FilterConfig, FlagQueueService, type ImapTransport, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
3
4
  import { type SecretsService } from "@remit/secrets-service";
4
5
  import type { SmtpGetSocket } from "@remit/smtp-service";
@@ -47,6 +48,7 @@ export interface RemitClientCore {
47
48
  outboxAttachment: OutboxAttachmentService;
48
49
  createConnectionScope: (accountId: string) => Promise<ConnectionScope>;
49
50
  mailTransports: MailTransports;
51
+ classificationDefault: ClassificationDeploymentDefault;
50
52
  }
51
53
  export interface SyncQueueServices {
52
54
  flagQueue: FlagQueueService;
@@ -106,6 +108,7 @@ export interface RemitClientSharedDeps {
106
108
  connectionScope: ConnectionScopeFactory;
107
109
  mailTransports: MailTransports;
108
110
  queues: RemitClientQueues;
111
+ classificationDefault: ClassificationDeploymentDefault;
109
112
  }
110
113
  export interface RemitClientDeps extends RemitClientSharedDeps {
111
114
  repositories: RemitClientRepositories;
@@ -45,7 +45,7 @@ export const buildReadPathFilterConfig = (repositories, queues) => {
45
45
  });
46
46
  };
47
47
  export const createRemitCore = (deps) => {
48
- const { repositories, storage, secrets, connectionScope, mailTransports } = deps;
48
+ const { repositories, storage, secrets, connectionScope, mailTransports, classificationDefault, } = deps;
49
49
  return {
50
50
  accountConfig: repositories.accountConfig,
51
51
  account: repositories.account,
@@ -90,6 +90,7 @@ export const createRemitCore = (deps) => {
90
90
  }),
91
91
  createConnectionScope: connectionScope(repositories.account, secrets),
92
92
  mailTransports,
93
+ classificationDefault,
93
94
  };
94
95
  };
95
96
  export const composeSyncServices = (core, queues) => {
@@ -1,5 +1,7 @@
1
+ import { classificationDefaultFromEnv } from "../config/classification-default.js";
1
2
  let clientPromise = null;
2
3
  let injected = null;
4
+ const classificationDefault = classificationDefaultFromEnv(process.env);
3
5
  /**
4
6
  * Register the client from the composition root. An adapter that lives outside
5
7
  * this shared, open-core module — DynamoDB, or a Postgres adapter in the
@@ -27,7 +29,7 @@ const buildRelationalClient = async () => {
27
29
  throw new Error("no client registered — register one with setClient() from your composition root");
28
30
  }
29
31
  const composition = await import("./compose-sqlite.js");
30
- return composition.buildSqliteClient();
32
+ return composition.buildSqliteClient(classificationDefault);
31
33
  };
32
34
  export const getClient = () => {
33
35
  clientPromise ??= injected
@@ -1,6 +1,7 @@
1
+ import type { ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
1
2
  import { type RemitClientSharedDeps } from "./create-remit-client.js";
2
3
  import type { QueueName, RemitClientQueues } from "./queues.js";
3
4
  export type SqsQueueUrls = Record<QueueName, string>;
4
5
  export declare const sqsQueueUrlsFromEnv: () => SqsQueueUrls;
5
6
  export declare const sqsQueues: (urls: SqsQueueUrls) => RemitClientQueues;
6
- export declare const buildSharedDeps: (queues: RemitClientQueues) => RemitClientSharedDeps;
7
+ export declare const buildSharedDeps: (queues: RemitClientQueues, classificationDefault: ClassificationDeploymentDefault) => RemitClientSharedDeps;
@@ -22,7 +22,7 @@ export const sqsQueues = (urls) => ({
22
22
  }),
23
23
  body: createStandardQueueProducer({ queueUrl: urls.body }),
24
24
  });
25
- export const buildSharedDeps = (queues) => {
25
+ export const buildSharedDeps = (queues, classificationDefault) => {
26
26
  const imapTransport = nodeTransport();
27
27
  return {
28
28
  storage: createStorageService(),
@@ -30,5 +30,6 @@ export const buildSharedDeps = (queues) => {
30
30
  connectionScope: passwordConnectionScope(imapTransport),
31
31
  mailTransports: { imap: () => imapTransport, smtp: directSmtpSocket },
32
32
  queues,
33
+ classificationDefault,
33
34
  };
34
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "2.6.2",
3
+ "version": "2.7.1",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -1,23 +0,0 @@
1
- import type { SenderTrust, ThreadMessageResponse } from "@remit/api-openapi-types";
2
- /**
3
- * Off-row search criteria — fields that live on the underlying Message/Address,
4
- * not on the ThreadMessage row. They are resolved by enriching the windowed
5
- * rows (see enrichThreadRows) and filtering in app code, because no index or
6
- * FilterExpression can serve them.
7
- *
8
- * `senderTrust` derives from AddressItem.flags and `dkimMismatch` from
9
- * MessageItem.authenticity. `category` is not one of these: it is denormalized
10
- * onto the ThreadMessage row and filtered in SQL, inside the window.
11
- */
12
- export interface OffRowCriteria {
13
- senderTrust?: SenderTrust[];
14
- dkimMismatch?: boolean;
15
- }
16
- export declare const hasOffRowCriteria: (criteria: OffRowCriteria) => boolean;
17
- /**
18
- * Filter enriched rows by the off-row criteria. Each active criterion is an
19
- * any-of set (AND across criteria, OR within a set). A row with no
20
- * `authenticity` signal never matches a `dkimMismatch` filter (absence means no
21
- * signal, not a verdict).
22
- */
23
- export declare const filterByOffRowCriteria: (rows: ThreadMessageResponse[], criteria: OffRowCriteria) => ThreadMessageResponse[];
@@ -1,24 +0,0 @@
1
- export const hasOffRowCriteria = (criteria) => Boolean(criteria.senderTrust?.length) || criteria.dkimMismatch !== undefined;
2
- /**
3
- * Filter enriched rows by the off-row criteria. Each active criterion is an
4
- * any-of set (AND across criteria, OR within a set). A row with no
5
- * `authenticity` signal never matches a `dkimMismatch` filter (absence means no
6
- * signal, not a verdict).
7
- */
8
- export const filterByOffRowCriteria = (rows, criteria) => {
9
- if (!hasOffRowCriteria(criteria))
10
- return rows;
11
- const trustSet = criteria.senderTrust?.length
12
- ? new Set(criteria.senderTrust)
13
- : undefined;
14
- const { dkimMismatch } = criteria;
15
- return rows.filter((row) => {
16
- if (trustSet && !trustSet.has(row.senderTrust))
17
- return false;
18
- if (dkimMismatch !== undefined &&
19
- row.authenticity?.dkimMismatch !== dkimMismatch) {
20
- return false;
21
- }
22
- return true;
23
- });
24
- };