@remit/backend 2.7.1 → 3.0.0

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.
@@ -1,8 +1,9 @@
1
- import type { ClassificationConfigured, ClassificationNotConfigured, ClassificationPreset, ClassificationUsage, UpdateUserSettingsInput } from "@remit/api-openapi-types";
2
- import type { IAccountSettingRepository } from "@remit/data-ports";
3
- import { type SavedClassificationProvider, type StoredClassification } from "@remit/data-ports/classification-presets";
1
+ import type { ClassificationConfigured, ClassificationNotConfigured, ClassificationPreset, ClassificationPresetId, ClassificationUnrunnable, ClassificationUsage, UpdateUserSettingsInput } from "@remit/api-openapi-types";
2
+ import type { IAccountSettingRepository, UpsertAccountSettingInput } from "@remit/data-ports";
3
+ import { CLASSIFICATION_SETTING_NAMES } from "@remit/data-ports/classification-presets";
4
+ import { type ClassificationResolverDeps, type ClassificationResolverInput, type ResolvedClassification, type SavedClassificationApiKey } from "@remit/data-ports/classification-resolver";
4
5
  import { type ClassificationLabelDeps } from "@remit/mailbox-service";
5
- import { type SecretsService, type SerializedEncryptedPayload } from "@remit/secrets-service";
6
+ import { type SecretsService } from "@remit/secrets-service";
6
7
  import type { UserSettingsOutcome } from "./user-settings.js";
7
8
  export type ClassificationSettingsDeps = {
8
9
  accountSetting: Pick<IAccountSettingRepository, "getMany" | "upsert" | "delete">;
@@ -16,32 +17,37 @@ type Decision<T> = {
16
17
  ok: true;
17
18
  value: T;
18
19
  } | Refusal;
19
- type ApiKeyChange = {
20
- action: "Keep";
21
- ciphertext: SerializedEncryptedPayload;
22
- } | {
23
- action: "Replace";
24
- apiKey: string;
20
+ type ClassificationSettingName = (typeof CLASSIFICATION_SETTING_NAMES)[number];
21
+ type SettingWrite = {
22
+ action: "Upsert";
23
+ name: ClassificationSettingName;
24
+ value: UpsertAccountSettingInput["value"];
25
25
  } | {
26
26
  action: "Delete";
27
+ name: ClassificationSettingName;
28
+ };
29
+ type PlannedWrite = SettingWrite | {
30
+ action: "Encrypt";
31
+ apiKey: string;
27
32
  };
28
- export interface ClassificationProviderSave {
29
- preset: ClassificationPreset;
30
- model: string;
31
- baseUrl: string;
32
- apiKey: ApiKeyChange;
33
+ export type PlannedApiKey = Partial<SavedClassificationApiKey>;
34
+ export interface ClassificationChange {
35
+ writes: PlannedWrite[];
36
+ resolved: KnownClassification<PlannedApiKey>;
37
+ ensuresLabels: boolean;
33
38
  }
34
- type ClassificationInput = Pick<UpdateUserSettingsInput, "classificationPresetId" | "classificationModel" | "classificationBaseUrl" | "classificationApiKey" | "classificationEnabled" | "classificationDailyLimit">;
35
- export declare const decideClassificationProvider: (input: ClassificationInput, stored: StoredClassification) => Decision<ClassificationProviderSave>;
36
- export declare const publicClassification: (stored: StoredClassification, usage: ClassificationUsage[]) => ClassificationNotConfigured | ClassificationConfigured;
37
- export declare const readClassificationUsage: (accountSetting: Pick<IAccountSettingRepository, "getMany">, accountConfigId: string, accountIds: readonly string[], saved: Pick<SavedClassificationProvider, "presetId" | "dailyLimit">, now: number) => Promise<ClassificationUsage[]>;
39
+ export type ClassificationInput = Pick<UpdateUserSettingsInput, "classificationPresetId" | "classificationModel" | "classificationBaseUrl" | "classificationApiKey" | "classificationEnabled" | "classificationDailyLimit">;
40
+ export type KnownClassification<Saved = SavedClassificationApiKey> = Exclude<ResolvedClassification<Saved>, {
41
+ state: "UnknownPreset";
42
+ }>;
43
+ export declare const readKnownClassification: (deps: ClassificationResolverDeps, accountConfigId: string) => Promise<KnownClassification>;
44
+ export declare const decideClassificationChange: (current: ClassificationResolverInput, input: ClassificationInput) => Decision<ClassificationChange>;
45
+ export declare const publicClassification: <Saved>(resolved: KnownClassification<Saved>, usage: ClassificationUsage[]) => ClassificationNotConfigured | ClassificationUnrunnable | ClassificationConfigured;
46
+ export declare const readClassificationUsage: (accountSetting: Pick<IAccountSettingRepository, "getMany">, accountConfigId: string, accountIds: readonly string[], saved: {
47
+ presetId: ClassificationPresetId;
48
+ dailyLimit: number;
49
+ }, now: number) => Promise<ClassificationUsage[]>;
38
50
  export declare const classificationPresetsResponse: () => ClassificationPreset[];
39
- interface ClassificationChange {
40
- provider: ClassificationProviderSave | undefined;
41
- enabled: boolean | undefined;
42
- dailyLimit: number | undefined;
43
- }
44
- export declare const decideClassificationChange: (input: ClassificationInput, stored: StoredClassification) => Decision<ClassificationChange>;
45
51
  export declare const writeClassificationChange: (deps: ClassificationSettingsDeps, accountConfigId: string, change: ClassificationChange) => Promise<void>;
46
52
  export declare const clearClassification: (accountSetting: Pick<IAccountSettingRepository, "delete">, accountConfigId: string) => Promise<void>;
47
53
  export {};
@@ -1,8 +1,16 @@
1
1
  import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
2
2
  import { CLASSIFICATION_PRESETS, CLASSIFICATION_SETTING_NAMES, classificationUsageName, utcDateOf, } from "@remit/data-ports/classification-presets";
3
- import { AccountSettingName } from "@remit/domain-enums";
3
+ import { readResolvedClassification, resolveClassification, resolveClassificationWith, savedClassificationApiKey, } from "@remit/data-ports/classification-resolver";
4
+ import { AccountSettingName, ClassificationValueSource as ValueSource, } from "@remit/domain-enums";
4
5
  import { ensureClassificationLabels, } from "@remit/mailbox-service";
5
6
  import { serializeEncryptedPayload, } from "@remit/secrets-service";
7
+ const knownOf = (resolved) => {
8
+ if (resolved.state === "UnknownPreset") {
9
+ throw new Error(`Stored classification preset is unknown: ${resolved.presetId}`);
10
+ }
11
+ return resolved;
12
+ };
13
+ export const readKnownClassification = async (deps, accountConfigId) => knownOf(await readResolvedClassification(deps, accountConfigId));
6
14
  const refuse = (code, message) => ({
7
15
  ok: false,
8
16
  error: { code, message },
@@ -17,59 +25,169 @@ const savesClassificationProvider = (input) => input.classificationPresetId !==
17
25
  input.classificationModel !== undefined ||
18
26
  input.classificationBaseUrl !== undefined ||
19
27
  input.classificationApiKey !== undefined;
20
- const baseUrlFor = (preset, requested) => {
21
- if (requested === "") {
22
- if (preset.defaultBaseUrl === undefined) {
23
- return refuse("InvalidClassificationBaseUrl", `classificationBaseUrl is required for ${preset.label}`);
24
- }
25
- return { ok: true, value: preset.defaultBaseUrl };
28
+ const presetIdOf = (known) => {
29
+ if (known.state === "NotConfigured")
30
+ return undefined;
31
+ if (known.state === "Unrunnable")
32
+ return known.presetId;
33
+ return known.presetId.value;
34
+ };
35
+ const textWrite = (name, requested, switched) => {
36
+ const value = requested?.trim();
37
+ if (value === undefined)
38
+ return switched ? [{ action: "Delete", name }] : [];
39
+ if (value === "")
40
+ return [{ action: "Delete", name }];
41
+ return [{ action: "Upsert", name, value: { kind: "String", value } }];
42
+ };
43
+ const apiKeyWrite = (preset, requested, switched) => {
44
+ const name = AccountSettingName.ClassificationApiKey;
45
+ if (!preset.needsKey)
46
+ return [{ action: "Delete", name }];
47
+ if (requested !== "")
48
+ return [{ action: "Encrypt", apiKey: requested }];
49
+ return switched ? [{ action: "Delete", name }] : [];
50
+ };
51
+ const planProviderWrites = (input, known) => {
52
+ const current = presetIdOf(known);
53
+ const presetId = input.classificationPresetId ?? current;
54
+ if (presetId === undefined) {
55
+ return refuse("ClassificationNotConfigured", "classificationPresetId is required: no classification provider is saved or inherited");
26
56
  }
27
- if (!isHttpUrl(requested)) {
57
+ const baseUrl = input.classificationBaseUrl?.trim();
58
+ if (baseUrl !== undefined && baseUrl !== "" && !isHttpUrl(baseUrl)) {
28
59
  return refuse("InvalidClassificationBaseUrl", "classificationBaseUrl must be an http or https URL");
29
60
  }
30
- return { ok: true, value: requested };
61
+ const switched = presetId !== current;
62
+ return {
63
+ ok: true,
64
+ value: [
65
+ ...apiKeyWrite(CLASSIFICATION_PRESETS[presetId], (input.classificationApiKey ?? "").trim(), switched),
66
+ ...textWrite(AccountSettingName.ClassificationModel, input.classificationModel, switched),
67
+ ...textWrite(AccountSettingName.ClassificationBaseUrl, baseUrl, switched),
68
+ {
69
+ action: "Upsert",
70
+ name: AccountSettingName.ClassificationPresetId,
71
+ value: { kind: "String", value: presetId },
72
+ },
73
+ ],
74
+ };
31
75
  };
32
- const apiKeyFor = (preset, requested, keptKey) => {
33
- if (!preset.needsKey)
34
- return { ok: true, value: { action: "Delete" } };
35
- if (requested !== "") {
36
- return { ok: true, value: { action: "Replace", apiKey: requested } };
76
+ const planWrites = (input, known) => {
77
+ const provider = savesClassificationProvider(input)
78
+ ? planProviderWrites(input, known)
79
+ : { ok: true, value: [] };
80
+ if (!provider.ok)
81
+ return provider;
82
+ const writes = [...provider.value];
83
+ if (input.classificationEnabled !== undefined) {
84
+ writes.push({
85
+ action: "Upsert",
86
+ name: AccountSettingName.ClassificationEnabled,
87
+ value: { kind: "Boolean", value: input.classificationEnabled },
88
+ });
37
89
  }
38
- if (keptKey) {
39
- return { ok: true, value: { action: "Keep", ciphertext: keptKey } };
90
+ if (input.classificationDailyLimit !== undefined) {
91
+ writes.push({
92
+ action: "Upsert",
93
+ name: AccountSettingName.ClassificationDailyLimit,
94
+ value: { kind: "Number", value: input.classificationDailyLimit },
95
+ });
40
96
  }
41
- return refuse("ClassificationApiKeyRequired", `classificationApiKey is required for ${preset.label}`);
97
+ return { ok: true, value: writes };
42
98
  };
43
- export const decideClassificationProvider = (input, stored) => {
44
- const saved = stored.state === "Configured" ? stored : undefined;
45
- const presetId = input.classificationPresetId ?? saved?.presetId;
46
- if (presetId === undefined) {
47
- return refuse("ClassificationNotConfigured", "classificationPresetId is required: no classification provider is saved");
99
+ const sealWrite = async (secrets, write) => {
100
+ if (write.action !== "Encrypt")
101
+ return write;
102
+ const ciphertext = serializeEncryptedPayload(await secrets.encrypt(write.apiKey));
103
+ return {
104
+ action: "Upsert",
105
+ name: AccountSettingName.ClassificationApiKey,
106
+ value: { kind: "Map", value: { ...ciphertext } },
107
+ };
108
+ };
109
+ const applyWrites = (rows, writes, now) => writes.reduce((applied, write) => {
110
+ if (write.action === "Encrypt")
111
+ return applied;
112
+ const kept = applied.filter((row) => row.name !== write.name);
113
+ if (write.action === "Delete")
114
+ return kept;
115
+ return [...kept, { name: write.name, value: write.value, updatedAt: now }];
116
+ }, Array.from(rows));
117
+ const plannedApiKeyOf = (rows, writes) => writes.some((write) => write.action === "Encrypt")
118
+ ? {}
119
+ : savedClassificationApiKey(rows);
120
+ const refuseUnrunnable = (resolved) => {
121
+ const { label } = CLASSIFICATION_PRESETS[resolved.presetId];
122
+ if (resolved.reason === "NoBaseUrl") {
123
+ return refuse("InvalidClassificationBaseUrl", `classificationBaseUrl is required for ${label}`);
124
+ }
125
+ return refuse("ClassificationApiKeyRequired", `classificationApiKey is required for ${label}: no key is saved, and the deployment's key only applies to its own preset and base URL`);
126
+ };
127
+ export const decideClassificationChange = (current, input) => {
128
+ const planned = planWrites(input, knownOf(resolveClassification(current)));
129
+ if (!planned.ok)
130
+ return planned;
131
+ const writes = planned.value;
132
+ const rows = applyWrites(current.rows, writes, Date.now());
133
+ const resolved = knownOf(resolveClassificationWith({ ...current, rows }, plannedApiKeyOf(rows, writes)));
134
+ if (resolved.state === "NotConfigured") {
135
+ if (input.classificationEnabled === true) {
136
+ return refuse("ClassificationNotConfigured", "classificationEnabled needs a saved or inherited classification provider");
137
+ }
138
+ if (input.classificationDailyLimit !== undefined) {
139
+ return refuse("ClassificationNotConfigured", "classificationDailyLimit needs a saved or inherited classification provider");
140
+ }
141
+ return {
142
+ ok: true,
143
+ value: {
144
+ writes: [],
145
+ resolved,
146
+ ensuresLabels: false,
147
+ },
148
+ };
149
+ }
150
+ if (resolved.state === "Unrunnable" &&
151
+ (savesClassificationProvider(input) || input.classificationEnabled === true)) {
152
+ return refuseUnrunnable(resolved);
48
153
  }
49
- const preset = CLASSIFICATION_PRESETS[presetId];
50
- const kept = saved?.presetId === presetId ? saved : undefined;
51
- const baseUrl = baseUrlFor(preset, (input.classificationBaseUrl ?? kept?.baseUrl ?? "").trim());
52
- if (!baseUrl.ok)
53
- return baseUrl;
54
- const apiKey = apiKeyFor(preset, (input.classificationApiKey ?? "").trim(), kept?.apiKeyCiphertext);
55
- if (!apiKey.ok)
56
- return apiKey;
57
- const model = (input.classificationModel ?? kept?.model ?? "").trim();
58
154
  return {
59
155
  ok: true,
60
156
  value: {
61
- preset,
62
- model: model === "" ? preset.defaultModel : model,
63
- baseUrl: baseUrl.value,
64
- apiKey: apiKey.value,
157
+ writes,
158
+ resolved,
159
+ ensuresLabels: input.classificationEnabled === true,
65
160
  },
66
161
  };
67
162
  };
68
- export const publicClassification = (stored, usage) => {
69
- if (stored.state === "NotConfigured")
70
- return stored;
71
- const { apiKeyCiphertext: _ciphertext, ...configured } = stored;
72
- return { ...configured, usage };
163
+ export const publicClassification = (resolved, usage) => {
164
+ if (resolved.state === "NotConfigured")
165
+ return resolved;
166
+ if (resolved.state === "Unrunnable") {
167
+ return {
168
+ state: "Unrunnable",
169
+ presetId: resolved.presetId,
170
+ reason: resolved.reason,
171
+ };
172
+ }
173
+ return {
174
+ state: "Configured",
175
+ presetId: resolved.presetId.value,
176
+ model: resolved.model.value,
177
+ baseUrl: resolved.baseUrl.value,
178
+ apiKeySaved: resolved.apiKey.source !== ValueSource.Default,
179
+ enabled: resolved.enabled.value,
180
+ dailyLimit: resolved.dailyLimit.value,
181
+ sources: {
182
+ presetId: resolved.presetId.source,
183
+ model: resolved.model.source,
184
+ baseUrl: resolved.baseUrl.source,
185
+ apiKey: resolved.apiKey.source,
186
+ enabled: resolved.enabled.source,
187
+ dailyLimit: resolved.dailyLimit.source,
188
+ },
189
+ usage,
190
+ };
73
191
  };
74
192
  const CLASSIFICATION_INPUT_TOKENS_PER_CALL = 1_500;
75
193
  const nextUtcMidnight = (epochMs) => {
@@ -109,80 +227,23 @@ export const readClassificationUsage = async (accountSetting, accountConfigId, a
109
227
  });
110
228
  };
111
229
  export const classificationPresetsResponse = () => Object.values(CLASSIFICATION_PRESETS);
112
- export const decideClassificationChange = (input, stored) => {
113
- let provider;
114
- if (savesClassificationProvider(input)) {
115
- const decided = decideClassificationProvider(input, stored);
116
- if (!decided.ok)
117
- return decided;
118
- provider = decided.value;
119
- }
120
- const configured = provider !== undefined || stored.state === "Configured";
121
- if (input.classificationEnabled === true && !configured) {
122
- return refuse("ClassificationNotConfigured", "classificationEnabled needs a saved classification provider");
123
- }
124
- if (input.classificationDailyLimit !== undefined && !configured) {
125
- return refuse("ClassificationNotConfigured", "classificationDailyLimit needs a saved classification provider");
126
- }
127
- return {
128
- ok: true,
129
- value: {
130
- provider,
131
- enabled: configured ? input.classificationEnabled : undefined,
132
- dailyLimit: input.classificationDailyLimit,
133
- },
134
- };
135
- };
136
- const writeProvider = async (deps, accountConfigId, save) => {
137
- const { accountSetting } = deps;
138
- if (save.apiKey.action === "Delete") {
139
- await accountSetting.delete(accountConfigId, AccountSettingName.ClassificationApiKey);
140
- }
141
- if (save.apiKey.action === "Replace") {
142
- const ciphertext = serializeEncryptedPayload(await deps.secrets.encrypt(save.apiKey.apiKey));
143
- await accountSetting.upsert({
144
- accountConfigId,
145
- name: AccountSettingName.ClassificationApiKey,
146
- value: { kind: "Map", value: { ...ciphertext } },
147
- });
148
- }
149
- await accountSetting.upsert({
150
- accountConfigId,
151
- name: AccountSettingName.ClassificationModel,
152
- value: { kind: "String", value: save.model },
153
- });
154
- await accountSetting.upsert({
155
- accountConfigId,
156
- name: AccountSettingName.ClassificationBaseUrl,
157
- value: { kind: "String", value: save.baseUrl },
158
- });
159
- await accountSetting.upsert({
160
- accountConfigId,
161
- name: AccountSettingName.ClassificationPresetId,
162
- value: { kind: "String", value: save.preset.presetId },
163
- });
164
- };
165
230
  export const writeClassificationChange = async (deps, accountConfigId, change) => {
166
- if (change.provider) {
167
- await writeProvider(deps, accountConfigId, change.provider);
168
- }
169
- if (change.enabled !== undefined) {
170
- await deps.accountSetting.upsert({
171
- accountConfigId,
172
- name: AccountSettingName.ClassificationEnabled,
173
- value: { kind: "Boolean", value: change.enabled },
174
- });
231
+ for (const planned of change.writes) {
232
+ const write = await sealWrite(deps.secrets, planned);
233
+ if (write.action === "Delete") {
234
+ await deps.accountSetting.delete(accountConfigId, write.name);
235
+ }
236
+ else {
237
+ await deps.accountSetting.upsert({
238
+ accountConfigId,
239
+ name: write.name,
240
+ value: write.value,
241
+ });
242
+ }
175
243
  }
176
- if (change.enabled === true) {
244
+ if (change.ensuresLabels) {
177
245
  await ensureClassificationLabels(deps.label, accountConfigId);
178
246
  }
179
- if (change.dailyLimit !== undefined) {
180
- await deps.accountSetting.upsert({
181
- accountConfigId,
182
- name: AccountSettingName.ClassificationDailyLimit,
183
- value: { kind: "Number", value: change.dailyLimit },
184
- });
185
- }
186
247
  };
187
248
  export const clearClassification = async (accountSetting, accountConfigId) => {
188
249
  for (const name of CLASSIFICATION_SETTING_NAMES) {
@@ -1,10 +1,9 @@
1
1
  import type { ClassificationTestFailed, ClassificationTestInput, ClassificationTestPassed } from "@remit/api-openapi-types";
2
- import type { IAccountSettingRepository } from "@remit/data-ports";
2
+ import { type ClassificationResolverDeps } from "@remit/data-ports/classification-resolver";
3
3
  import { type ClassifierLogger } from "@remit/mailbox-service/classifier";
4
4
  import { type SecretsService } from "@remit/secrets-service";
5
5
  import type { UserSettingsOutcome } from "./user-settings.js";
6
- export interface ClassificationTestDeps {
7
- accountSetting: Pick<IAccountSettingRepository, "getMany">;
6
+ export interface ClassificationTestDeps extends ClassificationResolverDeps {
8
7
  secrets: Pick<SecretsService, "decrypt">;
9
8
  logger: ClassifierLogger;
10
9
  }
@@ -1,8 +1,9 @@
1
- import { readStoredClassification } from "@remit/data-ports/classification-presets";
2
- import { ClassificationFailureKind as FailureKind, ClassificationField as Field, } from "@remit/domain-enums";
1
+ import { CLASSIFICATION_PRESETS } from "@remit/data-ports/classification-presets";
2
+ import { readClassificationInput, } from "@remit/data-ports/classification-resolver";
3
+ import { ClassificationFailureKind as FailureKind, ClassificationField as Field, ClassificationValueSource as ValueSource, } from "@remit/domain-enums";
3
4
  import { CLASSIFIER_LABELS, CLASSIFIER_SAMPLE_MESSAGE, classifyWithProvider, } from "@remit/mailbox-service/classifier";
4
5
  import { deserializeEncryptedPayload, } from "@remit/secrets-service";
5
- import { decideClassificationProvider, } from "./classification-settings.js";
6
+ import { decideClassificationChange, } from "./classification-settings.js";
6
7
  const REACH_FIX = "Check the endpoint runs and the Remit server can reach it";
7
8
  const DEFAULT_MODEL_FIX = "Use the preset's default model";
8
9
  const FAILURE_HELP = {
@@ -25,12 +26,17 @@ const FAILURE_HELP = {
25
26
  [FailureKind.TimedOut]: { field: Field.BaseUrl, fix: REACH_FIX },
26
27
  [FailureKind.Unparseable]: { field: Field.Model, fix: DEFAULT_MODEL_FIX },
27
28
  };
28
- const apiKeyOf = (deps, save) => {
29
- if (save.apiKey.action === "Replace") {
30
- return Promise.resolve(save.apiKey.apiKey);
29
+ const apiKeyOf = (deps, entered, apiKey) => {
30
+ if (entered !== "")
31
+ return Promise.resolve(entered);
32
+ if (apiKey.source === ValueSource.Saved) {
33
+ if (apiKey.ciphertext === undefined) {
34
+ throw new Error("A saved classification key resolved without ciphertext");
35
+ }
36
+ return deps.secrets.decrypt(deserializeEncryptedPayload(apiKey.ciphertext));
31
37
  }
32
- if (save.apiKey.action === "Keep") {
33
- return deps.secrets.decrypt(deserializeEncryptedPayload(save.apiKey.ciphertext));
38
+ if (apiKey.source === ValueSource.Deployment) {
39
+ return Promise.resolve(apiKey.apiKey);
34
40
  }
35
41
  return Promise.resolve("");
36
42
  };
@@ -54,21 +60,24 @@ const answerOf = (result) => {
54
60
  };
55
61
  };
56
62
  export const testClassificationProvider = async (deps, accountConfigId, input) => {
57
- const decided = decideClassificationProvider({
63
+ const decided = decideClassificationChange(await readClassificationInput(deps, accountConfigId), {
58
64
  classificationPresetId: input.presetId,
59
65
  classificationModel: input.model,
60
66
  classificationBaseUrl: input.baseUrl,
61
67
  classificationApiKey: input.apiKey,
62
- }, await readStoredClassification(deps.accountSetting, accountConfigId));
68
+ });
63
69
  if (!decided.ok)
64
70
  return decided;
65
- const save = decided.value;
71
+ const { resolved } = decided.value;
72
+ if (resolved.state !== "Configured") {
73
+ throw new Error(`A tested classification form resolved to ${resolved.state}`);
74
+ }
66
75
  const result = await classifyWithProvider({
67
76
  provider: {
68
- adapterShape: save.preset.adapterShape,
69
- baseUrl: save.baseUrl,
70
- model: save.model,
71
- apiKey: await apiKeyOf(deps, save),
77
+ adapterShape: CLASSIFICATION_PRESETS[resolved.presetId.value].adapterShape,
78
+ baseUrl: resolved.baseUrl.value,
79
+ model: resolved.model.value,
80
+ apiKey: await apiKeyOf(deps, input.apiKey.trim(), resolved.apiKey),
72
81
  },
73
82
  labels: CLASSIFIER_LABELS,
74
83
  message: CLASSIFIER_SAMPLE_MESSAGE,
@@ -1,10 +1,13 @@
1
1
  import type { UpdateUserSettingsInput, UserSettingsResponse } from "@remit/api-openapi-types";
2
- import type { IAccountRepository, IAccountSettingRepository } from "@remit/data-ports";
2
+ import type { IAccountConfigRepository, IAccountRepository, IAccountSettingRepository } from "@remit/data-ports";
3
+ import type { ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
3
4
  import type { ClassificationLabelDeps } from "@remit/mailbox-service";
4
5
  import type { SecretsService } from "@remit/secrets-service";
5
6
  import type { OperationHandler, UserSettingsOperationIds } from "../types.js";
6
7
  export interface UserSettingsDeps {
7
8
  accountSetting: Pick<IAccountSettingRepository, "get" | "getMany" | "upsert" | "delete">;
9
+ accountConfig: Pick<IAccountConfigRepository, "get">;
10
+ classificationDefault: ClassificationDeploymentDefault;
8
11
  accounts: Pick<IAccountRepository, "listAllByAccountConfig">;
9
12
  secrets: Pick<SecretsService, "encrypt">;
10
13
  label: ClassificationLabelDeps;
@@ -1,10 +1,10 @@
1
1
  import { invitationSenderInEffect, sendingAccountFor, } from "@remit/calendar-service";
2
2
  import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
3
- import { readStoredClassification } from "@remit/data-ports/classification-presets";
3
+ import { readClassificationInput } from "@remit/data-ports/classification-resolver";
4
4
  import { AccountSettingName } from "@remit/domain-enums";
5
5
  import { logger } from "@remit/logger";
6
6
  import { badRequest } from "./calendar.js";
7
- import { classificationPresetsResponse, clearClassification, decideClassificationChange, publicClassification, readClassificationUsage, writeClassificationChange, } from "./classification-settings.js";
7
+ import { classificationPresetsResponse, clearClassification, decideClassificationChange, publicClassification, readClassificationUsage, readKnownClassification, writeClassificationChange, } from "./classification-settings.js";
8
8
  import { testClassificationProvider } from "./classification-test.js";
9
9
  /** The account the reader chose to send invitations from, if they chose one. */
10
10
  export const chosenInvitationSender = async (accountSetting, accountConfigId) => {
@@ -17,19 +17,22 @@ export const readUserSettings = async (deps, accountConfigId) => {
17
17
  const chosen = await chosenInvitationSender(deps.accountSetting, accountConfigId);
18
18
  const accounts = await deps.accounts.listAllByAccountConfig(accountConfigId);
19
19
  const inEffect = invitationSenderInEffect(accounts, chosen);
20
- const stored = await readStoredClassification(deps.accountSetting, accountConfigId);
21
- const usage = stored.state === "Configured"
22
- ? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId), stored, Date.now())
20
+ const resolved = await readKnownClassification(deps, accountConfigId);
21
+ const usage = resolved.state === "Configured"
22
+ ? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId), {
23
+ presetId: resolved.presetId.value,
24
+ dailyLimit: resolved.dailyLimit.value,
25
+ }, Date.now())
23
26
  : [];
24
27
  return {
25
28
  ...(inEffect && { defaultInvitationSenderAccountId: inEffect.accountId }),
26
29
  defaultInvitationSenderChosen: inEffect !== undefined && inEffect.accountId === chosen,
27
30
  classificationPresets: classificationPresetsResponse(),
28
- classification: publicClassification(stored, usage),
31
+ classification: publicClassification(resolved, usage),
29
32
  };
30
33
  };
31
34
  export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
32
- const classification = decideClassificationChange(input, await readStoredClassification(deps.accountSetting, accountConfigId));
35
+ const classification = decideClassificationChange(await readClassificationInput(deps, accountConfigId), input);
33
36
  if (!classification.ok)
34
37
  return classification;
35
38
  const sender = input.defaultInvitationSenderAccountId;
@@ -54,6 +57,8 @@ export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
54
57
  };
55
58
  const depsOf = (ctx) => ({
56
59
  accountSetting: ctx.client.accountSetting,
60
+ accountConfig: ctx.client.accountConfig,
61
+ classificationDefault: ctx.client.classificationDefault,
57
62
  accounts: ctx.client.account,
58
63
  secrets: ctx.client.secrets,
59
64
  label: ctx.client.label,
@@ -79,6 +84,8 @@ export const UserSettingsOperations = {
79
84
  UserSettingsOperations_testClassificationProvider: async (context, ctx) => {
80
85
  const tested = await testClassificationProvider({
81
86
  accountSetting: ctx.client.accountSetting,
87
+ accountConfig: ctx.client.accountConfig,
88
+ classificationDefault: ctx.client.classificationDefault,
82
89
  secrets: ctx.client.secrets,
83
90
  logger,
84
91
  }, ctx.identity.accountConfigId, context.request.requestBody);
@@ -161,6 +161,8 @@ export const composeReadPathServices = (core, queues, sync) => ({
161
161
  accountService: core.account,
162
162
  }, {
163
163
  accountSetting: core.accountSetting,
164
+ accountConfig: core.accountConfig,
165
+ classificationDefault: core.classificationDefault,
164
166
  secrets: core.secrets,
165
167
  labelService: core.label,
166
168
  messageLabelService: core.messageLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "2.7.1",
3
+ "version": "3.0.0",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",