@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.
- package/dist/src/handlers/classification-settings.d.ts +31 -25
- package/dist/src/handlers/classification-settings.js +171 -110
- package/dist/src/handlers/classification-test.d.ts +2 -3
- package/dist/src/handlers/classification-test.js +24 -15
- package/dist/src/handlers/user-settings.d.ts +4 -1
- package/dist/src/handlers/user-settings.js +14 -7
- package/dist/src/service/create-remit-client.js +2 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
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,32 +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;
|
|
28
|
+
};
|
|
29
|
+
type PlannedWrite = SettingWrite | {
|
|
30
|
+
action: "Encrypt";
|
|
31
|
+
apiKey: string;
|
|
27
32
|
};
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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 {
|
|
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
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 (
|
|
39
|
-
|
|
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
|
|
97
|
+
return { ok: true, value: writes };
|
|
42
98
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
apiKey: apiKey.value,
|
|
157
|
+
writes,
|
|
158
|
+
resolved,
|
|
159
|
+
ensuresLabels: input.classificationEnabled === true,
|
|
65
160
|
},
|
|
66
161
|
};
|
|
67
162
|
};
|
|
68
|
-
export const publicClassification = (
|
|
69
|
-
if (
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
167
|
-
await
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
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
|
|
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 {
|
|
2
|
-
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";
|
|
3
4
|
import { CLASSIFIER_LABELS, CLASSIFIER_SAMPLE_MESSAGE, classifyWithProvider, } from "@remit/mailbox-service/classifier";
|
|
4
5
|
import { deserializeEncryptedPayload, } from "@remit/secrets-service";
|
|
5
|
-
import {
|
|
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,
|
|
29
|
-
if (
|
|
30
|
-
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));
|
|
31
37
|
}
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
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 =
|
|
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
|
-
}
|
|
68
|
+
});
|
|
63
69
|
if (!decided.ok)
|
|
64
70
|
return decided;
|
|
65
|
-
const
|
|
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:
|
|
69
|
-
baseUrl:
|
|
70
|
-
model:
|
|
71
|
-
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),
|
|
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 {
|
|
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
|
|
21
|
-
const usage =
|
|
22
|
-
? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId),
|
|
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(
|
|
31
|
+
classification: publicClassification(resolved, usage),
|
|
29
32
|
};
|
|
30
33
|
};
|
|
31
34
|
export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
|
|
32
|
-
const classification = decideClassificationChange(
|
|
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,
|