@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
@@ -0,0 +1,101 @@
1
+ import { S3Client } from "bun";
2
+ import { z } from "zod";
3
+ import { readStates } from "../services/settings/store";
4
+
5
+ export const objectStoreSettingKeys = [
6
+ "storage.endpoint",
7
+ "storage.region",
8
+ "storage.bucket",
9
+ "storage.access_key_id",
10
+ "storage.secret_access_key",
11
+ "storage.max_object_bytes",
12
+ ] as const;
13
+ const bucket = z
14
+ .string()
15
+ .min(3)
16
+ .max(63)
17
+ .regex(/^[a-z0-9][a-z0-9.-]*[a-z0-9]$/);
18
+ const Config = z.object({
19
+ endpoint: z.string().min(1),
20
+ region: z.string().min(1),
21
+ bucket,
22
+ access_key_id: z.string().min(1),
23
+ secret_access_key: z.string().min(1),
24
+ max_object_bytes: z
25
+ .number()
26
+ .int()
27
+ .min(1)
28
+ .max(5 * 1024 ** 3),
29
+ });
30
+ export class ObjectStoreConfigurationError extends Error {}
31
+ export class ObjectStoreConnectionError extends Error {}
32
+ let cached: { signature: string; client: S3Client } | undefined;
33
+
34
+ export const objectStoreBackend = async () => {
35
+ // Read one coherent DB snapshot, including stored-state checks. No local TTL:
36
+ // changes and secret rotation apply to the next operation in every process.
37
+ const states = await readStates(objectStoreSettingKeys);
38
+ if (objectStoreSettingKeys.some((key) => states.get(key)?.stored === "unreadable")) {
39
+ throw new ObjectStoreConfigurationError(
40
+ "Storage settings cannot be decrypted. Check this process uses the installation APP_SECRET, then replace unreadable settings in Administration > Settings > Storage.",
41
+ );
42
+ }
43
+ const parsed = Config.safeParse(
44
+ Object.fromEntries(objectStoreSettingKeys.map((key) => [key.slice("storage.".length), states.get(key)?.value])),
45
+ );
46
+ if (!parsed.success)
47
+ throw new ObjectStoreConfigurationError(
48
+ "Storage is not configured. Set the Garage endpoint, region, bucket and credentials in Administration > Settings > Storage.",
49
+ );
50
+ const config = parsed.data;
51
+ const endpoint = URL.parse(config.endpoint);
52
+ if (
53
+ !endpoint ||
54
+ !["http:", "https:"].includes(endpoint.protocol) ||
55
+ endpoint.username ||
56
+ endpoint.password ||
57
+ endpoint.search ||
58
+ endpoint.hash ||
59
+ endpoint.pathname !== "/"
60
+ ) {
61
+ throw new ObjectStoreConfigurationError("Storage endpoint must be an HTTP(S) origin without credentials, path, query or fragment.");
62
+ }
63
+ const signature = JSON.stringify(config);
64
+ if (cached?.signature !== signature) {
65
+ const options = {
66
+ endpoint: endpoint.href,
67
+ accessKeyId: config.access_key_id,
68
+ secretAccessKey: config.secret_access_key,
69
+ region: config.region,
70
+ };
71
+ cached = { signature, client: new S3Client({ ...options, bucket: config.bucket }) };
72
+ }
73
+ return { client: cached.client, limit: config.max_object_bytes };
74
+ };
75
+
76
+ /** Internal administrator check; never an application API or a credential response. */
77
+ export const testObjectStoreConnection = async (): Promise<void> => {
78
+ const backend = await objectStoreBackend();
79
+ const file = backend.client.file(`cloud-connection-test/${crypto.randomUUID()}`);
80
+ const content = "Cloud storage connection test";
81
+ let step = "write";
82
+ try {
83
+ await file.write(content, { type: "text/plain" });
84
+ step = "read";
85
+ if ((await file.text()) !== content) throw new Error("Unexpected content");
86
+ step = "delete";
87
+ await file.delete();
88
+ if (await file.exists()) throw new Error("Object still exists");
89
+ } catch {
90
+ try {
91
+ await file.delete();
92
+ } catch {
93
+ throw new ObjectStoreConnectionError(
94
+ `Storage test failed at ${step}; cleanup failed. Remove cloud-connection-test/ objects after fixing the connection.`,
95
+ );
96
+ }
97
+ throw new ObjectStoreConnectionError(
98
+ `Storage test failed at ${step}. Check the saved settings, Garage availability and bucket permissions, then retry.`,
99
+ );
100
+ }
101
+ };
@@ -0,0 +1,14 @@
1
+ // S3's complete object key, including the area, is limited to 1,024 UTF-8 bytes.
2
+ // Reject percent signs altogether, including nested encodings, before Bun sees a key.
3
+ export const validateObjectKey = (key: string, area: string, allowPrefix = false): string => {
4
+ if (typeof key !== "string" || !key.isWellFormed() || key !== key.normalize("NFC") || /[\\%\p{Cc}\p{Cf}]/u.test(key)) {
5
+ throw new TypeError("Object keys must be NFC text without controls, backslashes, or percent escapes.");
6
+ }
7
+ if (allowPrefix && key === "") return key;
8
+ const path = allowPrefix && key.endsWith("/") ? key.slice(0, -1) : key;
9
+ if (!path || path.split("/").some((segment) => !segment || segment === "." || segment === "..")) {
10
+ throw new TypeError("Object keys must be relative paths without empty, dot, or parent segments.");
11
+ }
12
+ if (Buffer.byteLength(area + key) > 1024) throw new RangeError("Object key exceeds S3's 1,024-byte limit including its area.");
13
+ return key;
14
+ };
@@ -1,11 +1,18 @@
1
1
  import type { HtmlFn } from "@k2b/ssr";
2
2
  import type { Context } from "hono";
3
3
  import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status";
4
+ import type { AppMeta } from "../contracts/app";
4
5
  import { PWA_AUTH_PATH, PWA_SCOPE } from "../contracts/pwa";
5
6
  import { getLocale } from "../server/locale";
6
- import { preloadLayoutAnnouncements } from "../server/middleware/settings";
7
+ import { runtime } from "../server/middleware/runtime";
8
+ import { preloadLayoutAnnouncements, settings } from "../server/middleware/settings";
9
+ import { appAccessAuthority } from "../services/app-access";
10
+ import { appAccessManagers, canRepairAppAccess } from "../services/app-access-management";
11
+ import { logger } from "../services/logging";
12
+ import { resolveAppPresentation } from "../shared/app-presentation";
7
13
  import { createLoginRedirectUrl } from "../shared/redirect";
8
14
  import { readThemePreferenceFromCookieHeader } from "../shared/theme";
15
+ import { preloadDisplayApps } from "../ssr/runtime";
9
16
  import type { PageOptions } from "./define-app";
10
17
 
11
18
  export type PageErrorOptions = {
@@ -19,10 +26,12 @@ export type PageErrorOptions = {
19
26
  export type PageErrorStatus = ClientErrorStatusCode | ServerErrorStatusCode;
20
27
 
21
28
  /** Explicit document responses, bound to the owning application's SSR template. */
22
- export const createPageResponses = (html: HtmlFn<PageOptions>) => {
29
+ export const createPageResponses = <T extends PageOptions>(html: HtmlFn<T>, appId: string, requestOptions?: (c: Context) => Partial<T>) => {
23
30
  const error = async (c: Context, status: PageErrorStatus, options: PageErrorOptions = {}): Promise<Response> => {
24
31
  // Auth middleware may reject before ssr() initializes page metadata.
25
32
  c.set("page", {
33
+ ...requestOptions?.(c),
34
+ pwaOwner: appId,
26
35
  lang: getLocale(c),
27
36
  themePreference: readThemePreferenceFromCookieHeader(c.req.header("Cookie")),
28
37
  });
@@ -58,5 +67,27 @@ export const createPageResponses = (html: HtmlFn<PageOptions>) => {
58
67
  },
59
68
  };
60
69
 
61
- return { access, pwaAccess, error };
70
+ const appAccessDenied = async (c: Context, app: AppMeta): Promise<Response> => {
71
+ await runtime()(c, async () => {});
72
+ await settings()(c, async () => {});
73
+ c.set("page", { lang: getLocale(c), themePreference: readThemePreferenceFromCookieHeader(c.req.header("Cookie")) });
74
+ await preloadDisplayApps(c);
75
+ const managers =
76
+ app.access?.kind === "grants"
77
+ ? await appAccessManagers(app.id).catch((error: unknown) => {
78
+ logger("app-access").error("Could not load application access contacts", { appId: app.id, error });
79
+ // Cloud administration remains the recovery contact when the list is unavailable.
80
+ return [];
81
+ })
82
+ : [];
83
+ const authority = appAccessAuthority(c);
84
+ const { renderAppAccessDenied } = await import("../ssr/AppAccessDenied");
85
+ const response = await html(
86
+ renderAppAccessDenied(c, resolveAppPresentation(app, getLocale(c)), managers, !!authority && canRepairAppAccess(authority)),
87
+ c.get("page"),
88
+ );
89
+ response.headers.set("Cache-Control", "private, no-store");
90
+ return new Response(response.body, { status: 403, headers: response.headers });
91
+ };
92
+ return { access, pwaAccess, error, appAccessDenied };
62
93
  };
@@ -2,20 +2,25 @@ import type { PlatformPermission } from "../contracts/outgoing-mail";
2
2
 
3
3
  /** Identity of the application started in this process, derived from its declaration. */
4
4
  let applicationId: string | undefined;
5
+ let applicationPackage: string | undefined;
5
6
  let platformPermissions: readonly PlatformPermission[] = [];
6
7
  export const getProcessPlatformPermissions = (): readonly PlatformPermission[] => platformPermissions;
7
8
 
8
9
  export const getProcessApplicationId = (): string | undefined => applicationId;
9
10
 
10
- export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = []): void => {
11
- if (applicationId !== undefined && applicationId !== id) {
11
+ export const getProcessApplicationPackage = (): string | undefined => applicationPackage;
12
+
13
+ export const bindProcessApplicationId = (id: string, permissions: readonly PlatformPermission[] = [], packageName = id): void => {
14
+ if (applicationId !== undefined && (applicationId !== id || applicationPackage !== packageName)) {
12
15
  throw new Error(`This process already hosts application "${applicationId}"`);
13
16
  }
14
17
  applicationId = id;
18
+ applicationPackage = packageName;
15
19
  platformPermissions = [...permissions];
16
20
  };
17
21
 
18
22
  export const clearProcessApplicationId = (): void => {
19
23
  applicationId = undefined;
24
+ applicationPackage = undefined;
20
25
  platformPermissions = [];
21
26
  };
@@ -1,5 +1,7 @@
1
1
  import { CLOUD_CLI_MODULE_NAME } from "../cli/plugin";
2
+ import { APP_ADMIN_AREAS } from "../contracts/app";
2
3
  import { AppAccessRuleSchema } from "../contracts/app-access";
4
+ import { validateInstallable } from "../contracts/installable";
3
5
  import { PlatformPermissionSchema } from "../contracts/outgoing-mail";
4
6
  import type { AppRegistryEntry } from "../contracts/registry";
5
7
  import { isOwnAppPath, SAME_ORIGIN_PATH } from "./app-path";
@@ -18,6 +20,9 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
18
20
  for (const field of ["id", "name", "icon", "description", "baseUrl"] as const) {
19
21
  if (!isString(value[field]) || value[field].length === 0) return invalid(field, "a non-empty string");
20
22
  }
23
+ if (value.package !== undefined && (typeof value.package !== "string" || !/^[a-z][a-z0-9-]{0,79}$/.test(value.package))) {
24
+ return invalid("package", "a lowercase package name of 1–80 characters");
25
+ }
21
26
  try {
22
27
  const baseUrl = new URL(value.baseUrl as string);
23
28
  if (!(["http:", "https:"] as const).includes(baseUrl.protocol as "http:" | "https:") || baseUrl.username || baseUrl.password) {
@@ -29,6 +34,9 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
29
34
  if (!isStringArray(value.routes) || value.routes.some((route) => !route.startsWith("/"))) {
30
35
  return invalid("routes", "an array of absolute paths");
31
36
  }
37
+ if (value.installable !== undefined && !validateInstallable(String(value.id), value.routes, value.installable)) {
38
+ return invalid("installable", "a canonical standalone surface covered by the app's own declared routes");
39
+ }
32
40
  if (value.access !== undefined && !AppAccessRuleSchema.safeParse(value.access).success)
33
41
  return invalid("access", "a declarative app access rule");
34
42
  if (value.startedAt !== undefined && !(typeof value.startedAt === "number" && Number.isFinite(value.startedAt))) {
@@ -96,7 +104,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
96
104
  (group) =>
97
105
  !isRecord(group) ||
98
106
  (group.id !== undefined && !isString(group.id)) ||
99
- (group.section !== undefined && group.section !== "ai") ||
107
+ (group.section !== undefined && !(APP_ADMIN_AREAS as readonly unknown[]).includes(group.section)) ||
100
108
  !isString(group.label) ||
101
109
  !Array.isArray(group.links) ||
102
110
  group.links.some((link) => !isRecord(link) || !isString(link.label) || !isString(link.href) || !isString(link.icon)),
@@ -137,6 +145,12 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
137
145
  if (value.pwa.requiresRoles !== undefined && !isStringArray(value.pwa.requiresRoles)) {
138
146
  return invalid("pwa.requiresRoles", "an array of strings");
139
147
  }
148
+ for (const field of ["badge", "summary"] as const) {
149
+ const path = value.pwa[field];
150
+ if (path !== undefined && (!isString(path) || !isOwnAppPath(path, value.routes))) {
151
+ return invalid(`pwa.${field}`, "a same-origin path below one of the app's routes");
152
+ }
153
+ }
140
154
  }
141
155
  if (value.widgets !== undefined) {
142
156
  if (
@@ -1,9 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { EphemeralConfig, MutexConfig } from "@k2b/sync";
3
3
  import type { AppAppearanceColor } from "../contracts/app";
4
+ import { calendarSourceIssues } from "../contracts/calendar-source";
4
5
  import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
5
6
  import { fileProviderIssues } from "../contracts/file-provider";
6
7
  import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
8
+ import { resourcePageIssue } from "../contracts/resource-pages";
7
9
  import { type Role, RoleSchema } from "../contracts/shared";
8
10
  import {
9
11
  type DashboardWidgetPresentation,
@@ -233,6 +235,7 @@ const reportLeftOutEntries = (sent: unknown, manifest: CapabilityManifest): void
233
235
  return typeof localId === "string" && !kept.has(localId) ? [`${group}/${localId}`] : [];
234
236
  });
235
237
  });
238
+ if (groups.calendarSource !== undefined && !manifest.calendarSource) leftOut.push("calendarSource");
236
239
  if (groups.fileProvider !== undefined && !manifest.fileProvider) leftOut.push("fileProvider");
237
240
  if (leftOut.length === 0) return;
238
241
  console.warn(
@@ -283,6 +286,40 @@ const withValidFileProvider = (manifest: CapabilityManifest): CapabilityManifest
283
286
  return rest;
284
287
  };
285
288
 
289
+ const checkedCalendarSources = new Map<string, { manifestHash: string; valid: boolean }>();
290
+
291
+ /**
292
+ * Ignores a calendar source that does not match the contract, logging once per manifest; the app's other
293
+ * capabilities stay available. The delivered `manifestHash` stays, because it identifies what the app registered.
294
+ */
295
+ const withValidCalendarSource = (manifest: CapabilityManifest): CapabilityManifest => {
296
+ if (!manifest.calendarSource) return manifest;
297
+ const checked = checkedCalendarSources.get(manifest.appId);
298
+ let valid = checked?.manifestHash === manifest.manifestHash ? checked.valid : undefined;
299
+ if (valid === undefined) {
300
+ const issues = calendarSourceIssues(manifest);
301
+ valid = issues.length === 0;
302
+ checkedCalendarSources.set(manifest.appId, { manifestHash: manifest.manifestHash, valid });
303
+ if (!valid) {
304
+ console.error(
305
+ JSON.stringify({
306
+ level: "error",
307
+ source: "capability-registry",
308
+ message: "Ignored a calendar source that does not match the calendar-source contract",
309
+ appId: manifest.appId,
310
+ manifestHash: manifest.manifestHash,
311
+ issues: issues
312
+ .slice(0, 10)
313
+ .map(({ function: name, localId, code, path, message }) => ({ function: name, localId, code, path, message })),
314
+ }),
315
+ );
316
+ }
317
+ }
318
+ if (valid) return manifest;
319
+ const { calendarSource: _ignored, ...rest } = manifest;
320
+ return rest;
321
+ };
322
+
286
323
  /** Ignores record fields from a newer release; the endpoint always comes from the live app registry. */
287
324
  export const resolveLiveCapabilityRegistryEntry = (
288
325
  key: string,
@@ -297,7 +334,13 @@ export const resolveLiveCapabilityRegistryEntry = (
297
334
  if (!endpoint) return null;
298
335
  try {
299
336
  const read = parseCapabilityManifest(record.manifest, app.id);
300
- const manifest = withValidFileProvider(read);
337
+ const supported = withValidCalendarSource(withValidFileProvider(read));
338
+ const manifest = supported.resourcePages
339
+ ? {
340
+ ...supported,
341
+ resourcePages: supported.resourcePages.filter((page) => !resourcePageIssue(page.path, app.routes, app.id)),
342
+ }
343
+ : supported;
301
344
  const presentation = compileCapabilityPresentation(manifest, record.presentation, "reader");
302
345
  if (app.capabilities.protocolVersion !== manifest.protocolVersion || app.capabilities.manifestHash !== manifest.manifestHash) {
303
346
  return null;
@@ -71,7 +71,15 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
71
71
  ),
72
72
  legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
73
73
  searchLinks: e.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
74
- pwa: e.pwa ? { href: e.pwa.href, requiresRoles: e.pwa.requiresRoles ? ([...e.pwa.requiresRoles] as Role[]) : undefined } : undefined,
74
+ pwa: e.pwa
75
+ ? {
76
+ href: e.pwa.href,
77
+ requiresRoles: e.pwa.requiresRoles ? ([...e.pwa.requiresRoles] as Role[]) : undefined,
78
+ ...(e.pwa.badge ? { badge: e.pwa.badge } : {}),
79
+ ...(e.pwa.summary ? { summary: e.pwa.summary } : {}),
80
+ }
81
+ : undefined,
82
+ installable: e.installable ? structuredClone(e.installable) : undefined,
75
83
  openapi: e.openapi,
76
84
  };
77
85
  }),
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import type { CloudRuntime } from "../contracts/app";
14
+ import { pwaBuilds } from "../contracts/pwa-builds";
14
15
  import { logger } from "../services/logging";
15
16
  import { superviseRuntimeTask } from "../services/runtime-lifecycle";
16
17
  import { listApps, listCapabilities, watchAppRegistry } from "./registry";
@@ -19,6 +20,11 @@ import { buildRuntimeFromRegistry } from "./runtime-context";
19
20
  const log = logger("runtime-watcher");
20
21
 
21
22
  let current: CloudRuntime | undefined;
23
+ let mobileBuilds: Record<string, string> = {};
24
+
25
+ /** Current registered release identities for the existing mobile session keepalive. */
26
+ export const registeredPwaBuilds = (): Record<string, string> => mobileBuilds;
27
+
22
28
  let coreRelease: string | undefined;
23
29
  let initPromise: Promise<void> | undefined;
24
30
  let watcherTask: Promise<void> | undefined;
@@ -28,6 +34,7 @@ const refresh = async () => {
28
34
  const apps = await listApps();
29
35
  const capabilities = await listCapabilities(apps);
30
36
  current = buildRuntimeFromRegistry(apps, capabilities);
37
+ mobileBuilds = pwaBuilds(apps);
31
38
  coreRelease = apps.find((app) => app.id === "core")?.runtime?.release;
32
39
  };
33
40
 
@@ -74,6 +81,7 @@ export const stopRuntimeWatcher = async (): Promise<void> => {
74
81
  initPromise = undefined;
75
82
  current = undefined;
76
83
  coreRelease = undefined;
84
+ mobileBuilds = {};
77
85
  };
78
86
 
79
87
  export const getCurrentRuntime = (): CloudRuntime => {
@@ -1,8 +1,10 @@
1
1
  import type { HtmlFn, RenderFn } from "@k2b/ssr";
2
2
  import { createSSRHandler } from "@k2b/ssr/hono";
3
3
  import type { Context, Env, MiddlewareHandler, TypedResponse } from "hono";
4
+ import { setCookie } from "hono/cookie";
4
5
  import type { StatusCode } from "hono/utils/http-status";
5
6
  import type { PanelConsentSnapshot } from "../contracts/panel-context";
7
+ import { PWA_COOKIES, PWA_LIMITS, PWA_SCOPE } from "../contracts/pwa";
6
8
  import type { RailSnapshot } from "../contracts/rail-preferences";
7
9
  import type { SettingsFlat, Unflatten } from "../contracts/settings-types";
8
10
  import type { User } from "../contracts/shared";
@@ -12,7 +14,7 @@ import { measureServerPhase } from "./server-timing";
12
14
 
13
15
  type PageEnv<T extends object> = {
14
16
  Variables: {
15
- page: Partial<T>;
17
+ page: Partial<T> & { pwaAddress?: string };
16
18
  user?: User;
17
19
  railPreferences?: RailSnapshot;
18
20
  panelConsent?: PanelConsentSnapshot;
@@ -62,6 +64,15 @@ export const createStatusPreservingSsrHandler = <T extends object>(
62
64
  response.headers.forEach((value, key) => {
63
65
  headers[key] = value;
64
66
  });
67
+ const address = context.get("page").pwaAddress;
68
+ if (address && status < 400) {
69
+ setCookie(context, PWA_COOKIES.lastAddress, address, {
70
+ path: PWA_SCOPE,
71
+ secure: true,
72
+ sameSite: "Lax",
73
+ maxAge: PWA_LIMITS.idleDays * 86_400,
74
+ });
75
+ }
65
76
  const document = context.newResponse(response.body, status as StatusCode, headers);
66
77
  // A rendered document carries the signed-in user's state. Without an
67
78
  // explicit policy, a browser may show it again on Back from its HTTP