@remit/backend 2.8.0 → 3.1.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.
package/dist/src/api.js CHANGED
@@ -33,6 +33,17 @@ class PrecompiledOpenAPIBackend extends OpenAPIBackend {
33
33
  }
34
34
  return initialized;
35
35
  }
36
+ ready() {
37
+ if (!this.initalized)
38
+ void this.init();
39
+ return this;
40
+ }
41
+ matchOperation(req) {
42
+ return this.ready().router.matchOperation(req);
43
+ }
44
+ validateRequest(req, operation) {
45
+ return this.ready().validator.validateRequest(req, operation);
46
+ }
36
47
  }
37
48
  const api = new PrecompiledOpenAPIBackend({
38
49
  definition: OpenAPISpec,
@@ -86,6 +97,5 @@ api.register("notImplemented", () => ({
86
97
  statusCode: 501,
87
98
  body: { code: defaultErrorCode(501), message: "Not implemented" },
88
99
  }));
89
- api.init();
90
100
  export { setLogger } from "@remit/logger";
91
101
  export { api };
@@ -1,8 +1,9 @@
1
- import type { ClassificationConfigured, ClassificationNotConfigured, ClassificationPreset, ClassificationPresetId, ClassificationUsage, UpdateUserSettingsInput } from "@remit/api-openapi-types";
2
- import type { IAccountSettingRepository } from "@remit/data-ports";
3
- import { type ClassificationResolverDeps, type ResolvedClassification, type ResolvedClassificationProvider } from "@remit/data-ports/classification-resolver";
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,41 +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;
27
28
  };
28
- export interface ClassificationProviderSave {
29
- preset: ClassificationPreset;
30
- model: string;
31
- baseUrl: string;
32
- apiKey: ApiKeyChange;
29
+ type PlannedWrite = SettingWrite | {
30
+ action: "Encrypt";
31
+ apiKey: string;
32
+ };
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 type KnownClassification = Exclude<ResolvedClassification, {
39
+ export type ClassificationInput = Pick<UpdateUserSettingsInput, "classificationPresetId" | "classificationModel" | "classificationBaseUrl" | "classificationApiKey" | "classificationEnabled" | "classificationDailyLimit">;
40
+ export type KnownClassification<Saved = SavedClassificationApiKey> = Exclude<ResolvedClassification<Saved>, {
36
41
  state: "UnknownPreset";
37
42
  }>;
38
43
  export declare const readKnownClassification: (deps: ClassificationResolverDeps, accountConfigId: string) => Promise<KnownClassification>;
39
- export declare const decideClassificationProvider: (input: ClassificationInput, known: KnownClassification) => Decision<ClassificationProviderSave>;
40
- export declare const publicClassification: (resolved: ResolvedClassificationProvider | {
41
- state: "NotConfigured";
42
- }, usage: ClassificationUsage[]) => ClassificationNotConfigured | ClassificationConfigured;
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;
43
46
  export declare const readClassificationUsage: (accountSetting: Pick<IAccountSettingRepository, "getMany">, accountConfigId: string, accountIds: readonly string[], saved: {
44
47
  presetId: ClassificationPresetId;
45
48
  dailyLimit: number;
46
49
  }, now: number) => Promise<ClassificationUsage[]>;
47
50
  export declare const classificationPresetsResponse: () => ClassificationPreset[];
48
- interface ClassificationChange {
49
- provider: ClassificationProviderSave | undefined;
50
- enabled: boolean | undefined;
51
- dailyLimit: number | undefined;
52
- }
53
- export declare const decideClassificationChange: (input: ClassificationInput, known: KnownClassification) => Decision<ClassificationChange>;
54
51
  export declare const writeClassificationChange: (deps: ClassificationSettingsDeps, accountConfigId: string, change: ClassificationChange) => Promise<void>;
55
52
  export declare const clearClassification: (accountSetting: Pick<IAccountSettingRepository, "delete">, accountConfigId: string) => Promise<void>;
56
53
  export {};
@@ -1,16 +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 { ClassificationSource, readResolvedClassification, } from "@remit/data-ports/classification-resolver";
4
- 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";
5
5
  import { ensureClassificationLabels, } from "@remit/mailbox-service";
6
6
  import { serializeEncryptedPayload, } from "@remit/secrets-service";
7
- export const readKnownClassification = async (deps, accountConfigId) => {
8
- const resolved = await readResolvedClassification(deps, accountConfigId);
7
+ const knownOf = (resolved) => {
9
8
  if (resolved.state === "UnknownPreset") {
10
9
  throw new Error(`Stored classification preset is unknown: ${resolved.presetId}`);
11
10
  }
12
11
  return resolved;
13
12
  };
13
+ export const readKnownClassification = async (deps, accountConfigId) => knownOf(await readResolvedClassification(deps, accountConfigId));
14
14
  const refuse = (code, message) => ({
15
15
  ok: false,
16
16
  error: { code, message },
@@ -25,29 +25,6 @@ const savesClassificationProvider = (input) => input.classificationPresetId !==
25
25
  input.classificationModel !== undefined ||
26
26
  input.classificationBaseUrl !== undefined ||
27
27
  input.classificationApiKey !== undefined;
28
- const baseUrlFor = (preset, requested) => {
29
- if (requested === "") {
30
- if (preset.defaultBaseUrl === undefined) {
31
- return refuse("InvalidClassificationBaseUrl", `classificationBaseUrl is required for ${preset.label}`);
32
- }
33
- return { ok: true, value: preset.defaultBaseUrl };
34
- }
35
- if (!isHttpUrl(requested)) {
36
- return refuse("InvalidClassificationBaseUrl", "classificationBaseUrl must be an http or https URL");
37
- }
38
- return { ok: true, value: requested };
39
- };
40
- const apiKeyFor = (preset, requested, keptKey) => {
41
- if (!preset.needsKey)
42
- return { ok: true, value: { action: "Delete" } };
43
- if (requested !== "") {
44
- return { ok: true, value: { action: "Replace", apiKey: requested } };
45
- }
46
- if (keptKey) {
47
- return { ok: true, value: { action: "Keep", ciphertext: keptKey } };
48
- }
49
- return refuse("ClassificationApiKeyRequired", `classificationApiKey is required for ${preset.label}`);
50
- };
51
28
  const presetIdOf = (known) => {
52
29
  if (known.state === "NotConfigured")
53
30
  return undefined;
@@ -55,46 +32,160 @@ const presetIdOf = (known) => {
55
32
  return known.presetId;
56
33
  return known.presetId.value;
57
34
  };
58
- export const decideClassificationProvider = (input, known) => {
59
- const presetId = input.classificationPresetId ?? presetIdOf(known);
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;
60
54
  if (presetId === undefined) {
61
- return refuse("ClassificationNotConfigured", "classificationPresetId is required: no classification provider is saved");
55
+ return refuse("ClassificationNotConfigured", "classificationPresetId is required: no classification provider is saved or inherited");
56
+ }
57
+ const baseUrl = input.classificationBaseUrl?.trim();
58
+ if (baseUrl !== undefined && baseUrl !== "" && !isHttpUrl(baseUrl)) {
59
+ return refuse("InvalidClassificationBaseUrl", "classificationBaseUrl must be an http or https URL");
60
+ }
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
+ };
75
+ };
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
+ });
89
+ }
90
+ if (input.classificationDailyLimit !== undefined) {
91
+ writes.push({
92
+ action: "Upsert",
93
+ name: AccountSettingName.ClassificationDailyLimit,
94
+ value: { kind: "Number", value: input.classificationDailyLimit },
95
+ });
96
+ }
97
+ return { ok: true, value: writes };
98
+ };
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);
62
153
  }
63
- const preset = CLASSIFICATION_PRESETS[presetId];
64
- const kept = known.state === "Configured" && known.presetId.value === presetId
65
- ? known
66
- : undefined;
67
- const baseUrl = baseUrlFor(preset, (input.classificationBaseUrl ?? kept?.baseUrl.value ?? "").trim());
68
- if (!baseUrl.ok)
69
- return baseUrl;
70
- const apiKey = apiKeyFor(preset, (input.classificationApiKey ?? "").trim(), kept?.apiKey.source === ClassificationSource.Saved
71
- ? kept.apiKey.ciphertext
72
- : undefined);
73
- if (!apiKey.ok)
74
- return apiKey;
75
- const model = (input.classificationModel ?? kept?.model.value ?? "").trim();
76
154
  return {
77
155
  ok: true,
78
156
  value: {
79
- preset,
80
- model: model === "" ? preset.defaultModel : model,
81
- baseUrl: baseUrl.value,
82
- apiKey: apiKey.value,
157
+ writes,
158
+ resolved,
159
+ ensuresLabels: input.classificationEnabled === true,
83
160
  },
84
161
  };
85
162
  };
86
163
  export const publicClassification = (resolved, usage) => {
87
164
  if (resolved.state === "NotConfigured")
88
165
  return resolved;
166
+ if (resolved.state === "Unrunnable") {
167
+ return {
168
+ state: "Unrunnable",
169
+ presetId: resolved.presetId,
170
+ reason: resolved.reason,
171
+ };
172
+ }
89
173
  return {
90
174
  state: "Configured",
91
175
  presetId: resolved.presetId.value,
92
176
  model: resolved.model.value,
93
177
  baseUrl: resolved.baseUrl.value,
94
- apiKeySaved: resolved.apiKey.source === ClassificationSource.Saved,
178
+ apiKeySaved: resolved.apiKey.source !== ValueSource.Default,
95
179
  enabled: resolved.enabled.value,
96
180
  dailyLimit: resolved.dailyLimit.value,
97
- savedAt: resolved.classifiesAfter,
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
+ },
98
189
  usage,
99
190
  };
100
191
  };
@@ -136,80 +227,23 @@ export const readClassificationUsage = async (accountSetting, accountConfigId, a
136
227
  });
137
228
  };
138
229
  export const classificationPresetsResponse = () => Object.values(CLASSIFICATION_PRESETS);
139
- export const decideClassificationChange = (input, known) => {
140
- let provider;
141
- if (savesClassificationProvider(input)) {
142
- const decided = decideClassificationProvider(input, known);
143
- if (!decided.ok)
144
- return decided;
145
- provider = decided.value;
146
- }
147
- const configured = provider !== undefined || known.state === "Configured";
148
- if (input.classificationEnabled === true && !configured) {
149
- return refuse("ClassificationNotConfigured", "classificationEnabled needs a saved classification provider");
150
- }
151
- if (input.classificationDailyLimit !== undefined && !configured) {
152
- return refuse("ClassificationNotConfigured", "classificationDailyLimit needs a saved classification provider");
153
- }
154
- return {
155
- ok: true,
156
- value: {
157
- provider,
158
- enabled: configured ? input.classificationEnabled : undefined,
159
- dailyLimit: input.classificationDailyLimit,
160
- },
161
- };
162
- };
163
- const writeProvider = async (deps, accountConfigId, save) => {
164
- const { accountSetting } = deps;
165
- if (save.apiKey.action === "Delete") {
166
- await accountSetting.delete(accountConfigId, AccountSettingName.ClassificationApiKey);
167
- }
168
- if (save.apiKey.action === "Replace") {
169
- const ciphertext = serializeEncryptedPayload(await deps.secrets.encrypt(save.apiKey.apiKey));
170
- await accountSetting.upsert({
171
- accountConfigId,
172
- name: AccountSettingName.ClassificationApiKey,
173
- value: { kind: "Map", value: { ...ciphertext } },
174
- });
175
- }
176
- await accountSetting.upsert({
177
- accountConfigId,
178
- name: AccountSettingName.ClassificationModel,
179
- value: { kind: "String", value: save.model },
180
- });
181
- await accountSetting.upsert({
182
- accountConfigId,
183
- name: AccountSettingName.ClassificationBaseUrl,
184
- value: { kind: "String", value: save.baseUrl },
185
- });
186
- await accountSetting.upsert({
187
- accountConfigId,
188
- name: AccountSettingName.ClassificationPresetId,
189
- value: { kind: "String", value: save.preset.presetId },
190
- });
191
- };
192
230
  export const writeClassificationChange = async (deps, accountConfigId, change) => {
193
- if (change.provider) {
194
- await writeProvider(deps, accountConfigId, change.provider);
195
- }
196
- if (change.enabled !== undefined) {
197
- await deps.accountSetting.upsert({
198
- accountConfigId,
199
- name: AccountSettingName.ClassificationEnabled,
200
- value: { kind: "Boolean", value: change.enabled },
201
- });
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
+ }
202
243
  }
203
- if (change.enabled === true) {
244
+ if (change.ensuresLabels) {
204
245
  await ensureClassificationLabels(deps.label, accountConfigId);
205
246
  }
206
- if (change.dailyLimit !== undefined) {
207
- await deps.accountSetting.upsert({
208
- accountConfigId,
209
- name: AccountSettingName.ClassificationDailyLimit,
210
- value: { kind: "Number", value: change.dailyLimit },
211
- });
212
- }
213
247
  };
214
248
  export const clearClassification = async (accountSetting, accountConfigId) => {
215
249
  for (const name of CLASSIFICATION_SETTING_NAMES) {
@@ -1,5 +1,5 @@
1
1
  import type { ClassificationTestFailed, ClassificationTestInput, ClassificationTestPassed } from "@remit/api-openapi-types";
2
- import type { ClassificationResolverDeps } from "@remit/data-ports/classification-resolver";
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";
@@ -1,7 +1,9 @@
1
- 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";
2
4
  import { CLASSIFIER_LABELS, CLASSIFIER_SAMPLE_MESSAGE, classifyWithProvider, } from "@remit/mailbox-service/classifier";
3
5
  import { deserializeEncryptedPayload, } from "@remit/secrets-service";
4
- import { decideClassificationProvider, readKnownClassification, } from "./classification-settings.js";
6
+ import { decideClassificationChange, } from "./classification-settings.js";
5
7
  const REACH_FIX = "Check the endpoint runs and the Remit server can reach it";
6
8
  const DEFAULT_MODEL_FIX = "Use the preset's default model";
7
9
  const FAILURE_HELP = {
@@ -24,12 +26,17 @@ const FAILURE_HELP = {
24
26
  [FailureKind.TimedOut]: { field: Field.BaseUrl, fix: REACH_FIX },
25
27
  [FailureKind.Unparseable]: { field: Field.Model, fix: DEFAULT_MODEL_FIX },
26
28
  };
27
- const apiKeyOf = (deps, save) => {
28
- if (save.apiKey.action === "Replace") {
29
- 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));
30
37
  }
31
- if (save.apiKey.action === "Keep") {
32
- return deps.secrets.decrypt(deserializeEncryptedPayload(save.apiKey.ciphertext));
38
+ if (apiKey.source === ValueSource.Deployment) {
39
+ return Promise.resolve(apiKey.apiKey);
33
40
  }
34
41
  return Promise.resolve("");
35
42
  };
@@ -53,21 +60,24 @@ const answerOf = (result) => {
53
60
  };
54
61
  };
55
62
  export const testClassificationProvider = async (deps, accountConfigId, input) => {
56
- const decided = decideClassificationProvider({
63
+ const decided = decideClassificationChange(await readClassificationInput(deps, accountConfigId), {
57
64
  classificationPresetId: input.presetId,
58
65
  classificationModel: input.model,
59
66
  classificationBaseUrl: input.baseUrl,
60
67
  classificationApiKey: input.apiKey,
61
- }, await readKnownClassification(deps, accountConfigId));
68
+ });
62
69
  if (!decided.ok)
63
70
  return decided;
64
- 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
+ }
65
75
  const result = await classifyWithProvider({
66
76
  provider: {
67
- adapterShape: save.preset.adapterShape,
68
- baseUrl: save.baseUrl,
69
- model: save.model,
70
- 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),
71
81
  },
72
82
  labels: CLASSIFIER_LABELS,
73
83
  message: CLASSIFIER_SAMPLE_MESSAGE,
@@ -1,5 +1,6 @@
1
1
  import { invitationSenderInEffect, sendingAccountFor, } from "@remit/calendar-service";
2
2
  import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
3
+ import { readClassificationInput } from "@remit/data-ports/classification-resolver";
3
4
  import { AccountSettingName } from "@remit/domain-enums";
4
5
  import { logger } from "@remit/logger";
5
6
  import { badRequest } from "./calendar.js";
@@ -17,9 +18,6 @@ export const readUserSettings = async (deps, accountConfigId) => {
17
18
  const accounts = await deps.accounts.listAllByAccountConfig(accountConfigId);
18
19
  const inEffect = invitationSenderInEffect(accounts, chosen);
19
20
  const resolved = await readKnownClassification(deps, accountConfigId);
20
- if (resolved.state === "Unrunnable") {
21
- throw new Error(`Classification provider ${resolved.presetId} cannot run: ${resolved.reason}`);
22
- }
23
21
  const usage = resolved.state === "Configured"
24
22
  ? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId), {
25
23
  presetId: resolved.presetId.value,
@@ -34,7 +32,7 @@ export const readUserSettings = async (deps, accountConfigId) => {
34
32
  };
35
33
  };
36
34
  export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
37
- const classification = decideClassificationChange(input, await readKnownClassification(deps, accountConfigId));
35
+ const classification = decideClassificationChange(await readClassificationInput(deps, accountConfigId), input);
38
36
  if (!classification.ok)
39
37
  return classification;
40
38
  const sender = input.defaultInvitationSenderAccountId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "2.8.0",
3
+ "version": "3.1.0",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -71,6 +71,10 @@
71
71
  "types": "./dist/src/config/msoauth.d.ts",
72
72
  "default": "./dist/src/config/msoauth.js"
73
73
  },
74
+ "./classification-default": {
75
+ "types": "./dist/src/config/classification-default.d.ts",
76
+ "default": "./dist/src/config/classification-default.js"
77
+ },
74
78
  "./content-handler": {
75
79
  "types": "./dist/dev-server/content-handler.d.ts",
76
80
  "default": "./dist/dev-server/content-handler.js"