@k2b/cloud 0.37.0 → 0.39.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/package.json +8 -3
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/define-app.ts +28 -2
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +30 -5
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/browser.ts +11 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +10 -0
- package/src/ai/chat/turn-view.tsx +35 -3
- package/src/ai/chat/view-block.tsx +7 -2
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-task-contracts.ts +28 -3
- package/src/ai/chat-task-reasons-migrate.ts +68 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +460 -66
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/migrate.ts +46 -3
- package/src/ai/readable-html.ts +1057 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +27 -1
- package/src/ai/runtime.ts +12 -27
- package/src/ai/store.ts +5 -3
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-wording.ts +45 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +12 -1
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-outgoing-mail.ts +4 -2
- package/src/api/app-approval.ts +9 -2
- package/src/api/approvals.ts +1 -1
- package/src/api/help.ts +23 -4
- package/src/api/index.ts +2 -1
- package/src/api/mcp.ts +8 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/app-approval.ts +7 -2
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/composer-catalog.ts +103 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +150 -0
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/panel-messages.ts +24 -0
- package/src/browser/panel-slot.ts +354 -0
- package/src/browser/panel.ts +28 -0
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +17 -2
- package/src/cli/admin/workflows.ts +1 -0
- package/src/config/env.ts +14 -0
- package/src/contracts/app-approval.ts +49 -9
- package/src/contracts/app.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/shared.ts +2 -0
- package/src/server/help.ts +136 -10
- package/src/server/services/access.ts +5 -1
- package/src/server/services/geo.ts +21 -4
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/app-approval.ts +158 -38
- package/src/services/approvals/device.ts +2 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +22 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +67 -43
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/shared/redirect.ts +12 -0
- package/src/ssr/CloudShell.island.tsx +86 -9
- package/src/ssr/Layout.tsx +62 -11
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/PanelLayout.tsx +27 -0
- package/src/ssr/PanelRuntime.island.tsx +87 -0
- package/src/ssr/PanelSlot.tsx +108 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +72 -25
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +100 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +15 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +431 -0
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/panel-slot.browser-harness.tsx +110 -0
- package/src/ssr/platform-messages.ts +34 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +11 -0
- package/src/ssr/shell-shortcuts.ts +19 -0
- package/src/styles/effects.css +689 -185
- package/src/styles/global.css +269 -55
- package/src/styles/panel.css +373 -0
- package/src/styles/resource-search.css +25 -0
- package/src/styles/tokens.css +23 -6
- package/src/styles/utilities-navigation.css +5 -21
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
|
@@ -46,9 +46,10 @@ const BulkSaveSchema = z.union([
|
|
|
46
46
|
.object({
|
|
47
47
|
updates: z.record(z.string(), z.unknown()).optional().default({}),
|
|
48
48
|
resets: z.array(z.string()).optional().default([]),
|
|
49
|
+
replaceUnreadable: z.array(z.string()).optional().default([]),
|
|
49
50
|
})
|
|
50
51
|
.strict(),
|
|
51
|
-
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[] })),
|
|
52
|
+
z.record(z.string(), z.unknown()).transform((updates) => ({ updates, resets: [] as string[], replaceUnreadable: [] as string[] })),
|
|
52
53
|
]);
|
|
53
54
|
const LEGAL_DOCUMENTS = [
|
|
54
55
|
{ kind: "terms", path: "/legal/terms" },
|
|
@@ -58,6 +59,13 @@ const LEGAL_DOCUMENTS = [
|
|
|
58
59
|
|
|
59
60
|
type FieldErrors = Record<string, string>;
|
|
60
61
|
|
|
62
|
+
const unreadableSettingMessage = (locale: string): string =>
|
|
63
|
+
locale.startsWith("de")
|
|
64
|
+
? "Der gespeicherte Wert kann von dieser Anwendung nicht gelesen werden. Ersetze ihn ausdrücklich oder nutze den Standardwert."
|
|
65
|
+
: "The stored value cannot be read by this application. Replace it explicitly or use the default.";
|
|
66
|
+
const settingsNotSavedMessage = (locale: string): string =>
|
|
67
|
+
locale.startsWith("de") ? "Die Einstellungen wurden nicht gespeichert." : "The settings were not saved.";
|
|
68
|
+
|
|
61
69
|
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
62
70
|
|
|
63
71
|
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
@@ -335,168 +343,211 @@ const app = new Hono<AuthContext>()
|
|
|
335
343
|
if (result.ok) return c.json({ ok: true });
|
|
336
344
|
return c.json({ message: result.error, kind: result.kind }, result.status);
|
|
337
345
|
})
|
|
338
|
-
.put(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
346
|
+
.put(
|
|
347
|
+
"/",
|
|
348
|
+
auth.requireRole("admin"),
|
|
349
|
+
describeRoute({
|
|
350
|
+
tags: ["Administration"],
|
|
351
|
+
summary: "Save platform settings",
|
|
352
|
+
...requiresAdmin,
|
|
353
|
+
responses: {
|
|
354
|
+
204: { description: "Settings saved" },
|
|
355
|
+
400: jsonResponse(z.object({ message: z.string(), errors: z.record(z.string(), z.string()).optional() }), "Invalid settings"),
|
|
356
|
+
409: jsonResponse(
|
|
357
|
+
z.object({ message: z.string(), errors: z.record(z.string(), z.string()), code: z.literal("setting_unreadable").optional() }),
|
|
358
|
+
"Stored setting unreadable or model access conflict",
|
|
359
|
+
),
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
v("json", BulkSaveSchema),
|
|
363
|
+
async (c) => {
|
|
364
|
+
const { updates, resets, replaceUnreadable } = c.req.valid("json");
|
|
365
|
+
const replace = new Set(replaceUnreadable);
|
|
366
|
+
const updateKeys = Object.keys(updates);
|
|
367
|
+
const keys = [...updateKeys, ...resets];
|
|
342
368
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
369
|
+
if (keys.length === 0 && replace.size === 0) {
|
|
370
|
+
return c.body(null, 204);
|
|
371
|
+
}
|
|
346
372
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
373
|
+
const ownership: FieldErrors = {};
|
|
374
|
+
for (const key of keys) {
|
|
375
|
+
if (!isKnownSetting(key)) {
|
|
376
|
+
ownership[key] = `Unknown setting "${key}"`;
|
|
377
|
+
}
|
|
351
378
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
379
|
+
for (const key of replace) {
|
|
380
|
+
if (!Object.hasOwn(updates, key)) ownership[key] = `Setting "${key}" must be included in updates to replace its unreadable value`;
|
|
381
|
+
}
|
|
382
|
+
for (const key of resets) {
|
|
383
|
+
if (key in updates) {
|
|
384
|
+
ownership[key] = `Setting "${key}" cannot be updated and reset in the same save`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (Object.keys(ownership).length > 0) {
|
|
388
|
+
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
356
389
|
}
|
|
357
|
-
}
|
|
358
|
-
if (Object.keys(ownership).length > 0) {
|
|
359
|
-
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
360
|
-
}
|
|
361
390
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
// Unsplittable means the keys could not be separated out, and storing the
|
|
367
|
-
// value as posted would put them back in a setting the browser reads.
|
|
368
|
-
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
369
|
-
}
|
|
370
|
-
let accessChanges: AiModelAccessChange[] = [];
|
|
371
|
-
if (aiSplit) {
|
|
372
|
-
try {
|
|
373
|
-
aiSplit.profilesJson = prepareAiModelProfileImages(aiSplit.profilesJson);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
if (error instanceof ImageMetadataError)
|
|
376
|
-
return c.json({ message: error.message, code: error.code, errors: { [AI_PROFILES_KEY]: error.message } }, 422);
|
|
377
|
-
throw error;
|
|
391
|
+
const states = await settings.readStates(updateKeys);
|
|
392
|
+
const unreadable: FieldErrors = {};
|
|
393
|
+
for (const key of updateKeys) {
|
|
394
|
+
if (states.get(key)?.stored === "unreadable" && !replace.has(key)) unreadable[key] = unreadableSettingMessage(getLocale(c));
|
|
378
395
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
aiSplit.profilesJson = split.profilesJson;
|
|
382
|
-
accessChanges = split.changes;
|
|
383
|
-
} catch {
|
|
384
|
-
return c.json(
|
|
385
|
-
{
|
|
386
|
-
message: "Invalid model permissions",
|
|
387
|
-
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
388
|
-
},
|
|
389
|
-
400,
|
|
390
|
-
);
|
|
396
|
+
if (Object.keys(unreadable).length > 0) {
|
|
397
|
+
return c.json({ message: settingsNotSavedMessage(getLocale(c)), errors: unreadable, code: "setting_unreadable" }, 409);
|
|
391
398
|
}
|
|
392
|
-
}
|
|
393
|
-
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
394
399
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (!def) continue;
|
|
403
|
-
const validated = validateSettingValue(def, value);
|
|
404
|
-
if (validated.ok) validatedValues[key] = validated.value;
|
|
405
|
-
else invalid[key] = validated.error;
|
|
406
|
-
if (key === "freeipa.ca_cert") {
|
|
407
|
-
const certificate = validateFreeIpaCaCert(value);
|
|
408
|
-
if (!certificate.ok) invalid[key] = certificate.error;
|
|
400
|
+
// Strip AI provider keys before anything is validated or written, so the
|
|
401
|
+
// value that gets checked is the value that gets stored.
|
|
402
|
+
const aiSplit = AI_PROFILES_KEY in updates ? splitAiProfileCredentials(updates[AI_PROFILES_KEY]) : null;
|
|
403
|
+
if (AI_PROFILES_KEY in updates && !aiSplit) {
|
|
404
|
+
// Unsplittable means the keys could not be separated out, and storing the
|
|
405
|
+
// value as posted would put them back in a setting the browser reads.
|
|
406
|
+
return c.json({ message: "Invalid values", errors: { [AI_PROFILES_KEY]: "Model profiles must be a JSON array" } }, 400);
|
|
409
407
|
}
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
let accessChanges: AiModelAccessChange[] = [];
|
|
409
|
+
if (aiSplit) {
|
|
410
|
+
try {
|
|
411
|
+
aiSplit.profilesJson = prepareAiModelProfileImages(aiSplit.profilesJson);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error instanceof ImageMetadataError)
|
|
414
|
+
return c.json({ message: error.message, code: error.code, errors: { [AI_PROFILES_KEY]: error.message } }, 422);
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const split = splitAiModelAccess(aiSplit.profilesJson);
|
|
419
|
+
aiSplit.profilesJson = split.profilesJson;
|
|
420
|
+
accessChanges = split.changes;
|
|
421
|
+
} catch {
|
|
422
|
+
return c.json(
|
|
423
|
+
{
|
|
424
|
+
message: "Invalid model permissions",
|
|
425
|
+
errors: { [AI_PROFILES_KEY]: "Invalid Assistant model permissions. Reload the settings and try again." },
|
|
426
|
+
},
|
|
427
|
+
400,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
412
430
|
}
|
|
413
|
-
|
|
414
|
-
if (Object.keys(invalid).length > 0) {
|
|
415
|
-
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
416
|
-
}
|
|
431
|
+
const finalValues: Record<string, unknown> = aiSplit ? { ...updates, [AI_PROFILES_KEY]: aiSplit.profilesJson } : updates;
|
|
417
432
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
// Validate everything up front. settings.set validates as it writes, so a
|
|
434
|
+
// single bad field used to leave the earlier keys already saved — with the
|
|
435
|
+
// AI profiles among them, whose save also prunes credentials.
|
|
436
|
+
const invalid: FieldErrors = {};
|
|
437
|
+
const validatedValues: Record<string, unknown> = {};
|
|
438
|
+
for (const [key, value] of Object.entries(finalValues)) {
|
|
439
|
+
const def = SETTINGS_MAP.get(key);
|
|
440
|
+
if (!def) continue;
|
|
441
|
+
const validated = validateSettingValue(def, value);
|
|
442
|
+
if (validated.ok) validatedValues[key] = validated.value;
|
|
443
|
+
else invalid[key] = validated.error;
|
|
444
|
+
if (key === "freeipa.ca_cert") {
|
|
445
|
+
const certificate = validateFreeIpaCaCert(value);
|
|
446
|
+
if (!certificate.ok) invalid[key] = certificate.error;
|
|
447
|
+
}
|
|
448
|
+
if (INTEGER_FREEIPA_SETTINGS.has(key) && (typeof value !== "number" || !Number.isInteger(value))) {
|
|
449
|
+
invalid[key] = "Value must be a whole number";
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (Object.keys(invalid).length > 0) {
|
|
453
|
+
return c.json({ message: "Invalid values", errors: invalid }, 400);
|
|
454
|
+
}
|
|
428
455
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
456
|
+
let aiPlan: AiSettingsMutationPlan;
|
|
457
|
+
try {
|
|
458
|
+
aiPlan = await prepareAiSettingsMutation(validatedValues, resets, aiSplit, getLocale(c));
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.error("[settings] failed to validate the prospective AI configuration", error);
|
|
461
|
+
return c.json({ message: "Failed to validate AI settings" }, 500);
|
|
462
|
+
}
|
|
463
|
+
if (Object.keys(aiPlan.errors).length > 0) {
|
|
464
|
+
return c.json({ message: aiSettingsNotSavedMessage(getLocale(c)), errors: aiPlan.errors, issues: aiPlan.issues ?? [] }, 400);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const fieldErrors: FieldErrors = {};
|
|
468
|
+
try {
|
|
469
|
+
// Every settings and AI credential write runs on the transaction's own
|
|
470
|
+
// connection, so a failure rolls the whole save back.
|
|
471
|
+
await sql.begin(async (tx) => {
|
|
472
|
+
for (const [key, value] of Object.entries(validatedValues)) {
|
|
473
|
+
try {
|
|
474
|
+
await settings.set(key, value, tx, { replaceUnreadable: replace.has(key) });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
fieldErrors[key] = `Failed to update ${key}`;
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
440
479
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
480
|
+
for (const key of resets) {
|
|
481
|
+
try {
|
|
482
|
+
await settings.remove(key, tx);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
fieldErrors[key] = `Failed to reset ${key}`;
|
|
485
|
+
throw error;
|
|
486
|
+
}
|
|
448
487
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
488
|
+
const categoryKeys = keys.filter((key) => key.startsWith("user.category."));
|
|
489
|
+
if (categoryKeys.length > 0)
|
|
490
|
+
await audit.record(
|
|
491
|
+
{
|
|
492
|
+
action: "accounts.categories.configure",
|
|
493
|
+
outcome: "allowed",
|
|
494
|
+
actor: { userId: c.get("user").id },
|
|
495
|
+
target: { type: "settings", id: "user.category" },
|
|
496
|
+
metadata: {
|
|
497
|
+
keys: categoryKeys,
|
|
498
|
+
updates: Object.fromEntries(
|
|
499
|
+
categoryKeys.filter((key) => key in validatedValues).map((key) => [key, validatedValues[key]]),
|
|
500
|
+
),
|
|
501
|
+
resets: resets.filter((key) => categoryKeys.includes(key)),
|
|
502
|
+
},
|
|
462
503
|
},
|
|
504
|
+
tx,
|
|
505
|
+
);
|
|
506
|
+
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
507
|
+
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
508
|
+
} else if (aiPlan.keepCredentialProfileIds) {
|
|
509
|
+
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
510
|
+
}
|
|
511
|
+
if (aiPlan.headerPlan) await storeAiRequestHeaderPlan(aiPlan.headerPlan, tx);
|
|
512
|
+
if (aiPlan.modelProfileIds) {
|
|
513
|
+
await aiModelAccess.syncProfiles(aiPlan.modelProfileIds, accessChanges, tx);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error instanceof settings.SettingUnreadableError) {
|
|
518
|
+
return c.json(
|
|
519
|
+
{
|
|
520
|
+
message: settingsNotSavedMessage(getLocale(c)),
|
|
521
|
+
errors: { [error.key]: unreadableSettingMessage(getLocale(c)) },
|
|
522
|
+
code: "setting_unreadable",
|
|
463
523
|
},
|
|
464
|
-
|
|
524
|
+
409,
|
|
465
525
|
);
|
|
466
|
-
if (aiSplit && aiPlan.keepCredentialProfileIds) {
|
|
467
|
-
await storeAiCredentials(aiSplit, aiPlan.keepCredentialProfileIds, tx);
|
|
468
|
-
} else if (aiPlan.keepCredentialProfileIds) {
|
|
469
|
-
await pruneAiCredentials(aiPlan.keepCredentialProfileIds, tx);
|
|
470
526
|
}
|
|
471
|
-
if (
|
|
472
|
-
if (
|
|
473
|
-
|
|
527
|
+
if (error instanceof ImageMetadataError) return c.json({ message: error.message, code: error.code, errors: fieldErrors }, 422);
|
|
528
|
+
if (error instanceof AiModelAccessConflict || error instanceof AiModelAccessInvalid) {
|
|
529
|
+
return c.json(
|
|
530
|
+
{ message: error.message, errors: { [AI_PROFILES_KEY]: error.message } },
|
|
531
|
+
error instanceof AiModelAccessConflict ? 409 : 400,
|
|
532
|
+
);
|
|
474
533
|
}
|
|
475
|
-
|
|
476
|
-
} catch (error) {
|
|
477
|
-
if (error instanceof ImageMetadataError) return c.json({ message: error.message, code: error.code, errors: fieldErrors }, 422);
|
|
478
|
-
if (error instanceof AiModelAccessConflict || error instanceof AiModelAccessInvalid) {
|
|
534
|
+
console.error("[settings] failed to commit settings update", error);
|
|
479
535
|
return c.json(
|
|
480
|
-
{
|
|
481
|
-
|
|
536
|
+
{
|
|
537
|
+
message: "Save failed",
|
|
538
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: "The settings could not be saved." },
|
|
539
|
+
},
|
|
540
|
+
500,
|
|
482
541
|
);
|
|
483
542
|
}
|
|
484
|
-
console.error("[settings] failed to commit settings update", error);
|
|
485
|
-
return c.json(
|
|
486
|
-
{
|
|
487
|
-
message: "Save failed",
|
|
488
|
-
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: "The settings could not be saved." },
|
|
489
|
-
},
|
|
490
|
-
500,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
543
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
544
|
+
// Only now: dropping the cache before the commit would let another
|
|
545
|
+
// container miss, read the pre-commit row and cache it for the full TTL.
|
|
546
|
+
await invalidateCommittedSettings(keys);
|
|
497
547
|
|
|
498
|
-
|
|
499
|
-
|
|
548
|
+
return c.body(null, 204);
|
|
549
|
+
},
|
|
550
|
+
)
|
|
500
551
|
.delete("/:key{.+}", auth.requireRole("admin"), async (c) => {
|
|
501
552
|
const key = c.req.param("key");
|
|
502
553
|
if (!isKnownSetting(key)) {
|
|
@@ -131,8 +131,10 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
131
131
|
const next = c.req.valid("json");
|
|
132
132
|
const old = await readMailRetention();
|
|
133
133
|
await sql.begin(async (tx) => {
|
|
134
|
-
|
|
135
|
-
await settings.set(MAIL_RETENTION_KEYS[
|
|
134
|
+
// complete value from explicit admin input; nothing is merged over a read (#1303)
|
|
135
|
+
await settings.set(MAIL_RETENTION_KEYS[0], next.contentDays, tx, { replaceUnreadable: true });
|
|
136
|
+
// complete value from explicit admin input; nothing is merged over a read (#1303)
|
|
137
|
+
await settings.set(MAIL_RETENTION_KEYS[1], next.recordDays, tx, { replaceUnreadable: true });
|
|
136
138
|
await audit.record(
|
|
137
139
|
{
|
|
138
140
|
...context(c),
|
package/src/api/app-approval.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
AppPairingStartSchema,
|
|
16
16
|
} from "../contracts/app-approval";
|
|
17
17
|
import { type AuthContext, auth, rateLimit, v } from "../server";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
type AppApprovalActor,
|
|
20
|
+
AppApprovalError,
|
|
21
|
+
type AppTerminalLogins,
|
|
22
|
+
appApproval,
|
|
23
|
+
type createAppApprovalService,
|
|
24
|
+
} from "../services/app-approval";
|
|
19
25
|
import { appNameIn } from "../ssr/runtime";
|
|
20
26
|
import { createDeviceActions } from "./app-approval-actions";
|
|
21
27
|
|
|
@@ -24,7 +30,7 @@ const DevicePageSchema = z.object({ after: z.string().uuid().optional() }).stric
|
|
|
24
30
|
type Service = ReturnType<typeof createAppApprovalService>;
|
|
25
31
|
|
|
26
32
|
/** This is a separate credential surface, never a user API-key or OAuth scope. */
|
|
27
|
-
export const createAppApprovalRoutes = (service: Service = appApproval, actions = createDeviceActions) => {
|
|
33
|
+
export const createAppApprovalRoutes = (service: Service = appApproval, actions = createDeviceActions, terminal?: AppTerminalLogins) => {
|
|
28
34
|
const actor = async (c: Parameters<typeof auth.session.getToken>[0]): Promise<AppApprovalActor> => {
|
|
29
35
|
// The web session (or an explicit bearer session), never the mobile app's session: a phone
|
|
30
36
|
// that pairs sign-in devices would hand out authority that outlives its removal.
|
|
@@ -111,6 +117,7 @@ export const createAppApprovalRoutes = (service: Service = appApproval, actions
|
|
|
111
117
|
await service.deviceCommand(
|
|
112
118
|
c.req.valid("json"),
|
|
113
119
|
actions((appId, locale) => appNameIn(c, appId, locale, appId)),
|
|
120
|
+
terminal,
|
|
114
121
|
),
|
|
115
122
|
),
|
|
116
123
|
)
|
package/src/api/approvals.ts
CHANGED
|
@@ -111,7 +111,7 @@ export const approvalRoutes = new Hono<AuthContext>()
|
|
|
111
111
|
client: c.req.header(APPROVAL_CLIENT_HEADER),
|
|
112
112
|
}),
|
|
113
113
|
},
|
|
114
|
-
toolApprovalContext: ctx.
|
|
114
|
+
toolApprovalContext: ctx.toolApprovalChangeContext,
|
|
115
115
|
signedInSession: ctx.signedInSession,
|
|
116
116
|
});
|
|
117
117
|
if (!result.ok) return toAiActionFailureResponse(c, result);
|
package/src/api/help.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import { keyboardPlatform } from "@k2b/ui";
|
|
1
2
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
3
|
+
import { helpAgentMarkdown } from "../_internal/help-catalog";
|
|
2
4
|
import { type AuthContext, auth, getLocale } from "../server";
|
|
3
5
|
import { createHelpReader, type HelpReaderFactory } from "../services/help";
|
|
4
|
-
import type
|
|
6
|
+
import { type HelpDocumentPayload, type HelpSearchPayload, parseHelpReference } from "../shared/help";
|
|
5
7
|
import { renderHelpMarkdown } from "../shared/markdown";
|
|
6
8
|
import { helpApiMessages } from "./help-messages";
|
|
7
9
|
|
|
8
10
|
export type HelpRouteDependencies = {
|
|
9
11
|
help?: HelpReaderFactory;
|
|
10
12
|
authenticate?: MiddlewareHandler<AuthContext>;
|
|
11
|
-
renderMarkdown?:
|
|
13
|
+
renderMarkdown?: typeof renderHelpMarkdown;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
@@ -34,12 +36,29 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
|
|
|
34
36
|
const { t } = helpApiMessages.resolve([locale]);
|
|
35
37
|
const document = await reader(locale, c.get("actor")).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
|
|
36
38
|
if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
|
|
39
|
+
const links = (reference: { appId?: string; documentId: string }) =>
|
|
40
|
+
document.links[`${reference.appId ?? document.appId}/${reference.documentId}`];
|
|
41
|
+
const resolve = (reference: string) => {
|
|
42
|
+
const ref = parseHelpReference(reference);
|
|
43
|
+
return ref ? links(ref) : undefined;
|
|
44
|
+
};
|
|
37
45
|
const payload: HelpDocumentPayload = {
|
|
38
46
|
locale: document.locale,
|
|
39
47
|
id: document.documentId,
|
|
40
48
|
title: document.title,
|
|
41
|
-
markdown: document
|
|
42
|
-
|
|
49
|
+
markdown: helpAgentMarkdown(document),
|
|
50
|
+
kind: document.kind,
|
|
51
|
+
needs: document.needs,
|
|
52
|
+
next: document.next ? resolve(document.next) : undefined,
|
|
53
|
+
related: document.related.flatMap((reference) => {
|
|
54
|
+
const target = resolve(reference);
|
|
55
|
+
return target ? [target] : [];
|
|
56
|
+
}),
|
|
57
|
+
html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown, {
|
|
58
|
+
locale: document.locale,
|
|
59
|
+
platform: keyboardPlatform(c.req.header("User-Agent") ?? ""),
|
|
60
|
+
links,
|
|
61
|
+
}),
|
|
43
62
|
};
|
|
44
63
|
return c.json(payload);
|
|
45
64
|
});
|
package/src/api/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export { createPwaPhoneRoutes } from "./pwa-phone";
|
|
|
56
56
|
* the typed shape for the client.
|
|
57
57
|
*/
|
|
58
58
|
export type CoreApiOptions = {
|
|
59
|
+
terminalLogins?: import("../services/app-approval").AppTerminalLogins;
|
|
59
60
|
notifications: import("../services/auth-flows/notification-sender").AuthNotificationSender;
|
|
60
61
|
};
|
|
61
62
|
|
|
@@ -68,7 +69,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
68
69
|
const widgetRoutes = createWidgetRoutes();
|
|
69
70
|
return new Hono()
|
|
70
71
|
.use(prettyJSON())
|
|
71
|
-
.route("/auth/app-approval/v1", createAppApprovalRoutes())
|
|
72
|
+
.route("/auth/app-approval/v1", createAppApprovalRoutes(undefined, undefined, options.terminalLogins))
|
|
72
73
|
.route("/auth/pwa/v1", createPwaRoutes())
|
|
73
74
|
.route("/auth", createAuthRoutes(options.notifications))
|
|
74
75
|
.route("/me", meRoutes)
|
package/src/api/mcp.ts
CHANGED
|
@@ -15,7 +15,13 @@ import {
|
|
|
15
15
|
import { Hono, type MiddlewareHandler } from "hono";
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
HELP_SEARCH_MAX_LIMIT,
|
|
20
|
+
helpAgentMarkdown,
|
|
21
|
+
helpResourceUri,
|
|
22
|
+
parseHelpResourceUri,
|
|
23
|
+
readHelpArticle,
|
|
24
|
+
} from "../_internal/help-catalog";
|
|
19
25
|
import { getCapability, listApps } from "../_internal/registry";
|
|
20
26
|
import { addCloudResourceHrefs } from "../ai/resource-refs";
|
|
21
27
|
import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
|
|
@@ -540,7 +546,7 @@ const createMcpServer = (
|
|
|
540
546
|
{
|
|
541
547
|
uri: message.params.uri,
|
|
542
548
|
mimeType: "text/markdown",
|
|
543
|
-
text: document
|
|
549
|
+
text: helpAgentMarkdown(document),
|
|
544
550
|
_meta: { "cloud/manifestHash": document.manifestHash, "cloud/locale": document.locale },
|
|
545
551
|
},
|
|
546
552
|
],
|
|
@@ -44,6 +44,9 @@ export type CloudResourceSearchProps = {
|
|
|
44
44
|
excludeRefs?: readonly CloudResourceRef[];
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
/** What `/api/search` accepts as `q`; a prefilled query is cut to it as typing is. */
|
|
48
|
+
const MAX_QUERY_LENGTH = 500;
|
|
49
|
+
|
|
47
50
|
const isLinkableCommand = (command: PaletteCommand) => typeof command.action !== "function" && "command" in command.action;
|
|
48
51
|
|
|
49
52
|
const itemKey = (item: SearchItem) => `${item.ref.type}:${item.ref.id}`;
|
|
@@ -62,23 +65,24 @@ export default function CloudResourceSearch(props: CloudResourceSearchProps) {
|
|
|
62
65
|
const locale = useLocale();
|
|
63
66
|
const t = () => resourceSearchMessages.resolve([locale()]).t;
|
|
64
67
|
const id = createUniqueId();
|
|
65
|
-
const [input, setInput] = createSignal("");
|
|
68
|
+
const [input, setInput] = createSignal((props.request?.query ?? "").slice(0, MAX_QUERY_LENGTH));
|
|
66
69
|
const [tags, setTags] = createSignal<string[]>([]);
|
|
67
70
|
const [scope, setScope] = createSignal<SearchScope | undefined>(
|
|
68
71
|
props.request?.scope ?? (props.initialAppId ? { appId: props.initialAppId, label: props.initialAppId } : undefined),
|
|
69
72
|
);
|
|
70
73
|
const appId = () => scope()?.appId ?? scope()?.ref?.type.split(".")[0];
|
|
71
74
|
const scopeLabel = () =>
|
|
72
|
-
!props.request && props.initialAppId
|
|
75
|
+
!props.request?.scope && props.initialAppId
|
|
73
76
|
? (catalogApps()?.find((app) => app.id === props.initialAppId)?.name ?? scope()?.label)
|
|
74
77
|
: scope()?.label;
|
|
75
78
|
createEffect(() => {
|
|
76
79
|
const request = props.request;
|
|
77
80
|
if (!request) return;
|
|
78
|
-
setScope(request.scope);
|
|
81
|
+
setScope(request.scope ?? (props.initialAppId ? { appId: props.initialAppId, label: props.initialAppId } : undefined));
|
|
79
82
|
if (request.query !== undefined) {
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const query = request.query.slice(0, MAX_QUERY_LENGTH);
|
|
84
|
+
setInput(query);
|
|
85
|
+
setCaret(query.length);
|
|
82
86
|
}
|
|
83
87
|
setTags([]);
|
|
84
88
|
setBrowsingTags(false);
|
|
@@ -556,7 +560,7 @@ export default function CloudResourceSearch(props: CloudResourceSearchProps) {
|
|
|
556
560
|
autocomplete="off"
|
|
557
561
|
autocapitalize="off"
|
|
558
562
|
spellcheck={false}
|
|
559
|
-
maxLength={
|
|
563
|
+
maxLength={MAX_QUERY_LENGTH}
|
|
560
564
|
/>
|
|
561
565
|
</div>
|
|
562
566
|
</label>
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@k2b/ui";
|
|
15
15
|
import { batch, createMemo, createSignal, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js";
|
|
16
16
|
import { createStore } from "solid-js/store";
|
|
17
|
-
import type { FileProviderEntry, FileProviderTag } from "../contracts/file-provider";
|
|
17
|
+
import type { FileProviderContext, FileProviderEntry, FileProviderTag } from "../contracts/file-provider";
|
|
18
18
|
import { fileChooserMessages } from "./file-chooser-messages";
|
|
19
19
|
import {
|
|
20
20
|
type EntryProblem,
|
|
@@ -39,6 +39,14 @@ export type ChooseFilesOptions = {
|
|
|
39
39
|
* above it, so it also bounds what the browser holds. The provider's own read limit applies to each file as well.
|
|
40
40
|
*/
|
|
41
41
|
maxBytes?: number;
|
|
42
|
+
/**
|
|
43
|
+
* What the person is working on, as resource refs, for providers that understand it. Only providers whose `list`
|
|
44
|
+
* accepts `context` receive it; others list as before. A function runs only when the person opens such a provider,
|
|
45
|
+
* at most once per chooser, and again after it failed. Refs are hints, never access.
|
|
46
|
+
*/
|
|
47
|
+
context?: FileProviderContext | ((signal: AbortSignal) => Promise<FileProviderContext>);
|
|
48
|
+
/** The empty root title for a provider that received context. Defaults to the chooser's "No matching files". */
|
|
49
|
+
noMatchingFiles?: string;
|
|
42
50
|
/** Closes the chooser, or stops waiting for the device's dialog, and resolves `[]` when aborted. */
|
|
43
51
|
signal?: AbortSignal;
|
|
44
52
|
};
|
|
@@ -95,7 +103,7 @@ export function FileChooser(props: {
|
|
|
95
103
|
if (first) (location() ? selection : sourceSelection).focus(first);
|
|
96
104
|
});
|
|
97
105
|
};
|
|
98
|
-
const folder = createProviderFolder({ caller: props.caller, t, settled: refocus });
|
|
106
|
+
const folder = createProviderFolder({ caller: props.caller, t, settled: refocus, context: props.options.context });
|
|
99
107
|
const { location, items, openFolder } = folder;
|
|
100
108
|
const [downloads, setDownloads] = createStore<Download[]>([]);
|
|
101
109
|
const [downloading, setDownloading] = createSignal(false);
|
|
@@ -297,7 +305,7 @@ export function FileChooser(props: {
|
|
|
297
305
|
</Switch>
|
|
298
306
|
</Match>
|
|
299
307
|
<Match when={true}>
|
|
300
|
-
<FolderContents folder={folder} t={t}>
|
|
308
|
+
<FolderContents folder={folder} t={t} noMatchingFiles={props.options.noMatchingFiles}>
|
|
301
309
|
<FileGrid
|
|
302
310
|
rows={items()}
|
|
303
311
|
getRowId={(entry) => entry.id}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
APP_APPROVAL_PROTOCOL,
|
|
11
11
|
type AppActionView,
|
|
12
12
|
AppActionViewSchema,
|
|
13
|
+
type AppApprovalAccept,
|
|
13
14
|
AppDeviceCommandSchema,
|
|
14
15
|
AppDeviceProofSchema,
|
|
15
16
|
type AppDevicePublicKey,
|
|
@@ -223,9 +224,13 @@ const connect = async (options: {
|
|
|
223
224
|
},
|
|
224
225
|
/** Whether this Cloud serves `inbox`, `action` and `act`. Older Clouds answer those commands with HTTP 400. */
|
|
225
226
|
actions: info.features?.includes("actions") === true,
|
|
227
|
+
/** Whether this Cloud can include terminal consent in an explicitly opted-in inbox. */
|
|
228
|
+
terminalLogins: info.features?.includes("terminal-logins") === true,
|
|
226
229
|
/** Pending sign-ins and the newest open Action approvals in one call; use it instead of `pending` where `actions` is true. */
|
|
227
|
-
inbox: async (device: AppApprovalDevice, signal?: AbortSignal) => {
|
|
228
|
-
|
|
230
|
+
inbox: async (device: AppApprovalDevice, signal?: AbortSignal, options?: { accept?: AppApprovalAccept[] }) => {
|
|
231
|
+
// SDK upgrades alone must not expose consent the app cannot show. Old Clouds never see the new key.
|
|
232
|
+
const accept = options?.accept?.filter((entry) => info.features?.includes(entry));
|
|
233
|
+
const result = await command(device, { operation: "inbox", ...(accept?.length ? { accept } : {}) }, signal);
|
|
229
234
|
if (!("actions" in result)) throw new AppApprovalClientError("INVALID_RESPONSE");
|
|
230
235
|
return result;
|
|
231
236
|
},
|