@k2b/cloud 0.6.0-rc.0 → 0.7.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 (315) hide show
  1. package/README.md +2 -2
  2. package/package.json +30 -17
  3. package/scripts/browser-performance.ts +14 -0
  4. package/scripts/build-pdf-renderer.ts +39 -0
  5. package/scripts/build.ts +15 -35
  6. package/scripts/preload.ts +9 -12
  7. package/scripts/sync-recovery-smoke.ts +2 -1
  8. package/src/_internal/app-presentation.ts +14 -2
  9. package/src/_internal/capabilities.ts +108 -7
  10. package/src/_internal/capability-streams.ts +81 -0
  11. package/src/_internal/define-app.ts +62 -36
  12. package/src/_internal/heartbeat.ts +4 -0
  13. package/src/_internal/help-catalog.ts +27 -172
  14. package/src/_internal/help.ts +13 -76
  15. package/src/_internal/page-responses.ts +2 -0
  16. package/src/_internal/postgres-application-name.ts +30 -0
  17. package/src/_internal/process-identity.ts +15 -0
  18. package/src/_internal/process-sync.ts +1 -1
  19. package/src/_internal/registry-validation.ts +29 -25
  20. package/src/_internal/registry.ts +2 -53
  21. package/src/_internal/runtime-context.ts +5 -16
  22. package/src/_internal/server-timing.ts +13 -0
  23. package/src/_internal/status-preserving-ssr.ts +18 -6
  24. package/src/_internal/web-vitals-asset.ts +10 -0
  25. package/src/access/PermissionEditor.tsx +14 -153
  26. package/src/access/PrincipalPicker.tsx +104 -0
  27. package/src/access/ui.ts +2 -0
  28. package/src/ai/admin.ts +14 -13
  29. package/src/ai/assistant-models.ts +12 -1
  30. package/src/ai/audio-format.ts +34 -0
  31. package/src/ai/audio-tool.ts +90 -0
  32. package/src/ai/browser-code-contracts.ts +79 -0
  33. package/src/ai/browser.ts +2 -0
  34. package/src/ai/capabilities.ts +55 -36
  35. package/src/ai/capability-execution.ts +47 -12
  36. package/src/ai/chat/blocks.tsx +157 -95
  37. package/src/ai/chat/capability-result.ts +15 -0
  38. package/src/ai/chat/capability-table.tsx +2 -10
  39. package/src/ai/chat/composer-adapter.ts +48 -3
  40. package/src/ai/chat/message-actions.tsx +8 -7
  41. package/src/ai/chat/message-utils.ts +12 -1
  42. package/src/ai/chat/messages.ts +2 -0
  43. package/src/ai/chat/presentation.tsx +19 -48
  44. package/src/ai/chat/primitives.tsx +43 -1
  45. package/src/ai/chat/tool-disclosure.tsx +17 -2
  46. package/src/ai/chat/tool-groups.ts +52 -0
  47. package/src/ai/chat-quotas.ts +25 -0
  48. package/src/ai/chat-task-contracts.ts +9 -1
  49. package/src/ai/chat-tasks.ts +276 -43
  50. package/src/ai/client/controller.ts +110 -33
  51. package/src/ai/client/file-source.ts +12 -6
  52. package/src/ai/client/live-connection.ts +1 -0
  53. package/src/ai/client/projection.ts +22 -11
  54. package/src/ai/client/transport.ts +4 -0
  55. package/src/ai/code-capability-routes.ts +110 -0
  56. package/src/ai/code-capability-transport.ts +38 -0
  57. package/src/ai/code-mode-skill.ts +114 -0
  58. package/src/ai/code-runtime-tools.ts +166 -0
  59. package/src/ai/code-source-contracts.ts +202 -0
  60. package/src/ai/code-source-tools.ts +145 -0
  61. package/src/ai/compaction.ts +27 -6
  62. package/src/ai/data-analysis-skill.ts +10 -0
  63. package/src/ai/default-tools.ts +25 -2
  64. package/src/ai/dictation-runtime.ts +171 -0
  65. package/src/ai/dictations.ts +188 -0
  66. package/src/ai/draft-content.ts +34 -0
  67. package/src/ai/enrich.ts +6 -0
  68. package/src/ai/executor.ts +191 -31
  69. package/src/ai/file-content-version.ts +22 -0
  70. package/src/ai/file-context.ts +2 -1
  71. package/src/ai/file-media-type.ts +12 -0
  72. package/src/ai/file-reference-contracts.ts +15 -0
  73. package/src/ai/file-tools.ts +2 -1
  74. package/src/ai/files-store.ts +174 -71
  75. package/src/ai/fixtures/vision-encrypted.pdf +0 -0
  76. package/src/ai/grids-skill.ts +40 -10
  77. package/src/ai/http.ts +15 -8
  78. package/src/ai/index.ts +26 -2
  79. package/src/ai/inference-calls.ts +198 -0
  80. package/src/ai/live-events.ts +5 -1
  81. package/src/ai/memory-learning.ts +60 -28
  82. package/src/ai/message-queue.ts +167 -0
  83. package/src/ai/migrate.ts +187 -11
  84. package/src/ai/model-pricing.ts +43 -0
  85. package/src/ai/pdf-render-worker.ts +74 -0
  86. package/src/ai/pdf-render.fixture.ts +28 -0
  87. package/src/ai/pdf-render.ts +77 -0
  88. package/src/ai/projects-routes.ts +3 -3
  89. package/src/ai/projects.ts +20 -6
  90. package/src/ai/protocol.ts +3 -0
  91. package/src/ai/provider.ts +1 -8
  92. package/src/ai/quota-provider.ts +191 -0
  93. package/src/ai/quota-report.ts +152 -0
  94. package/src/ai/quotas-migrate.ts +45 -0
  95. package/src/ai/quotas.ts +168 -0
  96. package/src/ai/routes.ts +215 -11
  97. package/src/ai/run-timeout.ts +10 -0
  98. package/src/ai/runtime-tools.ts +3 -0
  99. package/src/ai/runtime.ts +55 -16
  100. package/src/ai/settings.ts +36 -8
  101. package/src/ai/short-id.ts +2 -2
  102. package/src/ai/skill-catalog.ts +0 -10
  103. package/src/ai/skill-search.ts +47 -0
  104. package/src/ai/skill-seeds.ts +105 -20
  105. package/src/ai/skill-tool.ts +16 -5
  106. package/src/ai/skills-routes.ts +32 -4
  107. package/src/ai/skills.ts +324 -155
  108. package/src/ai/store.ts +373 -191
  109. package/src/ai/stream.ts +5 -0
  110. package/src/ai/structured-runs.ts +4 -3
  111. package/src/ai/structured.ts +8 -1
  112. package/src/ai/system-prompt.ts +12 -2
  113. package/src/ai/timeline.ts +2 -5
  114. package/src/ai/todo-contracts.ts +20 -0
  115. package/src/ai/todo-tool.ts +16 -0
  116. package/src/ai/tool-audit.ts +30 -40
  117. package/src/ai/tools.ts +6 -0
  118. package/src/ai/transcription.ts +202 -0
  119. package/src/ai/turn-timing.ts +114 -0
  120. package/src/ai/types.ts +58 -10
  121. package/src/ai/ui.tsx +2 -0
  122. package/src/ai/usage.ts +118 -118
  123. package/src/ai/vision-tool.ts +39 -10
  124. package/src/api/admin-ai-projects.ts +2 -2
  125. package/src/api/admin-ai-quotas.ts +74 -0
  126. package/src/api/admin-ai-skills.ts +48 -5
  127. package/src/api/admin-ai-usage.ts +2 -2
  128. package/src/api/admin-core-settings.ts +25 -0
  129. package/src/api/admin-identity.ts +91 -51
  130. package/src/api/admin-rail.ts +58 -0
  131. package/src/api/announcements.ts +14 -0
  132. package/src/api/capabilities.ts +292 -43
  133. package/src/api/capability-streams.ts +262 -0
  134. package/src/api/help.ts +8 -44
  135. package/src/api/index.ts +6 -0
  136. package/src/api/mcp.ts +44 -26
  137. package/src/api/me-web-vitals.ts +35 -0
  138. package/src/api/me.ts +2 -0
  139. package/src/api/search/schemas.ts +26 -15
  140. package/src/api/search.bench.ts +1 -1
  141. package/src/api/search.ts +54 -17
  142. package/src/browser/CloudResourceSearch.tsx +640 -702
  143. package/src/browser/app-approval-vault.ts +19 -7
  144. package/src/browser/command-bridge.ts +109 -0
  145. package/src/browser/command-shortcuts.ts +76 -0
  146. package/src/browser/commands.ts +66 -0
  147. package/src/browser/navigation-search.ts +48 -0
  148. package/src/browser/notifications.ts +1 -1
  149. package/src/browser/resource-picker.tsx +5 -18
  150. package/src/browser/resource-search-dialog.ts +9 -0
  151. package/src/browser/resource-search-input.ts +46 -0
  152. package/src/browser/resource-search-messages.ts +76 -40
  153. package/src/browser/resource-search.ts +7 -0
  154. package/src/browser/search-bridge.ts +102 -0
  155. package/src/browser/search-commands.ts +59 -0
  156. package/src/browser/search.ts +13 -0
  157. package/src/browser/spotlight-position.ts +208 -0
  158. package/src/browser/testing.ts +5 -0
  159. package/src/browser/web-vitals.ts +25 -0
  160. package/src/capabilities/claims.ts +179 -0
  161. package/src/capabilities/client.ts +34 -0
  162. package/src/capabilities/command-link.ts +16 -0
  163. package/src/capabilities/executions.ts +298 -0
  164. package/src/capabilities/migrate.ts +81 -0
  165. package/src/capabilities/server.ts +114 -34
  166. package/src/capabilities/stream-body.ts +16 -0
  167. package/src/capabilities/streams.ts +28 -0
  168. package/src/cli/admin/ai-quotas.ts +161 -0
  169. package/src/cli/admin/ai-skills.ts +64 -0
  170. package/src/cli/admin/ai-usage.ts +53 -30
  171. package/src/cli/admin/app-credentials.ts +79 -0
  172. package/src/cli/admin/data.ts +17 -0
  173. package/src/cli/admin/index.ts +9 -0
  174. package/src/cli/capabilities.ts +29 -0
  175. package/src/cli/capability-streams.ts +25 -0
  176. package/src/cli/index.ts +3 -2
  177. package/src/config/env.ts +11 -8
  178. package/src/contracts/app.ts +23 -3
  179. package/src/contracts/capabilities.ts +68 -5
  180. package/src/contracts/capability-streams.ts +46 -0
  181. package/src/contracts/commands.ts +49 -0
  182. package/src/contracts/index.ts +6 -1
  183. package/src/contracts/rail-admin.ts +38 -0
  184. package/src/contracts/rail-preferences.ts +16 -0
  185. package/src/contracts/registry.ts +3 -27
  186. package/src/contracts/settings-types.ts +1 -1
  187. package/src/contracts/shared.ts +9 -5
  188. package/src/contracts/web-vitals.ts +27 -0
  189. package/src/desktop/solid.tsx +3 -3
  190. package/src/index.ts +0 -4
  191. package/src/server/index.ts +3 -0
  192. package/src/server/middleware/auth.ts +3 -2
  193. package/src/server/middleware/route-template.ts +9 -3
  194. package/src/server/middleware/runtime.ts +2 -1
  195. package/src/server/middleware/settings.ts +17 -11
  196. package/src/server/services/access-revision.ts +10 -0
  197. package/src/server/services/access.ts +1 -1
  198. package/src/server/services/index.ts +1 -0
  199. package/src/services/account-category-policy.ts +9 -0
  200. package/src/services/account-lifecycle/audit.ts +1 -1
  201. package/src/services/accounts/app.ts +12 -2
  202. package/src/services/accounts/email-write.ts +23 -0
  203. package/src/services/accounts/groups.ts +15 -4
  204. package/src/services/accounts/identities.ts +249 -0
  205. package/src/services/accounts/identity-reconciliation.ts +87 -0
  206. package/src/services/accounts/ipa-data.ts +2 -2
  207. package/src/services/accounts/local-groups.ts +2 -2
  208. package/src/services/accounts/posix.ts +45 -29
  209. package/src/services/accounts/users.ts +2 -2
  210. package/src/services/announcements/index.ts +69 -8
  211. package/src/services/audit/index.ts +1 -1
  212. package/src/services/cache-fill.ts +35 -0
  213. package/src/services/help/index.ts +109 -0
  214. package/src/services/help/maintenance.ts +25 -0
  215. package/src/services/help/store.ts +180 -0
  216. package/src/services/help/types.ts +46 -0
  217. package/src/services/identity/invocation-actor.ts +2 -2
  218. package/src/services/identity/key-config.ts +2 -1
  219. package/src/services/identity/key-ring.ts +1 -1
  220. package/src/services/index.ts +15 -0
  221. package/src/services/ipa/users.ts +268 -221
  222. package/src/services/legal-consent.ts +1 -1
  223. package/src/services/logging/index.ts +15 -13
  224. package/src/services/logging/trace.ts +2 -1
  225. package/src/services/logging/web-vitals.ts +81 -0
  226. package/src/services/mandates/index.ts +18 -4
  227. package/src/services/mandates/policy.ts +65 -2
  228. package/src/services/notifications/batches.ts +124 -57
  229. package/src/services/notifications/browser.ts +12 -0
  230. package/src/services/notifications/index.ts +0 -2
  231. package/src/services/notifications/platform.ts +0 -4
  232. package/src/services/oauth-tokens.ts +3 -3
  233. package/src/services/pdf/gotenberg.ts +107 -84
  234. package/src/services/pdf/index.ts +3 -0
  235. package/src/services/providers/local/users.ts +25 -16
  236. package/src/services/public-http.ts +101 -0
  237. package/src/services/rail-shortcuts.ts +95 -0
  238. package/src/services/rail-snapshot.ts +53 -0
  239. package/src/services/request-cache-redis.ts +28 -0
  240. package/src/services/session/index.ts +15 -2
  241. package/src/services/session/user.ts +62 -37
  242. package/src/services/settings/core-settings.ts +21 -28
  243. package/src/services/settings/defaults.ts +5 -0
  244. package/src/services/settings/store.ts +58 -84
  245. package/src/shared/ai-costs.ts +42 -0
  246. package/src/shared/ai-platform-prompt.ts +1 -1
  247. package/src/shared/ai-quotas.ts +136 -0
  248. package/src/shared/ai-usage.ts +6 -16
  249. package/src/shared/app-presentation.ts +21 -15
  250. package/src/shared/capability-messages.ts +14 -0
  251. package/src/shared/help.ts +6 -42
  252. package/src/shared/index.ts +6 -0
  253. package/src/ssr/AdminLayout.tsx +7 -5
  254. package/src/ssr/AdminSidebar.tsx +29 -15
  255. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  256. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  257. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  258. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  259. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  260. package/src/ssr/Layout.tsx +17 -6
  261. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  262. package/src/ssr/LayoutFooter.tsx +1 -1
  263. package/src/ssr/LayoutHeader.tsx +46 -14
  264. package/src/ssr/LayoutHelp.tsx +23 -22
  265. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  266. package/src/ssr/LayoutRail.tsx +17 -9
  267. package/src/ssr/MobileNavigation.tsx +71 -0
  268. package/src/ssr/MobileProfileActions.tsx +49 -0
  269. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  270. package/src/ssr/RailApps.island.tsx +2 -2
  271. package/src/ssr/RailEditor.tsx +17 -2
  272. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  273. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  274. package/src/ssr/admin-navigation.ts +7 -1
  275. package/src/ssr/help.ts +29 -0
  276. package/src/ssr/islands/SearchBar.tsx +67 -0
  277. package/src/ssr/islands/index.ts +3 -1
  278. package/src/ssr/layout-context.ts +2 -2
  279. package/src/ssr/layout-help-search.ts +15 -0
  280. package/src/ssr/mobile-menu-history.ts +70 -0
  281. package/src/ssr/platform-messages.ts +16 -8
  282. package/src/ssr/profile-actions.ts +42 -0
  283. package/src/ssr/rail-context.ts +8 -4
  284. package/src/ssr/rail-messages.ts +2 -0
  285. package/src/ssr/rail-navigation.ts +3 -3
  286. package/src/ssr/workspace-navigation.ts +75 -0
  287. package/src/styles/effects.css +6 -6
  288. package/src/styles/global.css +54 -0
  289. package/src/styles/input.css +43 -41
  290. package/src/styles/resource-search.css +505 -0
  291. package/src/styles/utilities-feedback.css +13 -0
  292. package/src/styles/utilities-layout.css +1 -5
  293. package/src/styles/utilities-navigation.css +3 -39
  294. package/src/types/ambient.d.ts +5 -0
  295. package/src/workflows/ai/runtime.ts +5 -1
  296. package/src/workflows/ai/store.ts +2 -0
  297. package/src/workflows/ai-actions.ts +16 -13
  298. package/src/workflows/definition.ts +10 -5
  299. package/src/workflows/store/actions.ts +44 -20
  300. package/src/workflows/store/index.ts +2 -0
  301. package/src/workflows/store/runs.ts +36 -0
  302. package/src/workflows/store/worker-pool.ts +55 -0
  303. package/src/workflows/store/worker-runtime.ts +90 -0
  304. package/scripts/app-favicon.test.ts +0 -49
  305. package/src/ai/chat/blocks.render.test.tsx +0 -1108
  306. package/src/ai/kit-skill.ts +0 -22
  307. package/src/ai/usage-migrate.ts +0 -81
  308. package/src/browser/CloudResourceSearch.behavior.test.tsx +0 -74
  309. package/src/browser/notification-target.ts +0 -27
  310. package/src/contracts/notification-live.ts +0 -66
  311. package/src/services/notifications/live.ts +0 -50
  312. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  313. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +0 -96
  314. package/src/ssr/Layout.render.test.tsx +0 -148
  315. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -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
  };
@@ -1,6 +1,7 @@
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 { preloadLayoutAnnouncements } from "../server/middleware/settings";
4
5
  import { getLocale } from "../server/locale";
5
6
  import { createLoginRedirectUrl } from "../shared/redirect";
6
7
  import type { PageOptions } from "./define-app";
@@ -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"));
@@ -0,0 +1,30 @@
1
+ /** Name Bun's default pool before its first use without changing connection ownership. */
2
+ export const configurePostgresApplicationName = (
3
+ applicationId: string,
4
+ environment: Record<string, string | undefined> = process.env,
5
+ ): void => {
6
+ // Match Bun's precedence and preserve TLS_* variables, which imply required TLS.
7
+ const variable = ["DATABASE_URL", "DATABASEURL", "TLS_DATABASE_URL", "POSTGRES_URL", "PGURL", "PG_URL", "TLS_POSTGRES_DATABASE_URL"].find(
8
+ (key) => environment[key],
9
+ );
10
+ if (!variable) return;
11
+
12
+ let url: URL;
13
+ try {
14
+ url = new URL(environment[variable]!);
15
+ } catch {
16
+ // Diagnostics must not change how Bun handles an existing connection configuration.
17
+ return;
18
+ }
19
+ if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") return;
20
+
21
+ let operatorConfigured = false;
22
+ url.searchParams.forEach((_value, key) => {
23
+ // options can contain an operator-owned `-c application_name=...` setting.
24
+ if (key.toLowerCase() === "application_name" || key.toLowerCase() === "options") operatorConfigured = true;
25
+ });
26
+ if (operatorConfigured) return;
27
+
28
+ url.searchParams.set("application_name", `cloud:${applicationId}`);
29
+ environment[variable] = url.toString();
30
+ };
@@ -0,0 +1,15 @@
1
+ /** Identity of the application started in this process, derived from its declaration. */
2
+ let applicationId: string | undefined;
3
+
4
+ export const getProcessApplicationId = (): string | undefined => applicationId;
5
+
6
+ export const bindProcessApplicationId = (id: string): void => {
7
+ if (applicationId !== undefined && applicationId !== id) {
8
+ throw new Error(`This process already hosts application "${applicationId}"`);
9
+ }
10
+ applicationId = id;
11
+ };
12
+
13
+ export const clearProcessApplicationId = (): void => {
14
+ applicationId = undefined;
15
+ };
@@ -82,7 +82,7 @@ export const startProcessSync = async ({ application }: { application: string })
82
82
  connection,
83
83
  namespace: env.SYNC_NAMESPACE,
84
84
  application,
85
- defaults: { replicas: 3 },
85
+ defaults: { replicas: env.SYNC_REPLICAS },
86
86
  observe: (event) => observeSyncEvent(event, application),
87
87
  });
88
88
  bindProcessSync(sync);
@@ -39,11 +39,12 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
39
39
  }
40
40
  for (const [locale, translation] of Object.entries(value.presentation.translations)) {
41
41
  if (!isRecord(translation)) return invalid(`presentation.translations.${locale}`, "an object");
42
- if (translation.name !== undefined && !isString(translation.name)) return invalid(`presentation.translations.${locale}.name`, "a string");
42
+ if (translation.name !== undefined && !isString(translation.name))
43
+ return invalid(`presentation.translations.${locale}.name`, "a string");
43
44
  if (translation.description !== undefined && !isString(translation.description)) {
44
45
  return invalid(`presentation.translations.${locale}.description`, "a string");
45
46
  }
46
- for (const field of ["adminGroups", "adminLinks", "legalLinks"] as const) {
47
+ for (const field of ["adminGroups", "adminLinks", "legalLinks", "searchLinks", "searchLinkDescriptions"] as const) {
47
48
  if (translation[field] !== undefined && !isStringRecord(translation[field])) {
48
49
  return invalid(`presentation.translations.${locale}.${field}`, "a string map");
49
50
  }
@@ -66,6 +67,7 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
66
67
  (group) =>
67
68
  !isRecord(group) ||
68
69
  (group.id !== undefined && !isString(group.id)) ||
70
+ (group.section !== undefined && group.section !== "ai") ||
69
71
  !isString(group.label) ||
70
72
  !Array.isArray(group.links) ||
71
73
  group.links.some((link) => !isRecord(link) || !isString(link.label) || !isString(link.href) || !isString(link.icon)),
@@ -84,6 +86,23 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
84
86
  return invalid("legalLinks", "an array of valid links");
85
87
  }
86
88
  }
89
+ if (value.searchLinks !== undefined) {
90
+ if (
91
+ !Array.isArray(value.searchLinks) ||
92
+ value.searchLinks.some(
93
+ (link) =>
94
+ !isRecord(link) ||
95
+ !isString(link.label) ||
96
+ !link.label.trim() ||
97
+ (link.description !== undefined && !isString(link.description)) ||
98
+ !isString(link.href) ||
99
+ !/^\/(?![\/\\])[^\\\s]*$/.test(link.href) ||
100
+ (link.icon !== undefined && !isString(link.icon)) ||
101
+ (link.keywords !== undefined && !isStringArray(link.keywords)),
102
+ )
103
+ )
104
+ return invalid("searchLinks", "an array of same-origin navigation links");
105
+ }
87
106
  if (value.widgets !== undefined) {
88
107
  if (
89
108
  !Array.isArray(value.widgets) ||
@@ -102,32 +121,17 @@ export const validateAppRegistryEntry = (value: unknown): string | null => {
102
121
  return invalid("capabilities", "a valid capability summary");
103
122
  }
104
123
  }
105
- if (value.help !== undefined) {
106
- if (
107
- !isRecord(value.help) ||
124
+ if (
125
+ value.help !== undefined &&
126
+ (!isRecord(value.help) ||
108
127
  !isString(value.help.manifestHash) ||
128
+ !isString(value.help.baseLocale) ||
109
129
  !isString(value.help.pageBase) ||
110
- !Array.isArray(value.help.documents)
111
- ) {
112
- return invalid("help", "a valid Help summary");
113
- }
114
- if (
115
- !value.help.pageBase.startsWith("/") ||
116
- value.help.documents.some(
117
- (document) =>
118
- !isRecord(document) ||
119
- !isString(document.id) ||
120
- !isString(document.title) ||
121
- typeof document.order !== "number" ||
122
- !isString(document.searchUrl) ||
123
- !document.searchUrl.startsWith("/") ||
124
- !isString(document.url) ||
125
- !document.url.startsWith("/"),
126
- )
127
- ) {
128
- return invalid("help", "a valid Help summary");
129
- }
130
+ !value.help.pageBase.startsWith("/"))
131
+ ) {
132
+ return invalid("help", "a valid Help reference");
130
133
  }
134
+
131
135
  if (value.openapi !== undefined && !isString(value.openapi)) return invalid("openapi", "a string");
132
136
  return null;
133
137
  };
@@ -1,7 +1,7 @@
1
1
  import type { EphemeralConfig } from "@k2b/sync";
2
2
  import type { AppAppearanceColor } from "../contracts/app";
3
3
  import type { CapabilityManifest, CapabilityPresentationCatalog } from "../contracts/capabilities";
4
- import type { AppRegistryEntry, CapabilityRegistryEntry, HelpRegistryEntry } from "../contracts/registry";
4
+ import type { AppRegistryEntry, CapabilityRegistryEntry } from "../contracts/registry";
5
5
  import type { DashboardWidgetPresentation } from "../contracts/widgets";
6
6
  import { resolveAppPresentations } from "../shared/app-presentation";
7
7
  import { compileCapabilityPresentation, parseCapabilityManifest } from "./capabilities";
@@ -10,7 +10,7 @@ import { validateAppRegistryEntry } from "./registry-validation";
10
10
  import { watchRegistryChanges } from "./registry-watch";
11
11
 
12
12
  /**
13
- * Shared app registry: three @k2b/sync ephemerals on the process Sync
13
+ * Shared app registry: two @k2b/sync ephemerals on the process Sync
14
14
  * instance. Apps, the gateway, and gateway-ops all declare them through the
15
15
  * functions below, so id, owner, TTL, and value bound stay identical across
16
16
  * the fleet (a differing declaration fails with ResourceDriftError).
@@ -34,20 +34,12 @@ export const CAPABILITY_REGISTRY_CONFIG = {
34
34
  maxValueBytes: 512 * 1024,
35
35
  } as const satisfies EphemeralConfig;
36
36
 
37
- export const HELP_REGISTRY_CONFIG = {
38
- id: "cloud-help",
39
- owner: REGISTRY_OWNER,
40
- ttlMs: APP_REGISTRY_TTL_MS,
41
- maxValueBytes: 512 * 1024,
42
- } as const satisfies EphemeralConfig;
43
-
44
37
  export const APP_REGISTRY_PREFIX = "apps/";
45
38
 
46
39
  export type CapabilityRegistryRecord = { appId: string; manifest: CapabilityManifest; presentation?: CapabilityPresentationCatalog };
47
40
 
48
41
  export const appRegistry = lazySync((sync) => sync.ephemeral<AppRegistryEntry>(APP_REGISTRY_CONFIG));
49
42
  export const capabilityRegistry = lazySync((sync) => sync.ephemeral<CapabilityRegistryRecord>(CAPABILITY_REGISTRY_CONFIG));
50
- export const helpRegistry = lazySync((sync) => sync.ephemeral<HelpRegistryEntry>(HELP_REGISTRY_CONFIG));
51
43
 
52
44
  /**
53
45
  * Follow the app registry until `signal` aborts. The watch first replays the
@@ -232,49 +224,6 @@ export const getCapability = async (appId: string): Promise<CapabilityRegistryEn
232
224
  return entry ? resolveLiveCapabilityRegistryEntry(entry.key, entry.value, app ?? undefined) : null;
233
225
  };
234
226
 
235
- const isHelpRegistryEntry = (value: unknown): value is HelpRegistryEntry => {
236
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
237
- const entry = value as Partial<HelpRegistryEntry>;
238
- const validDocument = (document: unknown) =>
239
- !!document &&
240
- typeof document === "object" &&
241
- typeof (document as { id?: unknown }).id === "string" &&
242
- typeof (document as { title?: unknown }).title === "string" &&
243
- typeof (document as { order?: unknown }).order === "number" &&
244
- typeof (document as { markdown?: unknown }).markdown === "string" &&
245
- ((document as { searchText?: unknown }).searchText === undefined ||
246
- typeof (document as { searchText?: unknown }).searchText === "string") &&
247
- ((document as { icon?: unknown }).icon === undefined || typeof (document as { icon?: unknown }).icon === "string") &&
248
- ((document as { description?: unknown }).description === undefined ||
249
- typeof (document as { description?: unknown }).description === "string");
250
- return (
251
- typeof entry.appId === "string" &&
252
- typeof entry.appName === "string" &&
253
- typeof entry.appIcon === "string" &&
254
- typeof entry.manifestHash === "string" &&
255
- (entry.baseLocale === undefined || typeof entry.baseLocale === "string") &&
256
- Array.isArray(entry.documents) &&
257
- entry.documents.every(validDocument) &&
258
- (entry.documentsByLocale === undefined ||
259
- (!!entry.documentsByLocale &&
260
- typeof entry.documentsByLocale === "object" &&
261
- !Array.isArray(entry.documentsByLocale) &&
262
- Object.values(entry.documentsByLocale).every((documents) => Array.isArray(documents) && documents.every(validDocument))))
263
- );
264
- };
265
-
266
- export const listHelp = async (): Promise<HelpRegistryEntry[]> => {
267
- const snap = await helpRegistry().snapshot({ prefix: "help/" });
268
- return snap.entries.map((entry) => entry.value).filter(isHelpRegistryEntry);
269
- };
270
-
271
- export const getHelp = async (appId: string): Promise<HelpRegistryEntry | null> => {
272
- const key = `help/${appId}`;
273
- const snap = await helpRegistry().snapshot({ prefix: key });
274
- const value = snap.entries.find((entry) => entry.key === key)?.value;
275
- return isHelpRegistryEntry(value) ? value : null;
276
- };
277
-
278
227
  /**
279
228
  * Same as `listApps` but returns registry metadata for admin observability.
280
229
  */
@@ -29,6 +29,8 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
29
29
  adminGroups: translation.adminGroups ? { ...translation.adminGroups } : undefined,
30
30
  adminLinks: translation.adminLinks ? { ...translation.adminLinks } : undefined,
31
31
  legalLinks: translation.legalLinks ? { ...translation.legalLinks } : undefined,
32
+ searchLinks: translation.searchLinks ? { ...translation.searchLinks } : undefined,
33
+ searchLinkDescriptions: translation.searchLinkDescriptions ? { ...translation.searchLinkDescriptions } : undefined,
32
34
  },
33
35
  ]),
34
36
  ),
@@ -37,6 +39,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
37
39
  appearance: e.appearance,
38
40
  adminHref: e.nav?.adminHref,
39
41
  adminNav: e.adminNav?.map((group) => ({
42
+ ...(group.section ? {section:group.section} : {}),
40
43
  id: group.id,
41
44
  label: group.label,
42
45
  links: group.links.map((link) => ({ ...link })),
@@ -53,22 +56,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
53
56
  requiresRoles: e.nav.requiresRoles as Role[] | undefined,
54
57
  }
55
58
  : undefined,
56
- help: e.help
57
- ? {
58
- manifestHash: e.help.manifestHash,
59
- pageBase: e.help.pageBase,
60
- baseLocale: e.help.baseLocale,
61
- documentsByLocale: e.help.documentsByLocale
62
- ? Object.fromEntries(
63
- Object.entries(e.help.documentsByLocale).map(([locale, documents]) => [
64
- locale,
65
- documents.map((document) => ({ ...document })),
66
- ]),
67
- )
68
- : undefined,
69
- documents: e.help.documents.map((document) => ({ ...document })),
70
- }
71
- : undefined,
59
+ help: e.help ? { ...e.help } : undefined,
72
60
  searchTags,
73
61
  searchHelp: searchQueries?.map((query) => query.description).join(" "),
74
62
  searchTagHelp: searchQueries?.flatMap((query) =>
@@ -78,6 +66,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[], capabiliti
78
66
  ]),
79
67
  ),
80
68
  legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
69
+ searchLinks: e.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
81
70
  openapi: e.openapi,
82
71
  };
83
72
  }),
@@ -0,0 +1,13 @@
1
+ import type { Context } from "hono";
2
+
3
+ type Phase = "auth" | "settings" | "runtime" | "ssr_data" | "ssr_finalize" | "ssr_render";
4
+
5
+ /** Measure the owning operation, not downstream middleware or another request. */
6
+ export async function measureServerPhase<T>(c: Context, phase: Phase, operation: () => T | Promise<T>): Promise<T> {
7
+ const start = performance.now();
8
+ try {
9
+ return await operation();
10
+ } finally {
11
+ c.header("Server-Timing", `${phase};dur=${(performance.now() - start).toFixed(2)}`, { append: true });
12
+ }
13
+ }