@k2b/cloud 0.6.0-rc.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 (540) hide show
  1. package/README.md +98 -0
  2. package/package.json +128 -0
  3. package/public/logo.svg +1 -0
  4. package/scripts/app-favicon.test.ts +49 -0
  5. package/scripts/app-favicon.ts +51 -0
  6. package/scripts/build.ts +205 -0
  7. package/scripts/preload.ts +164 -0
  8. package/scripts/sync-recovery-smoke.ts +200 -0
  9. package/src/_internal/app-favicon.ts +2 -0
  10. package/src/_internal/app-presentation.ts +61 -0
  11. package/src/_internal/app-workspace-state.ts +24 -0
  12. package/src/_internal/bounded-json.ts +47 -0
  13. package/src/_internal/build-metadata.ts +9 -0
  14. package/src/_internal/capabilities.ts +1118 -0
  15. package/src/_internal/define-app.ts +801 -0
  16. package/src/_internal/heartbeat.ts +167 -0
  17. package/src/_internal/help-catalog.ts +241 -0
  18. package/src/_internal/help.ts +95 -0
  19. package/src/_internal/nats-connection.ts +34 -0
  20. package/src/_internal/page-responses.ts +40 -0
  21. package/src/_internal/process-sync.ts +117 -0
  22. package/src/_internal/readiness.ts +3 -0
  23. package/src/_internal/registry-validation.ts +136 -0
  24. package/src/_internal/registry-watch.ts +52 -0
  25. package/src/_internal/registry.ts +338 -0
  26. package/src/_internal/runtime-compatibility.ts +64 -0
  27. package/src/_internal/runtime-context.ts +84 -0
  28. package/src/_internal/runtime-watcher.ts +78 -0
  29. package/src/_internal/static-assets.ts +86 -0
  30. package/src/_internal/status-preserving-ssr.ts +49 -0
  31. package/src/access/PermissionEditor.tsx +486 -0
  32. package/src/access/ResourceApiKeys.tsx +271 -0
  33. package/src/access/messages.ts +121 -0
  34. package/src/access/ui.ts +16 -0
  35. package/src/account/Avatar.tsx +28 -0
  36. package/src/account/EntitySearch.tsx +363 -0
  37. package/src/account/avatar-upload-dialog.tsx +197 -0
  38. package/src/account/avatar-upload.ts +149 -0
  39. package/src/account/ui.ts +3 -0
  40. package/src/ai/admin.ts +47 -0
  41. package/src/ai/approval-routes.ts +130 -0
  42. package/src/ai/approvals.ts +139 -0
  43. package/src/ai/assistant-models.ts +61 -0
  44. package/src/ai/attachments.ts +40 -0
  45. package/src/ai/browser.ts +92 -0
  46. package/src/ai/capabilities.ts +714 -0
  47. package/src/ai/capability-execution.ts +151 -0
  48. package/src/ai/chat/blocks.render.test.tsx +1108 -0
  49. package/src/ai/chat/blocks.tsx +730 -0
  50. package/src/ai/chat/builtin-tools.tsx +428 -0
  51. package/src/ai/chat/capability-table.tsx +87 -0
  52. package/src/ai/chat/composer-adapter.ts +169 -0
  53. package/src/ai/chat/file-tools.tsx +81 -0
  54. package/src/ai/chat/message-actions.tsx +370 -0
  55. package/src/ai/chat/message-utils.ts +479 -0
  56. package/src/ai/chat/messages.ts +43 -0
  57. package/src/ai/chat/presentation.tsx +395 -0
  58. package/src/ai/chat/primitives.tsx +3 -0
  59. package/src/ai/chat/tool-disclosure.tsx +41 -0
  60. package/src/ai/chat/turn-navigator-utils.ts +28 -0
  61. package/src/ai/chat/turn-navigator.tsx +164 -0
  62. package/src/ai/chat/user-message.tsx +149 -0
  63. package/src/ai/chat/visual-tools.tsx +435 -0
  64. package/src/ai/chat/web-tools.tsx +202 -0
  65. package/src/ai/chat-task-contracts.ts +113 -0
  66. package/src/ai/chat-tasks.ts +952 -0
  67. package/src/ai/client/controller.ts +1211 -0
  68. package/src/ai/client/file-source.ts +95 -0
  69. package/src/ai/client/live-connection.ts +140 -0
  70. package/src/ai/client/projection.ts +178 -0
  71. package/src/ai/client/transport.ts +111 -0
  72. package/src/ai/compaction.ts +182 -0
  73. package/src/ai/credentials.ts +124 -0
  74. package/src/ai/default-tool-contracts.ts +108 -0
  75. package/src/ai/default-tools.ts +127 -0
  76. package/src/ai/enrich.ts +280 -0
  77. package/src/ai/executor.ts +1624 -0
  78. package/src/ai/fetch-file-tool.ts +313 -0
  79. package/src/ai/file-context.ts +92 -0
  80. package/src/ai/file-media-type.ts +28 -0
  81. package/src/ai/file-mount.ts +16 -0
  82. package/src/ai/file-tools.ts +566 -0
  83. package/src/ai/files-store.ts +390 -0
  84. package/src/ai/firecrawl-tools.ts +305 -0
  85. package/src/ai/grids-skill.ts +126 -0
  86. package/src/ai/http.ts +265 -0
  87. package/src/ai/index.ts +337 -0
  88. package/src/ai/kit-skill.ts +22 -0
  89. package/src/ai/limits.ts +5 -0
  90. package/src/ai/live-events.ts +289 -0
  91. package/src/ai/live-messages.ts +45 -0
  92. package/src/ai/live-outbox.ts +102 -0
  93. package/src/ai/live-routes.ts +412 -0
  94. package/src/ai/live.ts +2 -0
  95. package/src/ai/maintenance.ts +302 -0
  96. package/src/ai/markdown-pdf-tool.ts +95 -0
  97. package/src/ai/memories.ts +605 -0
  98. package/src/ai/memory-learning-runs.ts +222 -0
  99. package/src/ai/memory-learning.ts +619 -0
  100. package/src/ai/memory-tool.ts +120 -0
  101. package/src/ai/memory-workflow-evidence.ts +158 -0
  102. package/src/ai/migrate.ts +2082 -0
  103. package/src/ai/model-access-migrate.ts +30 -0
  104. package/src/ai/model-access.ts +191 -0
  105. package/src/ai/personal-agent.ts +6 -0
  106. package/src/ai/prefs.ts +72 -0
  107. package/src/ai/project-tool.ts +94 -0
  108. package/src/ai/projects-routes.ts +277 -0
  109. package/src/ai/projects.ts +902 -0
  110. package/src/ai/protocol.ts +279 -0
  111. package/src/ai/provider.ts +73 -0
  112. package/src/ai/public-projection.ts +25 -0
  113. package/src/ai/resource-markers.ts +31 -0
  114. package/src/ai/resource-refs.ts +70 -0
  115. package/src/ai/routes.ts +1131 -0
  116. package/src/ai/runtime-tools.ts +39 -0
  117. package/src/ai/runtime.ts +544 -0
  118. package/src/ai/settings.ts +529 -0
  119. package/src/ai/short-id.ts +45 -0
  120. package/src/ai/skill-catalog.ts +73 -0
  121. package/src/ai/skill-format.ts +145 -0
  122. package/src/ai/skill-seeds.ts +430 -0
  123. package/src/ai/skill-tool.ts +81 -0
  124. package/src/ai/skills-routes.ts +143 -0
  125. package/src/ai/skills.ts +934 -0
  126. package/src/ai/solid.ts +27 -0
  127. package/src/ai/store.ts +3113 -0
  128. package/src/ai/stream.ts +308 -0
  129. package/src/ai/structured-runs.ts +66 -0
  130. package/src/ai/structured.ts +186 -0
  131. package/src/ai/system-prompt.ts +170 -0
  132. package/src/ai/task-prompt.ts +22 -0
  133. package/src/ai/timeline.ts +148 -0
  134. package/src/ai/tool-audit.ts +177 -0
  135. package/src/ai/tool-result-budget.ts +16 -0
  136. package/src/ai/tools.ts +155 -0
  137. package/src/ai/types.ts +967 -0
  138. package/src/ai/ui.tsx +39 -0
  139. package/src/ai/usage-migrate.ts +81 -0
  140. package/src/ai/usage.ts +315 -0
  141. package/src/ai/validate.ts +54 -0
  142. package/src/ai/vision-tool.ts +79 -0
  143. package/src/api/accounts-entities.ts +174 -0
  144. package/src/api/admin-ai-projects.ts +74 -0
  145. package/src/api/admin-ai-skills.ts +76 -0
  146. package/src/api/admin-ai-usage.ts +42 -0
  147. package/src/api/admin-core-settings.ts +508 -0
  148. package/src/api/admin-identity.ts +148 -0
  149. package/src/api/admin-lifecycle.ts +206 -0
  150. package/src/api/admin-linux-identities.ts +66 -0
  151. package/src/api/announcements.ts +131 -0
  152. package/src/api/app-approval.ts +129 -0
  153. package/src/api/apps.ts +51 -0
  154. package/src/api/auth/schemas.ts +54 -0
  155. package/src/api/auth.ts +370 -0
  156. package/src/api/capabilities.ts +658 -0
  157. package/src/api/help-messages.ts +19 -0
  158. package/src/api/help.ts +78 -0
  159. package/src/api/index.ts +108 -0
  160. package/src/api/mcp.ts +744 -0
  161. package/src/api/me-rail.ts +45 -0
  162. package/src/api/me.ts +753 -0
  163. package/src/api/search/schemas.ts +79 -0
  164. package/src/api/search.bench.ts +185 -0
  165. package/src/api/search.ts +407 -0
  166. package/src/api/sync-ops.ts +79 -0
  167. package/src/api/widgets.ts +143 -0
  168. package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
  169. package/src/browser/CloudResourceSearch.tsx +791 -0
  170. package/src/browser/app-approval-vault.ts +305 -0
  171. package/src/browser/app-approval.ts +234 -0
  172. package/src/browser/live-websocket.ts +268 -0
  173. package/src/browser/locale-preference.ts +25 -0
  174. package/src/browser/notification-target.ts +27 -0
  175. package/src/browser/notification-timeout.ts +14 -0
  176. package/src/browser/notifications.ts +181 -0
  177. package/src/browser/resource-clipboard.ts +70 -0
  178. package/src/browser/resource-picker.tsx +39 -0
  179. package/src/browser/resource-search-messages.ts +55 -0
  180. package/src/browser/resource-search.ts +27 -0
  181. package/src/capabilities/client.ts +149 -0
  182. package/src/capabilities/response.ts +39 -0
  183. package/src/capabilities/server.ts +215 -0
  184. package/src/capabilities/signals.ts +4 -0
  185. package/src/capabilities/testing.ts +12 -0
  186. package/src/capabilities/types.ts +32 -0
  187. package/src/cli/access.ts +499 -0
  188. package/src/cli/account.ts +382 -0
  189. package/src/cli/admin/account-administration.ts +45 -0
  190. package/src/cli/admin/account-categories.ts +49 -0
  191. package/src/cli/admin/ai-usage.ts +118 -0
  192. package/src/cli/admin/app-sign-in.ts +49 -0
  193. package/src/cli/admin/data.ts +343 -0
  194. package/src/cli/admin/documentation.ts +34 -0
  195. package/src/cli/admin/gateway.ts +155 -0
  196. package/src/cli/admin/index.ts +61 -0
  197. package/src/cli/admin/instance.ts +250 -0
  198. package/src/cli/admin/jobs.ts +223 -0
  199. package/src/cli/admin/legal.ts +122 -0
  200. package/src/cli/admin/linux.ts +53 -0
  201. package/src/cli/admin/logs.ts +324 -0
  202. package/src/cli/admin/metrics.ts +142 -0
  203. package/src/cli/admin/nats.ts +167 -0
  204. package/src/cli/admin/notifications.ts +516 -0
  205. package/src/cli/admin/shared.ts +113 -0
  206. package/src/cli/admin/sync.ts +188 -0
  207. package/src/cli/admin/telemetry.ts +314 -0
  208. package/src/cli/admin/webhooks.ts +222 -0
  209. package/src/cli/admin/workflows.ts +367 -0
  210. package/src/cli/apps.ts +40 -0
  211. package/src/cli/capabilities.ts +239 -0
  212. package/src/cli/commands.ts +604 -0
  213. package/src/cli/index.ts +61 -0
  214. package/src/cli/locale.ts +31 -0
  215. package/src/cli/output.ts +58 -0
  216. package/src/clients/core.ts +20 -0
  217. package/src/config/env.ts +54 -0
  218. package/src/config/index.ts +6 -0
  219. package/src/contracts/account-categories.ts +62 -0
  220. package/src/contracts/announcements.ts +121 -0
  221. package/src/contracts/app-approval.ts +197 -0
  222. package/src/contracts/app.ts +162 -0
  223. package/src/contracts/capabilities.ts +484 -0
  224. package/src/contracts/index.ts +14 -0
  225. package/src/contracts/notification-live.ts +66 -0
  226. package/src/contracts/notification-types.ts +235 -0
  227. package/src/contracts/posix.ts +69 -0
  228. package/src/contracts/profile.ts +85 -0
  229. package/src/contracts/rail-preferences.ts +53 -0
  230. package/src/contracts/registry.ts +110 -0
  231. package/src/contracts/resource-clipboard.ts +33 -0
  232. package/src/contracts/settings-types.ts +98 -0
  233. package/src/contracts/shared.ts +408 -0
  234. package/src/contracts/user-notifications.ts +101 -0
  235. package/src/contracts/widgets.ts +183 -0
  236. package/src/desktop/index.ts +704 -0
  237. package/src/desktop/solid.tsx +938 -0
  238. package/src/index.ts +58 -0
  239. package/src/server/actor.ts +41 -0
  240. package/src/server/api/index.ts +2 -0
  241. package/src/server/api/respond.ts +96 -0
  242. package/src/server/api-client.ts +54 -0
  243. package/src/server/app-context.ts +39 -0
  244. package/src/server/help.ts +187 -0
  245. package/src/server/index.ts +89 -0
  246. package/src/server/locale.ts +55 -0
  247. package/src/server/middleware/auth.ts +386 -0
  248. package/src/server/middleware/index.ts +28 -0
  249. package/src/server/middleware/invocation.ts +55 -0
  250. package/src/server/middleware/middleware.ts +65 -0
  251. package/src/server/middleware/openapi.ts +126 -0
  252. package/src/server/middleware/rate-limit.ts +125 -0
  253. package/src/server/middleware/request-logger.ts +41 -0
  254. package/src/server/middleware/route-template.ts +46 -0
  255. package/src/server/middleware/runtime.ts +22 -0
  256. package/src/server/middleware/settings.ts +55 -0
  257. package/src/server/middleware/validator.ts +49 -0
  258. package/src/server/middleware/workload.ts +22 -0
  259. package/src/server/ratelimit.ts +151 -0
  260. package/src/server/request-cookies.ts +18 -0
  261. package/src/server/services/access.ts +644 -0
  262. package/src/server/services/freeipa/client.ts +137 -0
  263. package/src/server/services/freeipa/index.ts +9 -0
  264. package/src/server/services/freeipa/session.ts +123 -0
  265. package/src/server/services/freeipa/test-certificates.ts +44 -0
  266. package/src/server/services/freeipa/tls.ts +44 -0
  267. package/src/server/services/freeipa/transport.ts +116 -0
  268. package/src/server/services/freeipa/util.ts +59 -0
  269. package/src/server/services/geo.ts +198 -0
  270. package/src/server/services/index.ts +48 -0
  271. package/src/server/services/pagination.ts +22 -0
  272. package/src/server/services/services.ts +13 -0
  273. package/src/server/time.ts +33 -0
  274. package/src/services/account-category-policy.ts +51 -0
  275. package/src/services/account-lifecycle/audit.ts +41 -0
  276. package/src/services/account-lifecycle/index.ts +940 -0
  277. package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
  278. package/src/services/account-lifecycle/notification-sender.ts +13 -0
  279. package/src/services/account-lifecycle/scheduler.ts +506 -0
  280. package/src/services/account-model.ts +22 -0
  281. package/src/services/accounts/app.ts +1671 -0
  282. package/src/services/accounts/authz.ts +44 -0
  283. package/src/services/accounts/avatar.ts +120 -0
  284. package/src/services/accounts/base-group.ts +11 -0
  285. package/src/services/accounts/base-user.ts +46 -0
  286. package/src/services/accounts/duplicate-emails.ts +82 -0
  287. package/src/services/accounts/entities.ts +622 -0
  288. package/src/services/accounts/group-sql.ts +106 -0
  289. package/src/services/accounts/groups.ts +272 -0
  290. package/src/services/accounts/index.ts +14 -0
  291. package/src/services/accounts/ipa-data.ts +66 -0
  292. package/src/services/accounts/lifecycle.ts +2 -0
  293. package/src/services/accounts/local-groups.ts +499 -0
  294. package/src/services/accounts/model.ts +129 -0
  295. package/src/services/accounts/notification-sender.ts +31 -0
  296. package/src/services/accounts/posix.ts +386 -0
  297. package/src/services/accounts/request-policy.ts +11 -0
  298. package/src/services/accounts/switching.ts +126 -0
  299. package/src/services/accounts/users.ts +745 -0
  300. package/src/services/announcements/index.ts +224 -0
  301. package/src/services/app-approval.ts +498 -0
  302. package/src/services/audit/index.ts +438 -0
  303. package/src/services/auth-flows/index.ts +15 -0
  304. package/src/services/auth-flows/ipa.ts +197 -0
  305. package/src/services/auth-flows/magic-link.ts +185 -0
  306. package/src/services/auth-flows/notification-sender.ts +10 -0
  307. package/src/services/auth-flows/password-reset.ts +268 -0
  308. package/src/services/auth-flows/proxy-return.ts +49 -0
  309. package/src/services/document-extraction.ts +148 -0
  310. package/src/services/freeipa-config.ts +179 -0
  311. package/src/services/gateway.ts +186 -0
  312. package/src/services/identity/constants.ts +18 -0
  313. package/src/services/identity/index.ts +52 -0
  314. package/src/services/identity/invocation-actor.ts +120 -0
  315. package/src/services/identity/invocation-authority.ts +53 -0
  316. package/src/services/identity/invocation-operations.ts +15 -0
  317. package/src/services/identity/invocation-token.ts +276 -0
  318. package/src/services/identity/key-config.ts +41 -0
  319. package/src/services/identity/key-ring.ts +530 -0
  320. package/src/services/identity/metrics.ts +29 -0
  321. package/src/services/identity/routes.ts +18 -0
  322. package/src/services/identity/runtime-config.ts +88 -0
  323. package/src/services/identity/session-token.ts +152 -0
  324. package/src/services/identity/signer-refresh.ts +44 -0
  325. package/src/services/identity/workload-auth.ts +64 -0
  326. package/src/services/index.ts +187 -0
  327. package/src/services/ipa/auth.ts +216 -0
  328. package/src/services/ipa/connection.ts +57 -0
  329. package/src/services/ipa/effective-groups.ts +70 -0
  330. package/src/services/ipa/groups.ts +722 -0
  331. package/src/services/ipa/guard.ts +17 -0
  332. package/src/services/ipa/index.ts +17 -0
  333. package/src/services/ipa/posix.ts +19 -0
  334. package/src/services/ipa/profile.ts +132 -0
  335. package/src/services/ipa/runtime.ts +28 -0
  336. package/src/services/ipa/search.ts +154 -0
  337. package/src/services/ipa/service-account.ts +15 -0
  338. package/src/services/ipa/sync-planning.ts +100 -0
  339. package/src/services/ipa/sync.ts +862 -0
  340. package/src/services/ipa/users.ts +806 -0
  341. package/src/services/legal-consent.ts +76 -0
  342. package/src/services/logging/index.ts +477 -0
  343. package/src/services/logging/redaction.ts +20 -0
  344. package/src/services/logging/trace.ts +1231 -0
  345. package/src/services/mandates/index.ts +855 -0
  346. package/src/services/mandates/policy.ts +85 -0
  347. package/src/services/network-security.ts +31 -0
  348. package/src/services/notifications/batches.ts +722 -0
  349. package/src/services/notifications/browser.ts +252 -0
  350. package/src/services/notifications/catalog.ts +113 -0
  351. package/src/services/notifications/channels.ts +104 -0
  352. package/src/services/notifications/dispatcher.ts +139 -0
  353. package/src/services/notifications/email.ts +118 -0
  354. package/src/services/notifications/index.ts +633 -0
  355. package/src/services/notifications/live.ts +50 -0
  356. package/src/services/notifications/observability.ts +407 -0
  357. package/src/services/notifications/platform.ts +421 -0
  358. package/src/services/notifications/runtime.ts +120 -0
  359. package/src/services/notifications/user.ts +248 -0
  360. package/src/services/notifications/web-push-transport.ts +135 -0
  361. package/src/services/oauth-tokens.ts +209 -0
  362. package/src/services/outbox.ts +187 -0
  363. package/src/services/pdf/gotenberg.ts +290 -0
  364. package/src/services/pdf/index.ts +43 -0
  365. package/src/services/pdf/markdown.ts +209 -0
  366. package/src/services/pdf/template-preview.ts +122 -0
  367. package/src/services/postgres.ts +76 -0
  368. package/src/services/providers/index.ts +27 -0
  369. package/src/services/providers/local/auth.ts +51 -0
  370. package/src/services/providers/local/index.ts +4 -0
  371. package/src/services/providers/local/users.ts +263 -0
  372. package/src/services/rail-preferences.ts +27 -0
  373. package/src/services/runtime-lifecycle.ts +191 -0
  374. package/src/services/secrets.ts +10 -0
  375. package/src/services/service-account-credentials.ts +891 -0
  376. package/src/services/service-accounts.ts +184 -0
  377. package/src/services/session/index.ts +236 -0
  378. package/src/services/session/test-fixture.ts +18 -0
  379. package/src/services/session/user.ts +144 -0
  380. package/src/services/settings/api.ts +61 -0
  381. package/src/services/settings/app.ts +86 -0
  382. package/src/services/settings/core-settings.ts +796 -0
  383. package/src/services/settings/crypto.ts +69 -0
  384. package/src/services/settings/defaults.ts +374 -0
  385. package/src/services/settings/index.ts +215 -0
  386. package/src/services/settings/namespace.ts +9 -0
  387. package/src/services/settings/snapshot.ts +49 -0
  388. package/src/services/settings/store.ts +241 -0
  389. package/src/services/settings/templates.ts +4 -0
  390. package/src/services/sync-ops.ts +403 -0
  391. package/src/services/topic-cursor.ts +22 -0
  392. package/src/services/weather/forecast.ts +323 -0
  393. package/src/services/weather/geo.ts +119 -0
  394. package/src/services/weather/index.ts +67 -0
  395. package/src/services/weather/location.ts +24 -0
  396. package/src/services/weather/locations.ts +133 -0
  397. package/src/services/weather/migrate.ts +45 -0
  398. package/src/services/weather/settings.ts +66 -0
  399. package/src/services/weather/short-id.ts +57 -0
  400. package/src/services/weather/types.ts +61 -0
  401. package/src/services/weather/ui.ts +50 -0
  402. package/src/services/webauthn.ts +385 -0
  403. package/src/shared/account-action-notice.ts +70 -0
  404. package/src/shared/account-display.ts +15 -0
  405. package/src/shared/account-session.ts +15 -0
  406. package/src/shared/ai-platform-prompt.ts +134 -0
  407. package/src/shared/ai-usage.ts +49 -0
  408. package/src/shared/app-presentation.ts +48 -0
  409. package/src/shared/app-url.ts +12 -0
  410. package/src/shared/branding.ts +14 -0
  411. package/src/shared/capability-messages.ts +70 -0
  412. package/src/shared/email-html.ts +54 -0
  413. package/src/shared/format.ts +91 -0
  414. package/src/shared/help.ts +68 -0
  415. package/src/shared/icons.ts +400 -0
  416. package/src/shared/index.ts +54 -0
  417. package/src/shared/locale.ts +48 -0
  418. package/src/shared/login-method.ts +44 -0
  419. package/src/shared/markdown/client.ts +130 -0
  420. package/src/shared/markdown/extensions/code.ts +105 -0
  421. package/src/shared/markdown/extensions/guided-help.ts +100 -0
  422. package/src/shared/markdown/extensions/images.ts +79 -0
  423. package/src/shared/markdown/extensions/info-blocks.ts +97 -0
  424. package/src/shared/markdown/extensions/katex.ts +120 -0
  425. package/src/shared/markdown/extensions/links.ts +43 -0
  426. package/src/shared/markdown/extensions/mark.ts +48 -0
  427. package/src/shared/markdown/extensions/sub-sup.ts +60 -0
  428. package/src/shared/markdown/extensions/tables.ts +109 -0
  429. package/src/shared/markdown/extensions/task-list.ts +53 -0
  430. package/src/shared/markdown/formula.ts +1187 -0
  431. package/src/shared/markdown/index.ts +193 -0
  432. package/src/shared/markdown/shared.ts +36 -0
  433. package/src/shared/mock-cover.ts +130 -0
  434. package/src/shared/network-address.ts +106 -0
  435. package/src/shared/redirect.ts +56 -0
  436. package/src/shared/template-rendering.ts +140 -0
  437. package/src/shared/theme.ts +68 -0
  438. package/src/shared/time.ts +13 -0
  439. package/src/ssr/AdminLayout.tsx +53 -0
  440. package/src/ssr/AdminSidebar.tsx +50 -0
  441. package/src/ssr/AppLaunchpad.island.tsx +236 -0
  442. package/src/ssr/AppWorkspaceController.island.tsx +21 -0
  443. package/src/ssr/BrowserNotifications.island.tsx +135 -0
  444. package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
  445. package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
  446. package/src/ssr/GlobalSearchDialog.tsx +35 -0
  447. package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
  448. package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
  449. package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
  450. package/src/ssr/Layout.render.test.tsx +148 -0
  451. package/src/ssr/Layout.tsx +292 -0
  452. package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
  453. package/src/ssr/LayoutFooter.tsx +22 -0
  454. package/src/ssr/LayoutHeader.tsx +81 -0
  455. package/src/ssr/LayoutHelp.tsx +869 -0
  456. package/src/ssr/LayoutPreferences.island.tsx +36 -0
  457. package/src/ssr/LayoutRail.tsx +58 -0
  458. package/src/ssr/MinimalLayout.tsx +46 -0
  459. package/src/ssr/PageError.tsx +29 -0
  460. package/src/ssr/ProfilePreferences.island.tsx +92 -0
  461. package/src/ssr/RailApps.island.tsx +61 -0
  462. package/src/ssr/RailEditor.tsx +293 -0
  463. package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
  464. package/src/ssr/TimezoneCookie.island.tsx +23 -0
  465. package/src/ssr/admin-active-link.ts +47 -0
  466. package/src/ssr/admin-navigation.ts +78 -0
  467. package/src/ssr/app-appearance.ts +23 -0
  468. package/src/ssr/app-navigation.ts +41 -0
  469. package/src/ssr/help-messages.ts +104 -0
  470. package/src/ssr/index.ts +14 -0
  471. package/src/ssr/islands/SearchBar.island.tsx +80 -0
  472. package/src/ssr/islands/index.ts +16 -0
  473. package/src/ssr/layout-context.ts +17 -0
  474. package/src/ssr/layout-help-markdown.ts +9 -0
  475. package/src/ssr/layout-help-navigation.ts +24 -0
  476. package/src/ssr/layout-help-url.ts +5 -0
  477. package/src/ssr/layout-runtime.ts +22 -0
  478. package/src/ssr/page-error-messages.ts +25 -0
  479. package/src/ssr/platform-messages.ts +135 -0
  480. package/src/ssr/preference-controller.ts +44 -0
  481. package/src/ssr/profile-preferences-messages.ts +29 -0
  482. package/src/ssr/rail-context.ts +21 -0
  483. package/src/ssr/rail-messages.ts +66 -0
  484. package/src/ssr/rail-navigation.ts +60 -0
  485. package/src/ssr/runtime.ts +31 -0
  486. package/src/ssr/url-filter.ts +127 -0
  487. package/src/styles/base-popover.css +36 -0
  488. package/src/styles/effects.css +851 -0
  489. package/src/styles/global.css +268 -0
  490. package/src/styles/input.css +58 -0
  491. package/src/styles/tokens.css +230 -0
  492. package/src/styles/utilities-buttons.css +405 -0
  493. package/src/styles/utilities-data.css +33 -0
  494. package/src/styles/utilities-feedback.css +232 -0
  495. package/src/styles/utilities-layout.css +603 -0
  496. package/src/styles/utilities-markdown-editor.css +525 -0
  497. package/src/styles/utilities-markdown-table.css +234 -0
  498. package/src/styles/utilities-navigation.css +1054 -0
  499. package/src/types/ambient.d.ts +24 -0
  500. package/src/workflows/ai/index.ts +17 -0
  501. package/src/workflows/ai/runtime.ts +286 -0
  502. package/src/workflows/ai/store.ts +314 -0
  503. package/src/workflows/ai/types.ts +111 -0
  504. package/src/workflows/ai-actions.ts +269 -0
  505. package/src/workflows/authoring.ts +88 -0
  506. package/src/workflows/builtins.ts +193 -0
  507. package/src/workflows/contracts.ts +280 -0
  508. package/src/workflows/definition.ts +334 -0
  509. package/src/workflows/editor.ts +113 -0
  510. package/src/workflows/index.ts +71 -0
  511. package/src/workflows/language/binder.ts +45 -0
  512. package/src/workflows/language/canonical.ts +29 -0
  513. package/src/workflows/language/compiler.ts +396 -0
  514. package/src/workflows/language/expressions.ts +34 -0
  515. package/src/workflows/language/index.ts +20 -0
  516. package/src/workflows/language/references.ts +55 -0
  517. package/src/workflows/language/schema.ts +134 -0
  518. package/src/workflows/language/strict-yaml.ts +131 -0
  519. package/src/workflows/manifest.ts +24 -0
  520. package/src/workflows/module.ts +12 -0
  521. package/src/workflows/runtime/coordinator.ts +115 -0
  522. package/src/workflows/runtime/executor.ts +1003 -0
  523. package/src/workflows/runtime/index.ts +60 -0
  524. package/src/workflows/runtime/instant.ts +8 -0
  525. package/src/workflows/runtime/ports.ts +213 -0
  526. package/src/workflows/runtime/schedule.ts +203 -0
  527. package/src/workflows/runtime/trigger.ts +37 -0
  528. package/src/workflows/runtime/wake.ts +13 -0
  529. package/src/workflows/store/actions.ts +438 -0
  530. package/src/workflows/store/budget.ts +145 -0
  531. package/src/workflows/store/definitions.ts +345 -0
  532. package/src/workflows/store/events.ts +383 -0
  533. package/src/workflows/store/index.ts +107 -0
  534. package/src/workflows/store/observability.ts +594 -0
  535. package/src/workflows/store/runs.ts +975 -0
  536. package/src/workflows/store/transaction.ts +23 -0
  537. package/src/workflows/store/worker.ts +439 -0
  538. package/src/workflows/testing/conformance.ts +72 -0
  539. package/src/workflows/testing/index.ts +11 -0
  540. package/src/workflows/testing/process-fixtures.ts +275 -0
@@ -0,0 +1,167 @@
1
+ /** Keep an app discoverable even when the ephemeral registry is recreated. */
2
+
3
+ import { APP_REGISTRY_TTL_MS } from "./registry";
4
+
5
+ const HEARTBEAT_INTERVAL_MS = 60_000;
6
+ const HEARTBEAT_WRITE_TIMEOUT_MS = 10_000;
7
+ const HEARTBEAT_RETRY_MS = 5_000;
8
+ const HEARTBEAT_STALE_MARGIN_MS = 15_000;
9
+
10
+ /** The subset of a @k2b/sync `Ephemeral<T>` the heartbeat needs. */
11
+ type HeartbeatRegistry<T> = {
12
+ upsert: (input: { key: string; value: T }) => Promise<unknown>;
13
+ /** Renews the lease; false when the key is absent. */
14
+ touch: (input: { key: string }) => Promise<boolean>;
15
+ delete: (input: { key: string }) => Promise<unknown>;
16
+ };
17
+
18
+ type HeartbeatOptions<T> = {
19
+ registry: HeartbeatRegistry<T>;
20
+ key?: string;
21
+ intervalMs?: number;
22
+ retryMs?: number;
23
+ staleAfterMs?: number;
24
+ writeTimeoutMs?: number;
25
+ onError?: (error: unknown) => void;
26
+ onStale?: (error: unknown) => void;
27
+ };
28
+
29
+ export const createHeartbeat = <T>(appId: string, entry: T, options: HeartbeatOptions<T>) => {
30
+ const intervalMs = options.intervalMs ?? HEARTBEAT_INTERVAL_MS;
31
+ const retryMs = options.retryMs ?? Math.min(HEARTBEAT_RETRY_MS, intervalMs);
32
+ const staleAfterMs = options.staleAfterMs ?? APP_REGISTRY_TTL_MS - HEARTBEAT_STALE_MARGIN_MS;
33
+ const writeTimeoutMs = options.writeTimeoutMs ?? HEARTBEAT_WRITE_TIMEOUT_MS;
34
+ const registry = options.registry;
35
+ const onError = options.onError ?? ((error: unknown) => console.error(`[app:${appId}] Registry heartbeat failed`, error));
36
+ const onStale = options.onStale;
37
+
38
+ if (![intervalMs, retryMs, staleAfterMs, writeTimeoutMs].every((value) => Number.isFinite(value) && value > 0)) {
39
+ throw new RangeError("Heartbeat timings must be positive finite numbers");
40
+ }
41
+
42
+ let timer: Timer | null = null;
43
+ let inFlight: Promise<void> | null = null;
44
+ let running = false;
45
+ let lastSuccessAt = 0;
46
+ let staleReported = false;
47
+ const key = options.key ?? `apps/${appId}`;
48
+
49
+ const noteSuccess = (): void => {
50
+ if (!running) return;
51
+ lastSuccessAt = Date.now();
52
+ staleReported = false;
53
+ };
54
+
55
+ const register = async (): Promise<void> => {
56
+ await registry.upsert({ key, value: entry });
57
+ noteSuccess();
58
+ };
59
+
60
+ const refresh = async (): Promise<void> => {
61
+ // Touch renews the existing entry (and emits a registry invalidation).
62
+ // Avoid rebuilding the manifest here. A missing entry is repaired
63
+ // immediately, preserving restart-free registry recovery.
64
+ const touched = await registry.touch({ key });
65
+ if (!touched) await registry.upsert({ key, value: entry });
66
+ noteSuccess();
67
+ };
68
+
69
+ const writeWithTimeout = async (write: Promise<void>): Promise<void> => {
70
+ let timer: Timer | null = null;
71
+ let timedOut = false;
72
+ void write
73
+ .then(async () => {
74
+ // A write that completed after its timeout must not resurrect a stopped app.
75
+ if (timedOut && !running) await registry.delete({ key }).catch(() => false);
76
+ })
77
+ .catch(() => undefined);
78
+ try {
79
+ await Promise.race([
80
+ write,
81
+ new Promise<never>((_, reject) => {
82
+ timer = setTimeout(() => {
83
+ timedOut = true;
84
+ reject(new Error(`Registry write timed out after ${writeTimeoutMs}ms`));
85
+ }, writeTimeoutMs);
86
+ }),
87
+ ]);
88
+ } finally {
89
+ if (timer) clearTimeout(timer);
90
+ }
91
+ };
92
+
93
+ const refreshWithTimeout = (): Promise<void> => writeWithTimeout(refresh());
94
+
95
+ const reportError = (error: unknown): void => {
96
+ try {
97
+ onError(error);
98
+ } catch {
99
+ // Error reporting must never stop future registration attempts.
100
+ }
101
+ if (!staleReported && lastSuccessAt > 0 && Date.now() - lastSuccessAt >= staleAfterMs) {
102
+ staleReported = true;
103
+ try {
104
+ onStale?.(error);
105
+ } catch {
106
+ // A failed stale callback must not disable heartbeat recovery.
107
+ }
108
+ }
109
+ };
110
+
111
+ const schedule = (delayMs: number) => {
112
+ if (!running || timer) return;
113
+ timer = setTimeout(() => {
114
+ timer = null;
115
+ if (!running) return;
116
+
117
+ let nextDelayMs = intervalMs;
118
+ const operation = refreshWithTimeout();
119
+ inFlight = operation;
120
+ void operation
121
+ .catch((error) => {
122
+ nextDelayMs = retryMs;
123
+ reportError(error);
124
+ })
125
+ .finally(() => {
126
+ if (inFlight === operation) inFlight = null;
127
+ schedule(nextDelayMs);
128
+ });
129
+ }, delayMs);
130
+ };
131
+
132
+ return {
133
+ start: async () => {
134
+ if (running) {
135
+ await inFlight;
136
+ return;
137
+ }
138
+ running = true;
139
+ const operation = writeWithTimeout(register());
140
+ inFlight = operation;
141
+ try {
142
+ await operation;
143
+ } catch (error) {
144
+ running = false;
145
+ throw error;
146
+ } finally {
147
+ if (inFlight === operation) inFlight = null;
148
+ }
149
+ schedule(intervalMs);
150
+ },
151
+ stop: async () => {
152
+ if (!running) return;
153
+ running = false;
154
+ if (timer) clearTimeout(timer);
155
+ timer = null;
156
+
157
+ try {
158
+ await inFlight;
159
+ } catch {
160
+ // Recurring heartbeat failures are already reported above. Removal is
161
+ // still required so a stopped app cannot remain discoverable.
162
+ }
163
+ inFlight = null;
164
+ await registry.delete({ key });
165
+ },
166
+ };
167
+ };
@@ -0,0 +1,241 @@
1
+ import type { AppRegistryEntry, HelpRegistryDocument, HelpRegistryEntry } from "../contracts/registry";
2
+ import { helpLocaleChain } from "../shared/help";
3
+ import { markdownToPlainText } from "../shared/markdown";
4
+ import { getApp, getHelp, listApps, listHelp } from "./registry";
5
+
6
+ export const HELP_SEARCH_MAX_LIMIT = 25;
7
+ 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
+ const normalizeSearchText = (value: string): string =>
48
+ value
49
+ .normalize("NFKD")
50
+ .replace(/\p{M}/gu, "")
51
+ .toLocaleLowerCase()
52
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
53
+ .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;
173
+
174
+ const splitSections = (markdown: string): string[] => {
175
+ const starts = [
176
+ 0,
177
+ ...Array.from(markdown.matchAll(/^##\s+/gm)).flatMap((match) =>
178
+ typeof match.index === "number" && match.index > 0 ? [match.index] : [],
179
+ ),
180
+ ];
181
+ return starts.map((start, index) => markdown.slice(start, starts[index + 1] ?? markdown.length).trim()).filter(Boolean);
182
+ };
183
+
184
+ const boundedExcerpt = (markdown: string, terms: readonly string[]): string => {
185
+ if (markdown.length <= HELP_READ_MAX_CHARS) return markdown;
186
+ const marker = "[…]";
187
+ const budget = HELP_READ_MAX_CHARS - marker.length * 2 - 4;
188
+ const lower = markdown.toLocaleLowerCase();
189
+ const anchors = terms.map((term) => lower.indexOf(term)).filter((index) => index >= 0);
190
+ const anchor = anchors.length > 0 ? Math.min(...anchors) : 0;
191
+ const start = Math.max(0, Math.min(markdown.length - budget, anchor - Math.floor(budget / 3)));
192
+ const body = markdown.slice(start, start + budget).trim();
193
+ return `${start > 0 ? `${marker}\n\n` : ""}${body}${start + budget < markdown.length ? `\n\n${marker}` : ""}`;
194
+ };
195
+
196
+ const selectMarkdown = (markdown: string, query?: string): { markdown: string; truncated: boolean } => {
197
+ if (markdown.length <= HELP_READ_MAX_CHARS) return { markdown, truncated: false };
198
+ const terms = searchTerms(query ?? "");
199
+ const phrase = normalizeSearchText(query ?? "");
200
+ const ranked = splitSections(markdown)
201
+ .map((section, index) => {
202
+ const text = normalizeSearchText(markdownToPlainText(section));
203
+ const score = (phrase && text.includes(phrase) ? 10 : 0) + terms.filter((term) => text.includes(term)).length;
204
+ return { section, index, score };
205
+ })
206
+ .filter(({ score }) => score > 0)
207
+ .sort((left, right) => right.score - left.score || left.index - right.index);
208
+
209
+ if (ranked.length === 0) return { markdown: boundedExcerpt(markdown, terms), truncated: true };
210
+
211
+ let excerpt = "";
212
+ for (const { section } of ranked) {
213
+ const candidate = excerpt ? `${excerpt}\n\n${section}` : section;
214
+ if (candidate.length <= HELP_READ_MAX_CHARS) excerpt = candidate;
215
+ else if (!excerpt) excerpt = boundedExcerpt(section, terms);
216
+ }
217
+ return { markdown: excerpt || boundedExcerpt(markdown, terms), truncated: true };
218
+ };
219
+
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
+ };
229
+
230
+ export const helpResourceUri = (appId: string, documentId: string): string =>
231
+ `cloud://help/${encodeURIComponent(appId)}/${encodeURIComponent(documentId)}`;
232
+
233
+ export const parseHelpResourceUri = (uri: string): { appId: string; documentId: string } | null => {
234
+ const match = /^cloud:\/\/help\/([^/]+)\/([^/]+)$/.exec(uri);
235
+ if (!match?.[1] || !match[2]) return null;
236
+ try {
237
+ return { appId: decodeURIComponent(match[1]), documentId: decodeURIComponent(match[2]) };
238
+ } catch {
239
+ return null;
240
+ }
241
+ };
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { AppRegistryHelpSummary, HelpRegistryEntry } from "../contracts/registry";
3
+ import type { HelpDefinition } from "../server/help";
4
+ import type { HelpDocumentManifest } from "../shared/help";
5
+
6
+ export const HELP_REGISTRY_MAX_BYTES = 512 * 1024;
7
+ export const HELP_DOCUMENT_MAX_BYTES = 128 * 1024;
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][]) =>
37
+ source.map(({ id, title, icon, description, order, markdown, searchText }) => {
38
+ const markdownBytes = new TextEncoder().encode(markdown).byteLength;
39
+ if (markdownBytes > HELP_DOCUMENT_MAX_BYTES) {
40
+ throw new Error(`Help document "${id}" exceeds the ${HELP_DOCUMENT_MAX_BYTES}-byte limit`);
41
+ }
42
+ return { id, title, icon, description, order, markdown, searchText };
43
+ });
44
+ const baseLocale = input.definition.baseLocale ?? "en";
45
+ const documents = compileDocuments(input.definition.documents);
46
+ const documentsByLocale = Object.fromEntries(
47
+ Object.entries(input.definition.documentsByLocale ?? {})
48
+ .filter(([locale]) => locale !== baseLocale)
49
+ .map(([locale, localized]) => [locale, compileDocuments(localized)]),
50
+ );
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
+ }
91
+ return {
92
+ summary: { manifestHash, pageBase, baseLocale, documentsByLocale: manifestByLocale, documents: manifest },
93
+ registryEntry,
94
+ };
95
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * NATS connection for @k2b/sync — one connection per app process, created by
3
+ * the app lifecycle. Configuration comes from `env` (NATS_* variables); sync
4
+ * itself reads no ENV and loads no credentials.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { connect, credsAuthenticator, type NatsConnection } from "@nats-io/transport-node";
8
+ import { env } from "../config/env";
9
+
10
+ export type ConnectNatsOptions = {
11
+ /** Connection name shown in NATS monitoring (`connz`), e.g. `cloud-<appId>-<instanceId>`. */
12
+ name: string;
13
+ };
14
+
15
+ export const connectNats = async ({ name }: ConnectNatsOptions): Promise<NatsConnection> => {
16
+ if (env.NATS_SERVERS.length === 0) {
17
+ throw new Error(
18
+ `NATS_SERVERS is not set (connection "${name}"). @k2b/sync needs a comma-separated list of ` +
19
+ "nats://host:port bootstrap servers, e.g. NATS_SERVERS=nats://ipa_nats_1:4222,nats://ipa_nats_2:4222.",
20
+ );
21
+ }
22
+
23
+ return connect({
24
+ servers: env.NATS_SERVERS,
25
+ name,
26
+ ignoreClusterUpdates: env.NATS_IGNORE_CLUSTER_UPDATES,
27
+ // Keep reconnecting for the life of the process: sync work resumes when the cluster is back.
28
+ reconnect: true,
29
+ maxReconnectAttempts: -1,
30
+ reconnectTimeWait: 2_000,
31
+ ...(env.NATS_CREDS_FILE ? { authenticator: credsAuthenticator(readFileSync(env.NATS_CREDS_FILE)) } : {}),
32
+ ...(env.NATS_TLS_CA_FILE ? { tls: { caFile: env.NATS_TLS_CA_FILE } } : {}),
33
+ });
34
+ };
@@ -0,0 +1,40 @@
1
+ import type { HtmlFn } from "@k2b/ssr";
2
+ import type { Context } from "hono";
3
+ import type { ClientErrorStatusCode, ServerErrorStatusCode } from "hono/utils/http-status";
4
+ import { getLocale } from "../server/locale";
5
+ import { createLoginRedirectUrl } from "../shared/redirect";
6
+ import type { PageOptions } from "./define-app";
7
+
8
+ export type PageErrorOptions = {
9
+ title?: string;
10
+ description?: string;
11
+ action?: { label: string; href: string; icon?: string };
12
+ /** Keep standalone/public pages outside the Cloud navigation shell. */
13
+ layout?: "cloud" | "minimal";
14
+ };
15
+
16
+ export type PageErrorStatus = ClientErrorStatusCode | ServerErrorStatusCode;
17
+
18
+ /** Explicit document responses, bound to the owning application's SSR template. */
19
+ export const createPageResponses = (html: HtmlFn<PageOptions>) => {
20
+ const error = async (c: Context, status: PageErrorStatus, options: PageErrorOptions = {}): Promise<Response> => {
21
+ // Auth middleware may reject before ssr() initializes page metadata.
22
+ c.set("page", { lang: getLocale(c) });
23
+ c.header("Cache-Control", "private, no-store");
24
+ // Load JSX only after the application's SSR plugin has been installed.
25
+ const { renderPageError } = await import("../ssr/PageError");
26
+ const response = await html(renderPageError(c, status, options), c.get("page"));
27
+ return c.newResponse(response.body, { status, headers: response.headers });
28
+ };
29
+
30
+ const access = {
31
+ onReject: (c: Context, reason: "unauthenticated" | "forbidden") => {
32
+ c.header("Cache-Control", "private, no-store");
33
+ // Account policies can reject a resolved non-user actor as unauthenticated.
34
+ // A browser page must not send that actor into a login/return loop.
35
+ return reason === "unauthenticated" && !c.get("actor") ? createLoginRedirectUrl(c.req.url) : error(c, 403);
36
+ },
37
+ };
38
+
39
+ return { access, error };
40
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The one @k2b/sync instance of this process.
3
+ *
4
+ * `defineApp().start()` (and the gateway router, which has no `defineApp`)
5
+ * call `startProcessSync()`: connect NATS, create the instance, bind it here,
6
+ * and wait for `ready()`. Everything else reaches it lazily through
7
+ * `app.sync`, `lifecycleContext.sync`, `lazySync()`, or `getProcessSync()`;
8
+ * nothing touches sync during module evaluation.
9
+ */
10
+ import { hostname } from "node:os";
11
+ import { createSync, type Sync } from "@k2b/sync";
12
+ import { env } from "../config/env";
13
+ import { flushSyncTraceEvents, observeSyncEvent } from "../services/logging/trace";
14
+ import { connectNats } from "./nats-connection";
15
+
16
+ let current: Sync | undefined;
17
+ let starting = false;
18
+
19
+ /** Bind the process instance. `startProcessSync()` does this; tests may bind their own. */
20
+ export const bindProcessSync = (sync: Sync): void => {
21
+ if (current) throw new Error("A Sync instance is already bound to this process");
22
+ current = sync;
23
+ };
24
+
25
+ export const unbindProcessSync = (): void => {
26
+ current = undefined;
27
+ };
28
+
29
+ export const getProcessSync = (): Sync => {
30
+ if (!current) {
31
+ throw new Error(
32
+ "Sync is not available before app.start() has connected NATS. Declare primitives with " +
33
+ "lazySync((sync) => sync.job(...)) and use them from lifecycle.start() or request handlers.",
34
+ );
35
+ }
36
+ return current;
37
+ };
38
+
39
+ /**
40
+ * Memoize a sync primitive per process instance so it can be declared at
41
+ * module scope without any I/O at import time:
42
+ *
43
+ * const emails = lazySync((sync) => sync.job<Email>({ id: "emails" }));
44
+ * await emails().submit({ key, input });
45
+ *
46
+ * The factory runs on first use with the bound instance; a different bound
47
+ * instance (tests) gets its own handle.
48
+ */
49
+ export const lazySync = <T>(create: (sync: Sync) => T): (() => T) => {
50
+ const handles = new WeakMap<Sync, T>();
51
+ return () => {
52
+ const sync = getProcessSync();
53
+ const existing = handles.get(sync);
54
+ if (existing !== undefined) return existing;
55
+ const created = create(sync);
56
+ handles.set(sync, created);
57
+ return created;
58
+ };
59
+ };
60
+
61
+ export type ProcessSync = {
62
+ sync: Sync;
63
+ /** Drains sync work, then the NATS connection, and unbinds the instance. */
64
+ stop: () => Promise<void>;
65
+ };
66
+
67
+ /** Connect NATS, create and bind the process Sync instance, and wait until it is ready. */
68
+ export const startProcessSync = async ({ application }: { application: string }): Promise<ProcessSync> => {
69
+ if (!env.SYNC_NAMESPACE.trim()) {
70
+ throw new Error(
71
+ `SYNC_NAMESPACE is not set (application "${application}"). Every process of one Cloud installation ` +
72
+ "must share the same @k2b/sync namespace.",
73
+ );
74
+ }
75
+ if (current || starting) throw new Error("A Sync instance is already starting or bound to this process");
76
+ starting = true;
77
+ try {
78
+ const connection = await connectNats({ name: `${application}@${hostname()}` });
79
+ let sync: Sync | undefined;
80
+ try {
81
+ sync = createSync({
82
+ connection,
83
+ namespace: env.SYNC_NAMESPACE,
84
+ application,
85
+ defaults: { replicas: 3 },
86
+ observe: (event) => observeSyncEvent(event, application),
87
+ });
88
+ bindProcessSync(sync);
89
+ await sync.ready();
90
+ } catch (error) {
91
+ if (current === sync) unbindProcessSync();
92
+ await flushSyncTraceEvents();
93
+ await connection.close();
94
+ throw error;
95
+ }
96
+ const active = sync;
97
+ let stopPromise: Promise<void> | undefined;
98
+ return {
99
+ sync: active,
100
+ stop: () =>
101
+ (stopPromise ??= (async () => {
102
+ try {
103
+ await active.drain();
104
+ } finally {
105
+ try {
106
+ await flushSyncTraceEvents();
107
+ } finally {
108
+ if (current === active) unbindProcessSync();
109
+ await connection.drain();
110
+ }
111
+ }
112
+ })()),
113
+ };
114
+ } finally {
115
+ starting = false;
116
+ }
117
+ };