@k2b/cloud 0.6.0 → 0.8.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 (349) hide show
  1. package/README.md +2 -2
  2. package/package.json +22 -11
  3. package/scripts/README.md +10 -0
  4. package/scripts/browser-performance.ts +14 -0
  5. package/scripts/build-pdf-renderer.ts +39 -0
  6. package/scripts/build.ts +20 -35
  7. package/scripts/preload.ts +16 -12
  8. package/scripts/sync-recovery-smoke.ts +22 -6
  9. package/src/_internal/app-presentation.ts +14 -2
  10. package/src/_internal/build-metadata.ts +8 -1
  11. package/src/_internal/capabilities.ts +131 -7
  12. package/src/_internal/capability-streams.ts +81 -0
  13. package/src/_internal/define-app.ts +80 -37
  14. package/src/_internal/heartbeat.ts +4 -0
  15. package/src/_internal/help-catalog.ts +27 -172
  16. package/src/_internal/help.ts +13 -76
  17. package/src/_internal/page-responses.ts +2 -0
  18. package/src/_internal/postgres-application-name.ts +30 -0
  19. package/src/_internal/process-identity.ts +15 -0
  20. package/src/_internal/process-sync.ts +1 -1
  21. package/src/_internal/readiness.ts +4 -1
  22. package/src/_internal/registry-validation.ts +29 -25
  23. package/src/_internal/registry.ts +2 -53
  24. package/src/_internal/runtime-context.ts +5 -16
  25. package/src/_internal/server-timing.ts +13 -0
  26. package/src/_internal/status-preserving-ssr.ts +18 -6
  27. package/src/_internal/web-vitals-asset.ts +10 -0
  28. package/src/access/PermissionEditor.tsx +18 -151
  29. package/src/access/PrincipalPicker.tsx +110 -0
  30. package/src/access/ResourceApiKeys.tsx +1 -1
  31. package/src/access/ui.ts +1 -0
  32. package/src/ai/admin.ts +14 -13
  33. package/src/ai/assistant-models.ts +12 -1
  34. package/src/ai/audio-format.ts +34 -0
  35. package/src/ai/audio-tool.ts +90 -0
  36. package/src/ai/browser-code-contracts.ts +201 -0
  37. package/src/ai/browser.ts +4 -0
  38. package/src/ai/capabilities.ts +55 -36
  39. package/src/ai/capability-execution.ts +47 -12
  40. package/src/ai/chat/blocks.tsx +229 -90
  41. package/src/ai/chat/builtin-tools.tsx +2 -4
  42. package/src/ai/chat/capability-result.ts +15 -0
  43. package/src/ai/chat/capability-table.tsx +2 -10
  44. package/src/ai/chat/composer-adapter.ts +83 -3
  45. package/src/ai/chat/message-actions.tsx +8 -7
  46. package/src/ai/chat/message-utils.ts +25 -1
  47. package/src/ai/chat/messages.ts +2 -0
  48. package/src/ai/chat/presentation.tsx +31 -47
  49. package/src/ai/chat/primitives.tsx +43 -1
  50. package/src/ai/chat/tool-disclosure.tsx +21 -2
  51. package/src/ai/chat/tool-groups.ts +58 -0
  52. package/src/ai/chat-quotas.ts +25 -0
  53. package/src/ai/chat-task-contracts.ts +14 -10
  54. package/src/ai/chat-tasks.ts +297 -43
  55. package/src/ai/client/controller.ts +165 -39
  56. package/src/ai/client/file-source.ts +12 -6
  57. package/src/ai/client/live-connection.ts +1 -0
  58. package/src/ai/client/projection.ts +29 -11
  59. package/src/ai/client/transport.ts +6 -0
  60. package/src/ai/code-capability-routes.ts +105 -0
  61. package/src/ai/code-capability-transport.ts +39 -0
  62. package/src/ai/code-execution.ts +53 -0
  63. package/src/ai/code-mode-skill.ts +118 -0
  64. package/src/ai/code-runtime-tools.ts +179 -0
  65. package/src/ai/code-source-contracts.ts +270 -0
  66. package/src/ai/code-source-tools.ts +188 -0
  67. package/src/ai/compaction.ts +27 -6
  68. package/src/ai/data-analysis-skill.ts +10 -0
  69. package/src/ai/default-tools.ts +99 -3
  70. package/src/ai/dictation-runtime.ts +171 -0
  71. package/src/ai/dictations.ts +188 -0
  72. package/src/ai/draft-content.ts +49 -0
  73. package/src/ai/enrich.ts +6 -0
  74. package/src/ai/executor.ts +189 -33
  75. package/src/ai/fetch-file-tool.ts +2 -2
  76. package/src/ai/file-content-version.ts +27 -0
  77. package/src/ai/file-context.ts +2 -1
  78. package/src/ai/file-media-type.ts +12 -0
  79. package/src/ai/file-reference-contracts.ts +15 -0
  80. package/src/ai/file-tools.ts +8 -1
  81. package/src/ai/files-store.ts +212 -71
  82. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  83. package/src/ai/grids-skill.ts +42 -10
  84. package/src/ai/http.ts +26 -8
  85. package/src/ai/index.ts +26 -3
  86. package/src/ai/inference-calls.ts +197 -0
  87. package/src/ai/live-events.ts +5 -1
  88. package/src/ai/live.ts +1 -1
  89. package/src/ai/markdown-pdf-tool.ts +2 -2
  90. package/src/ai/memories.ts +30 -27
  91. package/src/ai/memory-learning-runs.ts +1 -1
  92. package/src/ai/memory-learning.ts +64 -32
  93. package/src/ai/memory-tool.ts +1 -5
  94. package/src/ai/memory-workflow-evidence.ts +9 -4
  95. package/src/ai/message-queue.ts +178 -0
  96. package/src/ai/migrate.ts +189 -11
  97. package/src/ai/model-pricing.ts +43 -0
  98. package/src/ai/pdf-render-worker.ts +75 -0
  99. package/src/ai/pdf-render.fixture.ts +28 -0
  100. package/src/ai/pdf-render.ts +77 -0
  101. package/src/ai/projects-routes.ts +18 -43
  102. package/src/ai/projects.ts +33 -14
  103. package/src/ai/protocol.ts +3 -0
  104. package/src/ai/provider.ts +1 -8
  105. package/src/ai/quota-provider.ts +192 -0
  106. package/src/ai/quota-report.ts +152 -0
  107. package/src/ai/quotas-migrate.ts +45 -0
  108. package/src/ai/quotas.ts +168 -0
  109. package/src/ai/routes.ts +241 -11
  110. package/src/ai/run-timeout.ts +12 -0
  111. package/src/ai/runtime-tools.ts +7 -5
  112. package/src/ai/runtime.ts +74 -17
  113. package/src/ai/settings.ts +36 -8
  114. package/src/ai/short-id.ts +4 -5
  115. package/src/ai/skill-catalog.ts +0 -10
  116. package/src/ai/skill-search.ts +58 -0
  117. package/src/ai/skill-seeds.ts +114 -20
  118. package/src/ai/skill-tool.ts +21 -5
  119. package/src/ai/skills-routes.ts +49 -4
  120. package/src/ai/skills.ts +358 -163
  121. package/src/ai/store.ts +416 -201
  122. package/src/ai/stream.ts +5 -0
  123. package/src/ai/structured-runs.ts +4 -3
  124. package/src/ai/structured.ts +12 -1
  125. package/src/ai/system-prompt.ts +14 -2
  126. package/src/ai/task-contracts.ts +84 -0
  127. package/src/ai/task-execution.ts +92 -0
  128. package/src/ai/timeline.ts +2 -5
  129. package/src/ai/todo-contracts.ts +32 -0
  130. package/src/ai/todo-tool.ts +19 -0
  131. package/src/ai/tool-audit.ts +30 -40
  132. package/src/ai/tools.ts +6 -0
  133. package/src/ai/transcription.ts +202 -0
  134. package/src/ai/turn-timing.ts +114 -0
  135. package/src/ai/types.ts +58 -10
  136. package/src/ai/ui.tsx +3 -0
  137. package/src/ai/usage.ts +118 -118
  138. package/src/ai/vision-tool.ts +60 -10
  139. package/src/api/admin-ai-projects.ts +2 -2
  140. package/src/api/admin-ai-quotas.ts +80 -0
  141. package/src/api/admin-ai-skills.ts +48 -5
  142. package/src/api/admin-ai-usage.ts +2 -2
  143. package/src/api/admin-core-settings.ts +25 -0
  144. package/src/api/admin-identity.ts +91 -51
  145. package/src/api/admin-linux-identities.ts +10 -4
  146. package/src/api/admin-rail.ts +58 -0
  147. package/src/api/announcements.ts +14 -0
  148. package/src/api/app-approval.ts +2 -2
  149. package/src/api/auth.ts +1 -1
  150. package/src/api/capabilities.ts +324 -43
  151. package/src/api/capability-streams.ts +262 -0
  152. package/src/api/help.ts +8 -44
  153. package/src/api/index.ts +8 -2
  154. package/src/api/mcp.ts +45 -34
  155. package/src/api/me-web-vitals.ts +35 -0
  156. package/src/api/me.ts +2 -0
  157. package/src/api/search/schemas.ts +26 -15
  158. package/src/api/search.bench.ts +1 -1
  159. package/src/api/search.ts +54 -17
  160. package/src/browser/CloudResourceSearch.tsx +640 -702
  161. package/src/browser/app-approval-vault.ts +19 -7
  162. package/src/browser/command-bridge.ts +109 -0
  163. package/src/browser/command-shortcuts.ts +76 -0
  164. package/src/browser/commands.ts +67 -0
  165. package/src/browser/navigation-search.ts +48 -0
  166. package/src/browser/notifications.ts +1 -1
  167. package/src/browser/resource-picker.tsx +5 -18
  168. package/src/browser/resource-search-dialog.ts +9 -0
  169. package/src/browser/resource-search-input.ts +46 -0
  170. package/src/browser/resource-search-messages.ts +76 -40
  171. package/src/browser/resource-search.ts +7 -0
  172. package/src/browser/search-bridge.ts +102 -0
  173. package/src/browser/search-commands.ts +59 -0
  174. package/src/browser/search.ts +13 -0
  175. package/src/browser/spotlight-position.ts +208 -0
  176. package/src/browser/testing.ts +6 -0
  177. package/src/browser/web-vitals.ts +25 -0
  178. package/src/capabilities/claims.ts +179 -0
  179. package/src/capabilities/client.ts +34 -0
  180. package/src/capabilities/command-link.ts +16 -0
  181. package/src/capabilities/executions.ts +298 -0
  182. package/src/capabilities/migrate.ts +81 -0
  183. package/src/capabilities/server.ts +116 -35
  184. package/src/capabilities/stream-body.ts +16 -0
  185. package/src/capabilities/streams.ts +28 -0
  186. package/src/cli/admin/ai-quotas.ts +161 -0
  187. package/src/cli/admin/ai-skills.ts +64 -0
  188. package/src/cli/admin/ai-usage.ts +53 -30
  189. package/src/cli/admin/app-credentials.ts +77 -0
  190. package/src/cli/admin/data.ts +17 -0
  191. package/src/cli/admin/index.ts +10 -1
  192. package/src/cli/admin/instance.ts +2 -2
  193. package/src/cli/admin/linux.ts +4 -2
  194. package/src/cli/admin/sync.ts +1 -1
  195. package/src/cli/capabilities.ts +42 -0
  196. package/src/cli/capability-streams.ts +25 -0
  197. package/src/cli/index.ts +3 -2
  198. package/src/config/define-env.ts +97 -0
  199. package/src/config/env.ts +147 -51
  200. package/src/config/index.ts +3 -1
  201. package/src/contracts/app.ts +23 -3
  202. package/src/contracts/capabilities.ts +75 -5
  203. package/src/contracts/capability-streams.ts +46 -0
  204. package/src/contracts/commands.ts +49 -0
  205. package/src/contracts/index.ts +5 -2
  206. package/src/contracts/rail-admin.ts +38 -0
  207. package/src/contracts/rail-preferences.ts +16 -0
  208. package/src/contracts/registry.ts +5 -27
  209. package/src/contracts/settings-types.ts +1 -1
  210. package/src/contracts/shared.ts +18 -5
  211. package/src/contracts/web-vitals.ts +27 -0
  212. package/src/desktop/index.ts +1 -0
  213. package/src/desktop/solid.tsx +1 -1
  214. package/src/index.ts +8 -6
  215. package/src/server/index.ts +3 -0
  216. package/src/server/middleware/auth.ts +3 -2
  217. package/src/server/middleware/route-template.ts +9 -3
  218. package/src/server/middleware/runtime.ts +2 -1
  219. package/src/server/middleware/settings.ts +17 -11
  220. package/src/server/ratelimit.ts +1 -1
  221. package/src/server/services/access-revision.ts +10 -0
  222. package/src/server/services/access.ts +3 -1
  223. package/src/server/services/index.ts +1 -0
  224. package/src/services/account-category-policy.ts +9 -0
  225. package/src/services/account-lifecycle/audit.ts +1 -1
  226. package/src/services/accounts/app.ts +12 -2
  227. package/src/services/accounts/email-write.ts +23 -0
  228. package/src/services/accounts/groups.ts +15 -4
  229. package/src/services/accounts/identities.ts +249 -0
  230. package/src/services/accounts/identity-reconciliation.ts +87 -0
  231. package/src/services/accounts/ipa-data.ts +2 -2
  232. package/src/services/accounts/local-groups.ts +2 -2
  233. package/src/services/accounts/notification-sender.ts +12 -2
  234. package/src/services/accounts/posix.ts +45 -29
  235. package/src/services/accounts/users.ts +2 -2
  236. package/src/services/announcements/index.ts +69 -8
  237. package/src/services/app-approval.ts +11 -10
  238. package/src/services/audit/index.ts +1 -1
  239. package/src/services/cache-fill.ts +35 -0
  240. package/src/services/help/index.ts +109 -0
  241. package/src/services/help/maintenance.ts +25 -0
  242. package/src/services/help/store.ts +180 -0
  243. package/src/services/help/types.ts +46 -0
  244. package/src/services/identity/invocation-actor.ts +2 -2
  245. package/src/services/identity/key-config.ts +8 -5
  246. package/src/services/identity/key-ring.ts +1 -1
  247. package/src/services/identity/runtime-config.ts +6 -5
  248. package/src/services/index.ts +15 -0
  249. package/src/services/ipa/posix.ts +1 -1
  250. package/src/services/ipa/sync.ts +1 -1
  251. package/src/services/ipa/users.ts +268 -221
  252. package/src/services/legal-consent.ts +1 -1
  253. package/src/services/logging/index.ts +15 -13
  254. package/src/services/logging/trace.ts +2 -1
  255. package/src/services/logging/web-vitals.ts +81 -0
  256. package/src/services/mandates/index.ts +21 -4
  257. package/src/services/mandates/policy.ts +165 -2
  258. package/src/services/notifications/batches.ts +125 -57
  259. package/src/services/notifications/browser.ts +12 -0
  260. package/src/services/notifications/index.ts +0 -2
  261. package/src/services/notifications/platform.ts +0 -4
  262. package/src/services/oauth-tokens.ts +3 -3
  263. package/src/services/pdf/gotenberg.ts +131 -72
  264. package/src/services/pdf/index.ts +11 -8
  265. package/src/services/providers/local/users.ts +25 -16
  266. package/src/services/public-http.ts +101 -0
  267. package/src/services/rail-shortcuts.ts +95 -0
  268. package/src/services/rail-snapshot.ts +53 -0
  269. package/src/services/request-cache-redis.ts +29 -0
  270. package/src/services/session/index.ts +15 -2
  271. package/src/services/session/user.ts +62 -37
  272. package/src/services/settings/app.ts +5 -1
  273. package/src/services/settings/core-settings.ts +26 -35
  274. package/src/services/settings/defaults.ts +5 -0
  275. package/src/services/settings/index.ts +3 -3
  276. package/src/services/settings/store.ts +58 -84
  277. package/src/services/webauthn.ts +1 -1
  278. package/src/shared/ai-costs.ts +42 -0
  279. package/src/shared/ai-platform-prompt.ts +1 -1
  280. package/src/shared/ai-quotas.ts +136 -0
  281. package/src/shared/ai-usage.ts +6 -16
  282. package/src/shared/app-presentation.ts +21 -15
  283. package/src/shared/capability-messages.ts +14 -0
  284. package/src/shared/help.ts +6 -42
  285. package/src/shared/index.ts +3 -0
  286. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  287. package/src/ssr/AdminLayout.tsx +7 -5
  288. package/src/ssr/AdminSidebar.tsx +30 -16
  289. package/src/ssr/AppLaunchpad.island.tsx +33 -136
  290. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  291. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  292. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  293. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  294. package/src/ssr/Layout.tsx +17 -6
  295. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  296. package/src/ssr/LayoutFooter.tsx +1 -1
  297. package/src/ssr/LayoutHeader.tsx +46 -14
  298. package/src/ssr/LayoutHelp.tsx +22 -23
  299. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  300. package/src/ssr/LayoutRail.tsx +17 -9
  301. package/src/ssr/MinimalLayout.tsx +1 -1
  302. package/src/ssr/MobileNavigation.tsx +71 -0
  303. package/src/ssr/MobileProfileActions.tsx +49 -0
  304. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  305. package/src/ssr/RailApps.island.tsx +2 -2
  306. package/src/ssr/RailEditor.tsx +17 -2
  307. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  308. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  309. package/src/ssr/admin-navigation.ts +11 -1
  310. package/src/ssr/help.ts +29 -0
  311. package/src/ssr/islands/SearchBar.tsx +67 -0
  312. package/src/ssr/islands/index.ts +3 -1
  313. package/src/ssr/layout-context.ts +2 -2
  314. package/src/ssr/layout-help-search.ts +15 -0
  315. package/src/ssr/mobile-menu-history.ts +70 -0
  316. package/src/ssr/platform-messages.ts +16 -8
  317. package/src/ssr/profile-actions.ts +42 -0
  318. package/src/ssr/rail-context.ts +8 -4
  319. package/src/ssr/rail-messages.ts +2 -0
  320. package/src/ssr/rail-navigation.ts +3 -3
  321. package/src/ssr/workspace-navigation.ts +75 -0
  322. package/src/styles/effects.css +6 -6
  323. package/src/styles/global.css +54 -0
  324. package/src/styles/input.css +43 -41
  325. package/src/styles/resource-search.css +505 -0
  326. package/src/styles/utilities-feedback.css +13 -0
  327. package/src/styles/utilities-layout.css +1 -5
  328. package/src/styles/utilities-navigation.css +3 -39
  329. package/src/types/ambient.d.ts +5 -0
  330. package/src/workflows/ai/index.ts +2 -2
  331. package/src/workflows/ai/runtime.ts +19 -90
  332. package/src/workflows/ai/store.ts +2 -0
  333. package/src/workflows/ai/types.ts +6 -82
  334. package/src/workflows/ai-actions.ts +16 -13
  335. package/src/workflows/definition.ts +10 -5
  336. package/src/workflows/store/actions.ts +44 -20
  337. package/src/workflows/store/index.ts +4 -2
  338. package/src/workflows/store/runs.ts +36 -0
  339. package/src/workflows/store/worker-pool.ts +55 -0
  340. package/src/workflows/store/worker-runtime.ts +90 -0
  341. package/src/ai/kit-skill.ts +0 -22
  342. package/src/ai/usage-migrate.ts +0 -81
  343. package/src/browser/notification-target.ts +0 -27
  344. package/src/contracts/notification-live.ts +0 -66
  345. package/src/server/services/freeipa/test-certificates.ts +0 -44
  346. package/src/services/notifications/live.ts +0 -50
  347. package/src/services/session/test-fixture.ts +0 -18
  348. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  349. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -1,3 +1,7 @@
1
+ import { registerHelp } from "../services/help";
2
+ import { preloadLayoutHelp } from "../ssr/help";
3
+ import { invokeCapabilityStream } from "./capability-streams";
4
+ import { bindProcessApplicationId, clearProcessApplicationId } from "./process-identity";
1
5
  /**
2
6
  * defineApp() — The single entry point for every cloud app.
3
7
  *
@@ -5,6 +9,8 @@
5
9
  * Returns `{ ssr, plugin, config, meta, start }`.
6
10
  */
7
11
 
12
+ import { resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
8
14
  import type { SsrConfig } from "@k2b/ssr";
9
15
  import { createConfig as createSsrConfig } from "@k2b/ssr";
10
16
  import { routes } from "@k2b/ssr/hono";
@@ -18,10 +24,17 @@ import type {
18
24
  AppLifecycle,
19
25
  AppMeta,
20
26
  AppPresentationCatalog,
27
+ AppSearchLink,
21
28
  CloudLifecycleContext,
22
29
  WidgetEndpoint,
23
30
  } from "../contracts/app";
24
- import { CAPABILITY_FRAMEWORK_ERROR_CODES, CAPABILITY_MAX_REQUEST_BYTES, type CapabilityDefinitions } from "../contracts/capabilities";
31
+ import {
32
+ CAPABILITY_FRAMEWORK_ERROR_CODES,
33
+ CAPABILITY_MAX_REQUEST_BYTES,
34
+ CAPABILITY_ORIGIN_HEADER,
35
+ type CapabilityDefinitions,
36
+ CapabilityOriginSchema,
37
+ } from "../contracts/capabilities";
25
38
  import { type BoundNotificationMap, bindNotificationDefinitions, type NotificationDefinitionMap } from "../contracts/notification-types";
26
39
  import type { AppRegistryEntry } from "../contracts/registry";
27
40
  import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
@@ -30,18 +43,19 @@ import type { HelpDefinition } from "../server/help";
30
43
  import { getLocale, resolveLocale } from "../server/locale";
31
44
  import { type AuthContext, auth } from "../server/middleware/auth";
32
45
  import { requireInvocation } from "../server/middleware/invocation";
33
- import { routeTemplate } from "../server/middleware/route-template";
46
+ import { matchedRouteTemplate, routeTemplate } from "../server/middleware/route-template";
34
47
  import { runtime as runtimeMiddleware } from "../server/middleware/runtime";
35
- import { settings as settingsMiddleware } from "../server/middleware/settings";
48
+ import { preloadLayoutAnnouncements, settings as settingsMiddleware } from "../server/middleware/settings";
36
49
  import {
37
50
  capabilityInvocationOperation,
38
51
  searchInvocationOperation,
39
52
  syncInvocationOperation,
40
53
  widgetInvocationOperation,
41
54
  } from "../services/identity/invocation-operations";
55
+ import { normalizeInvocationRequestId } from "../services/identity/invocation-token";
42
56
  import { logger } from "../services/logging";
43
57
  import { startNotificationDefinitionRegistration } from "../services/notifications/catalog";
44
- import { railPreferences } from "../services/rail-preferences";
58
+ import { readRailSnapshot } from "../services/rail-snapshot";
45
59
  import { get, loadCache as loadSettingsCache, set } from "../services/settings";
46
60
  import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
47
61
  import { registerSettings, toLegacySettingDefs } from "../services/settings/defaults";
@@ -58,12 +72,14 @@ import { compileCapabilities, invokeCompiledCapability, reviewCompiledCapability
58
72
  import { createHeartbeat } from "./heartbeat";
59
73
  import { compileHelp } from "./help";
60
74
  import { createPageResponses } from "./page-responses";
75
+ import { configurePostgresApplicationName } from "./postgres-application-name";
61
76
  import { getProcessSync, startProcessSync } from "./process-sync";
62
77
  import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
63
- import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry, helpRegistry } from "./registry";
78
+ import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
64
79
  import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
65
80
  import { servePublicAsset } from "./static-assets";
66
81
  import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
82
+ import { WEB_VITALS_ASSET_HREF } from "./web-vitals-asset";
67
83
 
68
84
  /** Cache-busting version stamp — changes on every server start / rebuild. */
69
85
  const v = Date.now();
@@ -76,6 +92,8 @@ export type PageOptions = {
76
92
  lang?: string;
77
93
  };
78
94
 
95
+ type ResolvedPageOptions = PageOptions & { performanceRoute?: string };
96
+
79
97
  // ── Public types ────────────────────────────────────────────────────────────
80
98
 
81
99
  /**
@@ -103,6 +121,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
103
121
  adminHref?: string;
104
122
  /** Multi-link admin navigation contributed by this app. */
105
123
  adminNav?: ReadonlyArray<{
124
+ section?: "ai";
106
125
  id?: string;
107
126
  label: string;
108
127
  links: ReadonlyArray<{ label: string; href: string; icon: string }>;
@@ -136,6 +155,8 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
136
155
  * always open in a new tab from the login footer.
137
156
  */
138
157
  legalLinks?: ReadonlyArray<{ label: string; href: string; icon?: string }>;
158
+ /** Static destinations for the global search UI; no capabilities are created. */
159
+ searchLinks?: readonly AppSearchLink[];
139
160
  /**
140
161
  * Dashboard widget endpoints this app exposes. Each entry references an
141
162
  * HTTP path on this app that returns a `WidgetResponse`. The dashboard
@@ -177,9 +198,10 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
177
198
  */
178
199
  openapi?: string;
179
200
  /**
180
- * Project root used by the SSR plugin to discover island/client files.
201
+ * Project root used for SSR component IDs and development assets.
181
202
  * Defaults to `process.cwd()`. Override only if you run the entrypoint
182
- * from a directory other than the project root.
203
+ * from a directory other than the project root. Discovery uses APP_DIR/src
204
+ * (or appRoot/src) plus the installed Cloud framework source.
183
205
  */
184
206
  appRoot?: string;
185
207
  };
@@ -280,7 +302,8 @@ export const defineApp = <
280
302
  >(
281
303
  opts: AppOptions<S, N, AppId>,
282
304
  ): AppDefinition<S, N, AppId> => {
283
- const isDevelopment = process.env.NODE_ENV === "development";
305
+ configurePostgresApplicationName(opts.id);
306
+ const isDevelopment = env.IS_DEVELOPMENT;
284
307
  const notifications = bindNotificationDefinitions(opts.id, opts.notifications);
285
308
 
286
309
  // ── 0. Register declared settings into the runtime registry ──────────
@@ -293,12 +316,14 @@ export const defineApp = <
293
316
  }
294
317
 
295
318
  // ── 1. SSR config ─────────────────────────────────────────────────────
296
- const { config, plugin, html } = createSsrConfig<PageOptions>({
319
+ const rootDir = opts.appRoot ?? process.cwd();
320
+ const { config, plugin, html } = createSsrConfig<ResolvedPageOptions>({
297
321
  dev: isDevelopment,
298
322
  verbose: true,
299
- rootDir: opts.appRoot ?? process.cwd(),
323
+ rootDir,
324
+ componentRoots: [resolve(env.APP_DIR ?? rootDir, "src"), fileURLToPath(new URL("../", import.meta.url))],
300
325
  basePath: opts.basePath,
301
- template: ({ body, scripts, title, description, theme, lang }) => {
326
+ template: ({ body, scripts, title, description, theme, lang, performanceRoute }) => {
302
327
  const themeFixed = theme !== undefined;
303
328
  return `<!DOCTYPE html>
304
329
  <html lang="${normalizeLocale(lang)}" class="${theme ?? "light"}"${themeFixed ? " data-theme-fixed" : ""}>
@@ -321,6 +346,7 @@ export const defineApp = <
321
346
  ${body}
322
347
  </body>
323
348
  ${scripts}
349
+ ${performanceRoute ? `<script type="module" src="${WEB_VITALS_ASSET_HREF}" data-cloud-web-vitals data-app-id="${escapeHtml(opts.id)}" data-route-template="${escapeHtml(performanceRoute)}"></script>` : ""}
324
350
  </html>`;
325
351
  },
326
352
  });
@@ -336,10 +362,16 @@ export const defineApp = <
336
362
  // `getDateConfig` share one canonical `getLocale(c)` that page handlers
337
363
  // cannot override.
338
364
  const ssr = Object.assign(
339
- createStatusPreservingSsrHandler<PageOptions>(html, async (c) => {
365
+ createStatusPreservingSsrHandler<ResolvedPageOptions>(html, async (c) => {
340
366
  c.get("page").lang = getLocale(c);
341
367
  const user = c.get("user");
342
- if (user) c.set("railPreferences", await railPreferences.get(user.id));
368
+ c.get("page").performanceRoute =
369
+ user && c.get("settings")?.observability?.web_vitals?.enabled === true ? (matchedRouteTemplate(c) ?? undefined) : undefined;
370
+ await Promise.all([
371
+ preloadLayoutAnnouncements(c),
372
+ preloadLayoutHelp(c),
373
+ user ? readRailSnapshot(user).then((snapshot) => c.set("railPreferences", snapshot)) : undefined,
374
+ ]);
343
375
  }),
344
376
  createPageResponses(html),
345
377
  );
@@ -354,6 +386,7 @@ export const defineApp = <
354
386
  adminHref: opts.adminHref,
355
387
  adminNav: opts.adminNav?.map(
356
388
  (group): AppAdminNavigationGroup => ({
389
+ ...(group.section ? { section: group.section } : {}),
357
390
  id: group.id,
358
391
  label: group.label,
359
392
  links: group.links.map((link) => ({ ...link })),
@@ -362,6 +395,7 @@ export const defineApp = <
362
395
  routes: [...opts.routes],
363
396
  nav: opts.nav,
364
397
  legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
398
+ searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
365
399
  widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
366
400
  settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
367
401
  openapi: opts.openapi,
@@ -400,6 +434,8 @@ export const defineApp = <
400
434
  }
401
435
  })());
402
436
  try {
437
+ bindProcessApplicationId(meta.id);
438
+ cleanup.unshift(clearProcessApplicationId);
403
439
  const startedAt = Date.now();
404
440
 
405
441
  // OpenAPI advertised in the registry only when there's a router to
@@ -410,8 +446,6 @@ export const defineApp = <
410
446
  const compiledHelp = startOpts.help
411
447
  ? compileHelp({
412
448
  appId: meta.id,
413
- appName: meta.name,
414
- appIcon: meta.icon,
415
449
  basePath: opts.basePath,
416
450
  definition: startOpts.help,
417
451
  })
@@ -441,6 +475,7 @@ export const defineApp = <
441
475
  }
442
476
  : undefined,
443
477
  adminNav: meta.adminNav?.map((group) => ({
478
+ ...(group.section ? { section: group.section } : {}),
444
479
  id: group.id,
445
480
  label: group.label,
446
481
  links: group.links.map((link) => ({ ...link })),
@@ -453,6 +488,7 @@ export const defineApp = <
453
488
  : undefined,
454
489
  help: compiledHelp?.summary,
455
490
  legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
491
+ searchLinks: meta.searchLinks,
456
492
  widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
457
493
  settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
458
494
  openapi: advertiseOpenapi ? opts.openapi : undefined,
@@ -461,6 +497,7 @@ export const defineApp = <
461
497
  // Heartbeat
462
498
  const heartbeat = createHeartbeat(meta.id, entry, {
463
499
  registry: appRegistry(),
500
+ beforeWrite: compiledHelp ? () => registerHelp(compiledHelp.corpus) : undefined,
464
501
  onError: (error) =>
465
502
  log.error("Registry heartbeat failed", {
466
503
  appId: meta.id,
@@ -499,27 +536,6 @@ export const defineApp = <
499
536
  },
500
537
  })
501
538
  : undefined;
502
- const helpHeartbeat = compiledHelp
503
- ? createHeartbeat(meta.id, compiledHelp.registryEntry, {
504
- key: `help/${meta.id}`,
505
- registry: helpRegistry(),
506
- onError: (error) =>
507
- log.error("Help registry heartbeat failed", {
508
- appId: meta.id,
509
- error: error instanceof Error ? error.message : String(error),
510
- }),
511
- onStale: (error) => {
512
- log.error("Help registry lease expired; restarting", {
513
- appId: meta.id,
514
- error: error instanceof Error ? error.message : String(error),
515
- });
516
- process.exit(1);
517
- },
518
- })
519
- : undefined;
520
- cleanup.push(async () => {
521
- await helpHeartbeat?.stop();
522
- });
523
539
  cleanup.push(async () => {
524
540
  await capabilityHeartbeat?.stop();
525
541
  });
@@ -614,6 +630,8 @@ export const defineApp = <
614
630
  user,
615
631
  idempotencyKey,
616
632
  locale: requestLocale,
633
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
634
+ origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
617
635
  signal: c.req.raw.signal,
618
636
  },
619
637
  onUnexpectedError: (error: unknown) =>
@@ -654,6 +672,32 @@ export const defineApp = <
654
672
  schemaHash: operation.manifest.schemaHash,
655
673
  };
656
674
  });
675
+ for (const kind of ["queries", "actions"] as const) {
676
+ server.post(
677
+ `/api/_internal/capabilities/v1/streams/${kind}/:capabilityId/:verb`,
678
+ capabilityAuth(kind),
679
+ auth.requireOAuthScope(kind === "queries" ? "read" : "write", "admin"),
680
+ async (c) => {
681
+ const actor = c.get("actor");
682
+ return invokeCapabilityStream({
683
+ compiled: compiledCapabilities,
684
+ kind,
685
+ localId: c.req.param("capabilityId") ?? "",
686
+ verb: c.req.param("verb") ?? "",
687
+ request: c.req.raw,
688
+ context: {
689
+ actor,
690
+ accessSubject: c.get("accessSubject"),
691
+ user: actor.kind === "user" ? actor.user : actor.delegatedUser,
692
+ locale: resolveLocale(c.req.raw.headers, await get<string>("app.locale")),
693
+ requestId: normalizeInvocationRequestId(c.req.header("x-request-id")) ?? crypto.randomUUID(),
694
+ origin: CapabilityOriginSchema.catch("http").parse(c.req.header(CAPABILITY_ORIGIN_HEADER)),
695
+ signal: c.req.raw.signal,
696
+ },
697
+ });
698
+ },
699
+ );
700
+ }
657
701
  const capabilityReadScope = auth.requireOAuthScope("read", "admin");
658
702
  const capabilityWriteScope = auth.requireOAuthScope("write", "admin");
659
703
  server.post("/api/_internal/capabilities/v1/queries/:capabilityId", capabilityAuth("queries"), capabilityReadScope, (c) =>
@@ -759,7 +803,6 @@ export const defineApp = <
759
803
 
760
804
  // Only advertise after setup, workers, and their declared resources are ready.
761
805
  // Publish supporting entries first; the app entry makes them discoverable.
762
- await helpHeartbeat?.start();
763
806
  await capabilityHeartbeat?.start();
764
807
  await heartbeat.start();
765
808
  log.info(`Registered "${meta.id}"`, { baseUrl });
@@ -18,6 +18,8 @@ type HeartbeatRegistry<T> = {
18
18
  type HeartbeatOptions<T> = {
19
19
  registry: HeartbeatRegistry<T>;
20
20
  key?: string;
21
+ /** Publish dependent data before advertising or renewing the app. */
22
+ beforeWrite?: () => Promise<void>;
21
23
  intervalMs?: number;
22
24
  retryMs?: number;
23
25
  staleAfterMs?: number;
@@ -53,6 +55,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
53
55
  };
54
56
 
55
57
  const register = async (): Promise<void> => {
58
+ await options.beforeWrite?.();
56
59
  await registry.upsert({ key, value: entry });
57
60
  noteSuccess();
58
61
  };
@@ -61,6 +64,7 @@ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOp
61
64
  // Touch renews the existing entry (and emits a registry invalidation).
62
65
  // Avoid rebuilding the manifest here. A missing entry is repaired
63
66
  // immediately, preserving restart-free registry recovery.
67
+ await options.beforeWrite?.();
64
68
  const touched = await registry.touch({ key });
65
69
  if (!touched) await registry.upsert({ key, value: entry });
66
70
  noteSuccess();
@@ -1,49 +1,7 @@
1
- import type { AppRegistryEntry, HelpRegistryDocument, HelpRegistryEntry } from "../contracts/registry";
2
- import { helpLocaleChain } from "../shared/help";
1
+ import type { HelpArticle } from "../services/help/types";
3
2
  import { markdownToPlainText } from "../shared/markdown";
4
- import { getApp, getHelp, listApps, listHelp } from "./registry";
5
-
6
3
  export const HELP_SEARCH_MAX_LIMIT = 25;
7
4
  export const HELP_READ_MAX_CHARS = 7_000;
8
-
9
- export type HelpCatalogDocument = {
10
- appId: string;
11
- appName: string;
12
- appIcon?: string;
13
- manifestHash: string;
14
- locale: string;
15
- documentId: string;
16
- title: string;
17
- description?: string;
18
- order: number;
19
- markdown: string;
20
- searchText: string;
21
- };
22
-
23
- export type HelpCatalogItem = Omit<HelpCatalogDocument, "appIcon" | "manifestHash" | "order" | "markdown" | "searchText"> & {
24
- kind: "help";
25
- };
26
-
27
- export type HelpCatalogRead = HelpCatalogItem & {
28
- markdown: string;
29
- truncated: boolean;
30
- };
31
-
32
- export type HelpCatalogDependencies = {
33
- listApps?: () => Promise<AppRegistryEntry[]>;
34
- listHelp?: () => Promise<HelpRegistryEntry[]>;
35
- };
36
-
37
- export type AppHelpDependencies = {
38
- getApp?: (appId: string) => Promise<AppRegistryEntry | null>;
39
- getHelp?: (appId: string) => Promise<HelpRegistryEntry | null>;
40
- };
41
-
42
- export type ResolvedAppHelp =
43
- | { status: "available"; app: AppRegistryEntry; help: HelpRegistryEntry }
44
- | { status: "missing" }
45
- | { status: "stale" };
46
-
47
5
  const normalizeSearchText = (value: string): string =>
48
6
  value
49
7
  .normalize("NFKD")
@@ -51,125 +9,7 @@ const normalizeSearchText = (value: string): string =>
51
9
  .toLocaleLowerCase()
52
10
  .replace(/[^\p{L}\p{N}]+/gu, " ")
53
11
  .trim();
54
-
55
- const searchTerms = (value: string): string[] =>
56
- Array.from(
57
- new Set(
58
- normalizeSearchText(value)
59
- .split(" ")
60
- .filter((term) => term.length > 0),
61
- ),
62
- );
63
-
64
- export const resolveAppHelp = async (appId: string, dependencies: AppHelpDependencies = {}): Promise<ResolvedAppHelp> => {
65
- const [app, help] = await Promise.all([(dependencies.getApp ?? getApp)(appId), (dependencies.getHelp ?? getHelp)(appId)]);
66
- if (!app?.help || !help) return { status: "missing" };
67
- if (app.help.manifestHash !== help.manifestHash) return { status: "stale" };
68
- return { status: "available", app, help };
69
- };
70
-
71
- export const loadCurrentHelp = async (dependencies: HelpCatalogDependencies = {}): Promise<HelpRegistryEntry[]> => {
72
- const [apps, entries] = await Promise.all([(dependencies.listApps ?? listApps)(), (dependencies.listHelp ?? listHelp)()]);
73
- const manifestHashes = new Map(apps.flatMap((app) => (app.help ? [[app.id, app.help.manifestHash] as const] : [])));
74
- return entries.filter((entry) => manifestHashes.get(entry.appId) === entry.manifestHash);
75
- };
76
-
77
- const resolveRegistryDocuments = (
78
- entry: HelpRegistryEntry,
79
- requestedLocale: string,
80
- ): { locale: string; documents: HelpRegistryDocument[] } => {
81
- const baseLocale = entry.baseLocale ?? "en";
82
- const variants = entry.documentsByLocale ?? {};
83
- const chain = helpLocaleChain(requestedLocale, baseLocale);
84
- const locale = chain.find((candidate) => candidate === baseLocale || variants[candidate] !== undefined) ?? baseLocale;
85
- const byId = new Map(entry.documents.map((document) => [document.id, document]));
86
- for (const candidate of [...chain.slice(0, chain.indexOf(baseLocale)), baseLocale].reverse()) {
87
- if (candidate === baseLocale) continue;
88
- for (const document of variants[candidate] ?? []) byId.set(document.id, { ...byId.get(document.id), ...document });
89
- }
90
- return { locale, documents: entry.documents.map((document) => byId.get(document.id) ?? document) };
91
- };
92
-
93
- export const createHelpCatalog = (entries: readonly HelpRegistryEntry[], requestedLocale = "en"): HelpCatalogDocument[] =>
94
- entries
95
- .flatMap((entry) => {
96
- const resolved = resolveRegistryDocuments(entry, requestedLocale);
97
- return resolved.documents.map((document) => ({
98
- appId: entry.appId,
99
- appName: entry.appName,
100
- appIcon: entry.appIcon,
101
- manifestHash: entry.manifestHash,
102
- locale: resolved.locale,
103
- documentId: document.id,
104
- title: document.title,
105
- description: document.description,
106
- order: document.order,
107
- markdown: document.markdown,
108
- searchText: document.searchText ?? markdownToPlainText(document.markdown),
109
- }));
110
- })
111
- .sort(
112
- (left, right) => left.appId.localeCompare(right.appId) || left.order - right.order || left.documentId.localeCompare(right.documentId),
113
- );
114
-
115
- export const loadHelpCatalog = async (dependencies: HelpCatalogDependencies = {}, requestedLocale = "en"): Promise<HelpCatalogDocument[]> =>
116
- createHelpCatalog(await loadCurrentHelp(dependencies), requestedLocale);
117
-
118
- const catalogItem = (document: HelpCatalogDocument): HelpCatalogItem => ({
119
- appId: document.appId,
120
- appName: document.appName,
121
- kind: "help",
122
- locale: document.locale,
123
- documentId: document.documentId,
124
- title: document.title,
125
- description: document.description,
126
- });
127
-
128
- const searchScore = (document: HelpCatalogDocument, query: string): number => {
129
- const phrase = normalizeSearchText(query);
130
- const terms = searchTerms(query);
131
- if (!phrase || terms.length === 0) return 0;
132
-
133
- const app = normalizeSearchText(`${document.appId} ${document.appName}`);
134
- const identity = normalizeSearchText(`${document.documentId} ${document.title}`);
135
- const description = normalizeSearchText(document.description ?? "");
136
- const body = normalizeSearchText(document.searchText);
137
- let score = 0;
138
- if (identity === phrase) score += 80;
139
- else if (identity.includes(phrase)) score += 40;
140
- if (app.includes(phrase)) score += 30;
141
- if (description.includes(phrase)) score += 20;
142
- if (body.includes(phrase)) score += 10;
143
- for (const term of terms) {
144
- if (identity.includes(term)) score += 8;
145
- if (app.includes(term)) score += 6;
146
- if (description.includes(term)) score += 4;
147
- if (body.includes(term)) score += 1;
148
- }
149
- return score;
150
- };
151
-
152
- export const searchHelpCatalog = (
153
- catalog: readonly HelpCatalogDocument[],
154
- input: { query: string; appId?: string; limit?: number },
155
- ): HelpCatalogItem[] => {
156
- const limit = Math.max(1, Math.min(Math.floor(input.limit ?? 10), HELP_SEARCH_MAX_LIMIT));
157
- return catalog
158
- .filter((document) => !input.appId || document.appId === input.appId)
159
- .map((document) => ({ document, score: searchScore(document, input.query) }))
160
- .filter(({ score }) => score > 0)
161
- .sort(
162
- (left, right) =>
163
- right.score - left.score ||
164
- left.document.appId.localeCompare(right.document.appId) ||
165
- left.document.documentId.localeCompare(right.document.documentId),
166
- )
167
- .slice(0, limit)
168
- .map(({ document }) => catalogItem(document));
169
- };
170
-
171
- export const findHelpDocument = (catalog: readonly HelpCatalogDocument[], appId: string, documentId: string): HelpCatalogDocument | null =>
172
- catalog.find((document) => document.appId === appId && document.documentId === documentId) ?? null;
12
+ const searchTerms = (value: string): string[] => [...new Set(normalizeSearchText(value).split(" ").filter(Boolean))];
173
13
 
174
14
  const splitSections = (markdown: string): string[] => {
175
15
  const starts = [
@@ -193,7 +33,21 @@ const boundedExcerpt = (markdown: string, terms: readonly string[]): string => {
193
33
  return `${start > 0 ? `${marker}\n\n` : ""}${body}${start + budget < markdown.length ? `\n\n${marker}` : ""}`;
194
34
  };
195
35
 
196
- const selectMarkdown = (markdown: string, query?: string): { markdown: string; truncated: boolean } => {
36
+ export const selectHelpMarkdown = (markdown: string, query?: string): { markdown: string; truncated: boolean } => {
37
+ // An exact heading identifies one complete reference section, even in short articles.
38
+ const headingQuery = query?.trim().toLocaleLowerCase();
39
+ const exact =
40
+ headingQuery &&
41
+ splitSections(markdown).find((section) => {
42
+ const heading = section
43
+ .match(/^##\s+([^\n]+)/)?.[1]
44
+ ?.replace(/\s*\{icon="[^"]*"\}\s*$/, "")
45
+ .replaceAll("`", "")
46
+ .trim()
47
+ .toLocaleLowerCase();
48
+ return heading === headingQuery;
49
+ });
50
+ if (exact) return { markdown: boundedExcerpt(exact, [headingQuery]), truncated: exact !== markdown };
197
51
  if (markdown.length <= HELP_READ_MAX_CHARS) return { markdown, truncated: false };
198
52
  const terms = searchTerms(query ?? "");
199
53
  const phrase = normalizeSearchText(query ?? "");
@@ -217,15 +71,16 @@ const selectMarkdown = (markdown: string, query?: string): { markdown: string; t
217
71
  return { markdown: excerpt || boundedExcerpt(markdown, terms), truncated: true };
218
72
  };
219
73
 
220
- export const readHelpCatalog = (
221
- catalog: readonly HelpCatalogDocument[],
222
- input: { appId: string; documentId: string; query?: string },
223
- ): HelpCatalogRead | null => {
224
- const document = findHelpDocument(catalog, input.appId, input.documentId);
225
- if (!document) return null;
226
- const selected = selectMarkdown(document.markdown, input.query);
227
- return { ...catalogItem(document), ...selected };
228
- };
74
+ export const readHelpArticle = (document: HelpArticle, query?: string) => ({
75
+ kind: "help" as const,
76
+ appId: document.appId,
77
+ appName: document.appName,
78
+ locale: document.locale,
79
+ documentId: document.documentId,
80
+ title: document.title,
81
+ description: document.description,
82
+ ...selectHelpMarkdown(document.markdown, query),
83
+ });
229
84
 
230
85
  export const helpResourceUri = (appId: string, documentId: string): string =>
231
86
  `cloud://help/${encodeURIComponent(appId)}/${encodeURIComponent(documentId)}`;
@@ -1,42 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { AppRegistryHelpSummary, HelpRegistryEntry } from "../contracts/registry";
2
+ import type { AppRegistryHelpSummary } from "../contracts/registry";
3
3
  import type { HelpDefinition } from "../server/help";
4
- import type { HelpDocumentManifest } from "../shared/help";
4
+ import type { HelpCorpus, HelpDocument } from "../services/help/types";
5
5
 
6
- export const HELP_REGISTRY_MAX_BYTES = 512 * 1024;
7
6
  export const HELP_DOCUMENT_MAX_BYTES = 128 * 1024;
7
+ export type CompiledHelp = { summary: AppRegistryHelpSummary; corpus: HelpCorpus };
8
8
 
9
- export type CompiledHelp = {
10
- summary: AppRegistryHelpSummary;
11
- registryEntry: HelpRegistryEntry;
12
- };
13
-
14
- const serializedBytes = (value: unknown): number => new TextEncoder().encode(JSON.stringify(value)).byteLength;
15
-
16
- const helpHash = (value: unknown): string => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
-
18
- const omitSearchText = (documents: readonly HelpRegistryEntry["documents"][number][]) =>
19
- documents.map(({ searchText: _searchText, ...document }) => document);
20
-
21
- const normalizedBasePath = (basePath: string | undefined): string => {
22
- const value = basePath?.replace(/\/$/, "") ?? "";
23
- return value || "";
24
- };
25
-
26
- export const compileHelp = (input: {
27
- appId: string;
28
- appName: string;
29
- appIcon: string;
30
- basePath?: string;
31
- definition: HelpDefinition;
32
- }): CompiledHelp => {
33
- const appId = encodeURIComponent(input.appId);
34
- const pageBase = `${normalizedBasePath(input.basePath)}/help`;
35
- const searchUrl = `/api/help/v1/${appId}/search`;
36
- const compileDocuments = (source: readonly (typeof input.definition.documents)[number][]) =>
9
+ export const compileHelp = (input: { appId: string; basePath?: string; definition: HelpDefinition }): CompiledHelp => {
10
+ const compileDocuments = (source: HelpDefinition["documents"]): HelpDocument[] =>
37
11
  source.map(({ id, title, icon, description, order, markdown, searchText }) => {
38
- const markdownBytes = new TextEncoder().encode(markdown).byteLength;
39
- if (markdownBytes > HELP_DOCUMENT_MAX_BYTES) {
12
+ if (Buffer.byteLength(markdown) > HELP_DOCUMENT_MAX_BYTES) {
40
13
  throw new Error(`Help document "${id}" exceeds the ${HELP_DOCUMENT_MAX_BYTES}-byte limit`);
41
14
  }
42
15
  return { id, title, icon, description, order, markdown, searchText };
@@ -46,50 +19,14 @@ export const compileHelp = (input: {
46
19
  const documentsByLocale = Object.fromEntries(
47
20
  Object.entries(input.definition.documentsByLocale ?? {})
48
21
  .filter(([locale]) => locale !== baseLocale)
49
- .map(([locale, localized]) => [locale, compileDocuments(localized)]),
22
+ .sort(([a], [b]) => a.localeCompare(b))
23
+ .map(([locale, docs]) => [locale, compileDocuments(docs)]),
50
24
  );
51
- const manifestHash = helpHash({ appId: input.appId, baseLocale, documents, documentsByLocale });
52
- const toManifest = (source: readonly (typeof documents)[number][]) =>
53
- source.map<HelpDocumentManifest>(({ id, title, icon, description, order }) => ({
54
- id,
55
- title,
56
- icon,
57
- description,
58
- order,
59
- searchUrl,
60
- url: `/api/help/v1/${appId}/documents/${encodeURIComponent(id)}`,
61
- }));
62
- const manifest = toManifest(documents);
63
- const manifestByLocale = Object.fromEntries(
64
- Object.entries(documentsByLocale).map(([locale, localized]) => [locale, toManifest(localized)]),
65
- );
66
- let registryEntry: HelpRegistryEntry = {
67
- appId: input.appId,
68
- appName: input.appName,
69
- appIcon: input.appIcon,
70
- manifestHash,
71
- baseLocale,
72
- documentsByLocale,
73
- documents,
74
- };
75
- let bytes = serializedBytes(registryEntry);
76
- if (bytes > HELP_REGISTRY_MAX_BYTES) {
77
- registryEntry = {
78
- ...registryEntry,
79
- documents: omitSearchText(registryEntry.documents),
80
- documentsByLocale: registryEntry.documentsByLocale
81
- ? Object.fromEntries(
82
- Object.entries(registryEntry.documentsByLocale).map(([locale, localized]) => [locale, omitSearchText(localized)]),
83
- )
84
- : undefined,
85
- };
86
- bytes = serializedBytes(registryEntry);
87
- }
88
- if (bytes > HELP_REGISTRY_MAX_BYTES) {
89
- throw new Error(`Help corpus exceeds the ${HELP_REGISTRY_MAX_BYTES}-byte registry limit`);
90
- }
25
+ const manifestHash = createHash("sha256")
26
+ .update(JSON.stringify({ revision: 1, appId: input.appId, baseLocale, documents, documentsByLocale }))
27
+ .digest("hex");
91
28
  return {
92
- summary: { manifestHash, pageBase, baseLocale, documentsByLocale: manifestByLocale, documents: manifest },
93
- registryEntry,
29
+ summary: { manifestHash, pageBase: `${input.basePath?.replace(/\/$/, "") ?? ""}/help`, baseLocale },
30
+ corpus: { appId: input.appId, manifestHash, baseLocale, documents, documentsByLocale },
94
31
  };
95
32
  };
@@ -2,6 +2,7 @@ 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
4
  import { getLocale } from "../server/locale";
5
+ import { preloadLayoutAnnouncements } from "../server/middleware/settings";
5
6
  import { createLoginRedirectUrl } from "../shared/redirect";
6
7
  import type { PageOptions } from "./define-app";
7
8
 
@@ -21,6 +22,7 @@ export const createPageResponses = (html: HtmlFn<PageOptions>) => {
21
22
  // Auth middleware may reject before ssr() initializes page metadata.
22
23
  c.set("page", { lang: getLocale(c) });
23
24
  c.header("Cache-Control", "private, no-store");
25
+ await preloadLayoutAnnouncements(c);
24
26
  // Load JSX only after the application's SSR plugin has been installed.
25
27
  const { renderPageError } = await import("../ssr/PageError");
26
28
  const response = await html(renderPageError(c, status, options), c.get("page"));