@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
@@ -1,9 +1,12 @@
1
1
  import { type AppSkillDefinition, appSkillManifestHash, registerAppSkills, validateAppSkillCatalog } from "../ai/app-skills";
2
+ import { type AppAccessDeclaration, compileAppAccess } from "../contracts/app-access";
2
3
  import { startLiveOutbox } from "../events/live";
3
4
  import { registerHelp } from "../services/help";
4
5
  import { preloadLayoutHelp } from "../ssr/help";
6
+ import { registerPageSwap } from "../ssr/page-swap";
5
7
  import { preloadPanelConsent } from "../ssr/panel-consent";
6
8
  import { invokeCapabilityStream } from "./capability-streams";
9
+ import { withCapabilityRequestTimeout } from "./capability-timeout";
7
10
  import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
8
11
  /**
9
12
  * defineApp() — The single entry point for every cloud app.
@@ -49,8 +52,10 @@ import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
49
52
  import type { Role } from "../contracts/shared";
50
53
  import type { HelpDefinition } from "../server/help";
51
54
  import { getLocale, resolveLocale } from "../server/locale";
55
+ import { requireAppAccess, stopAppAccessConnections } from "../server/middleware/app-access";
52
56
  import { type AuthContext, auth } from "../server/middleware/auth";
53
57
  import { requireInvocation } from "../server/middleware/invocation";
58
+ import "../events/app-access";
54
59
  import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
55
60
  import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
56
61
  import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
@@ -84,15 +89,17 @@ import { appRuntimeMetadata, buildMetadata } from "./build-metadata";
84
89
  import { compileCapabilities, invokeCompiledCapability, reviewCompiledCapability, serializeCapabilityProviderResult } from "./capabilities";
85
90
  import { cliPluginRoutePrefixes, createCliPluginRoutes, validateAppCliModules } from "./cli-plugins";
86
91
  import { FONT_PRELOAD_LINKS } from "./font-preloads";
87
- import { createHeartbeat } from "./heartbeat";
92
+ import { createHeartbeat, createRegistryMembership } from "./heartbeat";
88
93
  import { compileHelp } from "./help";
94
+ import { createHttpDrain } from "./http-drain";
89
95
  import { createPageResponses } from "./page-responses";
90
96
  import { configurePostgresApplicationName } from "./postgres-application-name";
91
97
  import { getProcessSync, startProcessSync } from "./process-sync";
92
98
  import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
93
- import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
99
+ import { appInstanceRegistry, appMembershipLock, appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
94
100
  import { canonicalRoute, isOwnAppPath } from "./registry-validation";
95
101
  import { ensureRuntimeWatcher, getCurrentRuntime, registeredCoreRelease, stopRuntimeWatcher } from "./runtime-watcher";
102
+ import { runSetupExclusively } from "./setup-lock";
96
103
  import { publicFileVersion, servePublicAsset, sharedStylesheetVersion, versionedPublicHref } from "./static-assets";
97
104
  import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
98
105
  import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
@@ -128,6 +135,7 @@ type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePrefe
128
135
  */
129
136
  export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefinitionMap = {}, AppId extends string = string> = {
130
137
  id: AppId;
138
+ access?: AppAccessDeclaration;
131
139
  name: string;
132
140
  icon: string;
133
141
  description: string;
@@ -136,6 +144,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
136
144
  appearance?: AppAppearance;
137
145
  /** URL prefix for SSR asset isolation. Omit for the global `/_ssr/` path (core). */
138
146
  basePath?: string;
147
+ /** Swap Layout content within this app while keeping the rail, tab bar and panel document. Default: false; focusMode pages still load a document. */
148
+ pageSwap?: boolean;
139
149
  /** Base URL as seen by other containers (e.g. "http://app-notebooks:3000"). */
140
150
  baseUrl: string;
141
151
  adminHref?: string;
@@ -529,6 +539,8 @@ export const defineApp = <
529
539
  // cannot override.
530
540
  const ssr = Object.assign(
531
541
  createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
542
+ if (opts.pageSwap)
543
+ registerPageSwap(c, isDevelopment ? String(developmentStamp) : `${buildMetadata.version}:${buildMetadata.release}`);
532
544
  c.get("page").lang = getLocale(c);
533
545
  c.get("page").themePreference = readThemePreferenceFromCookieHeader(c.req.header("Cookie"));
534
546
  const user = c.get("user");
@@ -577,13 +589,19 @@ export const defineApp = <
577
589
  openapi: opts.openapi,
578
590
  cli: opts.cli,
579
591
  };
580
- const meta: AppMeta = { ...baseMeta, presentation: compileAppPresentation(baseMeta, opts.presentation) };
592
+ const meta: AppMeta = {
593
+ ...baseMeta,
594
+ access: compileAppAccess(opts.access),
595
+ presentation: compileAppPresentation(baseMeta, opts.presentation),
596
+ };
581
597
 
582
598
  // ── 3. start() — builds and boots the Hono server ────────────────────
583
599
  const start = async (startOpts: StartOptions): Promise<StartResult> => {
584
600
  const port = startOpts.port ?? 3000;
585
601
  const baseUrl = opts.baseUrl;
586
602
  const log = logger("app");
603
+ const http = createHttpDrain();
604
+ if (!meta.access) log.warn("Application has no access declaration; legacy entry and discovery rules remain active", { appId: meta.id });
587
605
 
588
606
  // Settings are encrypted at rest, so every container needs the secret. It
589
607
  // is only read on the first decrypt, which meant a container without it
@@ -631,6 +649,7 @@ export const defineApp = <
631
649
  // Registry entry
632
650
  const entry: AppRegistryEntry = {
633
651
  id: meta.id,
652
+ access: meta.access,
634
653
  name: meta.name,
635
654
  icon: meta.icon,
636
655
  description: meta.description,
@@ -686,9 +705,16 @@ export const defineApp = <
686
705
  cliModules: cliModules.length > 0 ? cliModules : undefined,
687
706
  };
688
707
 
708
+ // Both shared entries follow one process membership; creating it touches no Sync resources.
709
+ const membership = createRegistryMembership(meta.id, {
710
+ registry: appInstanceRegistry,
711
+ lock: appMembershipLock,
712
+ startedAt: entry.startedAt,
713
+ });
689
714
  // Heartbeat
690
715
  const heartbeat = createHeartbeat(meta.id, entry, {
691
716
  registry: appRegistry(),
717
+ membership,
692
718
  beforeWrite: async () => {
693
719
  if (compiledHelp) await registerHelp(compiledHelp.corpus);
694
720
  if (appSkills && skillsManifestHash) await registerAppSkills(meta.id, appSkills, skillsManifestHash);
@@ -717,6 +743,7 @@ export const defineApp = <
717
743
  ? createHeartbeat(meta.id, capabilityEntry, {
718
744
  key: `capabilities/${meta.id}`,
719
745
  registry: capabilityRegistry(),
746
+ membership,
720
747
  onError: (error) =>
721
748
  log.error("Capability registry heartbeat failed", {
722
749
  appId: meta.id,
@@ -757,6 +784,7 @@ export const defineApp = <
757
784
  // Framework-owned mounts answer before the app's router ever runs, so the
758
785
  // app middleware cannot report their template. Without this, every hashed
759
786
  // island chunk would land in telemetry as its own route.
787
+ const invocationApp = { getApp: async (id: string) => (id === meta.id ? meta : null) };
760
788
  const server = new Hono<AuthContext>()
761
789
  .use("*", routeTemplate)
762
790
  .get(APP_READINESS_PATH, () => appReadinessResponse(meta.id))
@@ -765,11 +793,14 @@ export const defineApp = <
765
793
  .all("/public/*", servePublicAsset(isDevelopment, sharedStylesheetVersion(buildMetadata.release)))
766
794
  .use(
767
795
  "/_internal/sync/*",
768
- requireInvocation((c) => ({
769
- targetAppId: meta.id,
770
- operation: syncInvocationOperation(c.req.method, new URL(c.req.url).pathname.slice("/_internal/sync".length)),
771
- schemaHash: null,
772
- })),
796
+ requireInvocation(
797
+ (c) => ({
798
+ targetAppId: meta.id,
799
+ operation: syncInvocationOperation(c.req.method, new URL(c.req.url).pathname.slice("/_internal/sync".length)),
800
+ schemaHash: null,
801
+ }),
802
+ invocationApp,
803
+ ),
773
804
  )
774
805
  .use("/_internal/sync/*", auth.requireOAuthScope("admin"))
775
806
  .use("/_internal/sync/*", async (c, next) => {
@@ -778,79 +809,73 @@ export const defineApp = <
778
809
  })
779
810
  .route("/_internal/sync", createSyncOpsRoutes(getProcessSync));
780
811
 
781
- if (opts.cli && cliModules.length > 0) server.route(CLOUD_CLI_PLUGIN_ROUTE, createCliPluginRoutes(meta.id, opts.cli));
782
-
783
- if (compiledHelp) {
784
- const pageBase = compiledHelp.summary.pageBase;
785
- const centralPageBase = `/help/apps/${encodeURIComponent(meta.id)}`;
786
- server.get(pageBase, (c) => c.redirect(centralPageBase, 302));
787
- server.get(`${pageBase}/:topic`, (c) => c.redirect(`${centralPageBase}/${encodeURIComponent(c.req.param("topic"))}`, 302));
788
- }
789
-
790
812
  if (compiledCapabilities) {
791
- const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") => {
792
- // Framework-owned endpoints run before the app's settings middleware,
793
- // so the operator default is read directly instead of via a snapshot.
794
- const requestLocale = resolveLocale(c.req.raw.headers, await get<string>("app.locale"));
795
- const messages = capabilityMessages(requestLocale);
796
- const parsedBody = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
797
- if (!parsedBody.ok) {
798
- const message = parsedBody.reason === "too_large" ? messages.requestTooLarge : messages.requestBodyJson;
799
- return c.json({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, 400);
800
- }
801
- if (
802
- typeof parsedBody.data !== "object" ||
803
- parsedBody.data === null ||
804
- Array.isArray(parsedBody.data) ||
805
- !Object.hasOwn(parsedBody.data, "input") ||
806
- Object.keys(parsedBody.data).length !== 1
807
- ) {
808
- return c.json(
809
- {
810
- code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed,
811
- message: messages.requestInputOnly,
813
+ const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
814
+ withCapabilityRequestTimeout(c, kind, async () => {
815
+ // Framework-owned endpoints run before the app's settings middleware,
816
+ // so the operator default is read directly instead of via a snapshot.
817
+ const requestLocale = resolveLocale(c.req.raw.headers, await get<string>("app.locale"));
818
+ const messages = capabilityMessages(requestLocale);
819
+ const parsedBody = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
820
+ if (!parsedBody.ok) {
821
+ const message = parsedBody.reason === "too_large" ? messages.requestTooLarge : messages.requestBodyJson;
822
+ return c.json({ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed, message }, 400);
823
+ }
824
+ if (
825
+ typeof parsedBody.data !== "object" ||
826
+ parsedBody.data === null ||
827
+ Array.isArray(parsedBody.data) ||
828
+ !Object.hasOwn(parsedBody.data, "input") ||
829
+ Object.keys(parsedBody.data).length !== 1
830
+ ) {
831
+ return c.json(
832
+ {
833
+ code: CAPABILITY_FRAMEWORK_ERROR_CODES.validationFailed,
834
+ message: messages.requestInputOnly,
835
+ },
836
+ 400,
837
+ );
838
+ }
839
+ const body = parsedBody.data as { input: unknown };
840
+ const actor = c.get("actor");
841
+ const user = actor.kind === "user" ? actor.user : actor.delegatedUser;
842
+ const idempotencyKey = c.req.header("idempotency-key") || undefined;
843
+ const invocation = {
844
+ compiled: compiledCapabilities,
845
+ localId: c.req.param("capabilityId") ?? "",
846
+ input: body.input,
847
+ expectedSchemaHash: c.req.header("x-cloud-capability-schema-hash") ?? null,
848
+ context: {
849
+ authority: auth.getAuthority(c),
850
+ actor,
851
+ accessSubject: c.get("accessSubject"),
852
+ user,
853
+ idempotencyKey,
854
+ locale: requestLocale,
855
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
856
+ origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
857
+ signal: c.req.raw.signal,
812
858
  },
813
- 400,
814
- );
815
- }
816
- const body = parsedBody.data as { input: unknown };
817
- const actor = c.get("actor");
818
- const user = actor.kind === "user" ? actor.user : actor.delegatedUser;
819
- const idempotencyKey = c.req.header("idempotency-key") || undefined;
820
- const invocation = {
821
- compiled: compiledCapabilities,
822
- localId: c.req.param("capabilityId") ?? "",
823
- input: body.input,
824
- expectedSchemaHash: c.req.header("x-cloud-capability-schema-hash") ?? null,
825
- context: {
826
- actor,
827
- accessSubject: c.get("accessSubject"),
828
- user,
829
- idempotencyKey,
830
- locale: requestLocale,
831
- requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
832
- origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
833
- signal: c.req.raw.signal,
834
- },
835
- onUnexpectedError: (error: unknown) =>
836
- log.error(kind === "review" ? "Capability review failed" : "Capability execution failed", {
837
- appId: meta.id,
838
- kind,
839
- capabilityId: c.req.param("capabilityId") ?? "",
840
- error: error instanceof Error ? error.message : String(error),
841
- }),
842
- };
843
- const result =
844
- kind === "review" ? await reviewCompiledCapability(invocation) : await invokeCompiledCapability({ ...invocation, kind });
845
- const operation = kind === "action" ? compiledCapabilities.actions.get(invocation.localId) : undefined;
846
- const serialized = serializeCapabilityProviderResult(result, {
847
- nonIdempotentAction: kind === "action" && operation?.manifest.idempotency === "none",
848
- });
849
- return new Response(serialized.body, {
850
- status: serialized.status,
851
- headers: { "content-type": "application/json" },
859
+ onUnexpectedError: (error: unknown) =>
860
+ log.error(kind === "review" ? "Capability review failed" : "Capability execution failed", {
861
+ appId: meta.id,
862
+ kind,
863
+ capabilityId: c.req.param("capabilityId") ?? "",
864
+ error: error instanceof Error ? error.message : String(error),
865
+ }),
866
+ };
867
+ const result =
868
+ kind === "review" ? await reviewCompiledCapability(invocation) : await invokeCompiledCapability({ ...invocation, kind });
869
+ const operation = kind === "action" ? compiledCapabilities.actions.get(invocation.localId) : undefined;
870
+ const serialized = serializeCapabilityProviderResult(result, {
871
+ review: kind === "review",
872
+ nonIdempotentAction: kind === "action" && operation?.manifest.idempotency === "none",
873
+ });
874
+ return new Response(serialized.body, {
875
+ status: serialized.status,
876
+ headers: { "content-type": "application/json" },
877
+ });
852
878
  });
853
- };
854
879
  const capabilityAuth = (kind: "queries" | "actions", review = false) =>
855
880
  requireInvocation((c) => {
856
881
  const localId = c.req.param("capabilityId") ?? "";
@@ -869,7 +894,7 @@ export const defineApp = <
869
894
  operation: invocationOperation,
870
895
  schemaHash: operation.manifest.schemaHash,
871
896
  };
872
- });
897
+ }, invocationApp);
873
898
  for (const kind of ["queries", "actions"] as const) {
874
899
  server.post(
875
900
  `/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
@@ -884,6 +909,7 @@ export const defineApp = <
884
909
  verb: c.req.param("verb") ?? "",
885
910
  request: c.req.raw,
886
911
  context: {
912
+ authority: auth.getAuthority(c),
887
913
  actor,
888
914
  accessSubject: c.get("accessSubject"),
889
915
  user: actor.kind === "user" ? actor.user : actor.delegatedUser,
@@ -923,7 +949,7 @@ export const defineApp = <
923
949
  const widgetId = c.req.param("widgetId") ?? "";
924
950
  if (!declaredWidgetIds.has(widgetId) || !startOpts.widgets?.[widgetId]) return null;
925
951
  return { targetAppId: meta.id, operation: widgetInvocationOperation(widgetId), schemaHash: null };
926
- }),
952
+ }, invocationApp),
927
953
  auth.requireOAuthScope("read", "admin"),
928
954
  runtimeMiddleware(),
929
955
  settingsMiddleware(),
@@ -938,10 +964,31 @@ export const defineApp = <
938
964
  );
939
965
  }
940
966
 
967
+ cleanup.push(() => stopAppAccessConnections());
968
+ server.use(
969
+ "*",
970
+ requireAppAccess(meta, (c, message) =>
971
+ ssr.error(c, 403, {
972
+ title: "No access",
973
+ description: message,
974
+ layout: "minimal",
975
+ }),
976
+ ),
977
+ );
978
+
979
+ if (opts.cli && cliModules.length > 0) server.route(CLOUD_CLI_PLUGIN_ROUTE, createCliPluginRoutes(meta.id, opts.cli));
980
+
981
+ if (compiledHelp) {
982
+ const pageBase = compiledHelp.summary.pageBase;
983
+ const centralPageBase = `/help/apps/${encodeURIComponent(meta.id)}`;
984
+ server.get(pageBase, (c) => c.redirect(centralPageBase, 302));
985
+ server.get(`${pageBase}/:topic`, (c) => c.redirect(`${centralPageBase}/${encodeURIComponent(c.req.param("topic"))}`, 302));
986
+ }
987
+
941
988
  // OpenAPI spec mount. Registered on the framework server (before the
942
989
  // user-fetch catch-all below) so it bypasses any auth / rate-limit
943
990
  // middleware on the api router — the spec must stay reachable without
944
- // a session for `app-api-docs` to render it.
991
+ // a session for legacy apps. Declared app access is enforced above.
945
992
  //
946
993
  // The `servers` override is load-bearing: hono-openapi walks the
947
994
  // BARE api router and emits paths relative to its own root (e.g.
@@ -997,7 +1044,8 @@ export const defineApp = <
997
1044
 
998
1045
  if (!startOpts.skipSetup && startOpts.lifecycle?.setup) {
999
1046
  log.info(`Setup: ${meta.id}`);
1000
- await startOpts.lifecycle.setup(cloudCtx);
1047
+ const setup = startOpts.lifecycle.setup;
1048
+ await runSetupExclusively(meta.id, () => setup(cloudCtx));
1001
1049
  }
1002
1050
  if (meta.id === "core") await startOutbox();
1003
1051
 
@@ -1020,18 +1068,36 @@ export const defineApp = <
1020
1068
  log.info(`Registered "${meta.id}"`, { baseUrl });
1021
1069
 
1022
1070
  // Graceful shutdown
1023
- let stopping = false;
1024
- const shutdown = async () => {
1025
- if (stopping) return;
1026
- stopping = true;
1027
- log.info(`Stopping: ${meta.id}`);
1028
- await stopStartedResources();
1029
- };
1071
+ let shutdownPromise: Promise<void> | undefined;
1072
+ const shutdown = () =>
1073
+ (shutdownPromise ??= (async () => {
1074
+ const drained = http.stop();
1075
+ log.info(`Stopping: ${meta.id}`);
1076
+ try {
1077
+ await drained;
1078
+ } finally {
1079
+ await stopStartedResources();
1080
+ }
1081
+ })());
1030
1082
 
1031
- process.on("SIGTERM", () => void shutdown().then(() => process.exit(0)));
1032
- process.on("SIGINT", () => void shutdown().then(() => process.exit(0)));
1083
+ const stopAndExit = () =>
1084
+ void shutdown().then(
1085
+ () => process.exit(0),
1086
+ (error: unknown) => {
1087
+ log.error("HTTP shutdown failed", { appId: meta.id, error: error instanceof Error ? error.message : String(error) });
1088
+ process.exit(1);
1089
+ },
1090
+ );
1091
+ process.on("SIGTERM", stopAndExit);
1092
+ process.on("SIGINT", stopAndExit);
1033
1093
 
1034
- return { hostname: "0.0.0.0", port, development: isDevelopment, idleTimeout: REQUEST_IDLE_TIMEOUT_SECONDS, fetch: server.fetch };
1094
+ return {
1095
+ hostname: "0.0.0.0",
1096
+ port,
1097
+ development: isDevelopment,
1098
+ idleTimeout: REQUEST_IDLE_TIMEOUT_SECONDS,
1099
+ fetch: (request, bunServer) => http.fetch(request, bunServer, (request) => server.fetch(request, bunServer)),
1100
+ };
1035
1101
  } catch (error) {
1036
1102
  await stopStartedResources();
1037
1103
  throw error;