@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
package/src/api/me.ts CHANGED
@@ -4,8 +4,10 @@
4
4
  * third-party management lives in the accounts admin app.
5
5
  */
6
6
 
7
- import { err, fail, ok } from "@k2b/stdlib";
7
+ import { err, fail, ok, type Result } from "@k2b/stdlib";
8
+ import { sql } from "bun";
8
9
  import { type Context, Hono, type MiddlewareHandler } from "hono";
10
+ import { deleteCookie } from "hono/cookie";
9
11
  import { createMiddleware } from "hono/factory";
10
12
  import { describeRoute } from "hono-openapi";
11
13
  import { z } from "zod";
@@ -26,6 +28,7 @@ import {
26
28
  NotificationQuietSettingsSchema,
27
29
  RegisterBrowserNotificationEndpointSchema,
28
30
  ServiceAccountCredentialSchema,
31
+ SignOutOthersResponseSchema,
29
32
  UpdateAvatarResponseSchema,
30
33
  UpdateAvatarSchema,
31
34
  UpdateNotificationQuietSettingsSchema,
@@ -40,22 +43,29 @@ import {
40
43
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
41
44
  import { type AuthContext, auth, getLocale, getTimeZone, jsonResponse, rateLimit, requiresAuth, respond, v } from "../server";
42
45
  import { APP_SESSION_FORBIDDEN } from "../server/middleware/auth";
46
+ import { noTransform } from "../server/middleware/no-transform";
43
47
  import {
44
48
  accountLifecycle,
45
49
  accountsAppService as accountsService,
46
50
  audit,
47
51
  browserNotifications,
52
+ logger,
48
53
  mandates,
49
54
  notifications,
50
55
  serviceAccountCredentials,
51
56
  webauthn,
52
57
  } from "../services";
53
58
  import { MandatePolicyV1Schema } from "../services/mandates";
59
+ import { ConfirmedBrowserSignInError, requireConfirmedBrowserSignIn } from "../services/session/recent";
54
60
  import { coreSettings } from "../services/settings/api";
55
61
 
62
+ import { lastingAccessApiMessages } from "./lasting-access-messages";
63
+ import { createMePanelConsentRoutes } from "./me-panel-consent";
56
64
  import { createMeRailRoutes } from "./me-rail";
57
65
  import { createMeWebVitalsRoutes } from "./me-web-vitals";
58
66
 
67
+ const log = logger("api:me");
68
+
59
69
  const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
60
70
  userId: user.id,
61
71
  uid: user.uid,
@@ -67,19 +77,10 @@ const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
67
77
  const webOnly = (c: Context) => c.json(APP_SESSION_FORBIDDEN, 403);
68
78
 
69
79
  /**
70
- * Guards the endpoints that manage how the account is authenticated: passkeys,
71
- * API keys, the password, and account deletion.
72
- *
73
- * `auth.requireUser()` is not enough for these. A personal API key acts as
74
- * its user, so it passes that check — and could then enrol a passkey, which is
75
- * a full account takeover, or mint further keys that outlive the revocation of
76
- * the one that was leaked. No scope in the vocabulary means "may add an
77
- * authenticator", so a credential must not reach these endpoints at all.
78
- *
79
- * A credential may still *read* its account's passkeys and keys; only the
80
- * mutations are gated. Browser sessions and user-issued OAuth access tokens
81
- * both resolve to a `user` actor and are unaffected. The mobile app's session
82
- * is rejected too: whatever it created would outlive removing the phone.
80
+ * Guards passkey and key removal, password changes, and account deletion.
81
+ * Browser sessions and user OAuth actors may perform these actions; personal
82
+ * API keys and mobile app sessions may not. Adding keys and sign-in methods
83
+ * requires a confirmed browser sign-in through the separate creation gate.
83
84
  */
84
85
  const requireDirectUserActor = createMiddleware<AuthContext>(async (c, next) => {
85
86
  if (c.get("actor")?.kind !== "user") {
@@ -95,6 +96,27 @@ const requireDirectUserActor = createMiddleware<AuthContext>(async (c, next) =>
95
96
  return next();
96
97
  });
97
98
 
99
+ const requireBrowserSignIn = createMiddleware<AuthContext>(async (c, next) => {
100
+ if (!(await auth.browserSignIn(c))) {
101
+ return c.json(
102
+ { code: "BROWSER_SIGN_IN_REQUIRED", message: lastingAccessApiMessages.resolve([getLocale(c)]).t.browserSignInRequired },
103
+ 403,
104
+ );
105
+ }
106
+ return next();
107
+ });
108
+
109
+ const lastingAccessMessage = (c: Context<AuthContext>, code: "REAUTHENTICATE" | "FORBIDDEN") => {
110
+ const { t } = lastingAccessApiMessages.resolve([getLocale(c)]);
111
+ return code === "REAUTHENTICATE" ? t.reauthenticate : t.forbidden;
112
+ };
113
+
114
+ /** The creation services fail with these codes only when the browser sign-in does not confirm the write. */
115
+ const localizeLastingAccessResult = <T>(c: Context<AuthContext>, result: Result<T>): Result<T> =>
116
+ !result.ok && (result.error.code === "REAUTHENTICATE" || result.error.code === "FORBIDDEN")
117
+ ? fail({ ...result.error, message: lastingAccessMessage(c, result.error.code) })
118
+ : result;
119
+
98
120
  const ExtendAccountResponseSchema = z.object({
99
121
  message: z.string(),
100
122
  newExpiry: z.string().datetime().optional(),
@@ -194,11 +216,13 @@ const NotificationHistoryQuerySchema = z.object({
194
216
  });
195
217
  const mandateRoutes = createMeMandateRoutes();
196
218
  const app = new Hono<AuthContext>()
219
+ .use("/notifications/history/*", noTransform())
197
220
  .use(rateLimit())
198
221
  .use(auth.requireRole("authenticated"))
199
222
  .use(auth.requireUser())
200
223
  .route("/", mandateRoutes)
201
224
  .route("/", createMeRailRoutes())
225
+ .route("/", createMePanelConsentRoutes())
202
226
  .route("/", createMeWebVitalsRoutes())
203
227
 
204
228
  .get(
@@ -540,7 +564,7 @@ const app = new Hono<AuthContext>()
540
564
 
541
565
  .post(
542
566
  "/passkeys/registration/start",
543
- requireDirectUserActor,
567
+ requireBrowserSignIn,
544
568
  describeRoute({
545
569
  tags: ["Me"],
546
570
  summary: "Start passkey registration",
@@ -549,15 +573,28 @@ const app = new Hono<AuthContext>()
549
573
  responses: {
550
574
  200: jsonResponse(z.unknown(), "Passkey registration options"),
551
575
  401: jsonResponse(ErrorResponseSchema, "Authentication required"),
552
- 403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
576
+ 403: jsonResponse(ErrorResponseSchema, "Confirmed browser sign-in required (BROWSER_SIGN_IN_REQUIRED or REAUTHENTICATE)"),
553
577
  },
554
578
  }),
555
- async (c) => respond(c, webauthn.beginRegistration({ user: c.get("user") })),
579
+ async (c) => {
580
+ const signIn = await auth.browserSignIn(c);
581
+ if (!signIn) throw new Error("Browser sign-in gate was not applied");
582
+ try {
583
+ await requireConfirmedBrowserSignIn(sql, signIn, (code) => {
584
+ throw new ConfirmedBrowserSignInError(code);
585
+ });
586
+ } catch (error) {
587
+ if (error instanceof ConfirmedBrowserSignInError)
588
+ return c.json({ code: error.code, message: lastingAccessMessage(c, error.code) }, 403);
589
+ throw error;
590
+ }
591
+ return respond(c, webauthn.beginRegistration({ user: c.get("user") }));
592
+ },
556
593
  )
557
594
 
558
595
  .post(
559
596
  "/passkeys/registration/verify",
560
- requireDirectUserActor,
597
+ requireBrowserSignIn,
561
598
  describeRoute({
562
599
  tags: ["Me"],
563
600
  summary: "Verify passkey registration",
@@ -567,20 +604,26 @@ const app = new Hono<AuthContext>()
567
604
  201: jsonResponse(WebAuthnPasskeySchema, "Passkey created"),
568
605
  400: jsonResponse(ErrorResponseSchema, "Passkey registration failed"),
569
606
  401: jsonResponse(ErrorResponseSchema, "Authentication required"),
570
- 403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
607
+ 403: jsonResponse(ErrorResponseSchema, "Confirmed browser sign-in required (BROWSER_SIGN_IN_REQUIRED or REAUTHENTICATE)"),
571
608
  },
572
609
  }),
573
610
  v("json", CreateWebAuthnPasskeySchema),
574
611
  async (c) =>
575
612
  respond(
576
613
  c,
577
- () => {
614
+ async () => {
578
615
  const data = c.req.valid("json");
579
- return webauthn.finishRegistration({
580
- user: c.get("user"),
581
- name: data.name,
582
- response: data.response as never,
583
- });
616
+ const browserSignIn = await auth.browserSignIn(c);
617
+ if (!browserSignIn) throw new Error("Browser sign-in gate was not applied");
618
+ return localizeLastingAccessResult(
619
+ c,
620
+ await webauthn.finishRegistration({
621
+ browserSignIn,
622
+ user: c.get("user"),
623
+ name: data.name,
624
+ response: data.response as never,
625
+ }),
626
+ );
584
627
  },
585
628
  201,
586
629
  ),
@@ -611,7 +654,7 @@ const app = new Hono<AuthContext>()
611
654
 
612
655
  .post(
613
656
  "/api-keys",
614
- requireDirectUserActor,
657
+ requireBrowserSignIn,
615
658
  describeRoute({
616
659
  tags: ["Me"],
617
660
  summary: "Create current user API key",
@@ -622,7 +665,10 @@ const app = new Hono<AuthContext>()
622
665
  201: jsonResponse(CreateUserApiKeyResponseSchema, "API key created"),
623
666
  400: jsonResponse(ErrorResponseSchema, "Failed to create API key"),
624
667
  401: jsonResponse(ErrorResponseSchema, "Authentication required"),
625
- 403: jsonResponse(ErrorResponseSchema, "Credential not permitted or models-only API turned off"),
668
+ 403: jsonResponse(
669
+ ErrorResponseSchema,
670
+ "Confirmed browser sign-in required (BROWSER_SIGN_IN_REQUIRED or REAUTHENTICATE), or models-only API turned off",
671
+ ),
626
672
  },
627
673
  }),
628
674
  v("json", CreateUserApiKeySchema),
@@ -638,12 +684,18 @@ const app = new Hono<AuthContext>()
638
684
  "Models-only API keys are turned off. An administrator can turn on the OpenAI-compatible API in the AI settings.",
639
685
  ),
640
686
  );
641
- return serviceAccountCredentials.createUserApiToken({
642
- access: data.access,
643
- user,
644
- name: data.name,
645
- expiresAt: data.expiresAt ?? null,
646
- });
687
+ const browserSignIn = await auth.browserSignIn(c);
688
+ if (!browserSignIn) throw new Error("Browser sign-in gate was not applied");
689
+ return localizeLastingAccessResult(
690
+ c,
691
+ await serviceAccountCredentials.createUserApiToken({
692
+ browserSignIn,
693
+ access: data.access,
694
+ user,
695
+ name: data.name,
696
+ expiresAt: data.expiresAt ?? null,
697
+ }),
698
+ );
647
699
  },
648
700
  201,
649
701
  ),
@@ -700,25 +752,96 @@ const app = new Hono<AuthContext>()
700
752
  describeRoute({
701
753
  tags: ["Me"],
702
754
  summary: "Change current user password",
703
- description: "Change the authenticated user's password. Requires the current password for verification.",
755
+ description:
756
+ "Change the authenticated user's password and end every other sign-in. Requires the current password. The current browser stays signed in with a new cookie and its original sign-in time. signInAgain is true when the caller must sign in again: this browser could not be kept, or the request did not come from a browser sign-in.",
704
757
  ...requiresAuth,
705
758
  responses: {
706
- 200: jsonResponse(MessageResponseSchema, "Password changed"),
759
+ 200: jsonResponse(SignOutOthersResponseSchema, "Password changed"),
707
760
  400: jsonResponse(ErrorResponseSchema, "Failed to change password"),
708
761
  401: jsonResponse(ErrorResponseSchema, "Current password is incorrect"),
762
+ 500: jsonResponse(
763
+ ErrorResponseSchema,
764
+ "Password changed, but signing out other sign-ins failed (SIGN_OUT_OTHERS_FAILED). Retry POST /api/me/sign-ins/sign-out-others.",
765
+ ),
709
766
  403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
710
767
  },
711
768
  }),
712
769
  v("json", ChangePasswordSchema),
713
770
  async (c) =>
714
771
  respond(c, async () => {
772
+ const keep = await auth.browserSignIn(c);
715
773
  const result = await accountsService.user.changeOwnPassword({
774
+ keep: keep && { sid: keep.sid, authEpoch: keep.authEpoch },
716
775
  user: c.get("user"),
717
776
  currentPassword: c.req.valid("json").currentPassword,
718
777
  newPassword: c.req.valid("json").newPassword,
719
778
  });
720
779
  if (!result.ok) return result;
721
- return ok({ message: "Password changed successfully." });
780
+ if (result.data.kept) auth.session.setWebCookie(c, result.data.kept.token, result.data.kept.expiresAt);
781
+ const signInAgain = !result.data.kept;
782
+ if (keep && signInAgain) deleteCookie(c, "session_token", { path: "/" });
783
+ return ok({
784
+ message: signInAgain
785
+ ? keep
786
+ ? "Password changed. You are signed out everywhere, including this browser. Sign in again."
787
+ : "Password changed. You are signed out everywhere. Sign in again."
788
+ : "Password changed successfully.",
789
+ signInAgain,
790
+ });
791
+ }),
792
+ )
793
+
794
+ .post(
795
+ "/sign-ins/sign-out-others",
796
+ auth.rejectAppSession,
797
+ requireBrowserSignIn,
798
+ describeRoute({
799
+ tags: ["Me"],
800
+ summary: "Sign out every other sign-in",
801
+ description:
802
+ "End other sign-ins and rotate this browser's cookie while preserving its sign-in time and expiry; signInAgain is true if this browser could not be kept. Cloud Login phones stay paired. API keys, passkeys and mandates stay valid.",
803
+ ...requiresAuth,
804
+ responses: {
805
+ 200: jsonResponse(SignOutOthersResponseSchema, "Other sign-ins ended"),
806
+ 401: jsonResponse(ErrorResponseSchema, "Authentication required"),
807
+ 403: jsonResponse(
808
+ ErrorResponseSchema,
809
+ "Browser sign-in required (BROWSER_SIGN_IN_REQUIRED); mobile app sessions receive FORBIDDEN",
810
+ ),
811
+ 500: jsonResponse(ErrorResponseSchema, "Signing out other sign-ins failed (SIGN_OUT_OTHERS_FAILED)"),
812
+ },
813
+ }),
814
+ async (c) =>
815
+ respond(c, async () => {
816
+ const signIn = await auth.browserSignIn(c);
817
+ if (!signIn) throw new Error("Browser sign-in gate was not applied");
818
+ try {
819
+ const { kept } = await auth.session.endOtherSignIns(c.get("user").id, {
820
+ keep: { sid: signIn.sid, authEpoch: signIn.authEpoch },
821
+ reason: "sign_out_others",
822
+ });
823
+ const user = c.get("user");
824
+ try {
825
+ await audit.record({
826
+ action: "auth.session.sign_out_others",
827
+ outcome: "allowed",
828
+ actor: toAccountsActor(user),
829
+ target: { type: "user", id: user.id, label: user.uid, provider: user.provider },
830
+ metadata: { keptThisBrowser: !!kept },
831
+ });
832
+ } catch {
833
+ log.warn("Other sign-ins ended, but recording the audit event failed", { userId: user.id });
834
+ }
835
+ if (kept) auth.session.setWebCookie(c, kept.token, kept.expiresAt);
836
+ const signInAgain = !kept;
837
+ if (signInAgain) deleteCookie(c, "session_token", { path: "/" });
838
+ return ok({
839
+ message: signInAgain ? "Other sign-ins ended. Sign in again in this browser." : "Other sign-ins ended.",
840
+ signInAgain,
841
+ });
842
+ } catch {
843
+ return fail({ code: "SIGN_OUT_OTHERS_FAILED", status: 500, message: "Signing out your other sign-ins did not work. Try again." });
844
+ }
722
845
  }),
723
846
  )
724
847
 
package/src/api/search.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  UniversalSearchDataSchema,
13
13
  } from "../contracts";
14
14
  import { type AuthContext, auth, expectUserBackedActor, jsonResponse, preferredLocale, requiresAuth, v } from "../server";
15
+ import { noTransform } from "../server/middleware/no-transform";
15
16
  import { logger } from "../services";
16
17
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
17
18
  import { searchInvocationOperation } from "../services/identity/invocation-operations";
@@ -103,7 +104,8 @@ type AppOutcome = { appId: string; status: SearchProviderStatus; items: SearchIt
103
104
  export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) => {
104
105
  const registry = dependencies.listCapabilities ?? listCapabilities;
105
106
  const fetchProvider = dependencies.fetch ?? globalThis.fetch;
106
- return new Hono<AuthContext>().use(dependencies.authenticate ?? auth.requireRole("authenticated")).get(
107
+ const router = new Hono<AuthContext>().use("/search/*", noTransform());
108
+ return router.use(dependencies.authenticate ?? auth.requireRole("authenticated")).get(
107
109
  "/search",
108
110
  describeRoute({
109
111
  tags: ["Search"],
@@ -2,6 +2,7 @@ import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { readBoundedJson } from "../_internal/bounded-json";
3
3
  import { getApp, listApps } from "../_internal/registry";
4
4
  import { type AuthContext, auth, rejectReservedWorkloadCredential } from "../server";
5
+ import { noTransform } from "../server/middleware/no-transform";
5
6
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
6
7
  import { syncInvocationOperation } from "../services/identity/invocation-operations";
7
8
  import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
@@ -19,6 +20,7 @@ type Dependencies = {
19
20
  /** Core owns the credential-bearing hop; targets receive only an operation-bound invocation. */
20
21
  export const createSyncOpsProxyRoutes = (dependencies: Dependencies = {}) =>
21
22
  new Hono<AuthContext>()
23
+ .use("*", noTransform())
22
24
  .use(dependencies.authenticate ?? auth.requireRole("admin"))
23
25
  .use(rejectReservedWorkloadCredential)
24
26
  .use(auth.requireOAuthScope("admin"))
@@ -20,6 +20,7 @@ import {
20
20
  widgetKey,
21
21
  } from "../contracts/widgets";
22
22
  import { type AuthContext, auth, preferredLocale, rejectReservedWorkloadCredential } from "../server";
23
+ import { noTransform } from "../server/middleware/no-transform";
23
24
  import { WIDGET_SETTINGS_PARAM, WIDGET_SETTINGS_PARAM_MAX_LENGTH, WIDGET_SIZE_PARAM } from "../server/widget-request";
24
25
  import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS } from "../services/identity/constants";
25
26
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
@@ -299,6 +300,8 @@ export const createWidgetRoutes = (dependencies: WidgetRouteDependencies = {}) =
299
300
 
300
301
  return (
301
302
  new Hono<AuthContext>()
303
+ .use("/widgets/v1", noTransform())
304
+ .use("/widgets/v1/*", noTransform())
302
305
  .use(dependencies.authenticate ?? auth.requireRole("authenticated"))
303
306
  .use(rejectReservedWorkloadCredential)
304
307
  .use(auth.requireOAuthScope("read", "admin"))