@k2b/cloud 0.39.0 → 0.41.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 (237) hide show
  1. package/README.md +8 -0
  2. package/package.json +11 -4
  3. package/src/_internal/app-favicon.ts +4 -2
  4. package/src/_internal/capabilities.ts +72 -48
  5. package/src/_internal/capability-reviews.ts +81 -0
  6. package/src/_internal/capability-schemas.ts +29 -0
  7. package/src/_internal/capability-sentences.ts +217 -1
  8. package/src/_internal/capability-streams.ts +7 -0
  9. package/src/_internal/define-app.ts +49 -18
  10. package/src/_internal/font-preloads.ts +2 -1
  11. package/src/_internal/heartbeat.ts +203 -14
  12. package/src/_internal/registry.ts +28 -1
  13. package/src/_internal/runtime-watcher.ts +6 -0
  14. package/src/_internal/setup-lock.ts +103 -0
  15. package/src/_internal/static-assets.ts +52 -3
  16. package/src/_internal/status-preserving-ssr.ts +2 -0
  17. package/src/access/PermissionEditor.tsx +2 -2
  18. package/src/account/EntitySearch.tsx +1 -1
  19. package/src/ai/action-review.ts +101 -0
  20. package/src/ai/background-review.ts +72 -0
  21. package/src/ai/capabilities.ts +29 -11
  22. package/src/ai/capability-execution.ts +76 -43
  23. package/src/ai/capability-result.ts +95 -0
  24. package/src/ai/chat/action-card.ts +464 -0
  25. package/src/ai/chat/action-messages.ts +129 -0
  26. package/src/ai/chat/action-question.browser-harness.tsx +245 -0
  27. package/src/ai/chat/action-question.fixtures.ts +733 -0
  28. package/src/ai/chat/action-question.tsx +680 -0
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/blocks.tsx +19 -25
  31. package/src/ai/chat/message-actions.tsx +6 -0
  32. package/src/ai/chat/tool-action-words.ts +31 -0
  33. package/src/ai/chat/tool-action.tsx +456 -0
  34. package/src/ai/chat/turn-view.tsx +10 -160
  35. package/src/ai/chat/view-block.tsx +15 -5
  36. package/src/ai/chat-task-contracts.ts +4 -1
  37. package/src/ai/chat-task-reasons-migrate.ts +7 -0
  38. package/src/ai/chat-task-reasons.ts +1 -1
  39. package/src/ai/chat-tasks.ts +55 -25
  40. package/src/ai/client/controller.ts +59 -0
  41. package/src/ai/default-tools.ts +9 -1
  42. package/src/ai/executor.ts +198 -39
  43. package/src/ai/firecrawl-tools.ts +15 -2
  44. package/src/ai/index.ts +3 -0
  45. package/src/ai/migrate.ts +26 -2
  46. package/src/ai/open-questions.ts +172 -0
  47. package/src/ai/projects-routes.ts +3 -0
  48. package/src/ai/protocol.ts +14 -2
  49. package/src/ai/question-text.ts +8 -0
  50. package/src/ai/recent-resources.ts +240 -0
  51. package/src/ai/recent-sent-refs.ts +38 -0
  52. package/src/ai/routes.ts +195 -5
  53. package/src/ai/runtime.ts +1 -0
  54. package/src/ai/skills-routes.ts +3 -0
  55. package/src/ai/skills.ts +10 -1
  56. package/src/ai/store.ts +245 -24
  57. package/src/ai/testing.ts +36 -0
  58. package/src/ai/tool-audit.ts +8 -0
  59. package/src/ai/tool-sentences.ts +152 -34
  60. package/src/ai/tool-wording.ts +36 -8
  61. package/src/ai/tools.ts +8 -3
  62. package/src/ai/types.ts +43 -4
  63. package/src/api/accounts-entities.ts +2 -1
  64. package/src/api/admin-ai-projects.ts +2 -0
  65. package/src/api/admin-ai-skills.ts +2 -0
  66. package/src/api/admin-ai-usage.ts +2 -0
  67. package/src/api/admin-geoip.ts +80 -0
  68. package/src/api/admin-identity.ts +3 -0
  69. package/src/api/admin-lifecycle.ts +3 -0
  70. package/src/api/admin-outgoing-mail.ts +2 -0
  71. package/src/api/app-approval-actions.ts +31 -7
  72. package/src/api/app-approval.ts +46 -5
  73. package/src/api/approvals-loader.ts +23 -1
  74. package/src/api/approvals.ts +2 -0
  75. package/src/api/auth/schemas.ts +3 -0
  76. package/src/api/auth.ts +67 -12
  77. package/src/api/capabilities.ts +44 -11
  78. package/src/api/capability-streams.ts +12 -0
  79. package/src/api/help.ts +4 -28
  80. package/src/api/index.ts +6 -1
  81. package/src/api/lasting-access-messages.ts +17 -0
  82. package/src/api/mcp.ts +30 -3
  83. package/src/api/me-panel-consent.ts +71 -0
  84. package/src/api/me.ts +159 -36
  85. package/src/api/search.ts +3 -1
  86. package/src/api/sync-ops.ts +2 -0
  87. package/src/api/widgets.ts +3 -0
  88. package/src/approvals/Approvals.tsx +166 -262
  89. package/src/approvals/approval-card.ts +68 -0
  90. package/src/approvals/messages.ts +12 -24
  91. package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
  92. package/src/browser/app-approval.ts +54 -5
  93. package/src/browser/commands.ts +22 -6
  94. package/src/browser/composer-catalog.ts +45 -3
  95. package/src/browser/composer-insert.ts +10 -0
  96. package/src/browser/composer-open-messages.ts +17 -0
  97. package/src/browser/composer-open.ts +110 -0
  98. package/src/browser/discoverable-command.ts +6 -0
  99. package/src/browser/hints.ts +6 -0
  100. package/src/browser/live-connection.ts +26 -2
  101. package/src/browser/navigation-apps.ts +7 -0
  102. package/src/browser/panel-context-bridge.ts +30 -0
  103. package/src/browser/panel-context-receiver.ts +53 -0
  104. package/src/browser/panel-menu.tsx +56 -0
  105. package/src/browser/panel-messages.ts +11 -0
  106. package/src/browser/panel-shortcuts.ts +31 -0
  107. package/src/browser/panel-slot.ts +412 -10
  108. package/src/browser/panel.ts +37 -0
  109. package/src/browser/search-commands.ts +6 -3
  110. package/src/capabilities/client.ts +8 -2
  111. package/src/capabilities/reviews.ts +205 -0
  112. package/src/capabilities/server.ts +10 -3
  113. package/src/capabilities/testing.ts +47 -0
  114. package/src/capabilities/types.ts +2 -2
  115. package/src/cli/account.ts +13 -3
  116. package/src/cli/admin/geoip.ts +115 -0
  117. package/src/cli/admin/index.ts +3 -0
  118. package/src/contracts/app-approval.ts +123 -16
  119. package/src/contracts/approvals.ts +2 -2
  120. package/src/contracts/capabilities.ts +410 -18
  121. package/src/contracts/geoip.ts +42 -0
  122. package/src/contracts/index.ts +3 -0
  123. package/src/contracts/panel-context-plain.ts +99 -0
  124. package/src/contracts/panel-context.ts +93 -0
  125. package/src/contracts/seen-hints.ts +35 -0
  126. package/src/contracts/shared.ts +3 -0
  127. package/src/server/client-ip.ts +5 -0
  128. package/src/server/help-payload.ts +38 -0
  129. package/src/server/middleware/auth.ts +13 -1
  130. package/src/server/middleware/middleware.ts +9 -0
  131. package/src/server/middleware/no-transform.ts +19 -0
  132. package/src/server/middleware/rate-limit.ts +4 -6
  133. package/src/server/request-context/address.ts +45 -0
  134. package/src/server/request-context/geoip.ts +380 -0
  135. package/src/server/request-context/index.ts +33 -0
  136. package/src/server/request-context/user-agent.ts +95 -0
  137. package/src/services/accounts/app.ts +23 -3
  138. package/src/services/accounts/users.ts +12 -1
  139. package/src/services/app-approval.ts +382 -59
  140. package/src/services/approvals/device.ts +49 -31
  141. package/src/services/approvals/index.ts +7 -5
  142. package/src/services/approvals/list.ts +27 -6
  143. package/src/services/approvals/question.ts +68 -0
  144. package/src/services/audit/index.ts +1 -0
  145. package/src/services/auth-flows/ipa.ts +39 -2
  146. package/src/services/auth-flows/notification-sender.ts +3 -0
  147. package/src/services/auth-flows/password-reset.ts +32 -6
  148. package/src/services/geoip/index.ts +164 -0
  149. package/src/services/geoip/store.ts +30 -0
  150. package/src/services/geoip/upload.ts +150 -0
  151. package/src/services/identity/routes.ts +5 -1
  152. package/src/services/index.ts +13 -0
  153. package/src/services/known-browsers.ts +312 -0
  154. package/src/services/login-address-store.ts +51 -0
  155. package/src/services/login-context.ts +118 -0
  156. package/src/services/mandates/index.ts +3 -0
  157. package/src/services/mandates/policy.ts +15 -3
  158. package/src/services/notifications/email-frame.ts +1 -1
  159. package/src/services/oauth-tokens.ts +16 -2
  160. package/src/services/panel-context-consent.ts +58 -0
  161. package/src/services/pdf/markdown.ts +160 -2
  162. package/src/services/service-account-credentials.ts +21 -0
  163. package/src/services/session/index.ts +296 -41
  164. package/src/services/session/recent.ts +13 -0
  165. package/src/services/webauthn.ts +12 -0
  166. package/src/shared/email-html.ts +2 -0
  167. package/src/shared/help.ts +3 -0
  168. package/src/shared-text/cursor.ts +22 -0
  169. package/src/shared-text/documents.ts +142 -0
  170. package/src/shared-text/events.ts +43 -0
  171. package/src/shared-text/index.ts +12 -0
  172. package/src/shared-text/log.ts +48 -0
  173. package/src/shared-text/text-diff.ts +23 -0
  174. package/src/shared-text/types.ts +41 -0
  175. package/src/ssr/AdminLayout.tsx +2 -0
  176. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  177. package/src/ssr/CloudShell.island.tsx +10 -2
  178. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  179. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  180. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  181. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  182. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  183. package/src/ssr/HelpPanelView.island.tsx +27 -0
  184. package/src/ssr/Layout.tsx +25 -2
  185. package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
  186. package/src/ssr/LayoutHelp.tsx +222 -419
  187. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
  188. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  189. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  190. package/src/ssr/LayoutHelpTrigger.tsx +27 -2
  191. package/src/ssr/MobileNavigation.tsx +2 -2
  192. package/src/ssr/PanelLayout.tsx +9 -1
  193. package/src/ssr/PanelRuntime.island.tsx +7 -0
  194. package/src/ssr/PanelSlot.tsx +53 -3
  195. package/src/ssr/ShellRuntime.island.tsx +11 -2
  196. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  197. package/src/ssr/app-color.ts +14 -9
  198. package/src/ssr/app-navigation.ts +16 -3
  199. package/src/ssr/compact-shell.browser-harness.ts +30 -22
  200. package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
  201. package/src/ssr/help-messages.ts +4 -0
  202. package/src/ssr/help.ts +21 -0
  203. package/src/ssr/hints.ts +25 -0
  204. package/src/ssr/index.ts +3 -0
  205. package/src/ssr/layout-context.ts +2 -0
  206. package/src/ssr/layout-help-article.server.ts +50 -0
  207. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  208. package/src/ssr/layout-help-panel-document.ts +67 -0
  209. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  210. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  211. package/src/ssr/layout-help-panel-modules.ts +7 -0
  212. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  213. package/src/ssr/layout-help-panel-search.tsx +69 -0
  214. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  215. package/src/ssr/layout-help-panel-state.ts +141 -0
  216. package/src/ssr/layout-help-panel-types.ts +19 -0
  217. package/src/ssr/layout-help-start.ts +33 -7
  218. package/src/ssr/layout-help-views.tsx +484 -0
  219. package/src/ssr/panel-consent.ts +29 -0
  220. package/src/ssr/panel-context.ts +10 -0
  221. package/src/ssr/panel-idle.ts +9 -0
  222. package/src/ssr/panel-slot.browser-harness.tsx +53 -4
  223. package/src/ssr/platform-messages.ts +18 -0
  224. package/src/ssr/shell-data.tsx +2 -1
  225. package/src/styles/approvals.css +6 -0
  226. package/src/styles/effects.css +213 -9
  227. package/src/styles/file-chooser.css +2 -1
  228. package/src/styles/global.css +49 -12
  229. package/src/styles/panel.css +47 -4
  230. package/src/styles/resource-search.css +6 -2
  231. package/src/styles/tokens.css +17 -11
  232. package/src/styles/utilities-buttons.css +3 -2
  233. package/src/styles/utilities-data.css +2 -1
  234. package/src/styles/utilities-feedback.css +2 -2
  235. package/src/styles/utilities-layout.css +3 -13
  236. package/src/styles/utilities-markdown-editor.css +6 -6
  237. package/src/styles/utilities-navigation.css +17 -22
@@ -54,6 +54,9 @@ export const AuthResponseSchema = z.object({
54
54
  user: UserSchema,
55
55
  });
56
56
 
57
+ /** Password-change sign-in result; retry ending other sign-ins when signOutOthersFailed is true. */
58
+ export const PasswordChangeAuthResponseSchema = AuthResponseSchema.extend({ signOutOthersFailed: z.boolean() });
59
+
57
60
  export const VerifyPasskeyAuthenticationSchema = z.object({
58
61
  response: z.unknown(),
59
62
  acceptedAgb: z.literal(true).optional(),
package/src/api/auth.ts CHANGED
@@ -9,6 +9,7 @@ import { accounts, audit, authFlows, getFreeIpaConfig, logger, webauthn } from "
9
9
  import { isAccountCategoryAllowed } from "../services/account-category-policy";
10
10
  import type { AuthNotificationSender } from "../services/auth-flows/notification-sender";
11
11
  import { legalConsent } from "../services/legal-consent";
12
+ import { SessionEpochChangedError } from "../services/session";
12
13
  import * as categorySettings from "../services/settings";
13
14
  import { publicCloudOrigin } from "../shared/app-url";
14
15
 
@@ -20,6 +21,7 @@ import {
20
21
  AuthResponseSchema,
21
22
  EmailLoginSchema,
22
23
  LoginSchema,
24
+ PasswordChangeAuthResponseSchema,
23
25
  PasswordResetCompleteSchema,
24
26
  PasswordResetRequestSchema,
25
27
  VerifyPasskeyAuthenticationSchema,
@@ -52,6 +54,7 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
52
54
  responses: {
53
55
  200: jsonResponse(AuthResponseSchema, "Login successful"),
54
56
  401: jsonResponse(ErrorResponseSchema, "Invalid username or password"),
57
+ 409: jsonResponse(ErrorResponseSchema, "Signed out everywhere while signing in; sign in again"),
55
58
  503: jsonResponse(ErrorResponseSchema, "FreeIPA unavailable"),
56
59
  },
57
60
  }),
@@ -76,7 +79,18 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
76
79
  }
77
80
 
78
81
  // Issue through the configured migration gate (legacy opaque or JWT family).
79
- const sessionToken = await auth.session.create(c, loginResult.userId);
82
+ let sessionToken: string;
83
+ try {
84
+ sessionToken = await auth.session.create(c, loginResult.userId, {
85
+ method: "password",
86
+ notify: notificationSender.sendNewBrowserSignIn,
87
+ authEpoch: loginResult.authEpoch,
88
+ });
89
+ } catch (error) {
90
+ if (error instanceof SessionEpochChangedError)
91
+ return c.json({ message: "You were signed out everywhere while signing in. Sign in again." }, 409);
92
+ throw error;
93
+ }
80
94
 
81
95
  log.info("Login successful", { uid: loginResult.user.uid });
82
96
  return c.json({
@@ -119,7 +133,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
119
133
  return c.json({ message: result.error.message, code: result.error.code }, status as 400 | 401 | 403 | 404 | 409 | 500);
120
134
  }
121
135
 
122
- const sessionToken = await auth.session.create(c, result.data.user.id);
136
+ const sessionToken = await auth.session.create(c, result.data.user.id, {
137
+ method: "passkey",
138
+ notify: notificationSender.sendNewBrowserSignIn,
139
+ });
123
140
  log.info("Passkey login successful", { uid: result.data.user.uid });
124
141
  return c.json({ session_token: sessionToken, user: result.data.user });
125
142
  },
@@ -147,10 +164,11 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
147
164
  tags: ["Auth"],
148
165
  summary: "Change expired password",
149
166
  description:
150
- "Change an expired or temporary password using FreeIPA's change_password endpoint. No active session required. For regular password changes of a logged-in user, use POST /api/me/password instead.",
167
+ "Change an expired or temporary password using FreeIPA's change_password endpoint. Ends the account's existing sign-ins before creating the new browser session. When signOutOthersFailed is true, the new browser session is issued and the client retries POST /api/me/sign-ins/sign-out-others. No active session required. For regular password changes of a logged-in user, use POST /api/me/password instead.",
151
168
  responses: {
152
- 200: jsonResponse(AuthResponseSchema, "Password changed and logged in"),
169
+ 200: jsonResponse(PasswordChangeAuthResponseSchema, "Password changed and logged in"),
153
170
  400: jsonResponse(ErrorResponseSchema, "Failed to change password"),
171
+ 409: jsonResponse(ErrorResponseSchema, "Password changed, but signed out everywhere while signing in; sign in again"),
154
172
  503: jsonResponse(ErrorResponseSchema, "FreeIPA unavailable"),
155
173
  },
156
174
  }),
@@ -177,10 +195,24 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
177
195
  return jsonError(c, changeResult.message, changeResult.status === 401 ? 401 : 400);
178
196
  }
179
197
 
180
- const sessionToken = await auth.session.create(c, changeResult.userId);
198
+ let sessionToken: string;
199
+ try {
200
+ sessionToken = await auth.session.create(c, changeResult.userId, {
201
+ method: "expired_password",
202
+ notify: notificationSender.sendNewBrowserSignIn,
203
+ authEpoch: changeResult.authEpoch,
204
+ });
205
+ } catch (error) {
206
+ if (error instanceof SessionEpochChangedError)
207
+ return c.json(
208
+ { message: "Your password was changed, but you were signed out everywhere while signing in. Sign in again." },
209
+ 409,
210
+ );
211
+ throw error;
212
+ }
181
213
 
182
214
  log.info("Password changed via expired flow", { uid: username });
183
- return c.json({ session_token: sessionToken, user: changeResult.user });
215
+ return c.json({ session_token: sessionToken, user: changeResult.user, signOutOthersFailed: changeResult.signOutOthersFailed });
184
216
  },
185
217
  )
186
218
  .post(
@@ -228,7 +260,11 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
228
260
  }
229
261
 
230
262
  // Create session (no IPA session for email-only users)
231
- const sessionToken = await auth.session.create(c, verifyResult.userId);
263
+ const sessionToken = await auth.session.create(c, verifyResult.userId, {
264
+ method: "email_code",
265
+ // An account created by this sign-in has nobody else to warn.
266
+ notify: verifyResult.createdGuest ? null : notificationSender.sendNewBrowserSignIn,
267
+ });
232
268
 
233
269
  if (verifyResult.createdGuest) {
234
270
  log.info("Guest user created", {
@@ -265,11 +301,13 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
265
301
  tags: ["Auth"],
266
302
  summary: "Complete password reset",
267
303
  description:
268
- "Set a new password using a one-time reset token. Creates a normal Cloud session only after the password was changed successfully.",
304
+ "Use a one-time reset token to first sign the account out everywhere, including unpairing Cloud Login and mobile app phones, then set the password, sign out everywhere again and create the new browser session. If signing out everywhere fails before the change, the password stays unchanged; if it fails after the change, no session is issued. Both answer 500, and a new reset link is required. signOutOthersFailed is false whenever a session is issued.",
269
305
  responses: {
270
- 200: jsonResponse(AuthResponseSchema, "Password reset completed and session created"),
306
+ 200: jsonResponse(PasswordChangeAuthResponseSchema, "Password reset completed and session created"),
271
307
  400: jsonResponse(ErrorResponseSchema, "Password reset failed"),
272
308
  401: jsonResponse(ErrorResponseSchema, "Invalid or expired reset token"),
309
+ 409: jsonResponse(ErrorResponseSchema, "Password changed, but signed out everywhere while signing in; sign in again"),
310
+ 500: jsonResponse(ErrorResponseSchema, "Password reset failed"),
273
311
  },
274
312
  }),
275
313
  v("json", PasswordResetCompleteSchema),
@@ -288,9 +326,23 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
288
326
  return jsonError(c, result.message, status);
289
327
  }
290
328
 
291
- const sessionToken = await auth.session.create(c, result.userId);
329
+ let sessionToken: string;
330
+ try {
331
+ sessionToken = await auth.session.create(c, result.userId, {
332
+ method: "password_reset",
333
+ notify: notificationSender.sendNewBrowserSignIn,
334
+ authEpoch: result.authEpoch,
335
+ });
336
+ } catch (error) {
337
+ if (error instanceof SessionEpochChangedError)
338
+ return c.json(
339
+ { message: "Your password was changed, but you were signed out everywhere while signing in. Sign in again." },
340
+ 409,
341
+ );
342
+ throw error;
343
+ }
292
344
  log.info("Password reset completed", { uid: result.user.uid });
293
- return c.json({ user: result.user, session_token: sessionToken });
345
+ return c.json({ user: result.user, session_token: sessionToken, signOutOthersFailed: result.signOutOthersFailed });
294
346
  },
295
347
  )
296
348
  .post(
@@ -359,7 +411,10 @@ export const createAuthRoutes = (notificationSender: AuthNotificationSender) =>
359
411
  const user = await accounts.users.get({ uid: "admin" });
360
412
  if (!user) return jsonError(c, "Failed to resolve admin user.", 500);
361
413
 
362
- const sessionToken = await auth.session.create(c, user.id);
414
+ const sessionToken = await auth.session.create(c, user.id, {
415
+ method: "admin_token",
416
+ notify: notificationSender.sendNewBrowserSignIn,
417
+ });
363
418
  log.info("Admin login successful");
364
419
  return c.json({ session_token: sessionToken, user });
365
420
  },
@@ -3,6 +3,11 @@ import { describeRoute } from "hono-openapi";
3
3
  import { z } from "zod";
4
4
  import { readBoundedJson } from "../_internal/bounded-json";
5
5
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
6
+ import {
7
+ capabilityActionReceiptIssues,
8
+ capabilityActionReviewIssues,
9
+ capabilityQueryChecksActionOutcome,
10
+ } from "../_internal/capability-reviews";
6
11
  import { getCapability, listApps } from "../_internal/registry";
7
12
  import { resolveCommandLink } from "../capabilities/command-link";
8
13
  import {
@@ -25,12 +30,13 @@ import {
25
30
  CAPABILITY_MAX_RESULT_BYTES,
26
31
  CAPABILITY_ORIGIN_HEADER,
27
32
  CAPABILITY_PROTOCOL_VERSION,
28
- CapabilityActionReviewSchema,
33
+ CapabilityActionReviewDataSchema,
29
34
  type CapabilityCatalog,
30
35
  CapabilityCatalogSchema,
31
36
  CapabilityErrorSchema,
32
37
  CapabilityIdempotencyKeySchema,
33
38
  type CapabilityOrigin,
39
+ capabilityActionPolicy,
34
40
  capabilityResultJsonSchema,
35
41
  } from "../contracts/capabilities";
36
42
  import { CapabilityStreamSchema } from "../contracts/capability-streams";
@@ -48,6 +54,7 @@ import {
48
54
  requiresAuth,
49
55
  v,
50
56
  } from "../server";
57
+ import { noTransform } from "../server/middleware/no-transform";
51
58
  import { logger, settings } from "../services";
52
59
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
53
60
  import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
@@ -60,7 +67,7 @@ import { capabilityMessages } from "../shared/capability-messages";
60
67
  import { dispatchCapabilityStream, sealCapabilityStream } from "./capability-streams";
61
68
 
62
69
  const log = logger("capabilities");
63
- const QUERY_TIMEOUT_MS = 15_000;
70
+ export const QUERY_TIMEOUT_MS = 15_000;
64
71
  const ACTION_TIMEOUT_MS = 30_000;
65
72
  const DEFAULT_CATALOG_LIMIT = 10;
66
73
  const MAX_CATALOG_LIMIT = 25;
@@ -238,6 +245,8 @@ export type CapabilityDispatchParams = {
238
245
  authority?: RequestAuthority;
239
246
  mandate?: MandatedCapabilityAuthority;
240
247
  callingAppId?: string;
248
+ /** Internal observation only: called immediately before attempting the app request. */
249
+ onUpstreamAttempt?: () => void;
241
250
  dependencies?: CapabilityDispatchDependencies;
242
251
  };
243
252
 
@@ -381,6 +390,7 @@ const runCapabilityDispatch = async (
381
390
  actionApproval: mandate.actionApproval ?? "none",
382
391
  input: params.input,
383
392
  capabilityApproval: "approval" in operation ? (operation.approval ?? "always") : "always",
393
+ capabilityPolicy: capabilityActionPolicy(entry.manifest, params.capabilityId),
384
394
  requestId,
385
395
  },
386
396
  async (mandateAuthority) => {
@@ -547,6 +557,7 @@ const runCapabilityDispatch = async (
547
557
 
548
558
  let response: Response;
549
559
  try {
560
+ params.onUpstreamAttempt?.();
550
561
  response = await fetchUpstream(
551
562
  `${entry.endpoint}/${params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
552
563
  {
@@ -608,18 +619,39 @@ const runCapabilityDispatch = async (
608
619
  return settle(invalid, "uncertain");
609
620
  }
610
621
 
622
+ const reviewContext = {
623
+ policy: capabilityActionPolicy(entry.manifest, params.capabilityId),
624
+ approval: "approval" in operation ? operation.approval : undefined,
625
+ queries: entry.manifest.queries,
626
+ validateQueryInput: (query: (typeof entry.manifest.queries)[number], input: Record<string, unknown>) =>
627
+ schemaValidator(`${query.schemaHash}:input`, query.inputSchema)?.safeParse(input).success === true,
628
+ ...(entry.presentation ? { locales: [entry.presentation.baseLocale, ...Object.keys(entry.presentation.translations)] } : {}),
629
+ };
611
630
  const resultValidator = params.review
612
- ? CapabilityActionReviewSchema
631
+ ? CapabilityActionReviewDataSchema
613
632
  : schemaValidator(`${operation.schemaHash}:result`, capabilityResultJsonSchema(operation.dataSchema));
614
633
  const parsedResult = resultValidator?.safeParse(upstreamBody.data);
615
- const parsedReview = params.review ? CapabilityActionReviewSchema.safeParse(upstreamBody.data) : null;
616
- // A rememberable Action may leave out its scope for arguments that should ask every time; any other Action has none.
617
- const reviewApprovalScopeIsValid =
618
- !params.review ||
619
- !parsedReview?.success ||
620
- ("approval" in operation && operation.approval === "rememberable") ||
621
- parsedReview.data.approvalScope === undefined;
622
- if (!resultValidator || !parsedResult?.success || !reviewApprovalScopeIsValid) {
634
+ const field = (value: unknown, key: string): unknown =>
635
+ value !== null && typeof value === "object" ? Object.getOwnPropertyDescriptor(value, key)?.value : undefined;
636
+ const receipt = field(upstreamBody.data, "receipt");
637
+ const links = field(upstreamBody.data, "links");
638
+ const reviewIssues = params.review
639
+ ? capabilityActionReviewIssues(upstreamBody.data, reviewContext)
640
+ : receipt === undefined
641
+ ? []
642
+ : params.kind === "queries"
643
+ ? ["Queries cannot return a receipt"]
644
+ : capabilityActionReceiptIssues(receipt, links);
645
+ const nestedReceipt = field(field(upstreamBody.data, "data"), "receipt");
646
+ if (
647
+ !params.review &&
648
+ params.kind === "queries" &&
649
+ nestedReceipt !== undefined &&
650
+ !("idempotency" in operation) &&
651
+ capabilityQueryChecksActionOutcome(operation)
652
+ )
653
+ reviewIssues.push(...capabilityActionReceiptIssues(nestedReceipt, links));
654
+ if (!resultValidator || !parsedResult?.success || reviewIssues.length > 0) {
623
655
  if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
624
656
  const invalid = errorResponse("INVALID_APP_RESPONSE", messages.outsideResultSchema({ appId: params.appId }), 502);
625
657
  return settle(invalid, "uncertain");
@@ -741,6 +773,7 @@ export const createCapabilityRoutes = (dependencies: CapabilityRouteDependencies
741
773
  (c.req.param("kind") === "actions" ? requireWriteScope : requireReadScope)(c, next);
742
774
 
743
775
  return new Hono<AuthContext>()
776
+ .use("/capabilities/v1/*", noTransform())
744
777
  .use("/capabilities/v1/*", dependencies.authenticate ?? auth.requireRole("authenticated"))
745
778
  .use("/capabilities/v1/*", rejectReservedWorkloadCredential)
746
779
  .post(
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { readBoundedJson } from "../_internal/bounded-json";
3
+ import { capabilityActionReceiptIssues } from "../_internal/capability-reviews";
3
4
  import { getCapability } from "../_internal/registry";
4
5
  import { recordCapabilityExecution } from "../capabilities/executions";
5
6
  import { exactStream } from "../capabilities/stream-body";
@@ -245,6 +246,17 @@ export async function dispatchCapabilityStream(
245
246
  await record(502, "STREAM_FAILED");
246
247
  return denied(502, "STREAM_FAILED");
247
248
  }
249
+ // The same receipt rules as an ordinary Action result, so a stream completion cannot skip them.
250
+ if (
251
+ nested &&
252
+ typeof nested === "object" &&
253
+ "receipt" in nested &&
254
+ nested.receipt !== undefined &&
255
+ (grant.kind !== "actions" || capabilityActionReceiptIssues(nested.receipt, "links" in nested ? nested.links : undefined).length > 0)
256
+ ) {
257
+ await record(502, "STREAM_FAILED");
258
+ return denied(502, "STREAM_FAILED");
259
+ }
248
260
  }
249
261
  if (!response.ok) {
250
262
  const error = CapabilityErrorSchema.safeParse(receipt.data);
package/src/api/help.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { keyboardPlatform } from "@k2b/ui";
2
1
  import { Hono, type MiddlewareHandler } from "hono";
3
- import { helpAgentMarkdown } from "../_internal/help-catalog";
4
2
  import { type AuthContext, auth, getLocale } from "../server";
3
+ import { buildHelpDocumentPayload } from "../server/help-payload";
5
4
  import { createHelpReader, type HelpReaderFactory } from "../services/help";
6
- import { type HelpDocumentPayload, type HelpSearchPayload, parseHelpReference } from "../shared/help";
7
- import { renderHelpMarkdown } from "../shared/markdown";
5
+ import type { HelpSearchPayload } from "../shared/help";
6
+ import type { renderHelpMarkdown } from "../shared/markdown";
8
7
  import { helpApiMessages } from "./help-messages";
9
8
 
10
9
  export type HelpRouteDependencies = {
@@ -36,30 +35,7 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
36
35
  const { t } = helpApiMessages.resolve([locale]);
37
36
  const document = await reader(locale, c.get("actor")).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
38
37
  if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
39
- const links = (reference: { appId?: string; documentId: string }) =>
40
- document.links[`${reference.appId ?? document.appId}/${reference.documentId}`];
41
- const resolve = (reference: string) => {
42
- const ref = parseHelpReference(reference);
43
- return ref ? links(ref) : undefined;
44
- };
45
- const payload: HelpDocumentPayload = {
46
- locale: document.locale,
47
- id: document.documentId,
48
- title: document.title,
49
- markdown: helpAgentMarkdown(document),
50
- kind: document.kind,
51
- needs: document.needs,
52
- next: document.next ? resolve(document.next) : undefined,
53
- related: document.related.flatMap((reference) => {
54
- const target = resolve(reference);
55
- return target ? [target] : [];
56
- }),
57
- html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown, {
58
- locale: document.locale,
59
- platform: keyboardPlatform(c.req.header("User-Agent") ?? ""),
60
- links,
61
- }),
62
- };
38
+ const payload = buildHelpDocumentPayload(document, c.req.header("User-Agent") ?? "", dependencies.renderMarkdown);
63
39
  return c.json(payload);
64
40
  });
65
41
  };
package/src/api/index.ts CHANGED
@@ -26,6 +26,7 @@ import adminAiQuotaRoutes from "./admin-ai-quotas";
26
26
  import adminAiSkillsRoutes from "./admin-ai-skills";
27
27
  import adminAiUsageRoutes from "./admin-ai-usage";
28
28
  import adminCoreSettingsRoutes from "./admin-core-settings";
29
+ import adminGeoIpRoutes from "./admin-geoip";
29
30
  import adminIdentityRoutes from "./admin-identity";
30
31
  import adminLifecycleRoutes from "./admin-lifecycle";
31
32
  import adminLinuxIdentityRoutes from "./admin-linux-identities";
@@ -69,7 +70,10 @@ const buildCoreApi = (options: CoreApiOptions) => {
69
70
  const widgetRoutes = createWidgetRoutes();
70
71
  return new Hono()
71
72
  .use(prettyJSON())
72
- .route("/auth/app-approval/v1", createAppApprovalRoutes(undefined, undefined, options.terminalLogins))
73
+ .route(
74
+ "/auth/app-approval/v1",
75
+ createAppApprovalRoutes({ notify: options.notifications.sendNewBrowserSignIn }, undefined, undefined, options.terminalLogins),
76
+ )
73
77
  .route("/auth/pwa/v1", createPwaRoutes())
74
78
  .route("/auth", createAuthRoutes(options.notifications))
75
79
  .route("/me", meRoutes)
@@ -86,6 +90,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
86
90
  .route("/admin/core/settings", adminCoreSettingsRoutes)
87
91
  .route("/admin/core/rail", adminRailRoutes)
88
92
  .route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
93
+ .route("/admin/core/geoip", adminGeoIpRoutes)
89
94
  .route("/admin/identity", adminIdentityRoutes)
90
95
  .route("/admin/sync", createSyncOpsProxyRoutes())
91
96
  .route("/admin/lifecycle", adminLifecycleRoutes)
@@ -0,0 +1,17 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+
3
+ export const lastingAccessApiMessages = i18n.define({
4
+ baseLocale: "en",
5
+ messages: {
6
+ en: {
7
+ browserSignInRequired: "Create API keys and passkeys in the browser.",
8
+ reauthenticate: "Sign in again to confirm it is you.",
9
+ forbidden: "This account cannot create new access.",
10
+ },
11
+ de: {
12
+ browserSignInRequired: "API-Schlüssel und Passkeys legst du im Browser an.",
13
+ reauthenticate: "Melde dich erneut an, um zu bestätigen, dass du es bist.",
14
+ forbidden: "Dieses Konto kann keinen neuen Zugang anlegen.",
15
+ },
16
+ },
17
+ });
package/src/api/mcp.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import { Hono, type MiddlewareHandler } from "hono";
16
16
  import { z } from "zod";
17
17
  import { readBoundedJson } from "../_internal/bounded-json";
18
+ import { capabilitySentenceText } from "../_internal/capability-sentences";
18
19
  import {
19
20
  HELP_SEARCH_MAX_LIMIT,
20
21
  helpAgentMarkdown,
@@ -31,6 +32,7 @@ import {
31
32
  CAPABILITY_MAX_RESULT_BYTES,
32
33
  CAPABILITY_PROTOCOL_VERSION,
33
34
  type CapabilityActionManifest,
35
+ CapabilityActionReceiptSchema,
34
36
  type CapabilityQueryManifest,
35
37
  CapabilitySemanticLinkSchema,
36
38
  type CloudResourceRef,
@@ -42,11 +44,14 @@ import {
42
44
  } from "../contracts/capabilities";
43
45
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
44
46
  import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
47
+ import { preferredLocale } from "../server/locale";
48
+ import { noTransform } from "../server/middleware/no-transform";
45
49
  import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
46
50
  import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
47
51
  import { logger } from "../services/logging";
48
52
  import { get } from "../services/settings";
49
53
  import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
54
+ import { normalizeTimeZone } from "../shared/time";
50
55
  import { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
51
56
 
52
57
  const MCP_TOOL_PAGE_SIZE = 100;
@@ -66,6 +71,7 @@ type McpRouteDependencies = CapabilityDispatchDependencies & {
66
71
  listApps?: () => Promise<AppRegistryEntry[]>;
67
72
  help?: HelpReaderFactory;
68
73
  getOperatorLocale?: () => Promise<string | undefined>;
74
+ getOperatorTimeZone?: () => Promise<string | undefined>;
69
75
  getAppUrl?: () => Promise<string>;
70
76
  authenticate?: MiddlewareHandler<AuthContext>;
71
77
  limit?: MiddlewareHandler<AuthContext>;
@@ -681,14 +687,33 @@ const createMcpServer = (
681
687
  origin,
682
688
  );
683
689
  }
684
- const resultFor = (value: Record<string, unknown>): CallToolResult => {
690
+ const receipt = response.ok && selected.kind === "actions" ? CapabilityActionReceiptSchema.safeParse(body.receipt) : null;
691
+ let receiptText: string | undefined;
692
+ if (receipt?.success) {
693
+ // The receipt line is extra presentation: if Cloud cannot word it, the completed Action's result still goes out.
694
+ try {
695
+ const locale = preferredLocale(request.headers) ?? (await resolveHelpLocale());
696
+ const timeZone = normalizeTimeZone(
697
+ await (dependencies.getOperatorTimeZone ?? (() => get<string>("app.timezone")))().catch(() => "UTC"),
698
+ );
699
+ receiptText = capabilitySentenceText(receipt.data.text, receipt.data.slots ?? {}, { locale, timeZone });
700
+ } catch (error) {
701
+ log.warn("MCP receipt text unavailable", { error: error instanceof Error ? error.message : String(error) });
702
+ }
703
+ }
704
+ const resultFor = (value: Record<string, unknown>, withReceipt = true): CallToolResult => {
685
705
  const result = toolResult(value, !response.ok);
686
706
  if (origin) result.content.push(...semanticResourceLinks(value, origin));
707
+ if (withReceipt && receiptText) result.content.unshift({ type: "text", text: receiptText });
687
708
  return result;
688
709
  };
689
- // Added hrefs are optional: they never turn a result that fits into RESPONSE_TOO_LARGE.
710
+ // Both enrichments are optional: neither turns a result that fits into RESPONSE_TOO_LARGE.
690
711
  const result = resultFor(body);
691
- return ensureBoundedToolResult(fitsToolResult(result) || body === providerBody ? result : resultFor(providerBody));
712
+ if (fitsToolResult(result)) return result;
713
+ const plain = resultFor(body, false);
714
+ if (fitsToolResult(plain)) return plain;
715
+ const original = resultFor(providerBody);
716
+ return ensureBoundedToolResult(fitsToolResult(original) ? original : resultFor(providerBody, false));
692
717
  });
693
718
 
694
719
  return server;
@@ -750,6 +775,8 @@ export const createMcpProtectedResourceRoutes = (dependencies: Pick<McpRouteDepe
750
775
  /** One authenticated, stateless MCP endpoint projected from live capabilities. */
751
776
  export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
752
777
  new Hono<AuthContext>()
778
+ .use("/mcp/v1", noTransform())
779
+ .use("/mcp/v1/*", noTransform())
753
780
  .use("/mcp/v1", validateMcpOrigin(dependencies.getAppUrl ?? defaultAppUrl))
754
781
  .use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
755
782
  .use("/mcp/v1", rejectReservedWorkloadCredential)
@@ -0,0 +1,71 @@
1
+ import { Hono } from "hono";
2
+ import { bodyLimit } from "hono/body-limit";
3
+ import { describeRoute } from "hono-openapi";
4
+ import { z } from "zod";
5
+ import { CapabilityAppIdSchema } from "../contracts/capabilities";
6
+ import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
7
+ import { type AuthContext, jsonResponse, requiresAuth, v } from "../server";
8
+ import { PanelConsentCancelled, panelContextConsent } from "../services/panel-context-consent";
9
+ import { getRuntimeContext } from "../ssr/runtime";
10
+
11
+ const AnswerSchema = z.object({ allowed: z.boolean(), revision: z.number().int().min(0).max(2_147_483_647) }).strict();
12
+ const AnswersSchema = z.object({ answers: z.record(CapabilityAppIdSchema, AnswerSchema) }).strict();
13
+ const ProviderSchema = z.object({ provider: CapabilityAppIdSchema }).strict();
14
+ const SavedSchema = ProviderSchema.extend(AnswerSchema.shape).strict();
15
+
16
+ /** Mounted below /me's session and user guards. The live request registry owns provider membership. */
17
+ export const createMePanelConsentRoutes = (service = panelContextConsent) => {
18
+ const providers = (c: Parameters<typeof getRuntimeContext>[0]) =>
19
+ getRuntimeContext(c)
20
+ .apps.filter((app) => app.panel && panelProviderNeedsConsent(app.id))
21
+ .map((app) => app.id);
22
+ return new Hono<AuthContext>()
23
+ .use("*", async (c, next) => {
24
+ c.header("Cache-Control", "no-store");
25
+ if (!c.get("user")) return c.json({ message: "Authentication required" }, 401);
26
+ await next();
27
+ })
28
+ .get(
29
+ "/panel-consent",
30
+ describeRoute({
31
+ tags: ["Me"],
32
+ summary: "Read personal panel context answers",
33
+ ...requiresAuth,
34
+ responses: { 200: jsonResponse(AnswersSchema, "Answers for registered panel providers") },
35
+ }),
36
+ async (c) => c.json({ answers: await service.list(c.get("user").id, providers(c)) }),
37
+ )
38
+ .put(
39
+ "/panel-consent/:provider",
40
+ describeRoute({
41
+ tags: ["Me"],
42
+ summary: "Save a personal panel context answer",
43
+ ...requiresAuth,
44
+ responses: {
45
+ 200: jsonResponse(SavedSchema, "Saved answer"),
46
+ 404: { description: "Panel provider not found" },
47
+ 409: { description: "Answer changed since it was read" },
48
+ },
49
+ }),
50
+ // The answer and revision fit within 256 bytes, including ordinary JSON whitespace.
51
+ bodyLimit({ maxSize: 256 }),
52
+ v("param", ProviderSchema),
53
+ v("json", AnswerSchema),
54
+ async (c) => {
55
+ const { provider } = c.req.valid("param");
56
+ if (!providers(c).includes(provider)) return c.json({ code: "PANEL_PROVIDER_NOT_FOUND", message: "Panel provider not found" }, 404);
57
+ const { allowed, revision } = c.req.valid("json");
58
+ let saved: Awaited<ReturnType<typeof service.set>>;
59
+ try {
60
+ saved = await service.set(c.get("user").id, provider, allowed, revision, c.req.raw.signal);
61
+ } catch (error) {
62
+ // The browser stopped waiting; the save was rolled back and nobody reads this answer.
63
+ if (error instanceof PanelConsentCancelled) return c.json({ code: "CANCELLED", message: "Request cancelled" }, 503);
64
+ throw error;
65
+ }
66
+ return saved
67
+ ? c.json({ provider, ...saved })
68
+ : c.json({ code: "CONFLICT", message: "Panel consent changed. Read the current answer before saving again." }, 409);
69
+ },
70
+ );
71
+ };