@remit/backend 2.8.0 → 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.
- package/dist/src/handlers/classification-settings.d.ts +24 -27
- package/dist/src/handlers/classification-settings.js +153 -119
- package/dist/src/handlers/classification-test.d.ts +1 -1
- package/dist/src/handlers/classification-test.js +24 -14
- package/dist/src/handlers/user-settings.js +2 -4
- package/package.json +1 -1
|
@@ -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 {
|
|
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
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
40
|
-
export declare const publicClassification: (resolved:
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
const
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
178
|
+
apiKeySaved: resolved.apiKey.source !== ValueSource.Default,
|
|
95
179
|
enabled: resolved.enabled.value,
|
|
96
180
|
dailyLimit: resolved.dailyLimit.value,
|
|
97
|
-
|
|
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
|
-
|
|
194
|
-
await
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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.
|
|
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
|
|
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 {
|
|
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 {
|
|
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,
|
|
28
|
-
if (
|
|
29
|
-
return Promise.resolve(
|
|
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 (
|
|
32
|
-
return
|
|
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 =
|
|
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
|
-
}
|
|
68
|
+
});
|
|
62
69
|
if (!decided.ok)
|
|
63
70
|
return decided;
|
|
64
|
-
const
|
|
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:
|
|
68
|
-
baseUrl:
|
|
69
|
-
model:
|
|
70
|
-
apiKey: await apiKeyOf(deps,
|
|
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(
|
|
35
|
+
const classification = decideClassificationChange(await readClassificationInput(deps, accountConfigId), input);
|
|
38
36
|
if (!classification.ok)
|
|
39
37
|
return classification;
|
|
40
38
|
const sender = input.defaultInvitationSenderAccountId;
|