@k2b/cloud 0.10.0 → 0.10.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -89,50 +89,49 @@
|
|
|
89
89
|
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
|
90
90
|
},
|
|
91
91
|
"dependencies": {
|
|
92
|
-
"@firecrawl/anydoc": "0.
|
|
93
|
-
"@napi-rs/canvas": "1.0.
|
|
92
|
+
"@firecrawl/anydoc": "0.2.4",
|
|
93
|
+
"@napi-rs/canvas": "1.0.9",
|
|
94
94
|
"pdfjs-dist": "6.3.289",
|
|
95
95
|
"@hono/standard-validator": "0.2.2",
|
|
96
|
-
"@simplewebauthn/server": "
|
|
97
|
-
"@tabler/icons": "3.
|
|
96
|
+
"@simplewebauthn/server": "14.0.2",
|
|
97
|
+
"@tabler/icons": "3.47.0",
|
|
98
98
|
"@tailwindcss/typography": "0.5.20",
|
|
99
99
|
"@k2b/nessi": "0.12.1",
|
|
100
100
|
"@k2b/ssr": "0.14.0",
|
|
101
|
-
"@k2b/ui": "0.
|
|
101
|
+
"@k2b/ui": "0.5.0",
|
|
102
102
|
"@k2b/stdlib": "0.25.0",
|
|
103
103
|
"@k2b/sync": "6.4.0",
|
|
104
104
|
"@nats-io/transport-node": "3.4.0",
|
|
105
105
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
106
106
|
"bun-plugin-tailwind": "0.1.2",
|
|
107
107
|
"hono-openapi": "1.3.1",
|
|
108
|
-
"jose": "6.2.
|
|
109
|
-
"katex": "0.
|
|
110
|
-
"liquidjs": "10.
|
|
108
|
+
"jose": "6.2.12",
|
|
109
|
+
"katex": "0.18.7",
|
|
110
|
+
"liquidjs": "10.29.0",
|
|
111
111
|
"marked": "17.0.6",
|
|
112
112
|
"mermaid": "11.16.0",
|
|
113
|
-
"nodemailer": "
|
|
114
|
-
"postcss": "8.5.
|
|
113
|
+
"nodemailer": "10.0.10",
|
|
114
|
+
"postcss": "8.5.28",
|
|
115
115
|
"reflect-metadata": "0.2.2",
|
|
116
|
-
"sanitize-html": "2.17.
|
|
117
|
-
"seroval": "1.6.
|
|
116
|
+
"sanitize-html": "2.17.7",
|
|
117
|
+
"seroval": "1.6.7",
|
|
118
118
|
"tailwindcss": "4.3.3",
|
|
119
119
|
"web-push": "3.6.7",
|
|
120
|
-
"web-vitals": "6.2.
|
|
121
|
-
"yaml": "2.9.
|
|
120
|
+
"web-vitals": "6.2.2",
|
|
121
|
+
"yaml": "2.9.1",
|
|
122
122
|
"hash-wasm": "4.12.0",
|
|
123
|
-
"@types/nodemailer": "7.0.12",
|
|
124
123
|
"@types/sanitize-html": "2.16.1",
|
|
125
124
|
"@types/web-push": "3.6.4"
|
|
126
125
|
},
|
|
127
126
|
"devDependencies": {
|
|
128
|
-
"@babel/core": "
|
|
129
|
-
"@babel/preset-typescript": "
|
|
127
|
+
"@babel/core": "8.0.6",
|
|
128
|
+
"@babel/preset-typescript": "8.0.1",
|
|
130
129
|
"@types/bun": "1.4.2",
|
|
131
|
-
"babel-preset-solid": "1.9.
|
|
132
|
-
"hono": "4.
|
|
133
|
-
"solid-js": "1.9.
|
|
130
|
+
"babel-preset-solid": "1.9.15",
|
|
131
|
+
"hono": "4.13.8",
|
|
132
|
+
"solid-js": "1.9.15",
|
|
134
133
|
"typescript": "5.9.3",
|
|
135
|
-
"zod": "4.6.
|
|
134
|
+
"zod": "4.6.5"
|
|
136
135
|
},
|
|
137
136
|
"peerDependencies": {
|
|
138
137
|
"hono": "^4.12.25",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
import type { AiModelReferenceSetting, AiSettingsIssue } from "./settings";
|
|
3
|
+
|
|
4
|
+
type Reference = { setting: string; profile: string; next: string };
|
|
5
|
+
|
|
6
|
+
const catalog = i18n.define({
|
|
7
|
+
baseLocale: "en",
|
|
8
|
+
messages: {
|
|
9
|
+
en: {
|
|
10
|
+
notSaved: "The AI settings were not saved. Resolve the problems below and save again.",
|
|
11
|
+
defaultModel: "default model",
|
|
12
|
+
backgroundModel: "background model",
|
|
13
|
+
workflowModel: "workflow model",
|
|
14
|
+
visionModel: "Vision tool model",
|
|
15
|
+
audioModel: "audio model",
|
|
16
|
+
selectAnother: ({ setting }: { setting: string }) => `Select another ${setting}`,
|
|
17
|
+
useDefaultModel: "use the default model instead",
|
|
18
|
+
useBackgroundModel: "use the background model instead",
|
|
19
|
+
disableVisionFallback: "disable the view_image fallback",
|
|
20
|
+
disableAudio: "disable audio transcription",
|
|
21
|
+
or: ({ first, second }: { first: string; second: string }) => `${first} or ${second}`,
|
|
22
|
+
modelRequired: "Choose a default model while AI is enabled.",
|
|
23
|
+
profileMissing: ({ setting, profile, next }: Reference) =>
|
|
24
|
+
`The ${setting} still references “${profile}”. ${next} before removing this profile.`,
|
|
25
|
+
profileDisabled: ({ setting, profile, next }: Reference) =>
|
|
26
|
+
`The ${setting} uses “${profile}”, which is disabled. ${next}, or enable the profile again.`,
|
|
27
|
+
noVision: ({ setting, profile, next }: Reference) => `“${profile}” does not support Vision, so it cannot be the ${setting}. ${next}.`,
|
|
28
|
+
transcriptionOnly: ({ setting, profile, next }: Reference) =>
|
|
29
|
+
`“${profile}” only transcribes audio, so it cannot be the ${setting}. ${next}.`,
|
|
30
|
+
noTranscription: ({ setting, profile, next }: Reference) =>
|
|
31
|
+
`“${profile}” does not transcribe audio, so it cannot be the ${setting}. ${next}.`,
|
|
32
|
+
credentialMissing: ({ profile }: { profile: string }) =>
|
|
33
|
+
`“${profile}” needs a provider API key. Enter the key in the profile or disable the profile.`,
|
|
34
|
+
},
|
|
35
|
+
de: {
|
|
36
|
+
notSaved: "Die KI-Einstellungen wurden nicht gespeichert. Behebe die folgenden Probleme und speichere erneut.",
|
|
37
|
+
defaultModel: "Standardmodell",
|
|
38
|
+
backgroundModel: "Hintergrundmodell",
|
|
39
|
+
workflowModel: "Workflow-Modell",
|
|
40
|
+
visionModel: "Modell für das Bildwerkzeug",
|
|
41
|
+
audioModel: "Audio-Modell",
|
|
42
|
+
selectAnother: ({ setting }) => `Wähle ein anderes ${setting}`,
|
|
43
|
+
useDefaultModel: "verwende stattdessen das Standardmodell",
|
|
44
|
+
useBackgroundModel: "verwende stattdessen das Hintergrundmodell",
|
|
45
|
+
disableVisionFallback: "deaktiviere den view_image-Fallback",
|
|
46
|
+
disableAudio: "deaktiviere die Audio-Transkription",
|
|
47
|
+
or: ({ first, second }) => `${first} oder ${second}`,
|
|
48
|
+
modelRequired: "Wähle ein Standardmodell, solange KI aktiviert ist.",
|
|
49
|
+
profileMissing: ({ setting, profile, next }) =>
|
|
50
|
+
`Das ${setting} verweist noch auf „${profile}“. ${next}, bevor du dieses Profil entfernst.`,
|
|
51
|
+
profileDisabled: ({ setting, profile, next }) =>
|
|
52
|
+
`Das ${setting} verwendet „${profile}“, aber dieses Profil ist deaktiviert. ${next}. Du kannst das Profil auch wieder aktivieren.`,
|
|
53
|
+
noVision: ({ setting, profile, next }) =>
|
|
54
|
+
`„${profile}“ unterstützt keine Bildverarbeitung und kann nicht als ${setting} verwendet werden. ${next}.`,
|
|
55
|
+
transcriptionOnly: ({ setting, profile, next }) =>
|
|
56
|
+
`„${profile}“ transkribiert nur Audio und kann nicht als ${setting} verwendet werden. ${next}.`,
|
|
57
|
+
noTranscription: ({ setting, profile, next }) =>
|
|
58
|
+
`„${profile}“ transkribiert kein Audio und kann nicht als ${setting} verwendet werden. ${next}.`,
|
|
59
|
+
credentialMissing: ({ profile }) =>
|
|
60
|
+
`„${profile}“ benötigt einen API-Schlüssel des Anbieters. Gib den Schlüssel im Profil ein oder deaktiviere das Profil.`,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
type Messages = ReturnType<typeof catalog.resolve>["t"];
|
|
66
|
+
|
|
67
|
+
const settingNoun = (t: Messages, setting: AiModelReferenceSetting): string =>
|
|
68
|
+
({
|
|
69
|
+
"ai.default_model_id": t.defaultModel,
|
|
70
|
+
"ai.background_model_id": t.backgroundModel,
|
|
71
|
+
"ai.workflow_model_id": t.workflowModel,
|
|
72
|
+
"ai.vision_model_id": t.visionModel,
|
|
73
|
+
"ai.audio_model_id": t.audioModel,
|
|
74
|
+
})[setting];
|
|
75
|
+
|
|
76
|
+
/** The way out that keeps the feature working without this profile, if the setting is optional. */
|
|
77
|
+
const alternative = (t: Messages, setting: AiModelReferenceSetting): string | undefined =>
|
|
78
|
+
({
|
|
79
|
+
"ai.default_model_id": undefined,
|
|
80
|
+
"ai.background_model_id": t.useDefaultModel,
|
|
81
|
+
"ai.workflow_model_id": t.useBackgroundModel,
|
|
82
|
+
"ai.vision_model_id": t.disableVisionFallback,
|
|
83
|
+
"ai.audio_model_id": t.disableAudio,
|
|
84
|
+
})[setting];
|
|
85
|
+
|
|
86
|
+
export type AiSettingsIssueWithMessage = AiSettingsIssue & { message: string };
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Turn validation issues into request-localized, actionable messages.
|
|
90
|
+
*
|
|
91
|
+
* `profiles` should list the prospective profiles first and the stored ones
|
|
92
|
+
* after them, so a profile that is being removed still has its label. Only
|
|
93
|
+
* ids and labels are read; provider keys never reach the messages.
|
|
94
|
+
*/
|
|
95
|
+
export const describeAiSettingsIssues = (
|
|
96
|
+
issues: readonly AiSettingsIssue[],
|
|
97
|
+
options: { locale?: string | null; profiles: ReadonlyArray<{ id: string; label: string }> },
|
|
98
|
+
): { issues: AiSettingsIssueWithMessage[]; errors: Record<string, string> } => {
|
|
99
|
+
const t = catalog.resolve(options.locale ? [options.locale] : []).t;
|
|
100
|
+
const labelOf = (profileId: string) => options.profiles.find((profile) => profile.id === profileId)?.label ?? profileId;
|
|
101
|
+
|
|
102
|
+
const described = issues.map((issue): AiSettingsIssueWithMessage => {
|
|
103
|
+
if (issue.code === "model_required") return { ...issue, message: t.modelRequired };
|
|
104
|
+
const profile = labelOf(issue.profileId);
|
|
105
|
+
if (issue.code === "provider_credential_missing") return { ...issue, message: t.credentialMissing({ profile }) };
|
|
106
|
+
|
|
107
|
+
const setting = settingNoun(t, issue.setting);
|
|
108
|
+
const other = alternative(t, issue.setting);
|
|
109
|
+
const select = t.selectAnother({ setting });
|
|
110
|
+
const reference = { setting, profile, next: other ? t.or({ first: select, second: other }) : select };
|
|
111
|
+
if (issue.code === "model_profile_missing") return { ...issue, message: t.profileMissing(reference) };
|
|
112
|
+
if (issue.code === "model_profile_disabled") return { ...issue, message: t.profileDisabled(reference) };
|
|
113
|
+
const message =
|
|
114
|
+
issue.setting === "ai.vision_model_id"
|
|
115
|
+
? t.noVision(reference)
|
|
116
|
+
: issue.setting === "ai.audio_model_id"
|
|
117
|
+
? t.noTranscription(reference)
|
|
118
|
+
: t.transcriptionOnly(reference);
|
|
119
|
+
return { ...issue, message };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const errors: Record<string, string> = {};
|
|
123
|
+
for (const issue of described)
|
|
124
|
+
errors[issue.setting] = errors[issue.setting] ? `${errors[issue.setting]} ${issue.message}` : issue.message;
|
|
125
|
+
return { issues: described, errors };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const aiSettingsNotSavedMessage = (locale?: string | null): string => catalog.resolve(locale ? [locale] : []).t.notSaved;
|
|
129
|
+
export const checkAiSettingsMessages = () => catalog.check();
|
package/src/ai/settings.ts
CHANGED
|
@@ -185,6 +185,27 @@ export const parseAiModelProfiles = (rawJson: string): { profiles: AiModelProfil
|
|
|
185
185
|
return { profiles };
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
+
export type AiModelReferenceSetting =
|
|
189
|
+
| "ai.default_model_id"
|
|
190
|
+
| "ai.background_model_id"
|
|
191
|
+
| "ai.workflow_model_id"
|
|
192
|
+
| "ai.vision_model_id"
|
|
193
|
+
| "ai.audio_model_id";
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* One reason why a prospective AI configuration cannot be saved. Codes and
|
|
197
|
+
* profile ids are stable and locale-free; the admin route turns them into
|
|
198
|
+
* request-localized messages.
|
|
199
|
+
*/
|
|
200
|
+
export type AiSettingsIssue =
|
|
201
|
+
| { code: "model_required"; setting: AiModelReferenceSetting }
|
|
202
|
+
| {
|
|
203
|
+
code: "model_profile_missing" | "model_profile_disabled" | "model_profile_incompatible";
|
|
204
|
+
setting: AiModelReferenceSetting;
|
|
205
|
+
profileId: string;
|
|
206
|
+
}
|
|
207
|
+
| { code: "provider_credential_missing"; setting: "ai.model_profiles_json"; profileId: string };
|
|
208
|
+
|
|
188
209
|
export const validateAiSettingsConfiguration = (input: {
|
|
189
210
|
enabled: boolean;
|
|
190
211
|
defaultModelId: string;
|
|
@@ -194,51 +215,41 @@ export const validateAiSettingsConfiguration = (input: {
|
|
|
194
215
|
workflowModelId: string;
|
|
195
216
|
profiles: readonly AiModelProfile[];
|
|
196
217
|
credentialProfileIds: readonly string[];
|
|
197
|
-
}):
|
|
198
|
-
if (!input.enabled) return
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (input.visionModelId) {
|
|
214
|
-
const visionProfile = input.profiles.find((profile) => profile.id === input.visionModelId);
|
|
215
|
-
if (!visionProfile?.enabled || !visionProfile.capabilities.includes("vision")) {
|
|
216
|
-
errors["ai.vision_model_id"] = "Choose an enabled model profile with Vision support or disable the fallback.";
|
|
218
|
+
}): AiSettingsIssue[] => {
|
|
219
|
+
if (!input.enabled) return [];
|
|
220
|
+
|
|
221
|
+
const issues: AiSettingsIssue[] = [];
|
|
222
|
+
const isChatModel = (profile: AiModelProfile) => !profile.capabilities.includes("transcription");
|
|
223
|
+
const checkReference = (
|
|
224
|
+
setting: AiModelReferenceSetting,
|
|
225
|
+
profileId: string | undefined,
|
|
226
|
+
fits: (profile: AiModelProfile) => boolean,
|
|
227
|
+
required = false,
|
|
228
|
+
) => {
|
|
229
|
+
if (!profileId) {
|
|
230
|
+
if (required) issues.push({ code: "model_required", setting });
|
|
231
|
+
return;
|
|
217
232
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
errors["ai.workflow_model_id"] = "Choose an enabled chat model profile or use the background model.";
|
|
224
|
-
}
|
|
233
|
+
const profile = input.profiles.find((item) => item.id === profileId);
|
|
234
|
+
if (!profile) issues.push({ code: "model_profile_missing", setting, profileId });
|
|
235
|
+
else if (!profile.enabled) issues.push({ code: "model_profile_disabled", setting, profileId });
|
|
236
|
+
else if (!fits(profile)) issues.push({ code: "model_profile_incompatible", setting, profileId });
|
|
237
|
+
};
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
239
|
+
checkReference("ai.default_model_id", input.defaultModelId, isChatModel, true);
|
|
240
|
+
checkReference("ai.background_model_id", input.backgroundModelId, isChatModel);
|
|
241
|
+
checkReference("ai.workflow_model_id", input.workflowModelId, isChatModel);
|
|
242
|
+
checkReference("ai.vision_model_id", input.visionModelId, (profile) => profile.capabilities.includes("vision"));
|
|
243
|
+
checkReference("ai.audio_model_id", input.audioModelId, (profile) => profile.capabilities.includes("transcription"));
|
|
232
244
|
|
|
233
245
|
const configured = new Set(input.credentialProfileIds);
|
|
234
|
-
const
|
|
235
|
-
(profile
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
errors["ai.model_profiles_json"] = `Enter a provider API key for: ${missingCredentials.map((profile) => profile.label).join(", ")}.`;
|
|
246
|
+
for (const profile of input.profiles) {
|
|
247
|
+
if (profile.enabled && providerRequiresCredential(profile.provider) && !configured.has(profile.id)) {
|
|
248
|
+
issues.push({ code: "provider_credential_missing", setting: "ai.model_profiles_json", profileId: profile.id });
|
|
249
|
+
}
|
|
239
250
|
}
|
|
240
251
|
|
|
241
|
-
return
|
|
252
|
+
return issues;
|
|
242
253
|
};
|
|
243
254
|
|
|
244
255
|
export const planAiProfileCredentials = (input: {
|
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
splitAiModelAccess,
|
|
21
21
|
} from "../ai/model-access";
|
|
22
22
|
import { parseAiModelProfiles, planAiProfileCredentials, validateAiSettingsConfiguration } from "../ai/settings";
|
|
23
|
-
import { type
|
|
23
|
+
import { type AiSettingsIssueWithMessage, aiSettingsNotSavedMessage, describeAiSettingsIssues } from "../ai/settings-messages";
|
|
24
|
+
import { type AuthContext, auth, getLocale, jsonResponse, requiresAdmin, v } from "../server";
|
|
24
25
|
import { settingsDeleteLegacyKeys, settingsListLegacyKeys } from "../services";
|
|
25
26
|
import { readAccountCategoryPolicy } from "../services/account-category-policy";
|
|
26
27
|
import { audit } from "../services/audit";
|
|
@@ -97,6 +98,8 @@ const storeAiCredentials = async (
|
|
|
97
98
|
|
|
98
99
|
type AiSettingsMutationPlan = {
|
|
99
100
|
errors: FieldErrors;
|
|
101
|
+
/** Structured validation issues with stable codes; `errors` carries the same messages keyed by setting. */
|
|
102
|
+
issues?: AiSettingsIssueWithMessage[];
|
|
100
103
|
keepCredentialProfileIds?: string[];
|
|
101
104
|
modelProfileIds?: string[];
|
|
102
105
|
};
|
|
@@ -111,6 +114,7 @@ const prepareAiSettingsMutation = async (
|
|
|
111
114
|
updates: Record<string, unknown>,
|
|
112
115
|
resets: readonly string[],
|
|
113
116
|
aiSplit: ReturnType<typeof splitAiProfileCredentials>,
|
|
117
|
+
locale: string,
|
|
114
118
|
): Promise<AiSettingsMutationPlan> => {
|
|
115
119
|
const keys = [...Object.keys(updates), ...resets];
|
|
116
120
|
if (!keys.some((key) => AI_CONFIGURATION_KEYS.has(key))) return { errors: {} };
|
|
@@ -164,7 +168,7 @@ const prepareAiSettingsMutation = async (
|
|
|
164
168
|
keepCredentialProfileIds = credentialPlan.keepCredentialProfileIds;
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
const
|
|
171
|
+
const issues = validateAiSettingsConfiguration({
|
|
168
172
|
enabled: nextEnabled,
|
|
169
173
|
defaultModelId: String(valueAfterMutation(AI_DEFAULT_MODEL_KEY, currentDefaultModelId ?? "", updates, resets)),
|
|
170
174
|
backgroundModelId: String(valueAfterMutation(AI_BACKGROUND_MODEL_KEY, currentBackgroundModelId ?? "", updates, resets)),
|
|
@@ -174,8 +178,11 @@ const prepareAiSettingsMutation = async (
|
|
|
174
178
|
profiles: nextParsed.profiles,
|
|
175
179
|
credentialProfileIds: keepCredentialProfileIds ?? existingCredentialProfileIds,
|
|
176
180
|
});
|
|
181
|
+
// Stored profiles come second so a profile that is being removed keeps its label.
|
|
182
|
+
const described = describeAiSettingsIssues(issues, { locale, profiles: [...nextParsed.profiles, ...currentParsed.profiles] });
|
|
177
183
|
return {
|
|
178
|
-
errors,
|
|
184
|
+
errors: described.errors,
|
|
185
|
+
issues: described.issues,
|
|
179
186
|
keepCredentialProfileIds,
|
|
180
187
|
modelProfileIds: profilesUpdated || profilesReset ? nextParsed.profiles.map((profile) => profile.id) : undefined,
|
|
181
188
|
};
|
|
@@ -405,13 +412,13 @@ const app = new Hono<AuthContext>()
|
|
|
405
412
|
|
|
406
413
|
let aiPlan: AiSettingsMutationPlan;
|
|
407
414
|
try {
|
|
408
|
-
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit);
|
|
415
|
+
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit, getLocale(c));
|
|
409
416
|
} catch (error) {
|
|
410
417
|
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
411
418
|
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
412
419
|
}
|
|
413
420
|
if (Object.keys(aiPlan.errors).length > 0) {
|
|
414
|
-
return c.json({ message:
|
|
421
|
+
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
415
422
|
}
|
|
416
423
|
|
|
417
424
|
const fieldErrors: FieldErrors = {};
|
|
@@ -491,13 +498,13 @@ const app = new Hono<AuthContext>()
|
|
|
491
498
|
|
|
492
499
|
let aiPlan: AiSettingsMutationPlan;
|
|
493
500
|
try {
|
|
494
|
-
aiPlan = await prepareAiSettingsMutation({}, [key], null);
|
|
501
|
+
aiPlan = await prepareAiSettingsMutation({}, [key], null, getLocale(c));
|
|
495
502
|
} catch (error) {
|
|
496
503
|
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
497
504
|
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
498
505
|
}
|
|
499
506
|
if (Object.keys(aiPlan.errors).length > 0) {
|
|
500
|
-
return c.json({ message:
|
|
507
|
+
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
501
508
|
}
|
|
502
509
|
|
|
503
510
|
try {
|
|
@@ -106,6 +106,8 @@ const mapConversionError = (error: unknown, format: DocumentFormat): DocumentExt
|
|
|
106
106
|
case "malformed":
|
|
107
107
|
case "missingPart":
|
|
108
108
|
return new DocumentExtractionError("malformed", "The document is malformed or incomplete.");
|
|
109
|
+
case "needsOcr":
|
|
110
|
+
return new DocumentExtractionError("ocr_required", "The PDF has no readable text and requires OCR.");
|
|
109
111
|
case "unsupported":
|
|
110
112
|
return format === "pdf"
|
|
111
113
|
? new DocumentExtractionError("ocr_required", "The PDF has no readable text and requires OCR.")
|
package/src/services/webauthn.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { err, fail, ok, type Result } from "@k2b/stdlib";
|
|
2
2
|
import {
|
|
3
3
|
type AuthenticationResponseJSON,
|
|
4
|
-
type AuthenticatorTransportFuture,
|
|
5
4
|
generateAuthenticationOptions,
|
|
6
5
|
generateRegistrationOptions,
|
|
7
6
|
type PublicKeyCredentialCreationOptionsJSON,
|
|
@@ -137,7 +136,7 @@ const toCredential = (row: DbPasskeyRow): WebAuthnCredential => ({
|
|
|
137
136
|
id: row.credential_id,
|
|
138
137
|
publicKey: new Uint8Array(row.public_key) as WebAuthnCredential["publicKey"],
|
|
139
138
|
counter: Number(row.counter),
|
|
140
|
-
transports:
|
|
139
|
+
transports: row.transports ?? [],
|
|
141
140
|
});
|
|
142
141
|
|
|
143
142
|
const isExpired = (date: Date | null): boolean => Boolean(date && date.getTime() <= Date.now());
|
|
@@ -176,7 +175,7 @@ export const beginRegistration = async (params: { user: User }): Promise<Result<
|
|
|
176
175
|
},
|
|
177
176
|
excludeCredentials: existing.map((credential) => ({
|
|
178
177
|
id: credential.credentialId,
|
|
179
|
-
transports: credential.transports
|
|
178
|
+
transports: credential.transports,
|
|
180
179
|
})),
|
|
181
180
|
});
|
|
182
181
|
|