@k2b/cloud 0.40.0 → 0.42.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 (279) hide show
  1. package/package.json +9 -5
  2. package/src/_internal/bounded-json.ts +14 -2
  3. package/src/_internal/capabilities.ts +87 -8
  4. package/src/_internal/capability-reviews.ts +6 -0
  5. package/src/_internal/capability-sentences.ts +2 -1
  6. package/src/_internal/capability-timeout.ts +71 -0
  7. package/src/_internal/define-app.ts +158 -92
  8. package/src/_internal/heartbeat.ts +203 -14
  9. package/src/_internal/http-drain.ts +56 -0
  10. package/src/_internal/registry-validation.ts +3 -0
  11. package/src/_internal/registry.ts +28 -1
  12. package/src/_internal/runtime-context.ts +1 -0
  13. package/src/_internal/setup-lock.ts +103 -0
  14. package/src/_internal/status-preserving-ssr.ts +2 -0
  15. package/src/access/PermissionEditor.tsx +16 -17
  16. package/src/access/PrincipalPicker.tsx +45 -10
  17. package/src/access/messages.ts +8 -0
  18. package/src/access/principal-label.ts +18 -0
  19. package/src/access/ui.ts +1 -0
  20. package/src/account/EntitySearch.tsx +5 -4
  21. package/src/ai/action-review.ts +2 -1
  22. package/src/ai/app-skill-store.ts +1 -1
  23. package/src/ai/capabilities.ts +21 -3
  24. package/src/ai/capability-execution.ts +14 -12
  25. package/src/ai/chat/action-card.ts +76 -4
  26. package/src/ai/chat/action-messages.ts +14 -0
  27. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  28. package/src/ai/chat/action-question.tsx +214 -23
  29. package/src/ai/chat/action-text.ts +288 -0
  30. package/src/ai/chat/tool-action-words.ts +31 -0
  31. package/src/ai/chat/tool-action.tsx +1 -26
  32. package/src/ai/chat-tasks.ts +2 -1
  33. package/src/ai/client/controller.ts +150 -32
  34. package/src/ai/client/transport.ts +14 -2
  35. package/src/ai/code-capability-routes.ts +27 -23
  36. package/src/ai/code-runtime-tools.ts +14 -16
  37. package/src/ai/code-source-tools.ts +16 -18
  38. package/src/ai/executor.ts +64 -21
  39. package/src/ai/fetch-file-tool.ts +67 -76
  40. package/src/ai/firecrawl-tools.ts +15 -2
  41. package/src/ai/http.ts +1 -0
  42. package/src/ai/index.ts +1 -0
  43. package/src/ai/migrate.ts +28 -0
  44. package/src/ai/open-questions.ts +168 -0
  45. package/src/ai/projects.ts +1 -2
  46. package/src/ai/protocol.ts +2 -2
  47. package/src/ai/question-text.ts +8 -0
  48. package/src/ai/recent-resources.ts +12 -5
  49. package/src/ai/review-content.ts +77 -0
  50. package/src/ai/routes.ts +119 -35
  51. package/src/ai/runtime.ts +1 -0
  52. package/src/ai/skill-tool.ts +17 -6
  53. package/src/ai/skills-routes.ts +72 -27
  54. package/src/ai/skills.ts +139 -60
  55. package/src/ai/store.ts +184 -31
  56. package/src/ai/testing.ts +36 -0
  57. package/src/ai/tool-sentences.ts +152 -34
  58. package/src/ai/tool-wording.ts +12 -6
  59. package/src/ai/tools.ts +8 -3
  60. package/src/ai/types.ts +25 -4
  61. package/src/ai/usage.ts +2 -2
  62. package/src/api/admin-core-settings.ts +15 -2
  63. package/src/api/admin-geo.ts +54 -0
  64. package/src/api/admin-linux-identities.ts +6 -3
  65. package/src/api/admin-outgoing-mail.ts +29 -15
  66. package/src/api/app-approval-actions.ts +31 -7
  67. package/src/api/approvals-loader.ts +3 -3
  68. package/src/api/approvals.ts +51 -0
  69. package/src/api/apps.ts +4 -2
  70. package/src/api/auth/schemas.ts +3 -0
  71. package/src/api/auth.ts +54 -18
  72. package/src/api/capabilities.ts +258 -181
  73. package/src/api/capability-streams.ts +21 -13
  74. package/src/api/cli-plugins.ts +4 -1
  75. package/src/api/geo-glyphs.ts +11 -0
  76. package/src/api/geo.ts +128 -0
  77. package/src/api/help.ts +12 -4
  78. package/src/api/index.ts +5 -0
  79. package/src/api/mcp.ts +73 -41
  80. package/src/api/me-panel-consent.ts +8 -5
  81. package/src/api/me.ts +79 -3
  82. package/src/api/search.ts +21 -14
  83. package/src/api/sync-ops.ts +19 -13
  84. package/src/api/widgets.ts +42 -18
  85. package/src/approvals/Approvals.tsx +30 -66
  86. package/src/approvals/approval-card.ts +68 -0
  87. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  88. package/src/browser/CloudResourceSearch.tsx +3 -13
  89. package/src/browser/PanelLeaveNotice.tsx +18 -0
  90. package/src/browser/navigation-search.ts +78 -25
  91. package/src/browser/panel-leave.ts +69 -0
  92. package/src/browser/panel-messages.ts +12 -0
  93. package/src/browser/panel-slot.ts +173 -24
  94. package/src/browser/panel.ts +3 -0
  95. package/src/browser/resource-search-messages.ts +2 -0
  96. package/src/capabilities/client.ts +28 -1
  97. package/src/capabilities/executions.ts +22 -3
  98. package/src/capabilities/geo-client.ts +57 -0
  99. package/src/capabilities/response.ts +2 -1
  100. package/src/capabilities/review-storage.ts +24 -0
  101. package/src/capabilities/reviews.ts +17 -3
  102. package/src/capabilities/server.ts +36 -16
  103. package/src/cli/access.ts +15 -23
  104. package/src/cli/admin/linux.ts +13 -2
  105. package/src/cli/admin/outgoing-mail.ts +26 -4
  106. package/src/cli/capabilities.ts +19 -0
  107. package/src/config/env.ts +0 -6
  108. package/src/contracts/app-access.ts +48 -0
  109. package/src/contracts/app-approval.ts +3 -3
  110. package/src/contracts/app.ts +4 -1
  111. package/src/contracts/approvals.ts +2 -2
  112. package/src/contracts/avatar.ts +186 -0
  113. package/src/contracts/capabilities.ts +149 -55
  114. package/src/contracts/geo-admin.ts +127 -0
  115. package/src/contracts/geo-map.ts +19 -0
  116. package/src/contracts/geo-route.ts +168 -0
  117. package/src/contracts/geo.ts +248 -0
  118. package/src/contracts/index.ts +4 -0
  119. package/src/contracts/outgoing-mail.ts +17 -4
  120. package/src/contracts/registry.ts +2 -0
  121. package/src/contracts/shared.ts +3 -0
  122. package/src/events/app-access.ts +68 -0
  123. package/src/events/live-delivery.ts +128 -0
  124. package/src/events/live.ts +14 -130
  125. package/src/geo/CloudMap.tsx +95 -0
  126. package/src/geo/map-messages.ts +17 -0
  127. package/src/geo/ui.ts +1 -0
  128. package/src/index.ts +2 -0
  129. package/src/server/client-ip.ts +36 -0
  130. package/src/server/index.ts +3 -4
  131. package/src/server/middleware/app-access.ts +312 -0
  132. package/src/server/middleware/auth.ts +5 -3
  133. package/src/server/middleware/invocation.ts +11 -0
  134. package/src/server/middleware/rate-limit.ts +4 -4
  135. package/src/server/services/access.ts +13 -9
  136. package/src/server/services/index.ts +0 -2
  137. package/src/server/services/services.ts +0 -2
  138. package/src/services/accounts/app.ts +23 -3
  139. package/src/services/accounts/posix.ts +21 -7
  140. package/src/services/accounts/users.ts +12 -1
  141. package/src/services/allowed-network-fetch.ts +16 -10
  142. package/src/services/app-access-metrics.ts +24 -0
  143. package/src/services/app-access.ts +123 -0
  144. package/src/services/app-discovery.ts +27 -0
  145. package/src/services/approvals/content.ts +40 -0
  146. package/src/services/approvals/device.ts +49 -33
  147. package/src/services/approvals/index.ts +15 -6
  148. package/src/services/approvals/list.ts +6 -11
  149. package/src/services/approvals/question.ts +72 -0
  150. package/src/services/audit/index.ts +1 -0
  151. package/src/services/auth-flows/ipa.ts +39 -2
  152. package/src/services/auth-flows/password-reset.ts +32 -6
  153. package/src/services/downstream-budget.ts +87 -0
  154. package/src/services/geo/admin.ts +262 -0
  155. package/src/services/geo/bucket.ts +67 -0
  156. package/src/services/geo/cache.ts +57 -0
  157. package/src/services/geo/costs.ts +5 -0
  158. package/src/services/geo/errors.ts +41 -0
  159. package/src/services/geo/fixtures/README.md +17 -0
  160. package/src/services/geo/fixtures/along.json +355 -0
  161. package/src/services/geo/fixtures/autocomplete.json +218 -0
  162. package/src/services/geo/fixtures/geocode.json +129 -0
  163. package/src/services/geo/fixtures/places.json +355 -0
  164. package/src/services/geo/fixtures/reverse.json +61 -0
  165. package/src/services/geo/fixtures/routing.json +57 -0
  166. package/src/services/geo/geoapify-route.ts +93 -0
  167. package/src/services/geo/geoapify.ts +281 -0
  168. package/src/services/geo/ledger.ts +122 -0
  169. package/src/services/geo/map.ts +160 -0
  170. package/src/services/geo/migrate.ts +25 -0
  171. package/src/services/geo/polyline.ts +184 -0
  172. package/src/services/geo/route-chain.ts +145 -0
  173. package/src/services/geo/route-costs.ts +9 -0
  174. package/src/services/geo/route-geometry.ts +39 -0
  175. package/src/services/geo/route-plan.ts +172 -0
  176. package/src/services/geo/route-service.ts +433 -0
  177. package/src/services/geo/service.ts +300 -0
  178. package/src/services/geo/settings.ts +49 -0
  179. package/src/services/help/index.ts +16 -7
  180. package/src/services/help/types.ts +6 -1
  181. package/src/services/identity/invocation-actor.ts +20 -2
  182. package/src/services/identity/issue-invocation.ts +67 -0
  183. package/src/services/identity/key-ring.ts +5 -3
  184. package/src/services/identity/runtime-config.ts +23 -0
  185. package/src/services/index.ts +11 -0
  186. package/src/services/mandates/index.ts +40 -3
  187. package/src/services/network-security.ts +6 -3
  188. package/src/services/network-transport.ts +99 -0
  189. package/src/services/notifications/catalog.ts +3 -2
  190. package/src/services/notifications/runtime.ts +1 -0
  191. package/src/services/notifications/web-push-transport.ts +45 -38
  192. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  193. package/src/services/outgoing-mail/index.ts +55 -1
  194. package/src/services/outgoing-mail/messages.ts +82 -3
  195. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  196. package/src/services/outgoing-mail/retention.ts +13 -1
  197. package/src/services/outgoing-mail/store.ts +36 -6
  198. package/src/services/outgoing-mail/test-send.ts +34 -4
  199. package/src/services/outgoing-mail/transport.ts +1 -1
  200. package/src/services/pdf/gotenberg.ts +11 -38
  201. package/src/services/pdf/markdown.ts +160 -2
  202. package/src/services/pdf/template-preview.ts +1 -1
  203. package/src/services/public-http.ts +28 -40
  204. package/src/services/session/index.ts +203 -29
  205. package/src/services/settings/app.ts +4 -1
  206. package/src/services/settings/core-settings.ts +88 -0
  207. package/src/services/settings/defaults.ts +8 -2
  208. package/src/services/settings/errors.ts +11 -0
  209. package/src/services/settings/geo-budget.ts +40 -0
  210. package/src/services/settings/index.ts +4 -3
  211. package/src/services/settings/store.ts +25 -17
  212. package/src/services/weather/geo.ts +29 -111
  213. package/src/services/weather/index.ts +4 -3
  214. package/src/services/weather/locations.ts +8 -5
  215. package/src/services/weather/migrate.ts +1 -0
  216. package/src/services/weather/settings.ts +0 -16
  217. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  218. package/src/shared/avatar-art.ts +400 -0
  219. package/src/shared/index.ts +2 -0
  220. package/src/shared/network-address.ts +22 -3
  221. package/src/shared/template-rendering.ts +36 -4
  222. package/src/shared-text/documents.ts +1 -1
  223. package/src/shared-text/index.ts +2 -0
  224. package/src/shared-text/log.ts +51 -3
  225. package/src/shared-text/snapshots.ts +340 -0
  226. package/src/ssr/AdminLayout.tsx +4 -2
  227. package/src/ssr/CloudShell.island.tsx +6 -0
  228. package/src/ssr/HelpPanelArticle.island.tsx +308 -0
  229. package/src/ssr/HelpPanelBack.island.tsx +32 -0
  230. package/src/ssr/HelpPanelMenu.island.tsx +133 -0
  231. package/src/ssr/HelpPanelSearch.island.tsx +123 -0
  232. package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
  233. package/src/ssr/HelpPanelView.island.tsx +27 -0
  234. package/src/ssr/Layout.tsx +65 -15
  235. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  236. package/src/ssr/LayoutHelp.tsx +185 -542
  237. package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
  238. package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
  239. package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
  240. package/src/ssr/PanelRuntime.island.tsx +11 -7
  241. package/src/ssr/PwaLayout.tsx +2 -2
  242. package/src/ssr/RailApps.tsx +6 -0
  243. package/src/ssr/ShellRuntime.island.tsx +23 -7
  244. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  245. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
  246. package/src/ssr/admin-navigation.ts +1 -0
  247. package/src/ssr/app-badges.ts +6 -1
  248. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  249. package/src/ssr/help.ts +7 -5
  250. package/src/ssr/index.ts +2 -1
  251. package/src/ssr/islands/index.ts +3 -4
  252. package/src/ssr/layout-context.ts +2 -0
  253. package/src/ssr/layout-help-panel-controls.tsx +43 -0
  254. package/src/ssr/layout-help-panel-document.ts +67 -0
  255. package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
  256. package/src/ssr/layout-help-panel-menu.tsx +113 -0
  257. package/src/ssr/layout-help-panel-modules.ts +7 -0
  258. package/src/ssr/layout-help-panel-reader.tsx +159 -0
  259. package/src/ssr/layout-help-panel-search.tsx +69 -0
  260. package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
  261. package/src/ssr/layout-help-panel-state.ts +141 -0
  262. package/src/ssr/layout-help-panel-types.ts +19 -0
  263. package/src/ssr/layout-help-views.tsx +484 -0
  264. package/src/ssr/mobile-menu-history.ts +4 -0
  265. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  266. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  267. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  268. package/src/ssr/page-swap.ts +28 -0
  269. package/src/ssr/page-trail-runtime.ts +32 -13
  270. package/src/ssr/panel-consent.ts +2 -2
  271. package/src/ssr/panel-idle.ts +9 -0
  272. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  273. package/src/ssr/platform-messages.ts +8 -0
  274. package/src/ssr/runtime.ts +17 -0
  275. package/src/ssr/shell-data.tsx +10 -2
  276. package/src/styles/approvals.css +6 -0
  277. package/src/styles/effects.css +15 -0
  278. package/src/styles/global.css +70 -6
  279. package/src/server/services/geo.ts +0 -215
@@ -15,9 +15,10 @@ import {
15
15
  capabilityResultSchema,
16
16
  } from "../contracts/capabilities";
17
17
  import type { RequestAuthority } from "../server";
18
+ import { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/app-access";
18
19
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
19
20
  import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
20
- import { signInvocationToken } from "../services/identity/invocation-token";
21
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
21
22
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
22
23
  import { logger } from "../services/logging";
23
24
  import { decryptSecret, encryptSecret } from "../services/secrets";
@@ -142,17 +143,20 @@ export async function dispatchCapabilityStream(
142
143
  destructive = "destructive" in operation && operation.destructive;
143
144
  const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
144
145
  "invocation",
145
- (signer) =>
146
- (dependencies.signInvocation ?? signInvocationToken)({
147
- targetAppId: grant.appId,
148
- callingAppId: "core",
149
- operation: capabilityInvocationOperation(grant.kind, grant.capabilityId),
150
- schemaHash: operation.schemaHash,
151
- authority: invocationAuthorityFromRequest(authority),
152
- requestId: grant.requestId,
153
- signer,
154
- issuer: signer.issuer,
155
- }),
146
+ (signer, db) =>
147
+ (dependencies.signInvocation ?? signInvocationToken)(
148
+ {
149
+ targetAppId: grant.appId,
150
+ callingAppId: "core",
151
+ operation: capabilityInvocationOperation(grant.kind, grant.capabilityId),
152
+ schemaHash: operation.schemaHash,
153
+ authority: invocationAuthorityFromRequest(authority),
154
+ requestId: grant.requestId,
155
+ signer,
156
+ issuer: signer.issuer,
157
+ },
158
+ { db, groupsAdmin: signer.groupsAdmin },
159
+ ),
156
160
  { signal, timeoutMs: 30_000 },
157
161
  );
158
162
  const headers = new Headers({
@@ -265,7 +269,11 @@ export async function dispatchCapabilityStream(
265
269
  if (!error.success || response.status === 401) return denied(status, "STREAM_FAILED");
266
270
  } else await record(response.status);
267
271
  return Response.json(receipt.data, { status: response.status, headers: { "cache-control": "no-store" } });
268
- } catch {
272
+ } catch (error) {
273
+ if (error instanceof AppAccessDeniedError) {
274
+ await record(403, error.code);
275
+ return Response.json(appAccessDeniedBody(error.app), { status: 403 });
276
+ }
269
277
  const status = request.signal.aborted ? 499 : signal.aborted ? 504 : 502;
270
278
  const code = status === 499 ? "REQUEST_CANCELLED" : status === 504 ? "DEADLINE_EXCEEDED" : "STREAM_FAILED";
271
279
  await record(status, code);
@@ -4,13 +4,16 @@ import { listApps } from "../_internal/registry";
4
4
  import type { CloudCliPluginSummary } from "../cli/plugin";
5
5
  import type { AuthContext } from "../server/middleware/auth";
6
6
 
7
+ import { appAccessAuthority } from "../services/app-access";
8
+ import { discoverableApps } from "../services/app-discovery";
9
+
7
10
  /**
8
11
  * `GET /cli/plugins`: the `cld` plugins that live applications serve, under
9
12
  * the same access rule as the plugin downloads. Core mounts this list; each
10
13
  * application serves its own plugin files.
11
14
  */
12
15
  export const cliPluginListRoutes = new Hono<AuthContext>().get("/", requireCliPluginAccess, async (c) => {
13
- const plugins: CloudCliPluginSummary[] = (await listApps())
16
+ const plugins: CloudCliPluginSummary[] = (await discoverableApps(appAccessAuthority(c), await listApps()))
14
17
  .flatMap((app) => (app.cliModules ?? []).map((name) => ({ name, app: app.id, version: app.runtime?.version ?? "unknown" })))
15
18
  .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
16
19
  return c.json({ plugins }, 200, { "cache-control": "private, no-store" });
@@ -0,0 +1,11 @@
1
+ import { appAssetPath } from "../server/app-assets";
2
+
3
+ declare const __CLOUD_APP_ASSETS__: string | undefined;
4
+
5
+ /** The route validates the font and range before resolving a bundled asset. */
6
+ export const geoGlyphFile = (font: string, range: string) =>
7
+ Bun.file(
8
+ typeof __CLOUD_APP_ASSETS__ === "string"
9
+ ? appAssetPath("map-glyphs", font, range)
10
+ : new URL(`./map-glyphs/${encodeURIComponent(font)}/${range}`, import.meta.resolve("@k2b/ui/package.json")),
11
+ );
package/src/api/geo.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { mapStyle } from "@k2b/ui";
2
+ import { Hono, type MiddlewareHandler } from "hono";
3
+ import { describeRoute } from "hono-openapi";
4
+ import { z } from "zod";
5
+ import { GEO_MAP_MAX_ZOOM } from "../contracts/geo-map";
6
+ import { type AuthContext, auth, getLocale, requiresAuth, v } from "../server";
7
+ import { REQUEST_IDLE_TIMEOUT_SECONDS, setRequestTimeout } from "../server/request-timeout";
8
+ import { GEO_TIMEOUT_MS } from "../services/geo/bucket";
9
+ import { GeoError } from "../services/geo/errors";
10
+ import { coreGeoMap } from "../services/geo/map";
11
+
12
+ import { geoGlyphFile } from "./geo-glyphs";
13
+
14
+ const styleQuery = z.object({
15
+ theme: z.enum(["light", "dark"]).default("light"),
16
+ locale: z
17
+ .string()
18
+ .max(100)
19
+ .refine((locale) => {
20
+ try {
21
+ new Intl.Locale(locale);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ })
27
+ .optional(),
28
+ });
29
+ const tileParams = z.object({
30
+ z: z.coerce.number().int().min(0).max(GEO_MAP_MAX_ZOOM),
31
+ x: z.coerce.number().int().nonnegative(),
32
+ y: z.string().regex(/^\d+\.pbf$/),
33
+ });
34
+ const glyphParams = z.object({ font: z.literal("IBM Plex Sans Medium"), range: z.string().regex(/^\d+-\d+\.pbf$/) });
35
+
36
+ export const createGeoRoutes = (
37
+ authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("authenticated"),
38
+ service: typeof coreGeoMap = coreGeoMap,
39
+ ) =>
40
+ new Hono<AuthContext>()
41
+ .use("*", authenticate)
42
+ .use("*", auth.requireOAuthScope("read", "admin"))
43
+ .onError((cause, c) => {
44
+ const error = cause instanceof GeoError ? cause : new GeoError("GEO_PROVIDER_FAILED");
45
+ c.header("Cache-Control", "private, no-store");
46
+ if (error.code === "GEO_BUSY") c.header("Retry-After", String(error.details.retryAfterSeconds ?? 1));
47
+ return Response.json(
48
+ { code: error.code, details: error.details },
49
+ {
50
+ status: error.status,
51
+ headers: {
52
+ "Cache-Control": "private, no-store",
53
+ ...(error.code === "GEO_BUSY" ? { "Retry-After": String(error.details.retryAfterSeconds ?? 1) } : {}),
54
+ },
55
+ },
56
+ );
57
+ })
58
+ .get(
59
+ "/style",
60
+ describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Get the Cloud map style with same-origin tiles and glyphs" }),
61
+ v("query", styleQuery),
62
+ async (c) => {
63
+ const query = c.req.valid("query");
64
+ const locale = query.locale ?? getLocale(c);
65
+ await service.style({ theme: query.theme }, { actor: c.get("actor"), accessSubject: c.get("accessSubject"), locale });
66
+ c.header("Cache-Control", "private, no-store");
67
+ return c.json(
68
+ mapStyle({
69
+ theme: query.theme,
70
+ locale,
71
+ tiles: "/api/geo/tiles/{z}/{x}/{y}.pbf",
72
+ glyphs: "/api/geo/glyphs/{fontstack}/{range}.pbf",
73
+ maxzoom: GEO_MAP_MAX_ZOOM,
74
+ }),
75
+ );
76
+ },
77
+ )
78
+ .get(
79
+ "/tiles/:z/:x/:y",
80
+ describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Load one budgeted Geoapify EU vector tile" }),
81
+ v("param", tileParams),
82
+ async (c) => {
83
+ const params = c.req.valid("param");
84
+ setRequestTimeout(c, Math.ceil(GEO_TIMEOUT_MS.tiles / 1000) + REQUEST_IDLE_TIMEOUT_SECONDS);
85
+ try {
86
+ const bytes = await service.tile(
87
+ { z: params.z, x: params.x, y: Number(params.y.slice(0, -4)) },
88
+ {
89
+ actor: c.get("actor"),
90
+ accessSubject: c.get("accessSubject"),
91
+ locale: getLocale(c),
92
+ signal: c.req.raw.signal,
93
+ },
94
+ );
95
+ return new Response(bytes, {
96
+ headers: {
97
+ "Content-Type": "application/vnd.mapbox-vector-tile",
98
+ "Cache-Control": "private, max-age=604800",
99
+ "X-Content-Type-Options": "nosniff",
100
+ },
101
+ });
102
+ } finally {
103
+ setRequestTimeout(c, REQUEST_IDLE_TIMEOUT_SECONDS);
104
+ }
105
+ },
106
+ )
107
+ .get(
108
+ "/glyphs/:font/:range",
109
+ describeRoute({ ...requiresAuth, tags: ["Places and maps"], summary: "Load locally bundled IBM Plex map labels" }),
110
+ v("param", glyphParams),
111
+ async (c) => {
112
+ const { font, range } = c.req.valid("param");
113
+ const [start, end] = range.slice(0, -4).split("-").map(Number);
114
+ if (start === undefined || end === undefined || start < 0 || end > 65535 || start % 256 !== 0 || end !== start + 255)
115
+ return c.notFound();
116
+ const file = geoGlyphFile(font, range);
117
+ if (!(await file.exists())) return c.notFound();
118
+ return new Response(file, {
119
+ headers: {
120
+ "Content-Type": "application/x-protobuf",
121
+ "Cache-Control": "private, max-age=604800",
122
+ "X-Content-Type-Options": "nosniff",
123
+ },
124
+ });
125
+ },
126
+ );
127
+
128
+ export default createGeoRoutes();
package/src/api/help.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { type AuthContext, auth, getLocale } from "../server";
3
3
  import { buildHelpDocumentPayload } from "../server/help-payload";
4
+ import { appAccessAuthority } from "../services/app-access";
4
5
  import { createHelpReader, type HelpReaderFactory } from "../services/help";
5
6
  import type { HelpSearchPayload } from "../shared/help";
6
7
  import type { renderHelpMarkdown } from "../shared/markdown";
@@ -24,16 +25,23 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
24
25
  const query = c.req.query("q")?.trim().slice(0, 200) ?? "";
25
26
  const payload: HelpSearchPayload = {
26
27
  locale,
27
- ids: (await reader(locale, c.get("actor")).search({ query, appId: c.req.param("appId"), limit: 25 })).map(
28
- (document) => document.documentId,
29
- ),
28
+ ids: (
29
+ await reader(locale, c.get("actor"), { authority: appAccessAuthority(c) }).search({
30
+ query,
31
+ appId: c.req.param("appId"),
32
+ limit: 25,
33
+ })
34
+ ).map((document) => document.documentId),
30
35
  };
31
36
  return c.json(payload);
32
37
  })
33
38
  .get("/help/v1/:appId/documents/:documentId", async (c) => {
34
39
  const locale = getLocale(c);
35
40
  const { t } = helpApiMessages.resolve([locale]);
36
- const document = await reader(locale, c.get("actor")).read({ appId: c.req.param("appId"), documentId: c.req.param("documentId") });
41
+ const document = await reader(locale, c.get("actor"), { authority: appAccessAuthority(c) }).read({
42
+ appId: c.req.param("appId"),
43
+ documentId: c.req.param("documentId"),
44
+ });
37
45
  if (!document) return c.json({ code: "HELP_NOT_FOUND", message: t.notFound }, 404);
38
46
  const payload = buildHelpDocumentPayload(document, c.req.header("User-Agent") ?? "", dependencies.renderMarkdown);
39
47
  return c.json(payload);
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 adminGeoRoutes from "./admin-geo";
29
30
  import adminGeoIpRoutes from "./admin-geoip";
30
31
  import adminIdentityRoutes from "./admin-identity";
31
32
  import adminLifecycleRoutes from "./admin-lifecycle";
@@ -38,6 +39,7 @@ import { approvalRoutes } from "./approvals";
38
39
  import { appDiscoveryRoutes } from "./apps";
39
40
  import { createAuthRoutes } from "./auth";
40
41
  import { createCapabilityRoutes } from "./capabilities";
42
+ import geoRoutes from "./geo";
41
43
  import { createHelpRoutes } from "./help";
42
44
  import { createMcpRoutes } from "./mcp";
43
45
  import meRoutes from "./me";
@@ -46,6 +48,7 @@ import { createSearchRoutes } from "./search";
46
48
  import { createSyncOpsProxyRoutes } from "./sync-ops";
47
49
  import { createWidgetRoutes } from "./widgets";
48
50
 
51
+ export { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
49
52
  export { createAiOpenAiRoutes } from "../ai/openai-api";
50
53
  export { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
51
54
  export { cliPluginListRoutes } from "./cli-plugins";
@@ -78,6 +81,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
78
81
  .route("/auth", createAuthRoutes(options.notifications))
79
82
  .route("/me", meRoutes)
80
83
  .route("/accounts", accountsEntitiesRoutes)
84
+ .route("/geo", geoRoutes)
81
85
  .route("/apps", appDiscoveryRoutes)
82
86
  .route("/approvals", approvalRoutes)
83
87
  .route("/announcements", announcementRoutes)
@@ -91,6 +95,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
91
95
  .route("/admin/core/rail", adminRailRoutes)
92
96
  .route("/admin/core/linux-identities", adminLinuxIdentityRoutes)
93
97
  .route("/admin/core/geoip", adminGeoIpRoutes)
98
+ .route("/admin/core/geo", adminGeoRoutes)
94
99
  .route("/admin/identity", adminIdentityRoutes)
95
100
  .route("/admin/sync", createSyncOpsProxyRoutes())
96
101
  .route("/admin/lifecycle", adminLifecycleRoutes)
package/src/api/mcp.ts CHANGED
@@ -15,6 +15,8 @@ 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";
19
+ import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
18
20
  import {
19
21
  HELP_SEARCH_MAX_LIMIT,
20
22
  helpAgentMarkdown,
@@ -31,6 +33,7 @@ import {
31
33
  CAPABILITY_MAX_RESULT_BYTES,
32
34
  CAPABILITY_PROTOCOL_VERSION,
33
35
  type CapabilityActionManifest,
36
+ CapabilityActionReceiptSchema,
34
37
  type CapabilityQueryManifest,
35
38
  CapabilitySemanticLinkSchema,
36
39
  type CloudResourceRef,
@@ -42,12 +45,15 @@ import {
42
45
  } from "../contracts/capabilities";
43
46
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
44
47
  import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
48
+ import { preferredLocale } from "../server/locale";
45
49
  import { noTransform } from "../server/middleware/no-transform";
50
+ import { discoverableApps } from "../services/app-discovery";
46
51
  import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
47
52
  import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
48
53
  import { logger } from "../services/logging";
49
54
  import { get } from "../services/settings";
50
55
  import { cloudMcpResourceUri, publicCloudOrigin } from "../shared/app-url";
56
+ import { normalizeTimeZone } from "../shared/time";
51
57
  import { type CapabilityDispatchDependencies, dispatchCapability } from "./capabilities";
52
58
 
53
59
  const MCP_TOOL_PAGE_SIZE = 100;
@@ -67,6 +73,7 @@ type McpRouteDependencies = CapabilityDispatchDependencies & {
67
73
  listApps?: () => Promise<AppRegistryEntry[]>;
68
74
  help?: HelpReaderFactory;
69
75
  getOperatorLocale?: () => Promise<string | undefined>;
76
+ getOperatorTimeZone?: () => Promise<string | undefined>;
70
77
  getAppUrl?: () => Promise<string>;
71
78
  authenticate?: MiddlewareHandler<AuthContext>;
72
79
  limit?: MiddlewareHandler<AuthContext>;
@@ -488,7 +495,7 @@ const createMcpServer = (
488
495
  oauthScopes: string[] | null,
489
496
  authority?: RequestAuthority,
490
497
  ): Server => {
491
- const registry = dependencies.listApps ?? listApps;
498
+ const registry = async () => discoverableApps(authority ?? null, await (dependencies.listApps ?? listApps)());
492
499
  const capabilityLookup = dependencies.getCapability ?? getCapability;
493
500
  let helpLocale: Promise<string> | undefined;
494
501
  const resolveHelpLocale = () =>
@@ -496,9 +503,10 @@ const createMcpServer = (
496
503
  resolveLocale(request.headers, operatorDefault),
497
504
  ));
498
505
  const helpReader = async () =>
499
- (dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry })))(
506
+ (dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry, authority: authority ?? null })))(
500
507
  await resolveHelpLocale(),
501
508
  authority?.actor,
509
+ { authority: authority ?? null },
502
510
  );
503
511
  const hasScope = (scope: "read" | "write"): boolean =>
504
512
  oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
@@ -682,14 +690,33 @@ const createMcpServer = (
682
690
  origin,
683
691
  );
684
692
  }
685
- const resultFor = (value: Record<string, unknown>): CallToolResult => {
693
+ const receipt = response.ok && selected.kind === "actions" ? CapabilityActionReceiptSchema.safeParse(body.receipt) : null;
694
+ let receiptText: string | undefined;
695
+ if (receipt?.success) {
696
+ // The receipt line is extra presentation: if Cloud cannot word it, the completed Action's result still goes out.
697
+ try {
698
+ const locale = preferredLocale(request.headers) ?? (await resolveHelpLocale());
699
+ const timeZone = normalizeTimeZone(
700
+ await (dependencies.getOperatorTimeZone ?? (() => get<string>("app.timezone")))().catch(() => "UTC"),
701
+ );
702
+ receiptText = capabilitySentenceText(receipt.data.text, receipt.data.slots ?? {}, { locale, timeZone });
703
+ } catch (error) {
704
+ log.warn("MCP receipt text unavailable", { error: error instanceof Error ? error.message : String(error) });
705
+ }
706
+ }
707
+ const resultFor = (value: Record<string, unknown>, withReceipt = true): CallToolResult => {
686
708
  const result = toolResult(value, !response.ok);
687
709
  if (origin) result.content.push(...semanticResourceLinks(value, origin));
710
+ if (withReceipt && receiptText) result.content.unshift({ type: "text", text: receiptText });
688
711
  return result;
689
712
  };
690
- // Added hrefs are optional: they never turn a result that fits into RESPONSE_TOO_LARGE.
713
+ // Both enrichments are optional: neither turns a result that fits into RESPONSE_TOO_LARGE.
691
714
  const result = resultFor(body);
692
- return ensureBoundedToolResult(fitsToolResult(result) || body === providerBody ? result : resultFor(providerBody));
715
+ if (fitsToolResult(result)) return result;
716
+ const plain = resultFor(body, false);
717
+ if (fitsToolResult(plain)) return plain;
718
+ const original = resultFor(providerBody);
719
+ return ensureBoundedToolResult(fitsToolResult(original) ? original : resultFor(providerBody, false));
693
720
  });
694
721
 
695
722
  return server;
@@ -757,42 +784,47 @@ export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
757
784
  .use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
758
785
  .use("/mcp/v1", rejectReservedWorkloadCredential)
759
786
  .use("/mcp/v1", dependencies.limit ?? rateLimit())
760
- .all("/mcp/v1", async (c) => {
761
- if (c.req.method !== "POST") {
762
- return new Response(null, { status: 405, headers: { Allow: "POST" } });
763
- }
764
- let request = c.req.raw;
765
- const parsed = await readBoundedJson(request, CAPABILITY_MAX_REQUEST_BYTES);
766
- if (!parsed.ok) {
767
- return c.json(
768
- {
769
- jsonrpc: "2.0",
770
- id: null,
771
- error: {
772
- code: parsed.reason === "too_large" ? -32600 : -32700,
773
- message: parsed.reason === "too_large" ? "MCP request is too large" : "MCP request body must be JSON",
787
+ .all("/mcp/v1", async (c) =>
788
+ withCapabilityRequestTimeout(c, "query", async () => {
789
+ if (c.req.method !== "POST") {
790
+ return new Response(null, { status: 405, headers: { Allow: "POST" } });
791
+ }
792
+ let request = c.req.raw;
793
+ const parsed = await readBoundedJson(request, CAPABILITY_MAX_REQUEST_BYTES);
794
+ if (!parsed.ok) {
795
+ return c.json(
796
+ {
797
+ jsonrpc: "2.0",
798
+ id: null,
799
+ error: {
800
+ code: parsed.reason === "too_large" ? -32600 : -32700,
801
+ message: parsed.reason === "too_large" ? "MCP request is too large" : "MCP request body must be JSON",
802
+ },
774
803
  },
775
- },
776
- parsed.reason === "too_large" ? 413 : 400,
777
- );
778
- }
779
- const headers = new Headers(request.headers);
780
- headers.delete("content-length");
781
- request = new Request(request.url, {
782
- method: "POST",
783
- headers,
784
- body: JSON.stringify(parsed.data),
785
- signal: request.signal,
786
- });
787
- const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
788
- const server = createMcpServer(request, dependencies, c.get("oauthScopes") ?? null, auth.getAuthority(c));
789
- await server.connect(transport);
790
- try {
791
- return await transport.handleRequest(request);
792
- } finally {
793
- await transport.close();
794
- await server.close();
795
- }
796
- });
804
+ parsed.reason === "too_large" ? 413 : 400,
805
+ );
806
+ }
807
+ const headers = new Headers(request.headers);
808
+ headers.delete("content-length");
809
+ request = new Request(request.url, {
810
+ method: "POST",
811
+ headers,
812
+ body: JSON.stringify(parsed.data),
813
+ signal: request.signal,
814
+ });
815
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
816
+ const server = createMcpServer(request, dependencies, c.get("oauthScopes") ?? null, auth.getAuthority(c));
817
+ try {
818
+ await server.connect(transport);
819
+ return await transport.handleRequest(request);
820
+ } finally {
821
+ try {
822
+ await transport.close();
823
+ } finally {
824
+ await server.close();
825
+ }
826
+ }
827
+ }),
828
+ );
797
829
 
798
830
  export type McpApiType = ReturnType<typeof createMcpRoutes>;
@@ -5,6 +5,8 @@ import { z } from "zod";
5
5
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
6
6
  import { panelProviderNeedsConsent } from "../contracts/panel-context-plain";
7
7
  import { type AuthContext, jsonResponse, requiresAuth, v } from "../server";
8
+ import { appAccessAuthority } from "../services/app-access";
9
+ import { discoverableApps } from "../services/app-discovery";
8
10
  import { PanelConsentCancelled, panelContextConsent } from "../services/panel-context-consent";
9
11
  import { getRuntimeContext } from "../ssr/runtime";
10
12
 
@@ -15,9 +17,9 @@ const SavedSchema = ProviderSchema.extend(AnswerSchema.shape).strict();
15
17
 
16
18
  /** Mounted below /me's session and user guards. The live request registry owns provider membership. */
17
19
  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))
20
+ const providers = async (c: import("hono").Context<AuthContext>) =>
21
+ (await discoverableApps(appAccessAuthority(c), getRuntimeContext(c).apps))
22
+ .filter((app) => app.panel && panelProviderNeedsConsent(app.id))
21
23
  .map((app) => app.id);
22
24
  return new Hono<AuthContext>()
23
25
  .use("*", async (c, next) => {
@@ -33,7 +35,7 @@ export const createMePanelConsentRoutes = (service = panelContextConsent) => {
33
35
  ...requiresAuth,
34
36
  responses: { 200: jsonResponse(AnswersSchema, "Answers for registered panel providers") },
35
37
  }),
36
- async (c) => c.json({ answers: await service.list(c.get("user").id, providers(c)) }),
38
+ async (c) => c.json({ answers: await service.list(c.get("user").id, await providers(c)) }),
37
39
  )
38
40
  .put(
39
41
  "/panel-consent/:provider",
@@ -53,7 +55,8 @@ export const createMePanelConsentRoutes = (service = panelContextConsent) => {
53
55
  v("json", AnswerSchema),
54
56
  async (c) => {
55
57
  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);
58
+ if (!(await providers(c)).includes(provider))
59
+ return c.json({ code: "PANEL_PROVIDER_NOT_FOUND", message: "Panel provider not found" }, 404);
57
60
  const { allowed, revision } = c.req.valid("json");
58
61
  let saved: Awaited<ReturnType<typeof service.set>>;
59
62
  try {
package/src/api/me.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { err, fail, ok, type Result } from "@k2b/stdlib";
8
8
  import { sql } from "bun";
9
9
  import { type Context, Hono, type MiddlewareHandler } from "hono";
10
+ import { deleteCookie } from "hono/cookie";
10
11
  import { createMiddleware } from "hono/factory";
11
12
  import { describeRoute } from "hono-openapi";
12
13
  import { z } from "zod";
@@ -27,6 +28,7 @@ import {
27
28
  NotificationQuietSettingsSchema,
28
29
  RegisterBrowserNotificationEndpointSchema,
29
30
  ServiceAccountCredentialSchema,
31
+ SignOutOthersResponseSchema,
30
32
  UpdateAvatarResponseSchema,
31
33
  UpdateAvatarSchema,
32
34
  UpdateNotificationQuietSettingsSchema,
@@ -47,6 +49,7 @@ import {
47
49
  accountsAppService as accountsService,
48
50
  audit,
49
51
  browserNotifications,
52
+ logger,
50
53
  mandates,
51
54
  notifications,
52
55
  serviceAccountCredentials,
@@ -61,6 +64,8 @@ import { createMePanelConsentRoutes } from "./me-panel-consent";
61
64
  import { createMeRailRoutes } from "./me-rail";
62
65
  import { createMeWebVitalsRoutes } from "./me-web-vitals";
63
66
 
67
+ const log = logger("api:me");
68
+
64
69
  const toAccountsActor = (user: AuthContext["Variables"]["user"]) => ({
65
70
  userId: user.id,
66
71
  uid: user.uid,
@@ -747,25 +752,96 @@ const app = new Hono<AuthContext>()
747
752
  describeRoute({
748
753
  tags: ["Me"],
749
754
  summary: "Change current user password",
750
- 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.",
751
757
  ...requiresAuth,
752
758
  responses: {
753
- 200: jsonResponse(MessageResponseSchema, "Password changed"),
759
+ 200: jsonResponse(SignOutOthersResponseSchema, "Password changed"),
754
760
  400: jsonResponse(ErrorResponseSchema, "Failed to change password"),
755
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
+ ),
756
766
  403: jsonResponse(ErrorResponseSchema, "Not permitted for API keys or other credentials"),
757
767
  },
758
768
  }),
759
769
  v("json", ChangePasswordSchema),
760
770
  async (c) =>
761
771
  respond(c, async () => {
772
+ const keep = await auth.browserSignIn(c);
762
773
  const result = await accountsService.user.changeOwnPassword({
774
+ keep: keep && { sid: keep.sid, authEpoch: keep.authEpoch },
763
775
  user: c.get("user"),
764
776
  currentPassword: c.req.valid("json").currentPassword,
765
777
  newPassword: c.req.valid("json").newPassword,
766
778
  });
767
779
  if (!result.ok) return result;
768
- 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
+ }
769
845
  }),
770
846
  )
771
847