@k2b/cloud 0.38.0 → 0.40.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.
Files changed (244) hide show
  1. package/README.md +8 -0
  2. package/package.json +12 -5
  3. package/scripts/build-canvas-workers.ts +109 -29
  4. package/scripts/build.ts +3 -5
  5. package/src/_internal/app-favicon.ts +4 -2
  6. package/src/_internal/app-path.ts +25 -0
  7. package/src/_internal/app-presentation.ts +4 -0
  8. package/src/_internal/canvas-worker.ts +5 -3
  9. package/src/_internal/capabilities.ts +72 -48
  10. package/src/_internal/capability-reviews.ts +81 -0
  11. package/src/_internal/capability-schemas.ts +29 -0
  12. package/src/_internal/capability-sentences.ts +216 -1
  13. package/src/_internal/capability-streams.ts +7 -0
  14. package/src/_internal/define-app.ts +63 -16
  15. package/src/_internal/font-preloads.ts +2 -1
  16. package/src/_internal/panel-cookie.ts +39 -0
  17. package/src/_internal/registry-validation.ts +21 -26
  18. package/src/_internal/runtime-context.ts +1 -0
  19. package/src/_internal/runtime-watcher.ts +6 -0
  20. package/src/_internal/static-assets.ts +52 -3
  21. package/src/_internal/status-preserving-ssr.ts +2 -0
  22. package/src/access/PermissionEditor.browser-harness.tsx +57 -38
  23. package/src/access/PermissionEditor.tsx +32 -7
  24. package/src/access/ResourceApiKeys.tsx +5 -1
  25. package/src/account/EntitySearch.tsx +1 -1
  26. package/src/ai/action-review.ts +101 -0
  27. package/src/ai/approval-routes.ts +5 -2
  28. package/src/ai/approvals.ts +139 -28
  29. package/src/ai/background-review.ts +72 -0
  30. package/src/ai/browser.ts +10 -0
  31. package/src/ai/capabilities.ts +29 -11
  32. package/src/ai/capability-execution.ts +76 -43
  33. package/src/ai/capability-result.ts +95 -0
  34. package/src/ai/chat/action-card.ts +398 -0
  35. package/src/ai/chat/action-messages.ts +129 -0
  36. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  37. package/src/ai/chat/action-question.fixtures.ts +733 -0
  38. package/src/ai/chat/action-question.tsx +669 -0
  39. package/src/ai/chat/action-sentences.ts +1 -44
  40. package/src/ai/chat/blocks.tsx +19 -25
  41. package/src/ai/chat/message-actions.tsx +6 -0
  42. package/src/ai/chat/tool-action.tsx +481 -0
  43. package/src/ai/chat/turn-view.tsx +45 -163
  44. package/src/ai/chat/view-block.tsx +22 -7
  45. package/src/ai/chat-task-contracts.ts +31 -3
  46. package/src/ai/chat-task-reasons-migrate.ts +75 -0
  47. package/src/ai/chat-task-reasons.ts +92 -0
  48. package/src/ai/chat-tasks.ts +438 -78
  49. package/src/ai/client/controller.ts +59 -0
  50. package/src/ai/default-tools.ts +9 -1
  51. package/src/ai/executor.ts +190 -39
  52. package/src/ai/index.ts +14 -0
  53. package/src/ai/migrate.ts +39 -4
  54. package/src/ai/projects-routes.ts +3 -0
  55. package/src/ai/protocol.ts +14 -2
  56. package/src/ai/recent-resources.ts +240 -0
  57. package/src/ai/recent-sent-refs.ts +38 -0
  58. package/src/ai/request-context.ts +7 -1
  59. package/src/ai/routes.ts +172 -2
  60. package/src/ai/runtime.ts +13 -27
  61. package/src/ai/skills-routes.ts +3 -0
  62. package/src/ai/skills.ts +10 -1
  63. package/src/ai/store.ts +232 -27
  64. package/src/ai/timeline.ts +9 -1
  65. package/src/ai/tool-audit.ts +8 -0
  66. package/src/ai/tool-wording.ts +67 -0
  67. package/src/ai/turn-abort.ts +33 -0
  68. package/src/ai/types.ts +40 -4
  69. package/src/api/accounts-entities.ts +2 -1
  70. package/src/api/admin-ai-projects.ts +2 -0
  71. package/src/api/admin-ai-skills.ts +2 -0
  72. package/src/api/admin-ai-usage.ts +2 -0
  73. package/src/api/admin-core-settings.ts +190 -139
  74. package/src/api/admin-geoip.ts +80 -0
  75. package/src/api/admin-identity.ts +3 -0
  76. package/src/api/admin-lifecycle.ts +3 -0
  77. package/src/api/admin-outgoing-mail.ts +6 -2
  78. package/src/api/app-approval.ts +54 -6
  79. package/src/api/approvals-loader.ts +23 -1
  80. package/src/api/approvals.ts +3 -1
  81. package/src/api/auth.ts +25 -6
  82. package/src/api/capabilities.ts +44 -11
  83. package/src/api/capability-streams.ts +12 -0
  84. package/src/api/help.ts +4 -28
  85. package/src/api/index.ts +7 -1
  86. package/src/api/lasting-access-messages.ts +17 -0
  87. package/src/api/mcp.ts +3 -0
  88. package/src/api/me-panel-consent.ts +71 -0
  89. package/src/api/me.ts +80 -33
  90. package/src/api/search.ts +3 -1
  91. package/src/api/sync-ops.ts +2 -0
  92. package/src/api/widgets.ts +3 -0
  93. package/src/approvals/Approvals.tsx +180 -238
  94. package/src/approvals/messages.ts +12 -24
  95. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  96. package/src/browser/CloudResourceSearch.tsx +10 -6
  97. package/src/browser/app-approval.ts +60 -6
  98. package/src/browser/commands.ts +22 -6
  99. package/src/browser/composer-catalog.ts +145 -0
  100. package/src/browser/composer-insert-messages.ts +31 -0
  101. package/src/browser/composer-insert.ts +160 -0
  102. package/src/browser/composer-open-messages.ts +17 -0
  103. package/src/browser/composer-open.ts +110 -0
  104. package/src/browser/discoverable-command.ts +6 -0
  105. package/src/browser/hints.ts +6 -0
  106. package/src/browser/live-connection.ts +26 -2
  107. package/src/browser/navigation-apps.ts +7 -0
  108. package/src/browser/panel-context-bridge.ts +30 -0
  109. package/src/browser/panel-context-receiver.ts +53 -0
  110. package/src/browser/panel-menu.tsx +56 -0
  111. package/src/browser/panel-messages.ts +35 -0
  112. package/src/browser/panel-shortcuts.ts +31 -0
  113. package/src/browser/panel-slot.ts +756 -0
  114. package/src/browser/panel.ts +65 -0
  115. package/src/browser/resource-picker.tsx +12 -1
  116. package/src/browser/search-commands.ts +6 -3
  117. package/src/capabilities/client.ts +8 -2
  118. package/src/capabilities/reviews.ts +205 -0
  119. package/src/capabilities/server.ts +10 -3
  120. package/src/capabilities/testing.ts +47 -0
  121. package/src/capabilities/types.ts +2 -2
  122. package/src/cli/account.ts +13 -3
  123. package/src/cli/admin/geoip.ts +115 -0
  124. package/src/cli/admin/index.ts +3 -0
  125. package/src/contracts/app-approval.ts +159 -12
  126. package/src/contracts/app.ts +14 -0
  127. package/src/contracts/approvals.ts +2 -2
  128. package/src/contracts/capabilities.ts +410 -18
  129. package/src/contracts/geoip.ts +42 -0
  130. package/src/contracts/index.ts +3 -0
  131. package/src/contracts/panel-context-plain.ts +99 -0
  132. package/src/contracts/panel-context.ts +93 -0
  133. package/src/contracts/registry.ts +3 -1
  134. package/src/contracts/seen-hints.ts +35 -0
  135. package/src/contracts/shared.ts +2 -0
  136. package/src/server/client-ip.ts +5 -0
  137. package/src/server/help-payload.ts +38 -0
  138. package/src/server/middleware/auth.ts +11 -1
  139. package/src/server/middleware/middleware.ts +9 -0
  140. package/src/server/middleware/no-transform.ts +19 -0
  141. package/src/server/middleware/rate-limit.ts +4 -6
  142. package/src/server/request-context/address.ts +45 -0
  143. package/src/server/request-context/geoip.ts +380 -0
  144. package/src/server/request-context/index.ts +33 -0
  145. package/src/server/request-context/user-agent.ts +95 -0
  146. package/src/server/services/access.ts +5 -1
  147. package/src/services/accounts/posix.ts +2 -1
  148. package/src/services/app-approval.ts +533 -90
  149. package/src/services/approvals/device.ts +9 -5
  150. package/src/services/approvals/index.ts +6 -5
  151. package/src/services/approvals/list.ts +26 -5
  152. package/src/services/auth-flows/notification-sender.ts +3 -0
  153. package/src/services/geoip/index.ts +164 -0
  154. package/src/services/geoip/store.ts +30 -0
  155. package/src/services/geoip/upload.ts +150 -0
  156. package/src/services/identity/routes.ts +5 -1
  157. package/src/services/index.ts +32 -3
  158. package/src/services/known-browsers.ts +312 -0
  159. package/src/services/login-address-store.ts +51 -0
  160. package/src/services/login-context.ts +118 -0
  161. package/src/services/mandates/index.ts +3 -0
  162. package/src/services/mandates/policy.ts +15 -3
  163. package/src/services/notifications/browser.ts +16 -5
  164. package/src/services/notifications/email-frame.ts +1 -1
  165. package/src/services/oauth-tokens.ts +16 -2
  166. package/src/services/panel-context-consent.ts +58 -0
  167. package/src/services/pdf/image-protocol.ts +135 -0
  168. package/src/services/pdf/image-variants.ts +124 -0
  169. package/src/services/pdf/image-worker.ts +31 -0
  170. package/src/services/pdf/image.ts +109 -0
  171. package/src/services/pdf/index.ts +9 -0
  172. package/src/services/service-account-credentials.ts +21 -0
  173. package/src/services/session/index.ts +97 -16
  174. package/src/services/session/recent.ts +13 -0
  175. package/src/services/settings/api.ts +11 -6
  176. package/src/services/settings/app.ts +5 -4
  177. package/src/services/settings/core-settings.ts +4 -3
  178. package/src/services/settings/index.ts +19 -17
  179. package/src/services/settings/namespace.ts +2 -1
  180. package/src/services/settings/store.ts +83 -11
  181. package/src/services/webauthn.ts +12 -0
  182. package/src/shared/ai-platform-prompt.ts +1 -1
  183. package/src/shared/app-presentation.ts +1 -0
  184. package/src/shared/email-html.ts +2 -0
  185. package/src/shared/help.ts +3 -0
  186. package/src/shared/redirect.ts +12 -0
  187. package/src/shared-text/cursor.ts +22 -0
  188. package/src/shared-text/documents.ts +142 -0
  189. package/src/shared-text/events.ts +43 -0
  190. package/src/shared-text/index.ts +12 -0
  191. package/src/shared-text/log.ts +48 -0
  192. package/src/shared-text/text-diff.ts +23 -0
  193. package/src/shared-text/types.ts +41 -0
  194. package/src/ssr/AdminLayout.tsx +2 -0
  195. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  196. package/src/ssr/CloudShell.island.tsx +95 -10
  197. package/src/ssr/Layout.tsx +66 -10
  198. package/src/ssr/LayoutHeader.tsx +15 -9
  199. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  200. package/src/ssr/LayoutHelp.tsx +435 -274
  201. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
  202. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  203. package/src/ssr/LayoutRail.tsx +2 -2
  204. package/src/ssr/LayoutTabBar.tsx +29 -0
  205. package/src/ssr/MobileNavigation.tsx +2 -2
  206. package/src/ssr/PanelLayout.tsx +35 -0
  207. package/src/ssr/PanelRuntime.island.tsx +94 -0
  208. package/src/ssr/PanelSlot.tsx +158 -0
  209. package/src/ssr/PanelTrigger.tsx +49 -0
  210. package/src/ssr/ShellRuntime.island.tsx +69 -5
  211. package/src/ssr/app-appearance.ts +3 -2
  212. package/src/ssr/app-color.ts +105 -0
  213. package/src/ssr/app-launchpad.tsx +18 -1
  214. package/src/ssr/app-navigation.ts +28 -0
  215. package/src/ssr/compact-shell.baseline.json +5270 -0
  216. package/src/ssr/compact-shell.browser-harness.ts +439 -0
  217. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  218. package/src/ssr/help-messages.ts +4 -0
  219. package/src/ssr/help.ts +20 -0
  220. package/src/ssr/hints.ts +25 -0
  221. package/src/ssr/index.ts +5 -0
  222. package/src/ssr/islands/index.ts +4 -3
  223. package/src/ssr/layout-context.ts +2 -0
  224. package/src/ssr/layout-help-article.server.ts +50 -0
  225. package/src/ssr/layout-help-start.ts +33 -7
  226. package/src/ssr/panel-consent.ts +29 -0
  227. package/src/ssr/panel-context.ts +10 -0
  228. package/src/ssr/panel-slot.browser-harness.tsx +159 -0
  229. package/src/ssr/platform-messages.ts +48 -0
  230. package/src/ssr/rail-navigation.ts +44 -0
  231. package/src/ssr/shell-data.tsx +5 -0
  232. package/src/ssr/shell-shortcuts.ts +13 -2
  233. package/src/styles/effects.css +201 -11
  234. package/src/styles/file-chooser.css +2 -1
  235. package/src/styles/global.css +314 -63
  236. package/src/styles/panel.css +416 -0
  237. package/src/styles/resource-search.css +6 -2
  238. package/src/styles/tokens.css +36 -13
  239. package/src/styles/utilities-buttons.css +3 -2
  240. package/src/styles/utilities-data.css +2 -1
  241. package/src/styles/utilities-feedback.css +2 -2
  242. package/src/styles/utilities-layout.css +3 -13
  243. package/src/styles/utilities-markdown-editor.css +6 -6
  244. package/src/styles/utilities-navigation.css +22 -43
@@ -30,7 +30,23 @@ export const APP_ACTION_LIMITS = {
30
30
  valueChars: 300,
31
31
  } as const;
32
32
  /** Commands a Cloud serves beyond the first version, announced in `/info` as `features`. */
33
- export const APP_APPROVAL_FEATURES = ["actions"] as const;
33
+ export const APP_APPROVAL_FEATURES = ["actions", "terminal-logins", "login-context", "device-changes"] as const;
34
+ /**
35
+ * Optional inbox parts a device asks for in its signed `inbox` command. Without `accept`, the command, its
36
+ * signed message and the inbox answer keep their first form byte for byte.
37
+ */
38
+ export const APP_APPROVAL_ACCEPT = ["terminal-logins", "login-context", "device-changes"] as const;
39
+ export type AppApprovalAccept = (typeof APP_APPROVAL_ACCEPT)[number];
40
+ export const APP_APPROVAL_PENDING_ACCEPT = ["login-context"] as const;
41
+ export type AppApprovalPendingAccept = (typeof APP_APPROVAL_PENDING_ACCEPT)[number];
42
+
43
+ const acceptList = <T extends readonly [string, ...string[]]>(values: T) =>
44
+ z
45
+ .array(z.enum(values))
46
+ .min(1)
47
+ .max(8)
48
+ .refine((entries) => new Set(entries).size === entries.length)
49
+ .optional();
34
50
 
35
51
  const Id = z.string().uuid();
36
52
  const Secret = z.string().regex(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/);
@@ -67,21 +83,42 @@ export const AppLoginStartSchema = z
67
83
  export const AppLoginReferenceSchema = z.object({ requestId: Id, browserSecret: Secret }).strict();
68
84
  export const AppDeviceMutationSchema = z.discriminatedUnion("operation", [
69
85
  z.object({ operation: z.literal("rename"), deviceId: Id, name: z.string().trim().min(1).max(80) }).strict(),
70
- z.object({ operation: z.literal("revoke"), deviceId: Id }).strict(),
86
+ /**
87
+ * Asks the account's paired devices to approve removing this device. `endSessions` (default `true`) also ends
88
+ * the sign-ins this device approved, except the requesting session.
89
+ */
90
+ z.object({ operation: z.literal("revoke"), deviceId: Id, endSessions: z.boolean().optional() }).strict(),
71
91
  ]);
72
92
  export const AppDeviceCommandSchema = z.discriminatedUnion("operation", [
73
- z.object({ operation: z.literal("pending") }).strict(),
93
+ z.object({ operation: z.literal("pending"), accept: acceptList(APP_APPROVAL_PENDING_ACCEPT) }).strict(),
74
94
  z.object({ operation: z.literal("revoke") }).strict(),
75
95
  /** Reads the signing device's own account and device record, e.g. to show which account it signs in. */
76
96
  z.object({ operation: z.literal("account") }).strict(),
77
97
  /** Stores the authenticator's opaque push token for sign-in wake-ups on this device. */
78
98
  z.object({ operation: z.literal("push"), token: z.string().regex(/^[A-Za-z0-9_-]{43}$/) }).strict(),
79
99
  /** 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(),
100
+ z
101
+ .object({
102
+ operation: z.literal("inbox"),
103
+ /** Optional inbox parts this device shows; only send values the Cloud lists in `/info` `features`. */
104
+ accept: acceptList(APP_APPROVAL_ACCEPT),
105
+ })
106
+ .strict(),
81
107
  /** One open Action approval as the device shows it, with the digest the device recomputes before it decides. */
82
108
  z.object({ operation: z.literal("action"), actionId: Id }).strict(),
83
109
  /** Decides an Action approval. `digest` binds the decision to exactly the view the person saw. */
84
110
  z.object({ operation: z.literal("act"), actionId: Id, digest: Digest, decision: z.enum(["approve", "deny"]) }).strict(),
111
+ /** Decides a request to pair or remove a device of the signing device's account. Binds every fact the device showed. */
112
+ z
113
+ .object({
114
+ operation: z.literal("change"),
115
+ changeId: Id,
116
+ kind: z.enum(["pair", "revoke"]),
117
+ targetId: Id,
118
+ endSessions: z.boolean(),
119
+ decision: z.enum(["approve", "deny"]),
120
+ })
121
+ .strict(),
85
122
  z
86
123
  .object({
87
124
  operation: z.literal("decide"),
@@ -121,6 +158,8 @@ export const appDeviceProofMessage = (proof: AppDeviceProof): string => {
121
158
  c.operation,
122
159
  ...(c.operation === "decide" ? [c.requestId, c.challenge, c.comparison, c.decision] : []),
123
160
  ...(c.operation === "push" ? [c.token] : []),
161
+ ...((c.operation === "inbox" || c.operation === "pending") && c.accept !== undefined ? [c.accept] : []),
162
+ ...(c.operation === "change" ? [c.changeId, c.kind, c.targetId, c.endSessions, c.decision] : []),
124
163
  ...(c.operation === "action" ? [c.actionId] : []),
125
164
  ...(c.operation === "act" ? [c.actionId, c.digest, c.decision] : []),
126
165
  ]);
@@ -151,8 +190,79 @@ export const AppDeviceViewSchema = z
151
190
  assisted: z.boolean(),
152
191
  })
153
192
  .strict();
193
+ /** Plain text on one line: no control, format or bidirectional characters, so an app cannot fake Cloud's own frame. */
194
+ const PlainText = (max: number) =>
195
+ z
196
+ .string()
197
+ .refine((value) => Array.from(value).length <= max, `Must contain at most ${max} code points`)
198
+ .regex(/^[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}]*$/u);
199
+ export const AppTerminalLoginSchema = z
200
+ .object({
201
+ client: z
202
+ .object({
203
+ // Bound encoded bytes including escapes, so five complete consent rows fit the response budget.
204
+ id: PlainText(256)
205
+ .min(1)
206
+ .refine((value) => new TextEncoder().encode(JSON.stringify(value)).length <= 258),
207
+ name: PlainText(APP_ACTION_LIMITS.nameChars).min(1),
208
+ })
209
+ .strict(),
210
+ scopes: z
211
+ .array(z.string().regex(/^[a-z][a-z0-9_:.-]{0,39}$/))
212
+ .min(1)
213
+ .max(16),
214
+ lifetimeSeconds: z.number().int().positive(),
215
+ })
216
+ .strict();
217
+ export type AppTerminalLogin = z.infer<typeof AppTerminalLoginSchema>;
218
+ export const APP_LOGIN_CONTEXT_BROWSERS = ["firefox", "chrome", "safari", "edge", "samsung", "opera", "other"] as const;
219
+ export const APP_LOGIN_CONTEXT_OSES = ["windows", "macos", "mac-or-ipad", "ios", "android", "linux", "chromeos", "other"] as const;
220
+ const ContextName = PlainText(96)
221
+ .min(1)
222
+ .refine((value) => new TextEncoder().encode(value).length <= 96);
223
+ /** Core alone computes the level. The app displays it and never derives its own.
224
+ * Terminal logins never carry context. Opted-in browser logins carry it unless
225
+ * started before the upgrade or omitted to fit the response budget.
226
+ * History is null during introduction; sameNetwork is a positive, transient hint. */
227
+ export const AppLoginContextSchema = z
228
+ .object({
229
+ /** Computed by Core only; descriptive signals never change it. */
230
+ level: z.enum(["info", "hint", "warn"]),
231
+ browser: z.enum(APP_LOGIN_CONTEXT_BROWSERS),
232
+ os: z.enum(APP_LOGIN_CONTEXT_OSES),
233
+ country: z
234
+ .string()
235
+ .regex(/^[A-Z]{2}$/)
236
+ .nullable(),
237
+ city: ContextName.nullable(),
238
+ /** Operator name, never a VPN or data-center classification. */
239
+ network: ContextName.nullable(),
240
+ history: z
241
+ .object({
242
+ /** This browser's first_seen_at for the account, or null when unknown. */
243
+ knownSince: DateTime.nullable(),
244
+ /** A request country, geoip_since set, and no sign-in from it inside the 90-day window. */
245
+ newCountry: z.boolean(),
246
+ /** geoip_since only when less than 90 days ago; the app can say "since {date}". */
247
+ countriesSince: DateTime.nullable(),
248
+ })
249
+ .strict()
250
+ .nullable(),
251
+ sameNetwork: z.literal(true).optional(),
252
+ })
253
+ .strict();
254
+ export type AppLoginContext = z.infer<typeof AppLoginContextSchema>;
255
+
154
256
  export const AppPendingLoginSchema = z
155
- .object({ requestId: Id, challenge: Secret, comparison: Comparison, createdAt: DateTime, expiresAt: DateTime })
257
+ .object({
258
+ requestId: Id,
259
+ challenge: Secret,
260
+ comparison: Comparison,
261
+ createdAt: DateTime,
262
+ expiresAt: DateTime,
263
+ terminal: AppTerminalLoginSchema.optional(),
264
+ context: AppLoginContextSchema.optional(),
265
+ })
156
266
  .strict();
157
267
  export const AppPairingPayloadSchema = z
158
268
  .object({ protocol: z.literal(APP_APPROVAL_PROTOCOL), issuer: z.string().url(), pairingId: Id, secret: Secret, expiresAt: DateTime })
@@ -205,8 +315,29 @@ export const AppPairingClaimResultSchema = z.object({ deviceId: Id, comparison:
205
315
  export const AppPairingResultSchema = z
206
316
  .object({ state: z.enum(["pending", "claimed", "confirmed", "cancelled"]), deviceId: Id.nullable(), comparison: Comparison.nullable() })
207
317
  .strict();
318
+ /** Approval of a pairing by one of the account's paired devices, as the initiating browser follows it. */
319
+ export const AppDeviceChangeStatusSchema = z
320
+ .object({ state: z.enum(["pending", "approved", "denied", "cancelled", "expired"]), expiresAt: DateTime })
321
+ .strict();
322
+ export type AppDeviceChangeStatus = z.infer<typeof AppDeviceChangeStatusSchema>;
208
323
  /** Initiating Cloud session only; never returned to an unpaired device. */
209
- export const AppPairingInspectionSchema = AppPairingResultSchema.extend({ name: z.string().nullable(), userId: Id });
324
+ export const AppPairingInspectionSchema = AppPairingResultSchema.extend({
325
+ name: z.string().nullable(),
326
+ userId: Id,
327
+ /** Set once the code was confirmed while the account already had a paired device: one of them approves. */
328
+ approval: AppDeviceChangeStatusSchema.nullable(),
329
+ });
330
+ /** `approval` is set when one of the account's paired devices must approve before the device is paired. */
331
+ export const AppPairingConfirmResultSchema = z.object({ deviceId: Id, approval: AppDeviceChangeStatusSchema.nullable() }).strict();
332
+ export const AppDeviceChangeReferenceSchema = z.object({ changeId: Id }).strict();
333
+ /** A removal by its id, or this session's newest removal of a device, e.g. after the answer that named it was lost. */
334
+ export const AppDeviceChangeLookupSchema = z.union([AppDeviceChangeReferenceSchema, z.object({ deviceId: Id }).strict()]);
335
+ export const AppDeviceRemovalStatusSchema = AppDeviceChangeStatusSchema.extend({ changeId: Id, endSessions: z.boolean() }).strict();
336
+ /** A device that is already revoked answers `revoked`; an active one waits for approval on a paired device. */
337
+ export const AppDeviceRevokeResultSchema = z.union([
338
+ z.object({ state: z.literal("revoked") }).strict(),
339
+ z.object({ state: z.literal("pending"), changeId: Id, expiresAt: DateTime }).strict(),
340
+ ]);
210
341
  /** Answer to the `account` command: only the signing device's own account, never secrets or other users. */
211
342
  export const AppDeviceAccountSchema = z
212
343
  .object({
@@ -215,12 +346,6 @@ export const AppDeviceAccountSchema = z
215
346
  })
216
347
  .strict();
217
348
  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
349
  const ActionApp = z
225
350
  .object({ id: PlainText(APP_ACTION_LIMITS.nameChars).min(1), name: PlainText(APP_ACTION_LIMITS.nameChars).min(1) })
226
351
  .strict();
@@ -276,6 +401,27 @@ export const AppDeviceInboxSchema = z
276
401
  })
277
402
  .strict();
278
403
  export type AppDeviceInbox = z.infer<typeof AppDeviceInboxSchema>;
404
+ /** A request to pair or remove a device of this account, shown to the account's paired devices. */
405
+ export const AppDeviceChangeSchema = z
406
+ .object({
407
+ changeId: Id,
408
+ kind: z.enum(["pair", "revoke"]),
409
+ /** The device to pair or remove, with the name the account gave it. The name may be empty. */
410
+ device: z.object({ id: Id, name: PlainText(80) }).strict(),
411
+ /** Removal only: also end the sign-ins this device approved. Always `false` for a pairing. */
412
+ endSessions: z.boolean(),
413
+ createdAt: DateTime,
414
+ expiresAt: DateTime,
415
+ })
416
+ .strict();
417
+ export type AppDeviceChange = z.infer<typeof AppDeviceChangeSchema>;
418
+ /** The inbox for a device that sent `accept: ["device-changes"]`. Oldest first. */
419
+ export const AppDeviceInboxWithChangesSchema = AppDeviceInboxSchema.extend({
420
+ changes: z.array(AppDeviceChangeSchema).max(APP_APPROVAL_LIMITS.pendingPerAccount),
421
+ /** Pending device changes omitted to fit the response budget. */
422
+ moreChanges: z.number().int().nonnegative().max(APP_APPROVAL_LIMITS.pendingPerAccount),
423
+ }).strict();
424
+ export type AppDeviceInboxWithChanges = z.infer<typeof AppDeviceInboxWithChangesSchema>;
279
425
 
280
426
  /** UTF-8 of this exact JSON array is hashed into an Action's `digest`. Every field the device shows is part of it. */
281
427
  export const appActionDigestMessage = (issuer: string, view: Omit<AppActionView, "digest">): string =>
@@ -316,6 +462,7 @@ export const AppDeviceResponseSchema = z.union([
316
462
  z.object({ state: z.enum(["approved", "denied", "revoked", "updated"]) }).strict(),
317
463
  AppDeviceAccountSchema,
318
464
  AppDeviceInboxSchema,
465
+ AppDeviceInboxWithChangesSchema,
319
466
  AppActionViewSchema,
320
467
  ]);
321
468
  export const AppLoginStartResultSchema = z
@@ -58,6 +58,7 @@ export type AppAdminNavigationGroup = {
58
58
 
59
59
  export type AppPresentationTranslation = {
60
60
  name?: string;
61
+ panelLabel?: string;
61
62
  description?: string;
62
63
  /** Group labels keyed by `adminNav[].id`. */
63
64
  adminGroups?: Readonly<Record<string, string>>;
@@ -107,6 +108,18 @@ export type AppPwaPart = {
107
108
  requiresRoles?: Role[];
108
109
  };
109
110
 
111
+ /** One same-origin panel page contributed by an application. Routes and services still authorize. */
112
+ export type AppPanel = {
113
+ /** GET page below the app's routes, rendered with PanelLayout. */
114
+ href: string;
115
+ /** Panel name in the app's base locale; translated with presentation.translations[locale].panelLabel. */
116
+ label: string;
117
+ /** Icon class, for example "ti ti-sparkles". */
118
+ icon: string;
119
+ /** Coarse visibility like nav.requiresRoles; the slot grants no access. */
120
+ requiresRoles?: Role[];
121
+ };
122
+
110
123
  export type AppMeta = {
111
124
  platformPermissions?: readonly PlatformPermission[];
112
125
  id: string;
@@ -142,6 +155,7 @@ export type AppMeta = {
142
155
  searchLinks?: readonly AppSearchLink[];
143
156
  /** The app's pages in the installable mobile app (preview), at `href` = `/pwa/<id>`. */
144
157
  pwa?: AppPwaPart;
158
+ panel?: AppPanel;
145
159
  /**
146
160
  * Dashboard widget endpoints this app exposes. Each entry references an
147
161
  * HTTP endpoint that returns a `WidgetResponse` (see `contracts/widgets.ts`).
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { APP_APPROVAL_LIMITS } from "./app-approval";
3
- import { CapabilityActionReviewSchema, CapabilityAppIdSchema } from "./capabilities";
3
+ import { CapabilityActionReviewDataSchema, CapabilityAppIdSchema } from "./capabilities";
4
4
 
5
5
  export { APPROVAL_COUNTS_PATH, APPROVALS_API_PATH, APPROVALS_PAGE_PATH } from "./approval-paths";
6
6
 
@@ -42,7 +42,7 @@ export const PendingApprovalSchema = z
42
42
  /** What the person reads: the first line names the app and the Action, later lines add `Label: value` details. */
43
43
  sentence: z.string(),
44
44
  /** The owning app's review as the person saw it when Cloud asked. */
45
- review: CapabilityActionReviewSchema.nullable(),
45
+ review: CapabilityActionReviewDataSchema.nullable(),
46
46
  /** Where the request waits. The title is chat content, written by a person or a model, never Cloud's own words. */
47
47
  requester: z.object({ kind: z.literal("chat"), appId: z.string(), title: z.string().nullable(), href: z.string() }).strict(),
48
48
  /** Ways the person may remember this approval, the same the chat card offers. */