@k2b/cloud 0.6.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 (309) hide show
  1. package/README.md +2 -2
  2. package/package.json +19 -9
  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/index.ts +0 -4
  190. package/src/server/index.ts +3 -0
  191. package/src/server/middleware/auth.ts +3 -2
  192. package/src/server/middleware/route-template.ts +9 -3
  193. package/src/server/middleware/runtime.ts +2 -1
  194. package/src/server/middleware/settings.ts +17 -11
  195. package/src/server/services/access-revision.ts +10 -0
  196. package/src/server/services/access.ts +1 -1
  197. package/src/server/services/index.ts +1 -0
  198. package/src/services/account-category-policy.ts +9 -0
  199. package/src/services/account-lifecycle/audit.ts +1 -1
  200. package/src/services/accounts/app.ts +12 -2
  201. package/src/services/accounts/email-write.ts +23 -0
  202. package/src/services/accounts/groups.ts +15 -4
  203. package/src/services/accounts/identities.ts +249 -0
  204. package/src/services/accounts/identity-reconciliation.ts +87 -0
  205. package/src/services/accounts/ipa-data.ts +2 -2
  206. package/src/services/accounts/local-groups.ts +2 -2
  207. package/src/services/accounts/posix.ts +45 -29
  208. package/src/services/accounts/users.ts +2 -2
  209. package/src/services/announcements/index.ts +69 -8
  210. package/src/services/audit/index.ts +1 -1
  211. package/src/services/cache-fill.ts +35 -0
  212. package/src/services/help/index.ts +109 -0
  213. package/src/services/help/maintenance.ts +25 -0
  214. package/src/services/help/store.ts +180 -0
  215. package/src/services/help/types.ts +46 -0
  216. package/src/services/identity/invocation-actor.ts +2 -2
  217. package/src/services/identity/key-config.ts +2 -1
  218. package/src/services/identity/key-ring.ts +1 -1
  219. package/src/services/index.ts +15 -0
  220. package/src/services/ipa/users.ts +268 -221
  221. package/src/services/legal-consent.ts +1 -1
  222. package/src/services/logging/index.ts +15 -13
  223. package/src/services/logging/trace.ts +2 -1
  224. package/src/services/logging/web-vitals.ts +81 -0
  225. package/src/services/mandates/index.ts +18 -4
  226. package/src/services/mandates/policy.ts +65 -2
  227. package/src/services/notifications/batches.ts +124 -57
  228. package/src/services/notifications/browser.ts +12 -0
  229. package/src/services/notifications/index.ts +0 -2
  230. package/src/services/notifications/platform.ts +0 -4
  231. package/src/services/oauth-tokens.ts +3 -3
  232. package/src/services/pdf/gotenberg.ts +107 -84
  233. package/src/services/pdf/index.ts +3 -0
  234. package/src/services/providers/local/users.ts +25 -16
  235. package/src/services/public-http.ts +101 -0
  236. package/src/services/rail-shortcuts.ts +95 -0
  237. package/src/services/rail-snapshot.ts +53 -0
  238. package/src/services/request-cache-redis.ts +28 -0
  239. package/src/services/session/index.ts +15 -2
  240. package/src/services/session/user.ts +62 -37
  241. package/src/services/settings/core-settings.ts +21 -28
  242. package/src/services/settings/defaults.ts +5 -0
  243. package/src/services/settings/store.ts +58 -84
  244. package/src/shared/ai-costs.ts +42 -0
  245. package/src/shared/ai-platform-prompt.ts +1 -1
  246. package/src/shared/ai-quotas.ts +136 -0
  247. package/src/shared/ai-usage.ts +6 -16
  248. package/src/shared/app-presentation.ts +21 -15
  249. package/src/shared/capability-messages.ts +14 -0
  250. package/src/shared/help.ts +6 -42
  251. package/src/shared/index.ts +6 -0
  252. package/src/ssr/AdminLayout.tsx +7 -5
  253. package/src/ssr/AdminSidebar.tsx +29 -15
  254. package/src/ssr/AppLaunchpad.island.tsx +24 -137
  255. package/src/ssr/AppLaunchpadPanel.tsx +181 -0
  256. package/src/ssr/BrowserPushRegistration.island.tsx +12 -0
  257. package/src/ssr/GlobalSearchDialog.tsx +167 -20
  258. package/src/ssr/GlobalSearchTrigger.island.tsx +33 -22
  259. package/src/ssr/Layout.tsx +17 -6
  260. package/src/ssr/LayoutBreadcrumbs.island.tsx +3 -5
  261. package/src/ssr/LayoutFooter.tsx +1 -1
  262. package/src/ssr/LayoutHeader.tsx +46 -14
  263. package/src/ssr/LayoutHelp.tsx +23 -22
  264. package/src/ssr/{HotkeysHelpRail.island.tsx → LayoutHelpTrigger.island.tsx} +18 -12
  265. package/src/ssr/LayoutRail.tsx +17 -9
  266. package/src/ssr/MobileNavigation.tsx +71 -0
  267. package/src/ssr/MobileProfileActions.tsx +49 -0
  268. package/src/ssr/ProfilePreferences.island.tsx +3 -36
  269. package/src/ssr/RailApps.island.tsx +2 -2
  270. package/src/ssr/RailEditor.tsx +17 -2
  271. package/src/ssr/WorkspaceNavigation.island.tsx +7 -0
  272. package/src/ssr/WorkspaceNavigationProvider.tsx +16 -0
  273. package/src/ssr/admin-navigation.ts +7 -1
  274. package/src/ssr/help.ts +29 -0
  275. package/src/ssr/islands/SearchBar.tsx +67 -0
  276. package/src/ssr/islands/index.ts +3 -1
  277. package/src/ssr/layout-context.ts +2 -2
  278. package/src/ssr/layout-help-search.ts +15 -0
  279. package/src/ssr/mobile-menu-history.ts +70 -0
  280. package/src/ssr/platform-messages.ts +16 -8
  281. package/src/ssr/profile-actions.ts +42 -0
  282. package/src/ssr/rail-context.ts +8 -4
  283. package/src/ssr/rail-messages.ts +2 -0
  284. package/src/ssr/rail-navigation.ts +3 -3
  285. package/src/ssr/workspace-navigation.ts +75 -0
  286. package/src/styles/effects.css +6 -6
  287. package/src/styles/global.css +54 -0
  288. package/src/styles/input.css +43 -41
  289. package/src/styles/resource-search.css +505 -0
  290. package/src/styles/utilities-feedback.css +13 -0
  291. package/src/styles/utilities-layout.css +1 -5
  292. package/src/styles/utilities-navigation.css +3 -39
  293. package/src/types/ambient.d.ts +5 -0
  294. package/src/workflows/ai/runtime.ts +5 -1
  295. package/src/workflows/ai/store.ts +2 -0
  296. package/src/workflows/ai-actions.ts +16 -13
  297. package/src/workflows/definition.ts +10 -5
  298. package/src/workflows/store/actions.ts +44 -20
  299. package/src/workflows/store/index.ts +2 -0
  300. package/src/workflows/store/runs.ts +36 -0
  301. package/src/workflows/store/worker-pool.ts +55 -0
  302. package/src/workflows/store/worker-runtime.ts +90 -0
  303. package/src/ai/kit-skill.ts +0 -22
  304. package/src/ai/usage-migrate.ts +0 -81
  305. package/src/browser/notification-target.ts +0 -27
  306. package/src/contracts/notification-live.ts +0 -66
  307. package/src/services/notifications/live.ts +0 -50
  308. package/src/ssr/BrowserNotifications.island.tsx +0 -135
  309. package/src/ssr/islands/SearchBar.island.tsx +0 -80
@@ -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
+ }
@@ -2,10 +2,20 @@ import type { HtmlFn, RenderFn } from "@k2b/ssr";
2
2
  import { createSSRHandler } from "@k2b/ssr/hono";
3
3
  import type { Context, Env, MiddlewareHandler, TypedResponse } from "hono";
4
4
  import type { StatusCode } from "hono/utils/http-status";
5
- import type { RailPreferences } from "../contracts/rail-preferences";
5
+ import type { RailSnapshot } from "../contracts/rail-preferences";
6
+ import type { SettingsFlat, Unflatten } from "../contracts/settings-types";
6
7
  import type { User } from "../contracts/shared";
8
+ import type { CORE_SETTINGS } from "../services/settings/core-settings";
9
+ import { measureServerPhase } from "./server-timing";
7
10
 
8
- type PageEnv<T extends object> = { Variables: { page: Partial<T>; user?: User; railPreferences?: RailPreferences } };
11
+ type PageEnv<T extends object> = {
12
+ Variables: {
13
+ page: Partial<T>;
14
+ user?: User;
15
+ railPreferences?: RailSnapshot;
16
+ settings?: Unflatten<SettingsFlat<typeof CORE_SETTINGS>>;
17
+ };
18
+ };
9
19
  type SsrHandlerResult = RenderFn | Response | TypedResponse;
10
20
  type SsrHandler<E extends Env, T extends object> = (context: Context<E & PageEnv<T>>) => SsrHandlerResult | Promise<SsrHandlerResult>;
11
21
 
@@ -30,15 +40,17 @@ export const createStatusPreservingSsrHandler = <T extends object>(
30
40
  const handler = args[args.length - 1] as SsrHandler<E, T>;
31
41
 
32
42
  return createHandler<E>(...middlewares, async (context) => {
33
- const result = await handler(context as Context<E & PageEnv<T>>);
43
+ const result = await measureServerPhase(context, "ssr_data", () => handler(context as Context<E & PageEnv<T>>));
34
44
  if (result instanceof Response) return result;
35
- await finalizePage?.(context as unknown as Context<PageEnv<T>>);
45
+ await measureServerPhase(context, "ssr_finalize", () => finalizePage?.(context as unknown as Context<PageEnv<T>>));
36
46
  if (typeof result !== "function") return result;
37
47
 
38
48
  const status = context.newResponse(null).status;
39
- if (status === 200) return result;
49
+ if (result.constructor.name === "AsyncFunction") {
50
+ throw new Error("[ssr] ssr() render functions must be synchronous: return () => <Page />");
51
+ }
40
52
 
41
- const response = await html(result, context.get("page") as T);
53
+ const response = await measureServerPhase(context, "ssr_render", () => html(result, context.get("page") as T));
42
54
  const headers: Record<string, string> = {};
43
55
  response.headers.forEach((value, key) => {
44
56
  headers[key] = value;
@@ -0,0 +1,10 @@
1
+ import { createHash } from "node:crypto";
2
+ import { dependencies } from "../../package.json";
3
+ import * as collectorModule from "../browser/web-vitals.ts" with { type: "text" };
4
+
5
+ // All apps using the same collector share one Core asset and browser cache key.
6
+ // Both inputs are bundled, so production servers need no source files on disk.
7
+ if (!("default" in collectorModule) || typeof collectorModule.default !== "string") throw new Error("Expected collector source text");
8
+ const version = createHash("sha256").update(collectorModule.default).update(dependencies["web-vitals"]).digest("hex").slice(0, 16);
9
+ export const WEB_VITALS_ASSET_NAME = `web-vitals-${version}.js`;
10
+ export const WEB_VITALS_ASSET_HREF = `/public/${WEB_VITALS_ASSET_NAME}`;