@k2b/cloud 0.42.0 → 0.44.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 (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,10 +1,18 @@
1
1
  import { type AppSkillDefinition, appSkillManifestHash, registerAppSkills, validateAppSkillCatalog } from "../ai/app-skills";
2
+ import { createOwnedAppAccessRoutes } from "../api/app-access";
3
+ import { createAppEventDeliveryRoutes } from "../api/app-event-delivery";
4
+ import { createResourceAppAccessRoutes } from "../api/app-resource-access";
2
5
  import { type AppAccessDeclaration, compileAppAccess } from "../contracts/app-access";
6
+ import { startAppEventDelivery } from "../events/app-event-delivery";
7
+ import { bindAppEvents } from "../events/app-events";
3
8
  import { startLiveOutbox } from "../events/live";
4
9
  import { registerHelp } from "../services/help";
5
10
  import { preloadLayoutHelp } from "../ssr/help";
6
11
  import { registerPageSwap } from "../ssr/page-swap";
7
12
  import { preloadPanelConsent } from "../ssr/panel-consent";
13
+ import { appAccessManagementPath } from "./app-access-path";
14
+ import { bindResourceAppAccess, RESOURCE_ACCESS_PATH } from "./app-access-resources";
15
+ import { registerApplicationPackage, resolveApplicationPackage } from "./application-package";
8
16
  import { invokeCapabilityStream } from "./capability-streams";
9
17
  import { withCapabilityRequestTimeout } from "./capability-timeout";
10
18
  import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
@@ -17,7 +25,7 @@ import { bindProcessApplicationId, clearProcessApplicationId } from "./process-i
17
25
 
18
26
  import { resolve } from "node:path";
19
27
  import { fileURLToPath } from "node:url";
20
- import type { SsrConfig } from "@k2b/ssr";
28
+ import type { HtmlFn, SsrConfig } from "@k2b/ssr";
21
29
  import { createConfig as createSsrConfig } from "@k2b/ssr";
22
30
  import { routes } from "@k2b/ssr/hono";
23
31
  import type { Sync } from "@k2b/sync";
@@ -26,6 +34,7 @@ import { generateSpecs } from "hono-openapi";
26
34
  import { CLOUD_CLI_PLUGIN_ROUTE } from "../cli/plugin";
27
35
  import { env } from "../config/env";
28
36
  import type {
37
+ AppAdminArea,
29
38
  AppAdminNavigationGroup,
30
39
  AppAppearance,
31
40
  AppCliModules,
@@ -44,9 +53,11 @@ import {
44
53
  type CapabilityDefinitions,
45
54
  CapabilityOriginSchema,
46
55
  } from "../contracts/capabilities";
56
+ import { type AppInstallable, installableContains, validateInstallable } from "../contracts/installable";
47
57
  import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
48
58
  import { type PlatformPermission, PlatformPermissionSchema } from "../contracts/outgoing-mail";
49
59
  import { isPwaPartId, PWA_AUTH_PATH, PWA_CANVAS_COLORS, PWA_MANIFEST_PATH, PWA_SHELL_APP_ID } from "../contracts/pwa";
60
+ import { rememberedPwaAddress } from "../contracts/pwa-location";
50
61
  import type { AppRegistryEntry } from "../contracts/registry";
51
62
  import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
52
63
  import type { Role } from "../contracts/shared";
@@ -56,6 +67,7 @@ import { requireAppAccess, stopAppAccessConnections } from "../server/middleware
56
67
  import { type AuthContext, auth } from "../server/middleware/auth";
57
68
  import { requireInvocation } from "../server/middleware/invocation";
58
69
  import "../events/app-access";
70
+ import { createNestedCapabilityQuery } from "../capabilities/nested";
59
71
  import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
60
72
  import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
61
73
  import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
@@ -92,6 +104,8 @@ import { FONT_PRELOAD_LINKS } from "./font-preloads";
92
104
  import { createHeartbeat, createRegistryMembership } from "./heartbeat";
93
105
  import { compileHelp } from "./help";
94
106
  import { createHttpDrain } from "./http-drain";
107
+ import { createInstallableRoutes } from "./installable-assets";
108
+ import { installableHead, installableThemeScript } from "./installable-head";
95
109
  import { createPageResponses } from "./page-responses";
96
110
  import { configurePostgresApplicationName } from "./postgres-application-name";
97
111
  import { getProcessSync, startProcessSync } from "./process-sync";
@@ -117,9 +131,18 @@ export type PageOptions = {
117
131
  lang?: string;
118
132
  /** Renders the document as a page of the installable mobile app; set by `PwaLayout`. */
119
133
  pwa?: { name: string };
134
+ /** A successful app document may keep its same-document address changes. */
135
+ pwaAddress?: string;
136
+ /** The actual rendering application, including on error pages. */
137
+ pwaOwner?: string;
120
138
  };
121
139
 
122
- type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePreference?: CloudThemePreference; appAccent?: string };
140
+ type ResolvedPageOptions = PageOptions & {
141
+ installable?: AppInstallable;
142
+ performanceRoute?: string;
143
+ themePreference?: CloudThemePreference;
144
+ appAccent?: string;
145
+ };
123
146
 
124
147
  // ── Public types ────────────────────────────────────────────────────────────
125
148
 
@@ -135,6 +158,8 @@ type ResolvedPageOptions = PageOptions & { performanceRoute?: string; themePrefe
135
158
  */
136
159
  export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefinitionMap = {}, AppId extends string = string> = {
137
160
  id: AppId;
161
+ /** Shared object-store package. Defaults to the app ID. */
162
+ package?: string;
138
163
  access?: AppAccessDeclaration;
139
164
  name: string;
140
165
  icon: string;
@@ -151,7 +176,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
151
176
  adminHref?: string;
152
177
  /** Multi-link admin navigation contributed by this app. */
153
178
  adminNav?: ReadonlyArray<{
154
- section?: "ai";
179
+ section?: AppAdminArea;
155
180
  id?: string;
156
181
  label: string;
157
182
  links: ReadonlyArray<{ label: string; href: string; icon: string }>;
@@ -167,7 +192,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
167
192
  * Same-origin path of a `GET` route this app serves below one of its
168
193
  * `routes`. After the page loads, the shell reads it with the person's
169
194
  * session and shows the returned `AppNavBadge` count on the app's icon in
170
- * the rail and the app grid. Protect it like any other route.
195
+ * the rail, the phone bar, and the app grid. Protect it like any other route.
171
196
  */
172
197
  badge?: string;
173
198
  };
@@ -197,6 +222,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
197
222
  legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
198
223
  /** Static destinations for the global search UI; no capabilities are created. */
199
224
  searchLinks?: readonly AppSearchLink[];
225
+ /** One standalone App-PWA at a canonical, declared own /app/<id>/<surface>/ path. */
226
+ installable?: AppInstallable;
200
227
  /**
201
228
  * Preview: pages of this app in the installable mobile app. Declaring it adds
202
229
  * `/pwa/<id>` to the gateway routes and lists the app in the mobile app,
@@ -206,6 +233,17 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
206
233
  pwa?: {
207
234
  /** Coarse visibility in the mobile app, like `nav.requiresRoles`. Routes and services still authorize. */
208
235
  requiresRoles?: Role[];
236
+ /**
237
+ * Same-origin path of a `GET` route this app serves that returns an `AppNavBadge`: the count beside the part in
238
+ * the app's sidebar, such as unread messages. Like `nav.badge`, it must be one of the app's own paths.
239
+ */
240
+ badge?: string;
241
+ /**
242
+ * Same-origin path of a `GET` route this app serves that returns a `PwaPartSummary`: one line under the part on
243
+ * Start, such as "5 tasks for you", in the reader's locale, and its count beside the part in the sidebar when the
244
+ * part has no `badge`. It may be the badge's route. It must be one of the app's own paths.
245
+ */
246
+ summary?: string;
209
247
  };
210
248
  /**
211
249
  * Dashboard widget endpoints this app exposes. Each entry references an
@@ -226,8 +264,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
226
264
  *
227
265
  * Apps with non-standard URLs (core's `/auth`, `/me`, `/legal/*`, `/impressum`;
228
266
  * oauth's `/oauth`, `/.well-known/...`) list whatever
229
- * top-level paths they own. The gateway is dumb — it just builds a
230
- * prefix-trie from these strings.
267
+ * top-level paths they own. The gateway builds a prefix trie and enforces
268
+ * /app/<id> ownership and the reserved /pwa paths.
231
269
  */
232
270
  routes: readonly string[];
233
271
  /** Platform services requested by this application, visible to administrators. */
@@ -378,7 +416,21 @@ const validateAppPwaPart = (opts: Pick<AppOptions, "id" | "routes" | "pwa">): Ap
378
416
  if (!isPwaPartId(opts.id)) {
379
417
  throw new Error(`App "${opts.id}" cannot declare pwa; a part needs a plain id other than pwa, settings and offline`);
380
418
  }
381
- return { href: `/pwa/${opts.id}`, requiresRoles: opts.pwa.requiresRoles ? [...opts.pwa.requiresRoles] : undefined };
419
+ const { badge, summary } = opts.pwa;
420
+ for (const [field, path] of [
421
+ ["badge", badge],
422
+ ["summary", summary],
423
+ ] as const) {
424
+ if (path !== undefined && !isOwnAppPath(path, [...opts.routes, `/pwa/${opts.id}`])) {
425
+ throw new Error(`App "${opts.id}" declares pwa.${field} "${path}"; it must be a same-origin path below one of its routes`);
426
+ }
427
+ }
428
+ return {
429
+ href: `/pwa/${opts.id}`,
430
+ requiresRoles: opts.pwa.requiresRoles ? [...opts.pwa.requiresRoles] : undefined,
431
+ ...(badge ? { badge } : {}),
432
+ ...(summary ? { summary } : {}),
433
+ };
382
434
  };
383
435
 
384
436
  /** The badge route is read by the browser through the gateway, so it must be one of this app's own paths. */
@@ -430,6 +482,11 @@ export const defineApp = <
430
482
  const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
431
483
  const cliModules = validateAppCliModules(opts.id, opts.cli);
432
484
  const pwaPart = validateAppPwaPart(opts);
485
+ const packageName = resolveApplicationPackage(opts.id, opts.package);
486
+ const installable = opts.installable === undefined ? undefined : validateInstallable(opts.id, opts.routes, opts.installable);
487
+ if (installable === null)
488
+ throw new Error(`App "${opts.id}" declares invalid installable; use a canonical own /app/<id>/<surface>/ path covered by routes`);
489
+ const requestInstallable = (path: string) => (installable && installableContains(installable.path, path) ? installable : undefined);
433
490
  validateAppNavBadge(opts);
434
491
  const panel = validateAppPanel(opts);
435
492
 
@@ -444,7 +501,11 @@ export const defineApp = <
444
501
 
445
502
  // ── 1. SSR config ─────────────────────────────────────────────────────
446
503
  const rootDir = opts.appRoot ?? process.cwd();
447
- const { config, plugin, html } = createSsrConfig<ResolvedPageOptions>({
504
+ const {
505
+ config,
506
+ plugin,
507
+ html: renderHtml,
508
+ } = createSsrConfig<ResolvedPageOptions>({
448
509
  dev: isDevelopment,
449
510
  verbose: true,
450
511
  rootDir,
@@ -453,7 +514,19 @@ export const defineApp = <
453
514
  // Every island instance already mounts inside its own error boundary; this
454
515
  // only gives the fallback Cloud's look and the reader's language.
455
516
  errorFallback: fileURLToPath(new URL("../ssr/IslandError.tsx", import.meta.url)),
456
- template: ({ body, scripts, title, description, theme, themePreference, lang, performanceRoute, pwa, appAccent }) => {
517
+ template: ({
518
+ body,
519
+ scripts,
520
+ title,
521
+ description,
522
+ theme,
523
+ themePreference,
524
+ lang,
525
+ performanceRoute,
526
+ pwa,
527
+ appAccent,
528
+ installable: pageInstallable,
529
+ }) => {
457
530
  // A URL changes only when what it names changes: the app's own files by their content, Core's shared sheets by
458
531
  // the release of the Core build in the registry, so every app links them under one URL per Core build.
459
532
  const sharedVersion = isDevelopment ? String(developmentStamp) : sharedStylesheetVersion(registeredCoreRelease());
@@ -471,33 +544,35 @@ export const defineApp = <
471
544
  // status bar. No status-bar style: `black-translucent` lays a blur band
472
545
  // over the top edge on iOS 26.
473
546
  const canvas = resolved ? PWA_CANVAS_COLORS[resolved] : undefined;
474
- const head = pwa
475
- ? {
476
- html: canvas ? ` style="background-color:${canvas}"` : "",
477
- viewport: "width=device-width, initial-scale=1, viewport-fit=cover",
478
- app: `${
479
- canvas
480
- ? `<meta name="theme-color" content="${canvas}">`
481
- : `<meta name="theme-color" media="(prefers-color-scheme: light)" content="${PWA_CANVAS_COLORS.light}">
547
+ const head = pageInstallable
548
+ ? installableHead(pageInstallable, resolved)
549
+ : pwa
550
+ ? {
551
+ html: canvas ? ` style="background-color:${canvas}"` : "",
552
+ viewport: "width=device-width, initial-scale=1, viewport-fit=cover",
553
+ app: `${
554
+ canvas
555
+ ? `<meta name="theme-color" content="${canvas}">`
556
+ : `<meta name="theme-color" media="(prefers-color-scheme: light)" content="${PWA_CANVAS_COLORS.light}">
482
557
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="${PWA_CANVAS_COLORS.dark}">
483
558
  <style data-cloud-canvas>html{background-color:${PWA_CANVAS_COLORS.light}}@media (prefers-color-scheme: dark){html{background-color:${PWA_CANVAS_COLORS.dark}}}</style>`
484
- }
559
+ }
485
560
  <link rel="manifest" href="${PWA_MANIFEST_PATH}">
486
561
  <link rel="apple-touch-icon" href="/branding/apple-touch-icon.png">
487
562
  <meta name="apple-mobile-web-app-title" content="${escapeHtml(pwa.name)}">`,
488
- // The web cross-fades between pages; an app page swaps at once. The browser ignores taps while a
489
- // cross-document transition runs, so a tab tap right after the last one would be lost. After
490
- // global.css, whose `navigation: auto` it overrides.
491
- styles: `
563
+ // The web cross-fades between pages; an app page swaps at once. The browser ignores taps while a
564
+ // cross-document transition runs, so a tab tap right after the last one would be lost. After
565
+ // global.css, whose `navigation: auto` it overrides.
566
+ styles: `
492
567
  <style data-cloud-app-navigation>@view-transition{navigation:none}</style>`,
493
- }
494
- : {
495
- html: "",
496
- viewport: "width=device-width, initial-scale=1.0",
497
- app: `<meta name="theme-color" content="#09090b">
568
+ }
569
+ : {
570
+ html: "",
571
+ viewport: "width=device-width, initial-scale=1.0",
572
+ app: `<meta name="theme-color" content="#09090b">
498
573
  <meta name="mobile-web-app-capable" content="yes">`,
499
- styles: "",
500
- };
574
+ styles: "",
575
+ };
501
576
  // The inline layer statement fixes the cascade order before any
502
577
  // stylesheet declares a layer. Tailwind's `properties` layer resets
503
578
  // `--tw-*` in browsers without `@property` and must stay lowest.
@@ -516,7 +591,7 @@ export const defineApp = <
516
591
  <link rel="stylesheet" href="${versionedPublicHref("/public/tabler-icons.css", sharedVersion)}">
517
592
  <link rel="stylesheet" href="${versionedPublicHref(appStylesheet, appVersion)}">
518
593
  <link rel="stylesheet" href="${versionedPublicHref("/public/global.css", sharedVersion)}">${head.styles}
519
- <script>${themeBootstrapScript}</script>
594
+ <script>${themeBootstrapScript}</script>${pageInstallable ? `<script>${installableThemeScript(pageInstallable)}</script>` : ""}
520
595
  </head>
521
596
  <body class="k2b-ui${accentStyle ? " app-accent-scope" : ""}"${accentStyle ? ` style="${escapeHtml(accentStyle)}"` : ""} data-k2b-app-workspace-controller="global">
522
597
  ${body}
@@ -527,6 +602,13 @@ export const defineApp = <
527
602
  },
528
603
  });
529
604
 
605
+ const html: HtmlFn<ResolvedPageOptions> = async (render, page) => {
606
+ if (!page?.installable) return renderHtml(render, page);
607
+ // Load JSX only after the standard preload/build has installed the SSR plugin.
608
+ const { appPwaDocument } = await import("../ssr/app-pwa-document");
609
+ return renderHtml(appPwaDocument(render, page.installable, normalizeLocale(page.lang)), page);
610
+ };
611
+
530
612
  // Pass PageOptions explicitly so c.get("page") in apps' SSR handlers is
531
613
  // typed as Partial<PageOptions> (with title/description/theme), not the
532
614
  // bare `object` fallback the constraint would otherwise produce.
@@ -539,7 +621,8 @@ export const defineApp = <
539
621
  // cannot override.
540
622
  const ssr = Object.assign(
541
623
  createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
542
- if (opts.pageSwap)
624
+ c.get("page").installable = requestInstallable(c.req.path);
625
+ if (opts.pageSwap && !c.get("page").installable)
543
626
  registerPageSwap(c, isDevelopment ? String(developmentStamp) : `${buildMetadata.version}:${buildMetadata.release}`);
544
627
  c.get("page").lang = getLocale(c);
545
628
  c.get("page").themePreference = readThemePreferenceFromCookieHeader(c.req.header("Cookie"));
@@ -548,7 +631,14 @@ export const defineApp = <
548
631
  user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
549
632
  // Pages of the mobile app render in PwaLayout, which shows no announcements, Help, or rail; reading them would
550
633
  // only delay every tab switch. Core's pages below /pwa/_auth, such as its not-found page, keep the web layout.
551
- if (isAppPagePath(c.req.path)) return;
634
+ if (isAppPagePath(c.req.path)) {
635
+ c.get("page").pwaOwner = opts.id;
636
+ const url = new URL(c.req.url);
637
+ const address = rememberedPwaAddress(url.pathname + url.search);
638
+ if (user && address && c.newResponse(null).status < 400) c.get("page").pwaAddress = address;
639
+ return;
640
+ }
641
+ if (c.get("page").installable) return;
552
642
  await Promise.all([
553
643
  preloadLayoutAnnouncements(c),
554
644
  preloadLayoutHelp(c),
@@ -556,11 +646,12 @@ export const defineApp = <
556
646
  user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
557
647
  ]);
558
648
  }),
559
- createPageResponses(html),
649
+ createPageResponses(html, opts.id, (c) => ({ installable: requestInstallable(c.req.path) })),
560
650
  );
561
651
 
562
652
  // ── 2. Meta ───────────────────────────────────────────────────────────
563
653
  const baseMeta: AppMeta = {
654
+ package: packageName,
564
655
  id: opts.id,
565
656
  platformPermissions: opts.platformPermissions
566
657
  ? opts.platformPermissions.map((permission) => PlatformPermissionSchema.parse(permission))
@@ -583,6 +674,7 @@ export const defineApp = <
583
674
  legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
584
675
  searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
585
676
  pwa: pwaPart,
677
+ installable: installable ?? undefined,
586
678
  panel,
587
679
  widgets: compileWidgetDeclarations(opts.id, opts.widgets),
588
680
  settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
@@ -629,7 +721,8 @@ export const defineApp = <
629
721
  }
630
722
  })());
631
723
  try {
632
- bindProcessApplicationId(meta.id, meta.platformPermissions);
724
+ await registerApplicationPackage(meta.id, packageName);
725
+ bindProcessApplicationId(meta.id, meta.platformPermissions, packageName);
633
726
  cleanup.unshift(clearProcessApplicationId);
634
727
  const startedAt = Date.now();
635
728
 
@@ -637,7 +730,8 @@ export const defineApp = <
637
730
  // derive the spec from. The mount block lower down uses the same
638
731
  // flag so the registry never points at a URL that 404s.
639
732
  const advertiseOpenapi = !!(opts.openapi && startOpts.openapi);
640
- const compiledCapabilities = startOpts.capabilities ? compileCapabilities(meta.id, startOpts.capabilities) : undefined;
733
+ const compiledCapabilities = startOpts.capabilities ? compileCapabilities(meta.id, startOpts.capabilities, meta.routes) : undefined;
734
+ if (startOpts.capabilities) bindAppEvents(meta.id, startOpts.capabilities, compiledCapabilities);
641
735
  const compiledHelp = startOpts.help
642
736
  ? compileHelp({
643
737
  appId: meta.id,
@@ -648,6 +742,7 @@ export const defineApp = <
648
742
 
649
743
  // Registry entry
650
744
  const entry: AppRegistryEntry = {
745
+ package: packageName,
651
746
  id: meta.id,
652
747
  access: meta.access,
653
748
  name: meta.name,
@@ -688,8 +783,14 @@ export const defineApp = <
688
783
  legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
689
784
  searchLinks: meta.searchLinks,
690
785
  pwa: meta.pwa
691
- ? { href: meta.pwa.href, requiresRoles: meta.pwa.requiresRoles ? [...meta.pwa.requiresRoles] : undefined }
786
+ ? {
787
+ href: meta.pwa.href,
788
+ requiresRoles: meta.pwa.requiresRoles ? [...meta.pwa.requiresRoles] : undefined,
789
+ ...(meta.pwa.badge ? { badge: meta.pwa.badge } : {}),
790
+ ...(meta.pwa.summary ? { summary: meta.pwa.summary } : {}),
791
+ }
692
792
  : undefined,
793
+ installable: meta.installable,
693
794
  panel: meta.panel
694
795
  ? { ...meta.panel, requiresRoles: meta.panel.requiresRoles ? [...meta.panel.requiresRoles] : undefined }
695
796
  : undefined,
@@ -809,6 +910,11 @@ export const defineApp = <
809
910
  })
810
911
  .route("/_internal/sync", createSyncOpsRoutes(getProcessSync));
811
912
 
913
+ if (installable) server.route("/", createInstallableRoutes(meta.id, installable));
914
+
915
+ if (compiledCapabilities?.subscribes.size)
916
+ server.route("/api/_internal/events/v1", createAppEventDeliveryRoutes(compiledCapabilities));
917
+
812
918
  if (compiledCapabilities) {
813
919
  const invoke = async (c: Context<AuthContext>, kind: "query" | "action" | "review") =>
814
920
  withCapabilityRequestTimeout(c, kind, async () => {
@@ -846,6 +952,7 @@ export const defineApp = <
846
952
  input: body.input,
847
953
  expectedSchemaHash: c.req.header("x-cloud-capability-schema-hash") ?? null,
848
954
  context: {
955
+ query: createNestedCapabilityQuery(c.req.raw),
849
956
  authority: auth.getAuthority(c),
850
957
  actor,
851
958
  accessSubject: c.get("accessSubject"),
@@ -898,6 +1005,7 @@ export const defineApp = <
898
1005
  for (const kind of ["queries", "actions"] as const) {
899
1006
  server.post(
900
1007
  `/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
1008
+ kind === "queries" ? auth.readOnly : auth.requireOAuthScope("write", "admin"),
901
1009
  capabilityAuth(kind),
902
1010
  auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"),
903
1011
  async (c) => {
@@ -924,14 +1032,19 @@ export const defineApp = <
924
1032
  }
925
1033
  const capabilityReadScope = auth.requireOAuthScope("read", "admin");
926
1034
  const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
927
- server.post("/api/_internal/capabilities/v1/queries/:capabilityId", capabilityAuth("queries"), capabilityReadScope, (c) =>
928
- invoke(c, "query"),
1035
+ server.post(
1036
+ "/api/_internal/capabilities/v1/queries/:capabilityId",
1037
+ auth.readOnly,
1038
+ capabilityAuth("queries"),
1039
+ capabilityReadScope,
1040
+ (c) => invoke(c, "query"),
929
1041
  );
930
1042
  server.post("/api/_internal/capabilities/v1/actions/:capabilityId", capabilityAuth("actions"), capabilityWriteScope, (c) =>
931
1043
  invoke(c, "action"),
932
1044
  );
933
1045
  server.post(
934
1046
  "/api/_internal/capabilities/v1/actions/:capabilityId/review",
1047
+ auth.readOnly,
935
1048
  capabilityAuth("actions", true),
936
1049
  capabilityReadScope,
937
1050
  (c) => invoke(c, "review"),
@@ -964,16 +1077,15 @@ export const defineApp = <
964
1077
  );
965
1078
  }
966
1079
 
1080
+ cleanup.push(bindResourceAppAccess(meta.id, opts.access));
1081
+ server.route(RESOURCE_ACCESS_PATH, createResourceAppAccessRoutes(meta));
967
1082
  cleanup.push(() => stopAppAccessConnections());
1083
+ if (meta.access?.kind === "grants") {
1084
+ server.route(appAccessManagementPath(meta), createOwnedAppAccessRoutes(meta, opts.access?.canManage));
1085
+ }
968
1086
  server.use(
969
1087
  "*",
970
- requireAppAccess(meta, (c, message) =>
971
- ssr.error(c, 403, {
972
- title: "No access",
973
- description: message,
974
- layout: "minimal",
975
- }),
976
- ),
1088
+ requireAppAccess(meta, (c) => ssr.appAccessDenied(c, meta)),
977
1089
  );
978
1090
 
979
1091
  if (opts.cli && cliModules.length > 0) server.route(CLOUD_CLI_PLUGIN_ROUTE, createCliPluginRoutes(meta.id, opts.cli));
@@ -1037,6 +1149,8 @@ export const defineApp = <
1037
1149
  const startOutbox = async () => {
1038
1150
  const stopLiveOutbox = await startLiveOutbox(meta.id);
1039
1151
  if (stopLiveOutbox) cleanup.push(stopLiveOutbox);
1152
+ const stopAppEvents = await startAppEventDelivery(meta.id);
1153
+ if (stopAppEvents) cleanup.push(stopAppEvents);
1040
1154
  };
1041
1155
  // An application fails before any setup when Core has not created the platform outbox yet.
1042
1156
  // Core creates it in its own setup, so it starts publishing after that.
@@ -0,0 +1,90 @@
1
+ import { resolve } from "node:path";
2
+ import { Hono } from "hono";
3
+ import type { AppInstallable } from "../contracts/installable";
4
+ import { escapeHtml } from "../shared/markdown/shared";
5
+
6
+ export const INSTALLABLE_ICON_VARIANTS = {
7
+ "icon-192": { size: 192, box: 0.84 },
8
+ "icon-512": { size: 512, box: 0.84 },
9
+ // The square fits inside the maskable safe circle of radius 40%.
10
+ "icon-maskable-512": { size: 512, box: 0.56 },
11
+ "apple-touch-icon": { size: 180, box: 0.7 },
12
+ } as const;
13
+
14
+ export const installableManifest = (app: AppInstallable, theme: "light" | "dark") => ({
15
+ id: app.path,
16
+ start_url: app.path,
17
+ scope: app.path,
18
+ name: app.name,
19
+ short_name: app.shortName,
20
+ display: app.display,
21
+ theme_color: app.themeColor[theme],
22
+ background_color: app.themeColor[theme],
23
+ icons: [
24
+ { src: `${app.path}icon-192-${theme}.png`, sizes: "192x192", type: "image/png", purpose: "any" },
25
+ { src: `${app.path}icon-512-${theme}.png`, sizes: "512x512", type: "image/png", purpose: "any" },
26
+ { src: `${app.path}icon-maskable-512-${theme}.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
27
+ ],
28
+ });
29
+
30
+ /** One public bilingual document. It never includes a request, account, or application payload. */
31
+ export const installableOfflineHtml = (app: AppInstallable): string => `<!doctype html>
32
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
33
+ <title>${escapeHtml(app.name)} · Offline</title><meta name="referrer" content="no-referrer">
34
+ <style>:root{color-scheme:light dark;font-family:system-ui;background:#fafafa;color:#111}body{margin:0;padding: max(24px,env(safe-area-inset-top)) max(24px,env(safe-area-inset-right)) max(24px,env(safe-area-inset-bottom)) max(24px,env(safe-area-inset-left))}main{max-width:36rem;margin:10vh auto}button{font:inherit;min-height:44px;padding:8px 20px;cursor:pointer}@media(prefers-color-scheme:dark){:root{background:#090d12;color:#fff}}</style>
35
+ </head><body><main><h1>${escapeHtml(app.name)}</h1><p>You're offline. Connect to the internet and try again.</p><p lang="de">Du bist offline. Verbinde dich mit dem Internet und versuche es erneut.</p><button type="button">Try again / Erneut versuchen</button></main><script>document.querySelector('button').addEventListener('click',()=>location.reload())</script></body></html>`;
36
+
37
+ /** No cache storage, install/activate shortcuts, client messages, or private response retention. */
38
+ export const installableWorker = (app: AppInstallable): string => `"use strict";
39
+ const scope = ${JSON.stringify(app.path)};
40
+ const offline = ${JSON.stringify(installableOfflineHtml(app))};
41
+ self.addEventListener("fetch", event => {
42
+ const request = event.request;
43
+ const url = new URL(request.url);
44
+ if (request.method !== "GET" || request.mode !== "navigate" || url.origin !== self.location.origin || !url.pathname.startsWith(scope)) return;
45
+ event.respondWith(fetch(request).catch(() => new Response(offline, {
46
+ status: 503,
47
+ headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" }
48
+ })));
49
+ });
50
+ `;
51
+
52
+ /** Framework mounts before app access/auth and never delegates a missing generated file to app code. */
53
+ export const createInstallableRoutes = (appId: string, app: AppInstallable, publicDir = resolve(process.cwd(), "public")) => {
54
+ const router = new Hono();
55
+ const headers = { "Cache-Control": "no-cache", "X-Content-Type-Options": "nosniff" };
56
+ router.get(app.path.slice(0, -1), (c) => {
57
+ const url = new URL(c.req.url);
58
+ return c.redirect(`${app.path}${url.search}`, 308);
59
+ });
60
+ router.all(`${app.path}manifest.webmanifest`, (c) => {
61
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" });
62
+ return new Response(
63
+ c.req.method === "HEAD" ? null : JSON.stringify(installableManifest(app, c.req.query("theme") === "dark" ? "dark" : "light")),
64
+ {
65
+ headers: { ...headers, "Content-Type": "application/manifest+json" },
66
+ },
67
+ );
68
+ });
69
+ for (const [name, content, type] of [
70
+ ["sw.js", installableWorker(app), "text/javascript; charset=utf-8"],
71
+ ["offline.html", installableOfflineHtml(app), "text/html; charset=utf-8"],
72
+ ] as const) {
73
+ router.all(`${app.path}${name}`, (c) => {
74
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" });
75
+ return new Response(c.req.method === "HEAD" ? null : content, { headers: { ...headers, "Content-Type": type } });
76
+ });
77
+ }
78
+ for (const theme of ["light", "dark"] as const) {
79
+ for (const name of Object.keys(INSTALLABLE_ICON_VARIANTS)) {
80
+ const filename = `${name}-${theme}.png`;
81
+ router.all(`${app.path}${filename}`, async (c) => {
82
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.text("Method Not Allowed", 405, { Allow: "GET, HEAD" });
83
+ const file = Bun.file(resolve(publicDir, appId, "installable", filename));
84
+ if (!(await file.exists())) return c.notFound();
85
+ return new Response(c.req.method === "HEAD" ? null : file, { headers: { ...headers, "Content-Type": "image/png" } });
86
+ });
87
+ }
88
+ }
89
+ return router;
90
+ };
@@ -0,0 +1,22 @@
1
+ import type { AppInstallable } from "../contracts/installable";
2
+ import { escapeHtml } from "../shared/markdown/shared";
3
+
4
+ export const installableHead = (app: AppInstallable, resolved: "light" | "dark" | undefined) => {
5
+ const theme = resolved ?? "light";
6
+ return {
7
+ html: "",
8
+ viewport: "width=device-width, initial-scale=1, viewport-fit=cover",
9
+ app: `<meta name="theme-color" data-app-pwa-theme content="${app.themeColor[theme]}">
10
+ <link rel="manifest" data-app-pwa-manifest href="${app.path}manifest.webmanifest?theme=${theme}">
11
+ <link rel="apple-touch-icon" data-app-pwa-apple href="${app.path}apple-touch-icon-${theme}.png">
12
+ <meta name="apple-mobile-web-app-title" content="${escapeHtml(app.shortName)}">
13
+ <meta name="mobile-web-app-capable" content="yes">
14
+ <meta name="apple-mobile-web-app-capable" content="yes">
15
+ <style data-app-pwa-canvas>html{background:${app.themeColor.light}}html.dark{background:${app.themeColor.dark}}body{box-sizing:border-box;min-height:100dvh;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}[data-app-pwa-runtime]:not(:empty){max-width:40rem;margin:1rem auto}</style>`,
16
+ styles: "<style data-app-pwa-navigation>@view-transition{navigation:none}</style>",
17
+ };
18
+ };
19
+
20
+ /** Runs after the shared theme bootstrap, before first paint or island downloads. All interpolations are validated paths/hex colors. */
21
+ export const installableThemeScript = (app: AppInstallable): string =>
22
+ `!function(){var t=document.documentElement.classList.contains('dark')?'dark':'light';document.querySelector('meta[data-app-pwa-theme]').content=${JSON.stringify(app.themeColor)}[t];document.querySelector('link[data-app-pwa-manifest]').href=${JSON.stringify(`${app.path}manifest.webmanifest?theme=`)}+t;document.querySelector('link[data-app-pwa-apple]').href=${JSON.stringify(`${app.path}apple-touch-icon-`)}+t+'.png'}();`;
@@ -0,0 +1,9 @@
1
+ import { getProcessApplicationPackage } from "./process-identity";
2
+
3
+ /** The single application-to-area mapping. No caller-supplied application or area ID. */
4
+ export const objectStoreArea = (visibility: "private" | "public" = "private"): string => {
5
+ const packageName = getProcessApplicationPackage();
6
+ if (!packageName) throw new Error("Start the application before using the object store.");
7
+ if (!/^[a-z][a-z0-9-]{0,79}$/.test(packageName)) throw new Error("Invalid object-store application package.");
8
+ return `${visibility}/${packageName}/`;
9
+ };