@k2b/cloud 0.30.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 +7 -4
- package/src/_internal/app-presentation.ts +22 -1
- package/src/_internal/capabilities.ts +2 -2
- package/src/_internal/define-app.ts +17 -5
- package/src/_internal/registry.ts +50 -6
- package/src/_internal/widget-declarations.ts +53 -0
- package/src/ai/app-skills.ts +1 -7
- package/src/ai/approval-routes.ts +40 -4
- package/src/ai/approvals.ts +68 -10
- 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 +64 -39
- 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/live-turn.browser-harness.tsx +4 -1
- package/src/ai/chat/message-actions.tsx +9 -1
- package/src/ai/chat/message-utils.ts +7 -0
- package/src/ai/chat/messages.ts +34 -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 +67 -12
- package/src/ai/chat/tool-names.ts +18 -2
- package/src/ai/chat/turn-layout.ts +8 -0
- package/src/ai/chat/turn-view.tsx +111 -32
- 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/chat/web-tools.tsx +95 -1
- package/src/ai/client/controller.ts +6 -3
- package/src/ai/client-tool-results.ts +9 -0
- package/src/ai/code-runtime-tools.ts +50 -17
- package/src/ai/code-source-contracts.ts +25 -21
- package/src/ai/code-source-tools.ts +3 -1
- package/src/ai/default-tool-contracts.ts +334 -91
- package/src/ai/default-tools.ts +87 -11
- package/src/ai/executor.ts +343 -45
- package/src/ai/fetch-file-tool.ts +41 -4
- 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/firecrawl-tools.ts +87 -4
- package/src/ai/html-pdf-tool.ts +3 -2
- package/src/ai/http.ts +37 -12
- package/src/ai/index.ts +98 -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 +120 -2
- 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 +28 -3
- package/src/ai/provider.ts +16 -4
- package/src/ai/request-context.ts +33 -0
- package/src/ai/routes.ts +99 -49
- package/src/ai/runtime.ts +90 -15
- 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 +91 -1
- package/src/ai/system-prompt.ts +69 -10
- package/src/ai/timeline.ts +3 -1
- package/src/ai/tool-audit.ts +11 -1
- package/src/ai/tools.ts +94 -3
- package/src/ai/types.ts +48 -0
- package/src/ai/view-block.ts +941 -0
- package/src/ai/vision-tool.ts +2 -1
- package/src/ai/web-provenance.ts +103 -0
- 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/capabilities.ts +3 -2
- package/src/api/index.ts +3 -0
- package/src/api/me.ts +12 -3
- package/src/api/widgets.ts +36 -5
- 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/CloudResourceSearch.tsx +1 -1
- package/src/browser/app-approval.ts +32 -0
- package/src/browser/choose-files.tsx +26 -13
- package/src/browser/command-shortcuts.ts +21 -6
- 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/browser/spotlight-position.ts +4 -1
- package/src/browser/widgets.ts +7 -5
- package/src/capabilities/executions.ts +3 -0
- package/src/cli/account.ts +21 -2
- package/src/cli/commands.ts +24 -2
- package/src/cli/index.ts +16 -0
- package/src/cli/plugin.ts +30 -0
- package/src/contracts/app-approval.ts +120 -0
- package/src/contracts/app.ts +34 -2
- 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/registry.ts +9 -1
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +47 -2
- package/src/desktop/solid.tsx +6 -7
- 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 +3 -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/server/widget-request.ts +27 -0
- 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 +241 -0
- package/src/services/approvals/list.ts +120 -0
- package/src/services/approvals/migrate.ts +41 -0
- 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/app-presentation.ts +11 -0
- package/src/shared/email-html.ts +46 -7
- 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/AdminLayout.tsx +4 -7
- package/src/ssr/AppBadge.tsx +4 -4
- package/src/ssr/AppLaunchpad.island.tsx +1 -1
- package/src/ssr/AppLaunchpadPanel.tsx +4 -4
- package/src/ssr/Layout.tsx +38 -14
- package/src/ssr/LayoutFooter.tsx +1 -1
- package/src/ssr/LayoutHeader.tsx +17 -20
- package/src/ssr/LayoutHelp.tsx +1 -1
- package/src/ssr/LayoutRail.tsx +1 -2
- package/src/ssr/PageTrail.tsx +23 -0
- package/src/ssr/PageTrailRow.island.tsx +20 -0
- package/src/ssr/PageTrailRuntime.island.tsx +26 -0
- package/src/ssr/ProfilePreferences.island.tsx +7 -3
- package/src/ssr/RailApps.island.tsx +2 -2
- package/src/ssr/app-badges.ts +50 -6
- package/src/ssr/islands/index.ts +1 -0
- package/src/ssr/layout-runtime.ts +60 -0
- package/src/ssr/mobile-menu-history.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +313 -0
- package/src/ssr/page-trail.browser-harness.tsx +265 -0
- package/src/ssr/page-trail.ts +59 -0
- package/src/ssr/platform-messages.ts +4 -0
- 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/effects.css +21 -4
- package/src/styles/global.css +39 -3
- package/src/styles/utilities-layout.css +24 -1
- package/src/styles/utilities-navigation.css +6 -18
package/src/cli/index.ts
CHANGED
|
@@ -51,6 +51,12 @@ export type CloudCliContext = {
|
|
|
51
51
|
profiles?: {
|
|
52
52
|
saveClientCredentials: (input: CloudCliClientCredentialsProfile) => Promise<void>;
|
|
53
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Open an `http:` or `https:` URL in the user's browser, when the host can.
|
|
56
|
+
* Resolves `false` when nothing was opened, so the command still prints the
|
|
57
|
+
* link. Absent in hosts without a local browser.
|
|
58
|
+
*/
|
|
59
|
+
openUrl?: (url: string) => Promise<boolean>;
|
|
54
60
|
};
|
|
55
61
|
|
|
56
62
|
/** A profile that signs in with the OAuth client-credentials grant. */
|
|
@@ -70,6 +76,16 @@ export type CloudCliClientCredentialsProfile = {
|
|
|
70
76
|
export type CloudCliModule = {
|
|
71
77
|
name: string;
|
|
72
78
|
summary: string;
|
|
79
|
+
/**
|
|
80
|
+
* `cld <alias> …` runs `cld <name> …`. Names match CLOUD_CLI_MODULE_NAME;
|
|
81
|
+
* cld uses them only when exactly one installed module claims them and no built-in or module has that name.
|
|
82
|
+
*/
|
|
83
|
+
alias?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Top-level names mapped to space-separated command paths relative to this module, never preset flags or args.
|
|
86
|
+
* Names match CLOUD_CLI_MODULE_NAME; cld uses them only when exactly one installed module claims them and no built-in or module has that name.
|
|
87
|
+
*/
|
|
88
|
+
shortcuts?: Readonly<Record<string, string>>;
|
|
73
89
|
booleanFlags?: readonly string[];
|
|
74
90
|
requiresCloud?: boolean;
|
|
75
91
|
requiresCloudFor?: (args: string[], flags: CloudCliFlags) => boolean;
|
package/src/cli/plugin.ts
CHANGED
|
@@ -67,6 +67,27 @@ export const cloudCliPluginDigest = (files: readonly Pick<CloudCliPluginFile, "p
|
|
|
67
67
|
|
|
68
68
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
69
|
|
|
70
|
+
/** Whether an alias is a valid extra name for this module. */
|
|
71
|
+
export const isCloudCliAlias = (value: unknown, name: string): value is string =>
|
|
72
|
+
typeof value === "string" && CLOUD_CLI_MODULE_NAME.test(value) && value !== name;
|
|
73
|
+
|
|
74
|
+
/** Validate short names and command-path syntax without loading a module's command tree. */
|
|
75
|
+
export const isCloudCliShortcuts = (value: unknown, name: string, alias?: string): value is Record<string, string> =>
|
|
76
|
+
isRecord(value) &&
|
|
77
|
+
(Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) &&
|
|
78
|
+
Object.entries(value).every(
|
|
79
|
+
([key, target]) =>
|
|
80
|
+
CLOUD_CLI_MODULE_NAME.test(key) &&
|
|
81
|
+
key !== name &&
|
|
82
|
+
key !== alias &&
|
|
83
|
+
typeof target === "string" &&
|
|
84
|
+
target.trim() !== "" &&
|
|
85
|
+
target
|
|
86
|
+
.trim()
|
|
87
|
+
.split(/\s+/)
|
|
88
|
+
.every((segment) => !segment.startsWith("-")),
|
|
89
|
+
);
|
|
90
|
+
|
|
70
91
|
/** Validate a served manifest, including its digest. Throws an `Error` naming the first problem. */
|
|
71
92
|
export const parseCloudCliPluginManifest = (value: unknown): CloudCliPluginManifest => {
|
|
72
93
|
if (!isRecord(value)) throw new Error("plugin manifest is not an object");
|
|
@@ -111,6 +132,15 @@ export const validateCloudCliModule = (value: unknown): CloudCliModule => {
|
|
|
111
132
|
if (typeof value.name !== "string" || !CLOUD_CLI_MODULE_NAME.test(value.name)) {
|
|
112
133
|
throw new Error("default export needs a lowercase kebab-case name");
|
|
113
134
|
}
|
|
135
|
+
if (value.alias !== undefined && !isCloudCliAlias(value.alias, value.name)) {
|
|
136
|
+
throw new Error("alias must be a lowercase kebab-case name different from the module name");
|
|
137
|
+
}
|
|
138
|
+
if (
|
|
139
|
+
value.shortcuts !== undefined &&
|
|
140
|
+
!isCloudCliShortcuts(value.shortcuts, value.name, typeof value.alias === "string" ? value.alias : undefined)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("shortcuts must be a plain object with unique kebab-case names and non-empty command paths without flags");
|
|
143
|
+
}
|
|
114
144
|
if (typeof value.summary !== "string") throw new Error("default export needs a string summary");
|
|
115
145
|
if (typeof value.run !== "function") throw new Error("default export needs a run function");
|
|
116
146
|
for (const key of ["help", "requiresCloudFor"] as const) {
|
|
@@ -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() })
|
package/src/contracts/app.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Sync } from "@k2b/sync";
|
|
|
2
2
|
import type { PlatformPermission } from "./outgoing-mail";
|
|
3
3
|
import type { AppRegistryHelpSummary } from "./registry";
|
|
4
4
|
import type { Role } from "./shared";
|
|
5
|
-
import type { DashboardWidgetPresentation } from "./widgets";
|
|
5
|
+
import type { DashboardWidgetPresentation, DashboardWidgetSize } from "./widgets";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* One link entry contributed by an app to the global legal/info footer
|
|
@@ -69,6 +69,13 @@ export type AppPresentationTranslation = {
|
|
|
69
69
|
searchLinks?: Readonly<Record<string, string>>;
|
|
70
70
|
/** Search-link descriptions keyed by their stable same-origin href. */
|
|
71
71
|
searchLinkDescriptions?: Readonly<Record<string, string>>;
|
|
72
|
+
/** Dashboard widget titles and descriptions keyed by `widgets[].id`. */
|
|
73
|
+
widgets?: Readonly<Record<string, AppWidgetTranslation>>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type AppWidgetTranslation = {
|
|
77
|
+
title?: string;
|
|
78
|
+
description?: string;
|
|
72
79
|
};
|
|
73
80
|
|
|
74
81
|
export type AppPresentationCatalog = {
|
|
@@ -161,7 +168,32 @@ export type WidgetEndpoint = {
|
|
|
161
168
|
id: string;
|
|
162
169
|
/** Absolute path on the app's HTTP service, e.g. "/api/accounts/widget/open-requests". */
|
|
163
170
|
path: string;
|
|
164
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Name of the widget in the dashboard and its widget gallery, up to 80 characters, in the app's `baseLocale`;
|
|
173
|
+
* translate it with `presentation.translations[locale].widgets[id].title`. Defaults to the app's name.
|
|
174
|
+
*/
|
|
175
|
+
title?: string;
|
|
176
|
+
/** One sentence for the widget gallery, up to 200 characters; translated like `title`. Defaults to the app's description. */
|
|
177
|
+
description?: string;
|
|
178
|
+
/** Sizes the widget offers on the board. Defaults to `["large"]`; offer smaller sizes only when the handler fills them. */
|
|
179
|
+
sizes?: readonly DashboardWidgetSize[];
|
|
180
|
+
/** Size the widget starts in, one of `sizes`. Defaults to the largest offered size. */
|
|
181
|
+
defaultSize?: DashboardWidgetSize;
|
|
182
|
+
/**
|
|
183
|
+
* Put the widget on the default board of everyone who may see it and list it under "Suggested for you" in the
|
|
184
|
+
* gallery. Suggest only widgets that are useful without any setup.
|
|
185
|
+
*/
|
|
186
|
+
suggest?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Show the widget in the gallery and on the default board only to people with one of these roles, like
|
|
189
|
+
* `nav.requiresRoles`. Visibility only: the handler still authorizes every request and answers `403`.
|
|
190
|
+
*/
|
|
191
|
+
requiresRoles?: readonly Role[];
|
|
192
|
+
/**
|
|
193
|
+
* @deprecated The dashboard has no zones or widths any more; declare `sizes` and `defaultSize` instead. Keep an
|
|
194
|
+
* existing value: the dashboard reads it only to convert a board saved before sizes existed into the one the person
|
|
195
|
+
* saw.
|
|
196
|
+
*/
|
|
165
197
|
presentation?: DashboardWidgetPresentation;
|
|
166
198
|
};
|
|
167
199
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { APP_APPROVAL_LIMITS } from "./app-approval";
|
|
3
|
+
import { CapabilityActionReviewSchema, CapabilityAppIdSchema } from "./capabilities";
|
|
4
|
+
|
|
5
|
+
export { APPROVAL_COUNTS_PATH, APPROVALS_API_PATH, APPROVALS_PAGE_PATH } from "./approval-paths";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pending Action approvals as every app lists and decides them: one Core route under `/api/approvals` and the
|
|
9
|
+
* `Approvals` component from `@k2b/cloud/approvals/ui`. A list shows only the signed-in person's own open requests.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** One page of a list, the same as a Cloud Login inbox page. */
|
|
13
|
+
export const APPROVALS_PAGE_SIZE = APP_APPROVAL_LIMITS.pageSize;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A client that relays decisions from an editor over the Agent Client Protocol sends this header with the value
|
|
17
|
+
* `acp`. Such a client may reject any request, but approves only `standard` ones. Cloud cannot verify the header: it is
|
|
18
|
+
* the bridge's own declaration, which keeps the editor that drives the bridge from approving a `confirm` request.
|
|
19
|
+
*/
|
|
20
|
+
export const APPROVAL_CLIENT_HEADER = "x-cloud-client";
|
|
21
|
+
export const APPROVAL_ACP_CLIENT = "acp";
|
|
22
|
+
|
|
23
|
+
export const ApprovalListQuerySchema = z
|
|
24
|
+
.object({
|
|
25
|
+
/** Requests whose effect belongs to this app, or that this app asked for. */
|
|
26
|
+
app: CapabilityAppIdSchema.optional(),
|
|
27
|
+
/** Requests whose review links to this same-origin path, such as the draft a send Action would send. */
|
|
28
|
+
link: z.string().startsWith("/").max(2048).optional(),
|
|
29
|
+
cursor: z.string().min(1).max(200).optional(),
|
|
30
|
+
})
|
|
31
|
+
.strict();
|
|
32
|
+
export type ApprovalListQuery = z.infer<typeof ApprovalListQuerySchema>;
|
|
33
|
+
|
|
34
|
+
export const PendingApprovalSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
id: z.uuid(),
|
|
37
|
+
/** The app that owns the effect, named from the registry in the reader's language. */
|
|
38
|
+
app: z.object({ id: z.string(), name: z.string(), icon: z.string().nullable() }).strict(),
|
|
39
|
+
action: z.string(),
|
|
40
|
+
/** `confirm` asks every time and never offers to remember; `standard` may offer it. */
|
|
41
|
+
assurance: z.enum(["standard", "confirm"]),
|
|
42
|
+
/** What the person reads: the first line names the app and the Action, later lines add `Label: value` details. */
|
|
43
|
+
sentence: z.string(),
|
|
44
|
+
/** The owning app's review as the person saw it when Cloud asked. */
|
|
45
|
+
review: CapabilityActionReviewSchema.nullable(),
|
|
46
|
+
/** Where the request waits. The title is chat content, written by a person or a model, never Cloud's own words. */
|
|
47
|
+
requester: z.object({ kind: z.literal("chat"), appId: z.string(), title: z.string().nullable(), href: z.string() }).strict(),
|
|
48
|
+
/** Ways the person may remember this approval, the same the chat card offers. */
|
|
49
|
+
remember: z.object({ chat: z.boolean(), always: z.boolean(), website: z.boolean() }).strict(),
|
|
50
|
+
createdAt: z.string(),
|
|
51
|
+
expiresAt: z.string(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
export type PendingApproval = z.infer<typeof PendingApprovalSchema>;
|
|
55
|
+
|
|
56
|
+
export const ApprovalPageSchema = z
|
|
57
|
+
.object({
|
|
58
|
+
items: z.array(PendingApprovalSchema).max(APPROVALS_PAGE_SIZE),
|
|
59
|
+
nextCursor: z.string().nullable(),
|
|
60
|
+
/** Open requests that match the query, across all pages. */
|
|
61
|
+
count: z.number().int().nonnegative(),
|
|
62
|
+
/** Open requests of the person that do not match the query; 0 without `app` or `link`. */
|
|
63
|
+
elsewhere: z.number().int().nonnegative(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
export type ApprovalPage = z.infer<typeof ApprovalPageSchema>;
|
|
67
|
+
|
|
68
|
+
export const ApprovalCountsSchema = z
|
|
69
|
+
.object({
|
|
70
|
+
total: z.number().int().nonnegative(),
|
|
71
|
+
/** Per app ID: open requests whose effect belongs to the app or that the app asked for, as `?app=` lists them. */
|
|
72
|
+
apps: z.record(z.string(), z.number().int().positive()),
|
|
73
|
+
})
|
|
74
|
+
.strict();
|
|
75
|
+
export type ApprovalCounts = z.infer<typeof ApprovalCountsSchema>;
|
|
76
|
+
|
|
77
|
+
export const ApprovalDecisionInputSchema = z.object({ approved: z.boolean(), remember: z.enum(["chat", "always"]).optional() }).strict();
|
|
78
|
+
export type ApprovalDecisionInput = z.infer<typeof ApprovalDecisionInputSchema>;
|
|
79
|
+
|
|
80
|
+
export const ApprovalDecisionResultSchema = z.object({ status: z.enum(["approved", "rejected"]) }).strict();
|
|
81
|
+
export type ApprovalDecisionResult = z.infer<typeof ApprovalDecisionResultSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Commands carry small public references/options, never document bodies or secrets. */
|
|
2
|
+
export const COMMAND_MAX_URL_LENGTH = 8192;
|
|
3
|
+
|
|
4
|
+
/** The same-origin path rule of `CommandPathSchema`, without its length limit. */
|
|
5
|
+
export const isCommandPathSyntax = (value: string): boolean => {
|
|
6
|
+
if (!value.startsWith("/") || /[\\\u0000-\u0020]/u.test(value)) return false;
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value, "https://cloud.invalid");
|
|
9
|
+
// Dot segments can resolve to `//host`, which a browser reads as another host.
|
|
10
|
+
return url.origin === "https://cloud.invalid" && !url.pathname.startsWith("//");
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Whether `value` is a Cloud path that `CommandPathSchema` accepts; loads no validators. */
|
|
17
|
+
export const isCommandPath = (value: string): boolean => (value.length <= 2000 || [...value].length <= 2000) && isCommandPathSyntax(value);
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { COMMAND_MAX_URL_LENGTH, isCommandPathSyntax } from "./command-path";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
.max(2000)
|
|
8
|
-
.refine((value) => {
|
|
9
|
-
if (!value.startsWith("/") || /[\\\u0000-\u0020]/u.test(value)) return false;
|
|
10
|
-
try {
|
|
11
|
-
const url = new URL(value, "https://cloud.invalid");
|
|
12
|
-
// Dot segments can resolve to `//host`, which a browser reads as another host.
|
|
13
|
-
return url.origin === "https://cloud.invalid" && !url.pathname.startsWith("//");
|
|
14
|
-
} catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
}, "Expected a Cloud path");
|
|
4
|
+
export { COMMAND_MAX_URL_LENGTH, isCommandPath } from "./command-path";
|
|
5
|
+
|
|
6
|
+
// Keep the size error separate from the syntax error, as before.
|
|
7
|
+
export const CommandPathSchema = z.string().max(2000).refine(isCommandPathSyntax, "Expected a Cloud path");
|
|
18
8
|
|
|
19
9
|
export const CommandOptionsSchema = z.object({ returnTo: CommandPathSchema.optional() }).strict();
|
|
20
10
|
export type CommandOptions = z.infer<typeof CommandOptionsSchema>;
|
|
@@ -2,7 +2,7 @@ import type { AppAdminNavigationGroup, AppAppearance, AppAppearanceColor, AppPre
|
|
|
2
2
|
import type { CapabilityManifest, CapabilityPresentationCatalog } from "./capabilities";
|
|
3
3
|
import type { PlatformPermission } from "./outgoing-mail";
|
|
4
4
|
import type { Role } from "./shared";
|
|
5
|
-
import type { DashboardWidgetPresentation } from "./widgets";
|
|
5
|
+
import type { DashboardWidgetPresentation, DashboardWidgetSize } from "./widgets";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* App-registry entry type. Populated internally by `defineApp()` + the
|
|
@@ -52,10 +52,18 @@ export type AppRegistryLegalLink = {
|
|
|
52
52
|
icon?: string;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
/** A widget declaration as the registry carries it; see `WidgetEndpoint`. Older apps send only `id` and `path`. */
|
|
55
56
|
export type AppRegistryWidget = {
|
|
56
57
|
id: string;
|
|
57
58
|
/** Absolute path on the app's HTTP service, e.g. "/api/quotes/widget/random". */
|
|
58
59
|
path: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
sizes?: readonly DashboardWidgetSize[];
|
|
63
|
+
defaultSize?: DashboardWidgetSize;
|
|
64
|
+
suggest?: boolean;
|
|
65
|
+
requiresRoles?: readonly Role[];
|
|
66
|
+
/** @deprecated Read only to convert dashboard boards saved before sizes existed. */
|
|
59
67
|
presentation?: DashboardWidgetPresentation;
|
|
60
68
|
};
|
|
61
69
|
|
package/src/contracts/shared.ts
CHANGED
|
@@ -248,7 +248,12 @@ export const ServiceAccountCredentialSchema = z.object({
|
|
|
248
248
|
});
|
|
249
249
|
export type ServiceAccountCredential = z.infer<typeof ServiceAccountCredentialSchema>;
|
|
250
250
|
|
|
251
|
+
export const AI_INFERENCE_SCOPE = "ai:inference" as const;
|
|
252
|
+
|
|
253
|
+
export const isModelsOnlyApiKey = (credential: { scopes: readonly string[] }): boolean => credential.scopes.includes(AI_INFERENCE_SCOPE);
|
|
254
|
+
|
|
251
255
|
export const CreateUserApiKeySchema = z.object({
|
|
256
|
+
access: z.enum(["full", "models"]).optional(),
|
|
252
257
|
name: z.string().trim().min(1).max(120),
|
|
253
258
|
expiresAt: z.string().datetime().nullable().optional(),
|
|
254
259
|
});
|
package/src/contracts/widgets.ts
CHANGED
|
@@ -38,18 +38,63 @@ const WidgetHrefSchema = z
|
|
|
38
38
|
export const WidgetToneSchema = z.enum(["emerald", "amber", "red", "blue", "zinc"]);
|
|
39
39
|
export type WidgetTone = z.infer<typeof WidgetToneSchema>;
|
|
40
40
|
|
|
41
|
+
/** @deprecated The dashboard no longer has zones; declare `sizes` and `defaultSize` instead. */
|
|
41
42
|
export type DashboardWidgetZone = "focus" | "overview" | "context";
|
|
43
|
+
/** @deprecated The dashboard no longer has spans; declare `sizes` and `defaultSize` instead. */
|
|
42
44
|
export type DashboardWidgetSpan = "standard" | "wide";
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
+
* @deprecated The dashboard has no zones or widths any more. Declare `sizes` and `defaultSize` on the widget instead;
|
|
48
|
+
* the dashboard reads this recommendation only to convert a board saved before sizes existed.
|
|
47
49
|
*/
|
|
48
50
|
export type DashboardWidgetPresentation = {
|
|
49
51
|
defaultZone?: DashboardWidgetZone;
|
|
50
52
|
defaultSpan?: DashboardWidgetSpan;
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* The three sizes a widget can take on the dashboard board, which has four columns on wider screens and two on
|
|
57
|
+
* phones: `small` is one column and one row, `medium` two columns and one row, and `large` two columns and two rows.
|
|
58
|
+
* On a phone, `small` takes half the width and `medium` and `large` the full width.
|
|
59
|
+
*/
|
|
60
|
+
export const DASHBOARD_WIDGET_SIZES = ["small", "medium", "large"] as const;
|
|
61
|
+
export type DashboardWidgetSize = (typeof DASHBOARD_WIDGET_SIZES)[number];
|
|
62
|
+
|
|
63
|
+
/** Longest declared widget title, in characters, the same bound as a shortcut title. */
|
|
64
|
+
export const WIDGET_TITLE_MAX_LENGTH = 80;
|
|
65
|
+
/** Longest declared widget description, in characters: one sentence in the widget gallery. */
|
|
66
|
+
export const WIDGET_DESCRIPTION_MAX_LENGTH = 200;
|
|
67
|
+
|
|
68
|
+
export const isDashboardWidgetSize = (value: unknown): value is DashboardWidgetSize =>
|
|
69
|
+
DASHBOARD_WIDGET_SIZES.some((size) => size === value);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The sizes a widget offers and the size it starts with. A widget that declares no valid size offers only `large`,
|
|
73
|
+
* the height every widget had before sizes existed; without a valid `defaultSize`, it starts at its largest size.
|
|
74
|
+
*/
|
|
75
|
+
export const resolveWidgetSizes = (declaration: {
|
|
76
|
+
sizes?: readonly unknown[];
|
|
77
|
+
defaultSize?: unknown;
|
|
78
|
+
}): { sizes: DashboardWidgetSize[]; defaultSize: DashboardWidgetSize } => {
|
|
79
|
+
const declared = DASHBOARD_WIDGET_SIZES.filter((size) => declaration.sizes?.includes(size));
|
|
80
|
+
const sizes: DashboardWidgetSize[] = declared.length > 0 ? declared : ["large"];
|
|
81
|
+
const defaultSize =
|
|
82
|
+
isDashboardWidgetSize(declaration.defaultSize) && sizes.includes(declaration.defaultSize) ? declaration.defaultSize : sizes.at(-1)!;
|
|
83
|
+
return { sizes, defaultSize };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** `requested` when the widget offers it, otherwise the widget's default size. */
|
|
87
|
+
export const fitWidgetSize = (
|
|
88
|
+
requested: unknown,
|
|
89
|
+
widget: { sizes: readonly DashboardWidgetSize[]; defaultSize: DashboardWidgetSize },
|
|
90
|
+
): DashboardWidgetSize => (isDashboardWidgetSize(requested) && widget.sizes.includes(requested) ? requested : widget.defaultSize);
|
|
91
|
+
|
|
92
|
+
/** What the dashboard asks a widget handler for, read with `getWidgetRequest(c)`. */
|
|
93
|
+
export type WidgetRequest = {
|
|
94
|
+
/** The size the widget is shown in, always one the widget declares. */
|
|
95
|
+
size: DashboardWidgetSize;
|
|
96
|
+
};
|
|
97
|
+
|
|
53
98
|
export const WidgetAccentSchema = z
|
|
54
99
|
.object({
|
|
55
100
|
tone: WidgetToneSchema,
|
package/src/desktop/solid.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { suppressTextSelection } from "@k2b/ui";
|
|
1
2
|
import { deserialize as deserializeProps, serialize as serializeProps } from "seroval";
|
|
2
3
|
import type { Component, JSX, ParentProps } from "solid-js";
|
|
3
4
|
import { children, createContext, createMemo, createSignal, For, onCleanup, onMount, Show, useContext } from "solid-js";
|
|
@@ -483,10 +484,11 @@ const startDesktopResize = (event: PointerEvent, onMove: (dx: number, dy: number
|
|
|
483
484
|
const startX = event.clientX;
|
|
484
485
|
const startY = event.clientY;
|
|
485
486
|
const move = (next: PointerEvent) => onMove(next.clientX - startX, next.clientY - startY);
|
|
487
|
+
const releaseTextSelection = suppressTextSelection(event.pointerId);
|
|
486
488
|
const stop = () => {
|
|
487
489
|
window.removeEventListener("pointermove", move);
|
|
488
490
|
window.removeEventListener("pointerup", stop);
|
|
489
|
-
|
|
491
|
+
releaseTextSelection();
|
|
490
492
|
document.body.style.cursor = "";
|
|
491
493
|
};
|
|
492
494
|
window.addEventListener("pointermove", move);
|
|
@@ -631,10 +633,9 @@ export const DesktopWorkspace = ((props: DesktopWorkspaceProps) => {
|
|
|
631
633
|
|
|
632
634
|
const resizeSidebar = (event: PointerEvent) => {
|
|
633
635
|
const pane = sidebar();
|
|
634
|
-
if (!pane?.resizable) return;
|
|
636
|
+
if (!pane?.resizable || event.button !== 0) return;
|
|
635
637
|
const start = sidebarOpen() ? sidebarSize() : sidebarColumnSize();
|
|
636
638
|
document.body.style.cursor = "col-resize";
|
|
637
|
-
document.body.style.userSelect = "none";
|
|
638
639
|
startDesktopResize(event, (dx) => {
|
|
639
640
|
const raw = start + dx;
|
|
640
641
|
if (shouldRail(pane, raw)) {
|
|
@@ -650,10 +651,9 @@ export const DesktopWorkspace = ((props: DesktopWorkspaceProps) => {
|
|
|
650
651
|
|
|
651
652
|
const resizeRight = (event: PointerEvent) => {
|
|
652
653
|
const pane = right();
|
|
653
|
-
if (!pane?.resizable) return;
|
|
654
|
+
if (!pane?.resizable || event.button !== 0) return;
|
|
654
655
|
const start = rightOpen() ? rightSize() : rightColumnSize();
|
|
655
656
|
document.body.style.cursor = "col-resize";
|
|
656
|
-
document.body.style.userSelect = "none";
|
|
657
657
|
startDesktopResize(event, (dx) => {
|
|
658
658
|
const raw = start - dx;
|
|
659
659
|
if (shouldRail(pane, raw)) {
|
|
@@ -669,10 +669,9 @@ export const DesktopWorkspace = ((props: DesktopWorkspaceProps) => {
|
|
|
669
669
|
|
|
670
670
|
const resizeBottom = (event: PointerEvent) => {
|
|
671
671
|
const pane = bottom();
|
|
672
|
-
if (!pane?.resizable) return;
|
|
672
|
+
if (!pane?.resizable || event.button !== 0) return;
|
|
673
673
|
const start = bottomOpen() ? bottomSize() : bottomRowSize();
|
|
674
674
|
document.body.style.cursor = "row-resize";
|
|
675
|
-
document.body.style.userSelect = "none";
|
|
676
675
|
startDesktopResize(event, (_dx, dy) => {
|
|
677
676
|
const raw = start - dy;
|
|
678
677
|
if (shouldRail(pane, raw)) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
export type { LiveServerMessage } from "./live-server-message";
|
|
4
|
+
|
|
3
5
|
/** Messages a live socket accepts: subscriptions only. Writes go over HTTP. */
|
|
4
6
|
export const LiveClientMessageSchema = z.discriminatedUnion("t", [
|
|
5
7
|
z
|
|
@@ -28,4 +30,3 @@ export const LiveServerMessageSchema = z.discriminatedUnion("t", [
|
|
|
28
30
|
z.object({ t: z.literal("revoked"), id: z.string(), code: z.enum(["not_found", "access_denied"]) }),
|
|
29
31
|
z.object({ t: z.literal("error"), code: z.string(), message: z.string() }),
|
|
30
32
|
]);
|
|
31
|
-
export type LiveServerMessage = z.infer<typeof LiveServerMessageSchema>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The server's live wire messages; validation equivalence is checked against live-protocol. */
|
|
2
|
+
export type LiveServerMessage =
|
|
3
|
+
| { t: "ready"; id: string; cursor: string }
|
|
4
|
+
| { t: "event"; id: string; cursor: string; data: unknown }
|
|
5
|
+
| { t: "progress"; cursor: string }
|
|
6
|
+
| { t: "resync"; id: string; cursor: string }
|
|
7
|
+
| { t: "revoked"; id: string; code: "not_found" | "access_denied" }
|
|
8
|
+
| { t: "error"; code: string; message: string };
|
|
9
|
+
|
|
10
|
+
/** Strip undeclared keys just like the server-message schema. */
|
|
11
|
+
export const parseLiveServerMessage = (value: unknown): LiveServerMessage | null => {
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || !("t" in value)) return null;
|
|
13
|
+
switch (value.t) {
|
|
14
|
+
case "ready":
|
|
15
|
+
case "resync":
|
|
16
|
+
case "event": {
|
|
17
|
+
if (!("id" in value) || typeof value.id !== "string" || !("cursor" in value) || typeof value.cursor !== "string") return null;
|
|
18
|
+
if (value.t === "event") {
|
|
19
|
+
// Zod v4 requires the key even though its value is unknown (and may be undefined).
|
|
20
|
+
return "data" in value ? { t: value.t, id: value.id, cursor: value.cursor, data: value.data } : null;
|
|
21
|
+
}
|
|
22
|
+
return { t: value.t, id: value.id, cursor: value.cursor };
|
|
23
|
+
}
|
|
24
|
+
case "progress":
|
|
25
|
+
return "cursor" in value && typeof value.cursor === "string" ? { t: value.t, cursor: value.cursor } : null;
|
|
26
|
+
case "revoked":
|
|
27
|
+
return "id" in value &&
|
|
28
|
+
typeof value.id === "string" &&
|
|
29
|
+
"code" in value &&
|
|
30
|
+
(value.code === "not_found" || value.code === "access_denied")
|
|
31
|
+
? { t: value.t, id: value.id, code: value.code }
|
|
32
|
+
: null;
|
|
33
|
+
case "error":
|
|
34
|
+
return "code" in value && typeof value.code === "string" && "message" in value && typeof value.message === "string"
|
|
35
|
+
? { t: value.t, code: value.code, message: value.message }
|
|
36
|
+
: null;
|
|
37
|
+
default:
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|