@k2b/cloud 0.31.0 → 0.32.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 +6 -4
- package/src/_internal/define-app.ts +3 -1
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/audio-tool.ts +2 -1
- package/src/ai/browser.ts +109 -0
- package/src/ai/capabilities.ts +6 -2
- package/src/ai/chart-block.ts +431 -81
- package/src/ai/chat/action-sentences.ts +13 -7
- package/src/ai/chat/blocks.tsx +11 -7
- package/src/ai/chat/builtin-tools.tsx +9 -7
- package/src/ai/chat/chart-block.tsx +218 -44
- package/src/ai/chat/explorer-block.tsx +276 -0
- package/src/ai/chat/file-tools.tsx +40 -1
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +16 -0
- package/src/ai/chat/presentation.tsx +13 -3
- package/src/ai/chat/reference-text.tsx +34 -0
- package/src/ai/chat/tool-groups.ts +1 -0
- package/src/ai/chat/tool-names.ts +14 -0
- package/src/ai/chat/turn-view.tsx +17 -7
- package/src/ai/chat/view-block.browser-harness.tsx +35 -0
- package/src/ai/chat/view-block.tsx +417 -0
- package/src/ai/chat/visual-tools.tsx +32 -53
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +173 -31
- package/src/ai/file-error.ts +2 -0
- package/src/ai/file-tools.ts +27 -7
- package/src/ai/files-store.ts +60 -0
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +90 -0
- package/src/ai/local-tool-contracts.ts +244 -0
- package/src/ai/malformed-tool-call-repair.ts +144 -0
- package/src/ai/markdown-pdf-tool.ts +2 -1
- package/src/ai/migrate.ts +7 -0
- package/src/ai/openai-api-messages.ts +53 -0
- package/src/ai/openai-api-slots.ts +40 -0
- package/src/ai/openai-api.ts +383 -0
- package/src/ai/protocol.ts +6 -2
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +96 -57
- package/src/ai/runtime.ts +48 -12
- package/src/ai/settings.ts +5 -0
- package/src/ai/skill-catalog.ts +20 -5
- package/src/ai/skill-format.ts +17 -7
- package/src/ai/skill-tool.ts +15 -5
- package/src/ai/skills-routes.ts +20 -6
- package/src/ai/skills.ts +26 -7
- package/src/ai/store.ts +25 -0
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tools.ts +3 -0
- package/src/ai/types.ts +9 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/api/admin-identity.ts +6 -1
- package/src/api/app-approval-actions.ts +36 -0
- package/src/api/app-approval.ts +10 -2
- package/src/api/approvals-loader.ts +15 -0
- package/src/api/approvals.ts +120 -0
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/approvals/Approvals.browser-harness.tsx +16 -0
- package/src/approvals/Approvals.tsx +621 -0
- package/src/approvals/ApprovalsNavStatus.tsx +20 -0
- package/src/approvals/messages.ts +95 -0
- package/src/approvals/ui.ts +8 -0
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/commands.ts +13 -4
- package/src/browser/file-provider-error.ts +11 -0
- package/src/browser/file-providers.ts +3 -12
- package/src/browser/files.tsx +1 -2
- package/src/browser/live-connection.ts +2 -3
- package/src/browser/provider-list.ts +2 -1
- package/src/browser/save-files.tsx +13 -2
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +4 -2
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/approval-paths.ts +4 -0
- package/src/contracts/approvals.ts +81 -0
- package/src/contracts/command-path.ts +17 -0
- package/src/contracts/commands.ts +5 -15
- package/src/contracts/shared.ts +5 -0
- package/src/events/live-protocol.ts +2 -1
- package/src/events/live-server-message.ts +40 -0
- package/src/events/testing.ts +88 -0
- package/src/server/index.ts +2 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/request-timeout.ts +22 -0
- package/src/server/services/access.ts +4 -3
- package/src/services/app-approval.ts +103 -15
- package/src/services/approvals/cursor.ts +15 -0
- package/src/services/approvals/device.ts +112 -0
- package/src/services/approvals/index.ts +27 -11
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +9 -1
- package/src/services/identity/models-only-auth.ts +25 -0
- package/src/services/notifications/email-frame.ts +16 -3
- package/src/services/service-account-credentials.ts +22 -10
- package/src/services/settings/core-settings.ts +27 -0
- package/src/shared/ai-platform-prompt.ts +26 -1
- package/src/shared/email-html.ts +4 -0
- package/src/shared/index.ts +6 -1
- package/src/shared/markdown/extensions/code.ts +14 -0
- package/src/shared/markdown/index.ts +4 -4
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/page-trail.browser-harness.tsx +52 -3
- package/src/ssr/rail-navigation.ts +5 -3
- package/src/ssr/runtime.ts +6 -1
- package/src/styles/approvals.css +390 -0
- package/src/styles/global.css +5 -2
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
|
|
3
|
+
export const approvalMessages = i18n.define({
|
|
4
|
+
baseLocale: "en",
|
|
5
|
+
messages: {
|
|
6
|
+
en: {
|
|
7
|
+
title: "Approvals",
|
|
8
|
+
open: ({ count }: { count: number }) => `${count.toLocaleString("en")} open`,
|
|
9
|
+
rejectAll: "Reject all",
|
|
10
|
+
rejectAllTitle: "Reject all open approvals?",
|
|
11
|
+
rejectAllBody: ({ count }: { count: number }) =>
|
|
12
|
+
`${count === 1 ? "The open Action does not run." : `None of the ${count.toLocaleString("en")} open Actions runs.`} Where Cloud asked, it continues with your rejection.`,
|
|
13
|
+
emptyTitle: "Nothing is waiting for you.",
|
|
14
|
+
emptyBody: "When an Action needs your approval, it appears here and in the chat it comes from.",
|
|
15
|
+
loading: "Loading approvals…",
|
|
16
|
+
loadFailed: "Approvals could not be loaded.",
|
|
17
|
+
retry: "Try again",
|
|
18
|
+
showMore: "Show more",
|
|
19
|
+
loadMoreFailed: "More approvals could not be loaded.",
|
|
20
|
+
elsewhere: ({ count }: { count: number }) => `More approvals in other apps (${count.toLocaleString("en")})`,
|
|
21
|
+
allApprovals: "All approvals",
|
|
22
|
+
fromChat: ({ title }: { title: string }) => `from chat “${title}”`,
|
|
23
|
+
fromUntitledChat: "from a chat",
|
|
24
|
+
asksEveryTime: "Asks every time",
|
|
25
|
+
expiresIn: ({ time }: { time: string }) => `Expires ${time}`,
|
|
26
|
+
details: "Details",
|
|
27
|
+
reject: "Reject",
|
|
28
|
+
approve: "Approve",
|
|
29
|
+
approveForChat: "Approve for this chat",
|
|
30
|
+
allowWebsiteForChat: "Allow this website in this chat",
|
|
31
|
+
alwaysApprove: "Always approve",
|
|
32
|
+
moreOptions: "More approval options",
|
|
33
|
+
decidedHere: "Just now, decided here",
|
|
34
|
+
notApplied: "Your decision was not applied",
|
|
35
|
+
approved: "Approved",
|
|
36
|
+
rejected: "Rejected",
|
|
37
|
+
decidedElsewhere: "Already decided elsewhere",
|
|
38
|
+
gone: "Expired or withdrawn",
|
|
39
|
+
decisionFailed: "The decision was not saved. Try again.",
|
|
40
|
+
openIn: ({ app }: { app: string }) => `Open in ${app}`,
|
|
41
|
+
editIn: ({ app }: { app: string }) => `Edit in ${app}`,
|
|
42
|
+
statusIn: ({ app }: { app: string }) => `Status in ${app}`,
|
|
43
|
+
preview: "Preview",
|
|
44
|
+
download: "Download",
|
|
45
|
+
exactlyThis: "This approval applies to exactly this Action. If it changes before it runs, Cloud asks again.",
|
|
46
|
+
approvalFor: ({ title }: { title: string }) => `Approval: ${title}`,
|
|
47
|
+
close: "Close",
|
|
48
|
+
waiting: ({ count }: { count: number }) => (count === 0 ? "nothing waiting" : `${count.toLocaleString("en")} waiting`),
|
|
49
|
+
},
|
|
50
|
+
de: {
|
|
51
|
+
title: "Freigaben",
|
|
52
|
+
open: ({ count }) => `${count.toLocaleString("de")} offen`,
|
|
53
|
+
rejectAll: "Alle ablehnen",
|
|
54
|
+
rejectAllTitle: "Alle offenen Freigaben ablehnen?",
|
|
55
|
+
rejectAllBody: ({ count }) =>
|
|
56
|
+
`${count === 1 ? "Die offene Aktion läuft nicht." : `Keine der ${count.toLocaleString("de")} offenen Aktionen läuft.`} Wo Cloud gefragt hat, geht es mit deiner Ablehnung weiter.`,
|
|
57
|
+
emptyTitle: "Nichts wartet auf dich.",
|
|
58
|
+
emptyBody: "Wenn eine Aktion deine Freigabe braucht, erscheint sie hier und im Chat, aus dem sie kommt.",
|
|
59
|
+
loading: "Freigaben werden geladen …",
|
|
60
|
+
loadFailed: "Die Freigaben konnten nicht geladen werden.",
|
|
61
|
+
retry: "Erneut versuchen",
|
|
62
|
+
showMore: "Mehr anzeigen",
|
|
63
|
+
loadMoreFailed: "Weitere Freigaben konnten nicht geladen werden.",
|
|
64
|
+
elsewhere: ({ count }) => `Weitere Freigaben in anderen Apps (${count.toLocaleString("de")})`,
|
|
65
|
+
allApprovals: "Alle Freigaben",
|
|
66
|
+
fromChat: ({ title }) => `aus Chat „${title}“`,
|
|
67
|
+
fromUntitledChat: "aus einem Chat",
|
|
68
|
+
asksEveryTime: "Fragt immer",
|
|
69
|
+
expiresIn: ({ time }) => `Läuft ab ${time}`,
|
|
70
|
+
details: "Details",
|
|
71
|
+
reject: "Ablehnen",
|
|
72
|
+
approve: "Freigeben",
|
|
73
|
+
approveForChat: "Für diesen Chat freigeben",
|
|
74
|
+
allowWebsiteForChat: "Diese Website in diesem Chat erlauben",
|
|
75
|
+
alwaysApprove: "Immer freigeben",
|
|
76
|
+
moreOptions: "Weitere Freigabeoptionen",
|
|
77
|
+
decidedHere: "Gerade eben, hier entschieden",
|
|
78
|
+
notApplied: "Deine Entscheidung wurde nicht übernommen",
|
|
79
|
+
approved: "Freigegeben",
|
|
80
|
+
rejected: "Abgelehnt",
|
|
81
|
+
decidedElsewhere: "Schon anderswo entschieden",
|
|
82
|
+
gone: "Abgelaufen oder zurückgezogen",
|
|
83
|
+
decisionFailed: "Die Entscheidung wurde nicht gespeichert. Versuche es erneut.",
|
|
84
|
+
openIn: ({ app }) => `In ${app} öffnen`,
|
|
85
|
+
editIn: ({ app }) => `In ${app} bearbeiten`,
|
|
86
|
+
statusIn: ({ app }) => `Status in ${app}`,
|
|
87
|
+
preview: "Vorschau",
|
|
88
|
+
download: "Herunterladen",
|
|
89
|
+
exactlyThis: "Diese Freigabe gilt genau für diese Aktion. Ändert sie sich, bevor sie läuft, fragt Cloud neu.",
|
|
90
|
+
approvalFor: ({ title }) => `Freigabe: ${title}`,
|
|
91
|
+
close: "Schließen",
|
|
92
|
+
waiting: ({ count }) => (count === 0 ? "nichts offen" : `${count.toLocaleString("de")} offen`),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared list of pending Action approvals. Every app mounts the same component for its own scope; Core serves the
|
|
3
|
+
* route behind it and decides through the platform's approval requests.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type { ApprovalsProps } from "./Approvals";
|
|
7
|
+
export { default as Approvals } from "./Approvals";
|
|
8
|
+
export { approvalNavCount, default as ApprovalsNavStatus } from "./ApprovalsNavStatus";
|
|
@@ -4,9 +4,12 @@ export { type AppVaultBlob, type AppVaultConfig, AppVaultError, type AppVaultMet
|
|
|
4
4
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import {
|
|
7
|
+
APP_ACTION_LIMITS,
|
|
7
8
|
APP_APPROVAL_LIMITS,
|
|
8
9
|
APP_APPROVAL_PATH,
|
|
9
10
|
APP_APPROVAL_PROTOCOL,
|
|
11
|
+
type AppActionView,
|
|
12
|
+
AppActionViewSchema,
|
|
10
13
|
AppDeviceCommandSchema,
|
|
11
14
|
AppDeviceProofSchema,
|
|
12
15
|
type AppDevicePublicKey,
|
|
@@ -19,6 +22,7 @@ import {
|
|
|
19
22
|
AppPairingResultSchema,
|
|
20
23
|
type AppPendingLogin,
|
|
21
24
|
AppPendingLoginSchema,
|
|
25
|
+
appActionDigest,
|
|
22
26
|
appDeviceProofMessage,
|
|
23
27
|
appPairingLink,
|
|
24
28
|
appPairingProofMessage,
|
|
@@ -84,6 +88,8 @@ const InfoSchema = z.object({
|
|
|
84
88
|
appOrigin: z.string(),
|
|
85
89
|
algorithm: z.literal("ES256"),
|
|
86
90
|
limits: z.object(Object.fromEntries(Object.entries(APP_APPROVAL_LIMITS).map(([key, value]) => [key, z.literal(value)]))),
|
|
91
|
+
/** Absent on Clouds that serve only the first version's commands. */
|
|
92
|
+
features: z.array(z.string().max(40)).max(16).optional(),
|
|
87
93
|
});
|
|
88
94
|
|
|
89
95
|
/** Call only after the user explicitly trusts the issuer from a pairing link.
|
|
@@ -215,6 +221,31 @@ const connect = async (options: {
|
|
|
215
221
|
throw new AppApprovalClientError("INVALID_RESPONSE");
|
|
216
222
|
return result;
|
|
217
223
|
},
|
|
224
|
+
/** Whether this Cloud serves `inbox`, `action` and `act`. Older Clouds answer those commands with HTTP 400. */
|
|
225
|
+
actions: info.features?.includes("actions") === true,
|
|
226
|
+
/** 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
|
+
const result = await command(device, { operation: "inbox" }, signal);
|
|
229
|
+
if (!("actions" in result)) throw new AppApprovalClientError("INVALID_RESPONSE");
|
|
230
|
+
return result;
|
|
231
|
+
},
|
|
232
|
+
/** One open Action approval. The digest is recomputed here, so a view that does not match it is never returned. */
|
|
233
|
+
action: async (device: AppApprovalDevice, actionId: string, signal?: AbortSignal): Promise<AppActionView> => {
|
|
234
|
+
const result = await command(device, { operation: "action", actionId }, signal);
|
|
235
|
+
if (!("digest" in result) || result.actionId !== actionId || (await appActionDigest(issuer, result)) !== result.digest)
|
|
236
|
+
throw new AppApprovalClientError("INVALID_RESPONSE");
|
|
237
|
+
return result;
|
|
238
|
+
},
|
|
239
|
+
/** Signs the decision over the view's digest. Only call it on an explicit choice of the person. */
|
|
240
|
+
act: async (device: AppApprovalDevice, view: AppActionView, decision: "approve" | "deny", signal?: AbortSignal) => {
|
|
241
|
+
view = AppActionViewSchema.parse(view);
|
|
242
|
+
if (Date.parse(view.expiresAt) <= Date.now() || (await appActionDigest(issuer, view)) !== view.digest)
|
|
243
|
+
throw new AppApprovalClientError("INVALID_INPUT");
|
|
244
|
+
const result = await command(device, { operation: "act", actionId: view.actionId, digest: view.digest, decision }, signal);
|
|
245
|
+
if (!("state" in result) || result.state !== (decision === "approve" ? "approved" : "denied"))
|
|
246
|
+
throw new AppApprovalClientError("INVALID_RESPONSE");
|
|
247
|
+
return result;
|
|
248
|
+
},
|
|
218
249
|
/** Reads the account this device signs in and the device's own record. Older Clouds answer HTTP 400. */
|
|
219
250
|
account: async (device: AppApprovalDevice, signal?: AbortSignal) => {
|
|
220
251
|
const result = await command(device, { operation: "account" }, signal);
|
|
@@ -243,4 +274,5 @@ export const appApproval = {
|
|
|
243
274
|
createKey,
|
|
244
275
|
connect,
|
|
245
276
|
limits: APP_APPROVAL_LIMITS,
|
|
277
|
+
actionLimits: APP_ACTION_LIMITS,
|
|
246
278
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { showFileDialog } from "@k2b/stdlib/browser";
|
|
2
2
|
import { dialogCore, panelDialogWorkspaceOptions } from "@k2b/ui";
|
|
3
3
|
import { createSignal } from "solid-js";
|
|
4
|
-
import {
|
|
4
|
+
import type { ChooseFilesOptions, FileProviderList } from "./FileChooser";
|
|
5
5
|
import type { FileProviderCaller } from "./file-providers";
|
|
6
6
|
import type { ProviderList } from "./provider-list";
|
|
7
|
+
import { importOnDemand } from "./reload";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* One page's file choosing over its provider list. `caller` carries the locale and, in tests, the transport.
|
|
@@ -39,18 +40,30 @@ export const createFileChoosing = (
|
|
|
39
40
|
);
|
|
40
41
|
};
|
|
41
42
|
if (!known) load();
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
signal
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
43
|
+
return (async () => {
|
|
44
|
+
let abort = () => {};
|
|
45
|
+
const chooser = await Promise.race([
|
|
46
|
+
importOnDemand(() => import("./FileChooser")),
|
|
47
|
+
new Promise<undefined>((resolve) => {
|
|
48
|
+
abort = () => resolve(undefined);
|
|
49
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
50
|
+
if (signal?.aborted) abort();
|
|
51
|
+
}),
|
|
52
|
+
]).finally(() => signal?.removeEventListener("abort", abort));
|
|
53
|
+
if (!chooser) return [];
|
|
54
|
+
return dialogCore
|
|
55
|
+
.open<File[]>(
|
|
56
|
+
(close) => <chooser.FileChooser options={options} providers={providers} retryProviders={load} caller={caller} close={close} />,
|
|
57
|
+
{
|
|
58
|
+
...panelDialogWorkspaceOptions,
|
|
59
|
+
panelClassName: `${panelDialogWorkspaceOptions.panelClassName} cloud-file-chooser-dialog`,
|
|
60
|
+
initialFocus: (dialog) => dialog.querySelector<HTMLElement>('[role="gridcell"][tabindex="0"]'),
|
|
61
|
+
signal: options.signal,
|
|
62
|
+
history: true,
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
.then((files) => files ?? []);
|
|
66
|
+
})();
|
|
54
67
|
};
|
|
55
68
|
|
|
56
69
|
return chooseFiles;
|
package/src/browser/commands.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { toast } from "@k2b/ui";
|
|
2
|
-
import {
|
|
3
|
-
import { type CommandOptions, CommandOptionsSchema, clearCommand, readCommand } from "../contracts/commands";
|
|
2
|
+
import type { CommandOptions } from "../contracts/commands";
|
|
4
3
|
import {
|
|
5
4
|
type ContextAwareCommand,
|
|
6
5
|
collectContextAwareCommands,
|
|
7
6
|
requestCommandHandling,
|
|
8
7
|
requestContextCommandExecution,
|
|
9
8
|
} from "./command-bridge";
|
|
9
|
+
import { importOnDemand } from "./reload";
|
|
10
10
|
import { resourceSearchMessages } from "./resource-search-messages";
|
|
11
11
|
import { requestGlobalSearch } from "./search-bridge";
|
|
12
12
|
|
|
@@ -16,17 +16,26 @@ export { registerCommandHandler, registerContextAwareCommand } from "./command-b
|
|
|
16
16
|
|
|
17
17
|
/** Opens locally only when a mounted owner explicitly accepts; otherwise follows the app's URL. */
|
|
18
18
|
export const openCommand = async (command: string, input: unknown = {}, options: CommandOptions = {}): Promise<void> => {
|
|
19
|
-
const
|
|
19
|
+
const modules = await importOnDemand(() => Promise.all([import("../contracts/commands"), import("../capabilities/client")]));
|
|
20
|
+
if (!modules) return;
|
|
21
|
+
const [contracts, capabilities] = modules;
|
|
22
|
+
const parsedOptions = contracts.CommandOptionsSchema.parse(options);
|
|
20
23
|
const handled = requestCommandHandling({ command, input, options: parsedOptions });
|
|
21
24
|
if (handled) return handled;
|
|
22
|
-
const result = await resolveCommand(command, input, parsedOptions);
|
|
25
|
+
const result = await capabilities.resolveCommand(command, input, parsedOptions);
|
|
23
26
|
if (!result.ok) throw new Error(result.error.message);
|
|
24
27
|
window.location.assign(result.data.href);
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
/** Call after this page's handlers have mounted. Each entry is consumed, including invalid links. */
|
|
28
31
|
export const consumeCommandLink = async (): Promise<void> => {
|
|
32
|
+
const command = new URL(window.location.href).searchParams.get("command");
|
|
33
|
+
if (!command) return;
|
|
34
|
+
const contracts = await importOnDemand(() => import("../contracts/commands"));
|
|
35
|
+
if (!contracts) return;
|
|
29
36
|
const url = new URL(window.location.href);
|
|
37
|
+
if (url.searchParams.get("command") !== command) return;
|
|
38
|
+
const { clearCommand, readCommand } = contracts;
|
|
30
39
|
const messages = resourceSearchMessages.resolve([document.documentElement.lang]).t;
|
|
31
40
|
let target: ReturnType<typeof readCommand>;
|
|
32
41
|
try {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** A provider failure the chooser explains; `status` 0 means the browser is offline. */
|
|
2
|
+
export class FileProviderError extends Error {
|
|
3
|
+
constructor(
|
|
4
|
+
readonly code: string,
|
|
5
|
+
message: string,
|
|
6
|
+
readonly status: number,
|
|
7
|
+
) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "FileProviderError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
fileProviderIssues,
|
|
16
16
|
} from "../contracts/file-provider";
|
|
17
17
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
18
|
+
import { FileProviderError } from "./file-provider-error";
|
|
19
|
+
|
|
20
|
+
export { FileProviderError } from "./file-provider-error";
|
|
18
21
|
|
|
19
22
|
/** One live application whose catalog manifest offers files, reduced to what choosing a file needs. */
|
|
20
23
|
export type FileProviderSource = {
|
|
@@ -31,18 +34,6 @@ export type FileProviderSource = {
|
|
|
31
34
|
|
|
32
35
|
export type FileProviderCaller = Pick<CapabilityHttpOptions, "fetch" | "baseUrl"> & { locale: string };
|
|
33
36
|
|
|
34
|
-
/** A provider failure the chooser explains; `status` 0 means the browser is offline. */
|
|
35
|
-
export class FileProviderError extends Error {
|
|
36
|
-
constructor(
|
|
37
|
-
readonly code: string,
|
|
38
|
-
message: string,
|
|
39
|
-
readonly status: number,
|
|
40
|
-
) {
|
|
41
|
-
super(message);
|
|
42
|
-
this.name = "FileProviderError";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
/** The page size the chooser asks for; the contract allows 1 to 100. */
|
|
47
38
|
export const FILE_PROVIDER_PAGE_SIZE = 50;
|
|
48
39
|
/** At most this many provider reads run at the same time. */
|
package/src/browser/files.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createFileChoosing } from "./choose-files";
|
|
2
|
-
import { loadFileProviders } from "./file-providers";
|
|
3
2
|
import { createProviderList } from "./provider-list";
|
|
4
3
|
import { createFileSaving } from "./save-files";
|
|
5
4
|
|
|
@@ -7,7 +6,7 @@ export type { ChooseFilesOptions } from "./FileChooser";
|
|
|
7
6
|
export { SAVE_FILES_ICON, type SavedFile, type SaveFileSource, type SaveFilesOptions } from "./save-files";
|
|
8
7
|
|
|
9
8
|
const pageLocale = () => (typeof document === "undefined" ? "en" : document.documentElement.lang || "en");
|
|
10
|
-
const providers = createProviderList(() => loadFileProviders({ locale: pageLocale() }));
|
|
9
|
+
const providers = createProviderList(async () => (await import("./file-providers")).loadFileProviders({ locale: pageLocale() }));
|
|
11
10
|
const saving = createFileSaving(providers);
|
|
12
11
|
|
|
13
12
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LiveServerMessage,
|
|
1
|
+
import { type LiveServerMessage, parseLiveServerMessage } from "../events/live-server-message";
|
|
2
2
|
import { createLiveWebSocket, type LiveWebSocket } from "./live-websocket";
|
|
3
3
|
|
|
4
4
|
export type LiveSubscriptionHandlers<T> = {
|
|
@@ -41,8 +41,7 @@ const shared = new Map<string, Shared>();
|
|
|
41
41
|
|
|
42
42
|
const parseMessage = (raw: string): LiveServerMessage | null => {
|
|
43
43
|
try {
|
|
44
|
-
|
|
45
|
-
return parsed.success ? parsed.data : null;
|
|
44
|
+
return parseLiveServerMessage(JSON.parse(raw));
|
|
46
45
|
} catch {
|
|
47
46
|
return null;
|
|
48
47
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSignal } from "solid-js";
|
|
2
|
-
import { FileProviderError
|
|
2
|
+
import { FileProviderError } from "./file-provider-error";
|
|
3
|
+
import type { FileProviderSource } from "./file-providers";
|
|
3
4
|
|
|
4
5
|
/** The catalog refuses the caller, such as a visitor of a public page or a session that has just expired. */
|
|
5
6
|
const refused = (error: unknown) => error instanceof FileProviderError && (error.status === 401 || error.status === 403);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { dialogCore, IconButton, type IconButtonProps, panelDialogWorkspaceOptions, toast, useLocale } from "@k2b/ui";
|
|
2
2
|
import { createSignal, type JSX, splitProps } from "solid-js";
|
|
3
3
|
import type { FileProviderList } from "./FileChooser";
|
|
4
|
-
import {
|
|
4
|
+
import type { SavedFile } from "./FileSaver";
|
|
5
5
|
import { fileChooserMessages } from "./file-chooser-messages";
|
|
6
6
|
import type { FileProviderCaller, SaveFileSource } from "./file-providers";
|
|
7
7
|
import type { ProviderList } from "./provider-list";
|
|
8
|
+
import { importOnDemand } from "./reload";
|
|
8
9
|
|
|
9
10
|
export type { SavedFile } from "./FileSaver";
|
|
10
11
|
export type { SaveFileSource } from "./file-providers";
|
|
@@ -40,9 +41,19 @@ export const createFileSaving = (
|
|
|
40
41
|
if (!known) load();
|
|
41
42
|
const saved: SavedFile[] = [];
|
|
42
43
|
const running = new Set<Promise<void>>();
|
|
44
|
+
let abort = () => {};
|
|
45
|
+
const saver = await Promise.race([
|
|
46
|
+
importOnDemand(() => import("./FileSaver")),
|
|
47
|
+
new Promise<undefined>((resolve) => {
|
|
48
|
+
abort = () => resolve(undefined);
|
|
49
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
50
|
+
if (options.signal?.aborted) abort();
|
|
51
|
+
}),
|
|
52
|
+
]).finally(() => options.signal?.removeEventListener("abort", abort));
|
|
53
|
+
if (!saver) return [];
|
|
43
54
|
await dialogCore.open<void>(
|
|
44
55
|
(close) => (
|
|
45
|
-
<FileSaver
|
|
56
|
+
<saver.FileSaver
|
|
46
57
|
files={files}
|
|
47
58
|
providers={providers}
|
|
48
59
|
retryProviders={load}
|
package/src/cli/account.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { isModelsOnlyApiKey } from "../contracts/shared";
|
|
2
3
|
import { arg, type CliInputFlagValue, type CloudCliContext, command, confirmFlag, defineCliCommands, flag, readCliInput } from "./index";
|
|
3
4
|
|
|
4
5
|
type User = {
|
|
@@ -31,6 +32,7 @@ type User = {
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
type ApiKey = {
|
|
35
|
+
scopes: string[];
|
|
34
36
|
id: string;
|
|
35
37
|
name: string;
|
|
36
38
|
status: "active" | "revoked";
|
|
@@ -121,6 +123,7 @@ const apiKeyRows = (keys: ApiKey[]) =>
|
|
|
121
123
|
id: key.id,
|
|
122
124
|
name: key.name,
|
|
123
125
|
prefix: key.tokenPrefix,
|
|
126
|
+
access: isModelsOnlyApiKey(key) ? "models" : "full",
|
|
124
127
|
expires: key.expiresAt ?? "never",
|
|
125
128
|
lastUsed: key.lastUsedAt ?? "never",
|
|
126
129
|
created: key.createdAt,
|
|
@@ -283,6 +286,7 @@ export default defineCliCommands({
|
|
|
283
286
|
printJsonOrTable(ctx, result, apiKeyRows(result.items), [
|
|
284
287
|
{ key: "name" },
|
|
285
288
|
{ key: "prefix" },
|
|
289
|
+
{ key: "access" },
|
|
286
290
|
{ key: "expires" },
|
|
287
291
|
{ key: "lastUsed" },
|
|
288
292
|
{ key: "id" },
|
|
@@ -294,15 +298,30 @@ export default defineCliCommands({
|
|
|
294
298
|
args: { name: arg.required({ valueLabel: "name", description: "API key name" }) },
|
|
295
299
|
flags: {
|
|
296
300
|
expires: flag.string({ description: "ISO timestamp or duration like 90d" }),
|
|
301
|
+
access: flag.enum(["full", "models"], { default: "full", description: "Full access or models-only inference access" }),
|
|
297
302
|
},
|
|
298
303
|
run: async ({ ctx, args, flags }) => {
|
|
299
304
|
const result = await readApi<{ credential: ApiKey; token: string }>(
|
|
300
305
|
ctx,
|
|
301
306
|
"/api-keys",
|
|
302
|
-
jsonRequest("POST", { name: args.name, expiresAt: parseExpiry(flags.expires) }),
|
|
307
|
+
jsonRequest("POST", { name: args.name, expiresAt: parseExpiry(flags.expires), access: flags.access }),
|
|
303
308
|
);
|
|
309
|
+
const modelsOnly = isModelsOnlyApiKey(result.credential);
|
|
310
|
+
if (flags.access === "models" && !modelsOnly) {
|
|
311
|
+
try {
|
|
312
|
+
await readApi<{ message: string }>(ctx, `/api-keys/${encodeURIComponent(result.credential.id)}`, jsonRequest("DELETE"));
|
|
313
|
+
} catch {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`The key was created with full access and could not be revoked automatically. Run: cld account api-keys revoke ${result.credential.id} --yes`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
throw new Error("This Cloud does not support models-only API keys. The full-access key it created was revoked.");
|
|
319
|
+
}
|
|
304
320
|
if (ctx.options.output === "json") ctx.json(result);
|
|
305
|
-
else
|
|
321
|
+
else {
|
|
322
|
+
ctx.print(`Token: ${result.token}\nPrefix: ${result.credential.tokenPrefix}\nStore this token now. It cannot be shown again.`);
|
|
323
|
+
if (modelsOnly) ctx.print("This key works only with the OpenAI-compatible API at /api/ai/openai/v1 on this Cloud.");
|
|
324
|
+
}
|
|
306
325
|
},
|
|
307
326
|
}),
|
|
308
327
|
command("api-keys revoke", {
|
package/src/cli/commands.ts
CHANGED
|
@@ -617,8 +617,10 @@ const renderFlagUsage = (key: string, spec: CliFlagSpec): string => {
|
|
|
617
617
|
|
|
618
618
|
const renderFlagDescription = (spec: CliFlagSpec): string => {
|
|
619
619
|
const parts = [spec.description];
|
|
620
|
-
|
|
621
|
-
|
|
620
|
+
const hasDefault = "default" in spec && spec.default !== undefined;
|
|
621
|
+
if ("required" in spec && spec.required && !hasDefault) parts.push("Required.");
|
|
622
|
+
if (hasDefault) parts.push(`Default: ${Array.isArray(spec.default) ? spec.default.join(",") : spec.default}.`);
|
|
622
623
|
if (spec.kind === "enum") parts.push(`Values: ${spec.values.join(", ")}.`);
|
|
624
|
+
if (parts.length > 1 && parts[0] && !/[.!?:]$/.test(parts[0])) parts[0] += ".";
|
|
623
625
|
return parts.filter(Boolean).join(" ");
|
|
624
626
|
};
|
|
@@ -14,10 +14,29 @@ export const APP_APPROVAL_LIMITS = {
|
|
|
14
14
|
pageSize: 20,
|
|
15
15
|
pollSeconds: 5,
|
|
16
16
|
} as const;
|
|
17
|
+
/**
|
|
18
|
+
* Bounds of Action approvals on a device. Kept apart from `APP_APPROVAL_LIMITS`, whose every value a version 1
|
|
19
|
+
* authenticator checks when it connects. The texts are cut so that a whole inbox or Action stays within `bodyBytes`.
|
|
20
|
+
*/
|
|
21
|
+
export const APP_ACTION_LIMITS = {
|
|
22
|
+
inboxActions: 5,
|
|
23
|
+
nameChars: 60,
|
|
24
|
+
titleChars: 80,
|
|
25
|
+
summaryChars: 120,
|
|
26
|
+
sentenceChars: 280,
|
|
27
|
+
messageChars: 500,
|
|
28
|
+
details: 8,
|
|
29
|
+
labelChars: 40,
|
|
30
|
+
valueChars: 300,
|
|
31
|
+
} as const;
|
|
32
|
+
/** Commands a Cloud serves beyond the first version, announced in `/info` as `features`. */
|
|
33
|
+
export const APP_APPROVAL_FEATURES = ["actions"] as const;
|
|
17
34
|
|
|
18
35
|
const Id = z.string().uuid();
|
|
19
36
|
const Secret = z.string().regex(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/);
|
|
20
37
|
const Signature = z.string().regex(/^[A-Za-z0-9_-]{85}[AQgw]$/);
|
|
38
|
+
/** Unpadded base64url of a SHA-256 hash. */
|
|
39
|
+
const Digest = Secret;
|
|
21
40
|
export const AppDevicePublicKeySchema = z
|
|
22
41
|
.object({
|
|
23
42
|
kty: z.literal("EC"),
|
|
@@ -57,6 +76,12 @@ export const AppDeviceCommandSchema = z.discriminatedUnion("operation", [
|
|
|
57
76
|
z.object({ operation: z.literal("account") }).strict(),
|
|
58
77
|
/** Stores the authenticator's opaque push token for sign-in wake-ups on this device. */
|
|
59
78
|
z.object({ operation: z.literal("push"), token: z.string().regex(/^[A-Za-z0-9_-]{43}$/) }).strict(),
|
|
79
|
+
/** Pending sign-ins and the newest open Action approvals of the signing device's account in one call. */
|
|
80
|
+
z.object({ operation: z.literal("inbox") }).strict(),
|
|
81
|
+
/** One open Action approval as the device shows it, with the digest the device recomputes before it decides. */
|
|
82
|
+
z.object({ operation: z.literal("action"), actionId: Id }).strict(),
|
|
83
|
+
/** Decides an Action approval. `digest` binds the decision to exactly the view the person saw. */
|
|
84
|
+
z.object({ operation: z.literal("act"), actionId: Id, digest: Digest, decision: z.enum(["approve", "deny"]) }).strict(),
|
|
60
85
|
z
|
|
61
86
|
.object({
|
|
62
87
|
operation: z.literal("decide"),
|
|
@@ -96,6 +121,8 @@ export const appDeviceProofMessage = (proof: AppDeviceProof): string => {
|
|
|
96
121
|
c.operation,
|
|
97
122
|
...(c.operation === "decide" ? [c.requestId, c.challenge, c.comparison, c.decision] : []),
|
|
98
123
|
...(c.operation === "push" ? [c.token] : []),
|
|
124
|
+
...(c.operation === "action" ? [c.actionId] : []),
|
|
125
|
+
...(c.operation === "act" ? [c.actionId, c.digest, c.decision] : []),
|
|
99
126
|
]);
|
|
100
127
|
};
|
|
101
128
|
export const appPairingProofMessage = (issuer: string, claim: Omit<z.infer<typeof AppPairingClaimSchema>, "signature">): string =>
|
|
@@ -188,6 +215,97 @@ export const AppDeviceAccountSchema = z
|
|
|
188
215
|
})
|
|
189
216
|
.strict();
|
|
190
217
|
export type AppDeviceAccount = z.infer<typeof AppDeviceAccountSchema>;
|
|
218
|
+
/** Plain text on one line: no control, format or bidirectional characters, so an app cannot fake Cloud's own frame. */
|
|
219
|
+
const PlainText = (max: number) =>
|
|
220
|
+
z
|
|
221
|
+
.string()
|
|
222
|
+
.refine((value) => Array.from(value).length <= max, `Must contain at most ${max} code points`)
|
|
223
|
+
.regex(/^[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}]*$/u);
|
|
224
|
+
const ActionApp = z
|
|
225
|
+
.object({ id: PlainText(APP_ACTION_LIMITS.nameChars).min(1), name: PlainText(APP_ACTION_LIMITS.nameChars).min(1) })
|
|
226
|
+
.strict();
|
|
227
|
+
const actionFields = {
|
|
228
|
+
actionId: Id,
|
|
229
|
+
/** The app that owns the effect, named from the Cloud's registry, never from the request. */
|
|
230
|
+
app: ActionApp,
|
|
231
|
+
/** The app that asked, such as the Assistant, and the title of its chat. The title is chat content, not Cloud's words. */
|
|
232
|
+
requester: z.object({ app: ActionApp, title: PlainText(APP_ACTION_LIMITS.titleChars).nullable() }).strict(),
|
|
233
|
+
/** `confirm` asks every time; `standard` could also be remembered in Cloud. A device decides both alike. */
|
|
234
|
+
assurance: z.enum(["standard", "confirm"]),
|
|
235
|
+
createdAt: DateTime,
|
|
236
|
+
expiresAt: DateTime,
|
|
237
|
+
};
|
|
238
|
+
/** One inbox row. `sentence` is the start of the app's sentence; read the Action for everything the person approves. */
|
|
239
|
+
export const AppActionSummarySchema = z.object({ ...actionFields, sentence: PlainText(APP_ACTION_LIMITS.summaryChars).min(1) }).strict();
|
|
240
|
+
export const AppActionViewSchema = z
|
|
241
|
+
.object({
|
|
242
|
+
...actionFields,
|
|
243
|
+
/** What the Action does, in the app's words and the person's language. */
|
|
244
|
+
sentence: PlainText(APP_ACTION_LIMITS.sentenceChars).min(1),
|
|
245
|
+
/** The app's review of this call, or `null`. */
|
|
246
|
+
message: PlainText(APP_ACTION_LIMITS.messageChars).nullable(),
|
|
247
|
+
details: z
|
|
248
|
+
.array(
|
|
249
|
+
z
|
|
250
|
+
.object({
|
|
251
|
+
label: PlainText(APP_ACTION_LIMITS.labelChars).min(1),
|
|
252
|
+
value: PlainText(APP_ACTION_LIMITS.valueChars),
|
|
253
|
+
/** A date (`YYYY-MM-DD` or RFC 3339) or an RFC 3339 date-time the device shows in its own locale and time zone. */
|
|
254
|
+
format: z.enum(["date", "date-time"]).optional(),
|
|
255
|
+
})
|
|
256
|
+
.strict(),
|
|
257
|
+
)
|
|
258
|
+
.max(APP_ACTION_LIMITS.details),
|
|
259
|
+
/** Cloud shortened or left out text; the complete request is in Cloud. */
|
|
260
|
+
truncated: z.boolean(),
|
|
261
|
+
/** SHA-256 of the Action's real input. The device cannot check it; it binds the decision to that input. */
|
|
262
|
+
argsDigest: z.string().min(1).max(128),
|
|
263
|
+
digest: Digest,
|
|
264
|
+
})
|
|
265
|
+
.strict();
|
|
266
|
+
export type AppActionSummary = z.infer<typeof AppActionSummarySchema>;
|
|
267
|
+
export type AppActionView = z.infer<typeof AppActionViewSchema>;
|
|
268
|
+
export const AppDeviceInboxSchema = z
|
|
269
|
+
.object({
|
|
270
|
+
logins: z.array(AppPendingLoginSchema).max(APP_APPROVAL_LIMITS.pendingPerAccount),
|
|
271
|
+
/** Newest first. */
|
|
272
|
+
actions: z.array(AppActionSummarySchema).max(APP_ACTION_LIMITS.inboxActions),
|
|
273
|
+
/** Open Action approvals beyond `actions`. */
|
|
274
|
+
moreActions: z.number().int().nonnegative(),
|
|
275
|
+
pollAfterSeconds: z.number().positive(),
|
|
276
|
+
})
|
|
277
|
+
.strict();
|
|
278
|
+
export type AppDeviceInbox = z.infer<typeof AppDeviceInboxSchema>;
|
|
279
|
+
|
|
280
|
+
/** UTF-8 of this exact JSON array is hashed into an Action's `digest`. Every field the device shows is part of it. */
|
|
281
|
+
export const appActionDigestMessage = (issuer: string, view: Omit<AppActionView, "digest">): string =>
|
|
282
|
+
JSON.stringify([
|
|
283
|
+
"cloud-action-v1",
|
|
284
|
+
issuer,
|
|
285
|
+
view.actionId,
|
|
286
|
+
view.app.id,
|
|
287
|
+
view.app.name,
|
|
288
|
+
view.requester.app.id,
|
|
289
|
+
view.requester.app.name,
|
|
290
|
+
view.requester.title,
|
|
291
|
+
view.assurance,
|
|
292
|
+
view.sentence,
|
|
293
|
+
view.message,
|
|
294
|
+
view.details.map((detail) => [detail.label, detail.value, detail.format ?? null]),
|
|
295
|
+
view.truncated,
|
|
296
|
+
view.createdAt,
|
|
297
|
+
view.expiresAt,
|
|
298
|
+
view.argsDigest,
|
|
299
|
+
]);
|
|
300
|
+
/** Unpadded base64url of SHA-256 over `appActionDigestMessage`. Cloud and the device compute it alike. */
|
|
301
|
+
export const appActionDigest = async (issuer: string, view: Omit<AppActionView, "digest">): Promise<string> => {
|
|
302
|
+
const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appActionDigestMessage(issuer, view))));
|
|
303
|
+
return btoa(String.fromCharCode(...hash))
|
|
304
|
+
.replaceAll("+", "-")
|
|
305
|
+
.replaceAll("/", "_")
|
|
306
|
+
.replaceAll("=", "");
|
|
307
|
+
};
|
|
308
|
+
|
|
191
309
|
export const AppDeviceResponseSchema = z.union([
|
|
192
310
|
z
|
|
193
311
|
.object({
|
|
@@ -197,6 +315,8 @@ export const AppDeviceResponseSchema = z.union([
|
|
|
197
315
|
.strict(),
|
|
198
316
|
z.object({ state: z.enum(["approved", "denied", "revoked", "updated"]) }).strict(),
|
|
199
317
|
AppDeviceAccountSchema,
|
|
318
|
+
AppDeviceInboxSchema,
|
|
319
|
+
AppActionViewSchema,
|
|
200
320
|
]);
|
|
201
321
|
export const AppLoginStartResultSchema = z
|
|
202
322
|
.object({ requestId: Id, browserSecret: Secret, comparison: Comparison, expiresAt: DateTime, pollAfterSeconds: z.number().positive() })
|