@k2b/cloud 0.41.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 (227) hide show
  1. package/package.json +6 -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-timeout.ts +71 -0
  6. package/src/_internal/define-app.ts +145 -89
  7. package/src/_internal/http-drain.ts +56 -0
  8. package/src/_internal/registry-validation.ts +3 -0
  9. package/src/_internal/runtime-context.ts +1 -0
  10. package/src/_internal/status-preserving-ssr.ts +2 -0
  11. package/src/access/PermissionEditor.tsx +16 -17
  12. package/src/access/PrincipalPicker.tsx +45 -10
  13. package/src/access/messages.ts +8 -0
  14. package/src/access/principal-label.ts +18 -0
  15. package/src/access/ui.ts +1 -0
  16. package/src/account/EntitySearch.tsx +5 -4
  17. package/src/ai/action-review.ts +2 -1
  18. package/src/ai/app-skill-store.ts +1 -1
  19. package/src/ai/capabilities.ts +21 -3
  20. package/src/ai/capability-execution.ts +14 -12
  21. package/src/ai/chat/action-card.ts +7 -1
  22. package/src/ai/chat/action-messages.ts +14 -0
  23. package/src/ai/chat/action-question.browser-harness.tsx +12 -0
  24. package/src/ai/chat/action-question.tsx +202 -22
  25. package/src/ai/chat/action-text.ts +1 -1
  26. package/src/ai/chat-tasks.ts +2 -1
  27. package/src/ai/client/controller.ts +150 -32
  28. package/src/ai/client/transport.ts +14 -2
  29. package/src/ai/code-capability-routes.ts +27 -23
  30. package/src/ai/code-runtime-tools.ts +14 -16
  31. package/src/ai/code-source-tools.ts +16 -18
  32. package/src/ai/executor.ts +56 -21
  33. package/src/ai/fetch-file-tool.ts +67 -76
  34. package/src/ai/http.ts +1 -0
  35. package/src/ai/migrate.ts +13 -0
  36. package/src/ai/open-questions.ts +4 -8
  37. package/src/ai/projects.ts +1 -2
  38. package/src/ai/protocol.ts +2 -2
  39. package/src/ai/recent-resources.ts +12 -5
  40. package/src/ai/review-content.ts +77 -0
  41. package/src/ai/routes.ts +69 -31
  42. package/src/ai/runtime.ts +1 -0
  43. package/src/ai/skill-tool.ts +17 -6
  44. package/src/ai/skills-routes.ts +72 -27
  45. package/src/ai/skills.ts +139 -60
  46. package/src/ai/store.ts +168 -33
  47. package/src/ai/types.ts +10 -3
  48. package/src/ai/usage.ts +2 -2
  49. package/src/api/admin-core-settings.ts +15 -2
  50. package/src/api/admin-geo.ts +54 -0
  51. package/src/api/admin-linux-identities.ts +6 -3
  52. package/src/api/admin-outgoing-mail.ts +29 -15
  53. package/src/api/approvals-loader.ts +3 -3
  54. package/src/api/approvals.ts +51 -0
  55. package/src/api/apps.ts +4 -2
  56. package/src/api/capabilities.ts +258 -181
  57. package/src/api/capability-streams.ts +21 -13
  58. package/src/api/cli-plugins.ts +4 -1
  59. package/src/api/geo-glyphs.ts +11 -0
  60. package/src/api/geo.ts +128 -0
  61. package/src/api/help.ts +12 -4
  62. package/src/api/index.ts +5 -0
  63. package/src/api/mcp.ts +46 -38
  64. package/src/api/me-panel-consent.ts +8 -5
  65. package/src/api/search.ts +21 -14
  66. package/src/api/sync-ops.ts +19 -13
  67. package/src/api/widgets.ts +42 -18
  68. package/src/approvals/Approvals.tsx +5 -3
  69. package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
  70. package/src/browser/CloudResourceSearch.tsx +3 -13
  71. package/src/browser/PanelLeaveNotice.tsx +18 -0
  72. package/src/browser/navigation-search.ts +78 -25
  73. package/src/browser/panel-leave.ts +69 -0
  74. package/src/browser/panel-messages.ts +12 -0
  75. package/src/browser/panel-slot.ts +173 -24
  76. package/src/browser/panel.ts +3 -0
  77. package/src/browser/resource-search-messages.ts +2 -0
  78. package/src/capabilities/client.ts +28 -1
  79. package/src/capabilities/executions.ts +22 -3
  80. package/src/capabilities/geo-client.ts +57 -0
  81. package/src/capabilities/response.ts +2 -1
  82. package/src/capabilities/review-storage.ts +24 -0
  83. package/src/capabilities/reviews.ts +17 -3
  84. package/src/capabilities/server.ts +36 -16
  85. package/src/cli/access.ts +15 -23
  86. package/src/cli/admin/linux.ts +13 -2
  87. package/src/cli/admin/outgoing-mail.ts +26 -4
  88. package/src/cli/capabilities.ts +19 -0
  89. package/src/config/env.ts +0 -6
  90. package/src/contracts/app-access.ts +48 -0
  91. package/src/contracts/app.ts +4 -1
  92. package/src/contracts/approvals.ts +2 -2
  93. package/src/contracts/avatar.ts +186 -0
  94. package/src/contracts/capabilities.ts +148 -54
  95. package/src/contracts/geo-admin.ts +127 -0
  96. package/src/contracts/geo-map.ts +19 -0
  97. package/src/contracts/geo-route.ts +168 -0
  98. package/src/contracts/geo.ts +248 -0
  99. package/src/contracts/index.ts +4 -0
  100. package/src/contracts/outgoing-mail.ts +17 -4
  101. package/src/contracts/registry.ts +2 -0
  102. package/src/events/app-access.ts +68 -0
  103. package/src/events/live-delivery.ts +128 -0
  104. package/src/events/live.ts +14 -130
  105. package/src/geo/CloudMap.tsx +95 -0
  106. package/src/geo/map-messages.ts +17 -0
  107. package/src/geo/ui.ts +1 -0
  108. package/src/index.ts +2 -0
  109. package/src/server/client-ip.ts +36 -0
  110. package/src/server/index.ts +3 -4
  111. package/src/server/middleware/app-access.ts +312 -0
  112. package/src/server/middleware/auth.ts +1 -1
  113. package/src/server/middleware/invocation.ts +11 -0
  114. package/src/server/middleware/rate-limit.ts +4 -4
  115. package/src/server/services/access.ts +13 -9
  116. package/src/server/services/index.ts +0 -2
  117. package/src/server/services/services.ts +0 -2
  118. package/src/services/accounts/posix.ts +21 -7
  119. package/src/services/allowed-network-fetch.ts +16 -10
  120. package/src/services/app-access-metrics.ts +24 -0
  121. package/src/services/app-access.ts +123 -0
  122. package/src/services/app-discovery.ts +27 -0
  123. package/src/services/approvals/content.ts +40 -0
  124. package/src/services/approvals/index.ts +14 -6
  125. package/src/services/approvals/list.ts +6 -11
  126. package/src/services/approvals/question.ts +11 -7
  127. package/src/services/downstream-budget.ts +87 -0
  128. package/src/services/geo/admin.ts +262 -0
  129. package/src/services/geo/bucket.ts +67 -0
  130. package/src/services/geo/cache.ts +57 -0
  131. package/src/services/geo/costs.ts +5 -0
  132. package/src/services/geo/errors.ts +41 -0
  133. package/src/services/geo/fixtures/README.md +17 -0
  134. package/src/services/geo/fixtures/along.json +355 -0
  135. package/src/services/geo/fixtures/autocomplete.json +218 -0
  136. package/src/services/geo/fixtures/geocode.json +129 -0
  137. package/src/services/geo/fixtures/places.json +355 -0
  138. package/src/services/geo/fixtures/reverse.json +61 -0
  139. package/src/services/geo/fixtures/routing.json +57 -0
  140. package/src/services/geo/geoapify-route.ts +93 -0
  141. package/src/services/geo/geoapify.ts +281 -0
  142. package/src/services/geo/ledger.ts +122 -0
  143. package/src/services/geo/map.ts +160 -0
  144. package/src/services/geo/migrate.ts +25 -0
  145. package/src/services/geo/polyline.ts +184 -0
  146. package/src/services/geo/route-chain.ts +145 -0
  147. package/src/services/geo/route-costs.ts +9 -0
  148. package/src/services/geo/route-geometry.ts +39 -0
  149. package/src/services/geo/route-plan.ts +172 -0
  150. package/src/services/geo/route-service.ts +433 -0
  151. package/src/services/geo/service.ts +300 -0
  152. package/src/services/geo/settings.ts +49 -0
  153. package/src/services/help/index.ts +16 -7
  154. package/src/services/help/types.ts +6 -1
  155. package/src/services/identity/invocation-actor.ts +20 -2
  156. package/src/services/identity/issue-invocation.ts +67 -0
  157. package/src/services/identity/key-ring.ts +5 -3
  158. package/src/services/identity/runtime-config.ts +23 -0
  159. package/src/services/index.ts +9 -0
  160. package/src/services/mandates/index.ts +40 -3
  161. package/src/services/network-security.ts +6 -3
  162. package/src/services/network-transport.ts +99 -0
  163. package/src/services/notifications/catalog.ts +3 -2
  164. package/src/services/notifications/runtime.ts +1 -0
  165. package/src/services/notifications/web-push-transport.ts +45 -38
  166. package/src/services/outgoing-mail/dispatcher.ts +6 -1
  167. package/src/services/outgoing-mail/index.ts +55 -1
  168. package/src/services/outgoing-mail/messages.ts +82 -3
  169. package/src/services/outgoing-mail/resolve-host.ts +2 -2
  170. package/src/services/outgoing-mail/retention.ts +13 -1
  171. package/src/services/outgoing-mail/store.ts +36 -6
  172. package/src/services/outgoing-mail/test-send.ts +34 -4
  173. package/src/services/outgoing-mail/transport.ts +1 -1
  174. package/src/services/pdf/gotenberg.ts +11 -38
  175. package/src/services/pdf/template-preview.ts +1 -1
  176. package/src/services/public-http.ts +28 -40
  177. package/src/services/settings/app.ts +4 -1
  178. package/src/services/settings/core-settings.ts +88 -0
  179. package/src/services/settings/defaults.ts +8 -2
  180. package/src/services/settings/errors.ts +11 -0
  181. package/src/services/settings/geo-budget.ts +40 -0
  182. package/src/services/settings/index.ts +4 -3
  183. package/src/services/settings/store.ts +25 -17
  184. package/src/services/weather/geo.ts +29 -111
  185. package/src/services/weather/index.ts +4 -3
  186. package/src/services/weather/locations.ts +8 -5
  187. package/src/services/weather/migrate.ts +1 -0
  188. package/src/services/weather/settings.ts +0 -16
  189. package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
  190. package/src/shared/avatar-art.ts +400 -0
  191. package/src/shared/index.ts +2 -0
  192. package/src/shared/network-address.ts +22 -3
  193. package/src/shared/template-rendering.ts +36 -4
  194. package/src/shared-text/documents.ts +1 -1
  195. package/src/shared-text/index.ts +2 -0
  196. package/src/shared-text/log.ts +51 -3
  197. package/src/shared-text/snapshots.ts +340 -0
  198. package/src/ssr/AdminLayout.tsx +4 -2
  199. package/src/ssr/CloudShell.island.tsx +6 -0
  200. package/src/ssr/Layout.tsx +65 -15
  201. package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
  202. package/src/ssr/LayoutHelp.tsx +3 -2
  203. package/src/ssr/PanelRuntime.island.tsx +11 -7
  204. package/src/ssr/PwaLayout.tsx +2 -2
  205. package/src/ssr/RailApps.tsx +6 -0
  206. package/src/ssr/ShellRuntime.island.tsx +23 -7
  207. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
  208. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +6 -6
  209. package/src/ssr/admin-navigation.ts +1 -0
  210. package/src/ssr/app-badges.ts +6 -1
  211. package/src/ssr/compact-shell.browser-harness.ts +44 -5
  212. package/src/ssr/help.ts +3 -2
  213. package/src/ssr/index.ts +1 -1
  214. package/src/ssr/layout-context.ts +2 -0
  215. package/src/ssr/mobile-menu-history.ts +4 -0
  216. package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
  217. package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
  218. package/src/ssr/page-swap.browser-harness.tsx +177 -0
  219. package/src/ssr/page-swap.ts +28 -0
  220. package/src/ssr/page-trail-runtime.ts +32 -13
  221. package/src/ssr/panel-consent.ts +2 -2
  222. package/src/ssr/panel-slot.browser-harness.tsx +3 -0
  223. package/src/ssr/platform-messages.ts +8 -0
  224. package/src/ssr/runtime.ts +17 -0
  225. package/src/ssr/shell-data.tsx +10 -2
  226. package/src/styles/global.css +70 -6
  227. 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
@@ -16,6 +16,7 @@ import { Hono, type MiddlewareHandler } from "hono";
16
16
  import { z } from "zod";
17
17
  import { readBoundedJson } from "../_internal/bounded-json";
18
18
  import { capabilitySentenceText } from "../_internal/capability-sentences";
19
+ import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
19
20
  import {
20
21
  HELP_SEARCH_MAX_LIMIT,
21
22
  helpAgentMarkdown,
@@ -46,6 +47,7 @@ import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/reg
46
47
  import { type AuthContext, auth, type RequestAuthority, rateLimit, rejectReservedWorkloadCredential, resolveLocale } from "../server";
47
48
  import { preferredLocale } from "../server/locale";
48
49
  import { noTransform } from "../server/middleware/no-transform";
50
+ import { discoverableApps } from "../services/app-discovery";
49
51
  import { createHelpReader, type HelpArticle, type HelpMetadata, type HelpReader, type HelpReaderFactory } from "../services/help";
50
52
  import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
51
53
  import { logger } from "../services/logging";
@@ -493,7 +495,7 @@ const createMcpServer = (
493
495
  oauthScopes: string[] | null,
494
496
  authority?: RequestAuthority,
495
497
  ): Server => {
496
- const registry = dependencies.listApps ?? listApps;
498
+ const registry = async () => discoverableApps(authority ?? null, await (dependencies.listApps ?? listApps)());
497
499
  const capabilityLookup = dependencies.getCapability ?? getCapability;
498
500
  let helpLocale: Promise<string> | undefined;
499
501
  const resolveHelpLocale = () =>
@@ -501,9 +503,10 @@ const createMcpServer = (
501
503
  resolveLocale(request.headers, operatorDefault),
502
504
  ));
503
505
  const helpReader = async () =>
504
- (dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry })))(
506
+ (dependencies.help ?? ((locale, viewer) => createHelpReader(locale, viewer, { listApps: registry, authority: authority ?? null })))(
505
507
  await resolveHelpLocale(),
506
508
  authority?.actor,
509
+ { authority: authority ?? null },
507
510
  );
508
511
  const hasScope = (scope: "read" | "write"): boolean =>
509
512
  oauthScopes === null || oauthScopes.includes(scope) || oauthScopes.includes("admin");
@@ -781,42 +784,47 @@ export const createMcpRoutes = (dependencies: McpRouteDependencies = {}) =>
781
784
  .use("/mcp/v1", dependencies.authenticate ?? mcpAuthentication(dependencies.getAppUrl ?? defaultAppUrl))
782
785
  .use("/mcp/v1", rejectReservedWorkloadCredential)
783
786
  .use("/mcp/v1", dependencies.limit ?? rateLimit())
784
- .all("/mcp/v1", async (c) => {
785
- if (c.req.method !== "POST") {
786
- return new Response(null, { status: 405, headers: { Allow: "POST" } });
787
- }
788
- let request = c.req.raw;
789
- const parsed = await readBoundedJson(request, CAPABILITY_MAX_REQUEST_BYTES);
790
- if (!parsed.ok) {
791
- return c.json(
792
- {
793
- jsonrpc: "2.0",
794
- id: null,
795
- error: {
796
- code: parsed.reason === "too_large" ? -32600 : -32700,
797
- 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
+ },
798
803
  },
799
- },
800
- parsed.reason === "too_large" ? 413 : 400,
801
- );
802
- }
803
- const headers = new Headers(request.headers);
804
- headers.delete("content-length");
805
- request = new Request(request.url, {
806
- method: "POST",
807
- headers,
808
- body: JSON.stringify(parsed.data),
809
- signal: request.signal,
810
- });
811
- const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
812
- const server = createMcpServer(request, dependencies, c.get("oauthScopes") ?? null, auth.getAuthority(c));
813
- await server.connect(transport);
814
- try {
815
- return await transport.handleRequest(request);
816
- } finally {
817
- await transport.close();
818
- await server.close();
819
- }
820
- });
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
+ );
821
829
 
822
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/search.ts CHANGED
@@ -2,7 +2,7 @@ import { Hono, type MiddlewareHandler } from "hono";
2
2
  import { describeRoute, resolver } from "hono-openapi";
3
3
  import { readBoundedJson } from "../_internal/bounded-json";
4
4
  import { resolveCapabilityManifestPresentation } from "../_internal/capabilities";
5
- import { listCapabilities } from "../_internal/registry";
5
+ import { type listApps, listCapabilities } from "../_internal/registry";
6
6
  import type { CapabilityRegistryEntry } from "../contracts";
7
7
  import {
8
8
  CAPABILITY_FRAMEWORK_ERROR_CODES,
@@ -14,9 +14,12 @@ import {
14
14
  import { type AuthContext, auth, expectUserBackedActor, jsonResponse, preferredLocale, requiresAuth, v } from "../server";
15
15
  import { noTransform } from "../server/middleware/no-transform";
16
16
  import { logger } from "../services";
17
+ import { appAccessAuthority } from "../services/app-access";
18
+ import { discoverableAppEntries } from "../services/app-discovery";
17
19
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
18
20
  import { searchInvocationOperation } from "../services/identity/invocation-operations";
19
- import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
21
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
22
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
20
23
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
21
24
  import { LOCALE_HEADER } from "../shared/locale";
22
25
  import { ndjsonLine, ndjsonStream, ndjsonStreamHeaders, startBounded, waitWithin } from "./fanout";
@@ -83,6 +86,7 @@ type SearchRouteDependencies = {
83
86
  listCapabilities?: () => Promise<CapabilityRegistryEntry[]>;
84
87
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
85
88
  authenticate?: MiddlewareHandler<AuthContext>;
89
+ listApps?: typeof listApps;
86
90
  signInvocation?: typeof signInvocationToken;
87
91
  withActiveSigner?: typeof withActiveIdentitySigner;
88
92
  };
@@ -137,7 +141,7 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
137
141
  const invocationAuthority = invocationAuthorityFromRequest(auth.getAuthority(c));
138
142
  let entries: CapabilityRegistryEntry[];
139
143
  try {
140
- entries = await registry();
144
+ entries = await discoverableAppEntries(appAccessAuthority(c), await registry(), dependencies.listApps);
141
145
  } catch (error) {
142
146
  log.warn("Search capability registry unavailable", { error: error instanceof Error ? error.message : String(error) });
143
147
  return c.json(
@@ -237,22 +241,25 @@ export const createSearchRoutes = (dependencies: SearchRouteDependencies = {}) =
237
241
  signedInvocations = await waitWithin(
238
242
  (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
239
243
  "invocation",
240
- (signer) =>
244
+ (signer, db) =>
241
245
  Promise.all(
242
246
  startBounded(
243
247
  active,
244
248
  PROVIDER_CONCURRENCY,
245
249
  (provider) =>
246
- (dependencies.signInvocation ?? signInvocationToken)({
247
- targetAppId: provider.appId,
248
- callingAppId: "core",
249
- operation: searchInvocationOperation,
250
- schemaHash: provider.schemaHash,
251
- authority: invocationAuthority,
252
- requestId,
253
- signer,
254
- issuer: signer.issuer,
255
- }),
250
+ (dependencies.signInvocation ?? signInvocationToken)(
251
+ {
252
+ targetAppId: provider.appId,
253
+ callingAppId: "core",
254
+ operation: searchInvocationOperation,
255
+ schemaHash: provider.schemaHash,
256
+ authority: invocationAuthority,
257
+ requestId,
258
+ signer,
259
+ issuer: signer.issuer,
260
+ },
261
+ { db, groupsAdmin: signer.groupsAdmin },
262
+ ),
256
263
  fanoutSignal,
257
264
  ),
258
265
  ),
@@ -2,10 +2,12 @@ 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 { AppAccessDeniedError, appAccessDeniedBody } from "../server/middleware/app-access";
5
6
  import { noTransform } from "../server/middleware/no-transform";
6
7
  import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
7
8
  import { syncInvocationOperation } from "../services/identity/invocation-operations";
8
- import { normalizeInvocationRequestId, signInvocationToken } from "../services/identity/invocation-token";
9
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
10
+ import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
9
11
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
10
12
 
11
13
  type Dependencies = {
@@ -53,17 +55,20 @@ export const createSyncOpsProxyRoutes = (dependencies: Dependencies = {}) =>
53
55
  try {
54
56
  const signed = await (dependencies.withActiveSigner ?? withActiveIdentitySigner)(
55
57
  "invocation",
56
- (signer) =>
57
- (dependencies.signInvocation ?? signInvocationToken)({
58
- targetAppId: app.id,
59
- callingAppId: "core",
60
- operation,
61
- schemaHash: null,
62
- authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
63
- requestId,
64
- signer,
65
- issuer: signer.issuer,
66
- }),
58
+ (signer, db) =>
59
+ (dependencies.signInvocation ?? signInvocationToken)(
60
+ {
61
+ targetAppId: app.id,
62
+ callingAppId: "core",
63
+ operation,
64
+ schemaHash: null,
65
+ authority: invocationAuthorityFromRequest(auth.getAuthority(c)),
66
+ requestId,
67
+ signer,
68
+ issuer: signer.issuer,
69
+ },
70
+ { db, groupsAdmin: signer.groupsAdmin },
71
+ ),
67
72
  { signal, timeoutMs: 5_000 },
68
73
  );
69
74
  const headers = new Headers({ authorization: `Bearer ${signed.token}`, accept: "application/json" });
@@ -75,7 +80,8 @@ export const createSyncOpsProxyRoutes = (dependencies: Dependencies = {}) =>
75
80
  const result = await readBoundedJson(response, 4 * 1024 * 1024);
76
81
  if (!result.ok) return c.json({ message: "Invalid Sync operations response" }, 502);
77
82
  return Response.json(result.data, { status: response.status, headers: { "cache-control": "no-store" } });
78
- } catch {
83
+ } catch (error) {
84
+ if (error instanceof AppAccessDeniedError) return c.json(appAccessDeniedBody(error.app), 403);
79
85
  return c.json({ message: "Sync operations unavailable" }, 502);
80
86
  }
81
87
  });