@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,530 @@
1
+ import { crypto } from "@k2b/stdlib";
2
+ import { sql } from "bun";
3
+ import { CompactSign, compactVerify, exportJWK, generateKeyPair, importJWK, type JWK } from "jose";
4
+ import { logger } from "../logging";
5
+ import {
6
+ CLOUD_IDENTITY_ALGORITHM,
7
+ IDENTITY_ACTIVATION_LEAD_MS,
8
+ IDENTITY_CLOCK_TOLERANCE_SECONDS,
9
+ IDENTITY_ROLLOUT_MARGIN_MS,
10
+ IDENTITY_ROTATION_AGE_MS,
11
+ IDENTITY_SIGNING_CACHE_MS,
12
+ } from "./constants";
13
+ import { type IdentityKeyEncryptionConfig, readIdentityKeyEncryptionConfig } from "./key-config";
14
+ import { identityMetrics } from "./metrics";
15
+ import { getIdentityRuntimeConfig } from "./runtime-config";
16
+ import { SignerRefreshes } from "./signer-refresh";
17
+
18
+ export type SigningKeyPurpose = "session" | "invocation" | "oauth";
19
+ export type SigningKeyState = "pending" | "active" | "retired" | "revoked";
20
+ const SIGNING_KEY_PURPOSES = ["session", "invocation", "oauth"] as const satisfies readonly SigningKeyPurpose[];
21
+
22
+ type SigningKeyRow = {
23
+ id: string;
24
+ purpose: SigningKeyPurpose;
25
+ state: SigningKeyState;
26
+ kid: string;
27
+ alg: "RS256";
28
+ public_jwk: JWK | string;
29
+ encrypted_private_jwk: string;
30
+ encryption_key_id: string;
31
+ created_at: Date | string;
32
+ activate_at: Date | string;
33
+ activated_at: Date | string | null;
34
+ sign_until: Date | string;
35
+ retired_at: Date | string | null;
36
+ verify_until: Date | string;
37
+ revoked_at: Date | string | null;
38
+ };
39
+
40
+ export type PreparedIdentitySigner = {
41
+ kid: string;
42
+ key: CryptoKey;
43
+ signUntil: Date;
44
+ };
45
+
46
+ export type IdentitySigningKeyStatus = {
47
+ purpose: SigningKeyPurpose;
48
+ state: SigningKeyState;
49
+ kid: string;
50
+ alg: "RS256";
51
+ encryptionKeyId: string;
52
+ createdAt: string;
53
+ activateAt: string;
54
+ signUntil: string;
55
+ verifyUntil: string;
56
+ revokedAt: string | null;
57
+ };
58
+
59
+ type GeneratedKey = {
60
+ kid: string;
61
+ publicJwk: JWK;
62
+ encryptedPrivateJwk: string;
63
+ encryptionKeyId: string;
64
+ privateKey: CryptoKey;
65
+ };
66
+
67
+ const log = logger("core:identity-keyring");
68
+ const encoder = new TextEncoder();
69
+ const PURPOSE_FALLBACK_TOKEN_TTL_MS: Record<SigningKeyPurpose, number> = {
70
+ session: 24 * 60 * 60_000,
71
+ invocation: 30_000,
72
+ oauth: 2 * 60 * 60_000,
73
+ };
74
+
75
+ const date = (value: Date | string): Date => (value instanceof Date ? value : new Date(value));
76
+ const publicJwk = (value: JWK | string): JWK => (typeof value === "string" ? (JSON.parse(value) as JWK) : value);
77
+ const serializedSize = (value: unknown): number => encoder.encode(JSON.stringify(value)).byteLength;
78
+
79
+ const assertRsaJwk = (value: JWK, privateRequired: boolean): void => {
80
+ if (value.kty !== "RSA" || typeof value.n !== "string" || typeof value.e !== "string") {
81
+ throw new Error("Cloud identity key is not an RSA JWK");
82
+ }
83
+ if (privateRequired && typeof value.d !== "string") throw new Error("Cloud identity private JWK is incomplete");
84
+ if (value.alg && value.alg !== CLOUD_IDENTITY_ALGORITHM) throw new Error("Cloud identity key uses an unsupported algorithm");
85
+ };
86
+
87
+ const verifyPair = async (privateJwk: JWK, expectedPublicJwk: JWK): Promise<CryptoKey> => {
88
+ assertRsaJwk(privateJwk, true);
89
+ assertRsaJwk(expectedPublicJwk, false);
90
+ if (privateJwk.n !== expectedPublicJwk.n || privateJwk.e !== expectedPublicJwk.e) {
91
+ throw new Error("Cloud identity private/public key pair does not match");
92
+ }
93
+ const importedPrivate = await importJWK(privateJwk, CLOUD_IDENTITY_ALGORITHM);
94
+ const importedPublic = await importJWK(expectedPublicJwk, CLOUD_IDENTITY_ALGORITHM);
95
+ if (!(importedPrivate instanceof CryptoKey) || !(importedPublic instanceof CryptoKey)) {
96
+ throw new Error("Cloud identity RSA JWK imported as an unexpected key type");
97
+ }
98
+ const probe = encoder.encode("cloud-identity-key-pair-check");
99
+ const signature = await new CompactSign(probe).setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM }).sign(importedPrivate);
100
+ const verified = await compactVerify(signature, importedPublic, { algorithms: [CLOUD_IDENTITY_ALGORITHM] });
101
+ if (new TextDecoder().decode(verified.payload) !== "cloud-identity-key-pair-check") {
102
+ throw new Error("Cloud identity key-pair verification failed");
103
+ }
104
+ return importedPrivate;
105
+ };
106
+
107
+ const decryptPrivateJwk = async (row: SigningKeyRow, keys: IdentityKeyEncryptionConfig): Promise<JWK> => {
108
+ const candidate =
109
+ row.encryption_key_id === keys.current.id
110
+ ? keys.current
111
+ : row.encryption_key_id === keys.previous?.id
112
+ ? keys.previous
113
+ : row.encryption_key_id === keys.next?.id
114
+ ? keys.next
115
+ : null;
116
+ if (!candidate) throw new Error(`No configured KEK can decrypt Cloud identity key ${row.kid}`);
117
+ const plaintext = await crypto.symmetric.decrypt({ payload: row.encrypted_private_jwk, key: candidate.key });
118
+ const parsed = JSON.parse(plaintext) as JWK;
119
+ if (serializedSize(parsed) > 16_384) throw new Error("Cloud identity private JWK exceeds its size bound");
120
+ return parsed;
121
+ };
122
+
123
+ const generateEncryptedKey = async (keys: IdentityKeyEncryptionConfig): Promise<GeneratedKey> => {
124
+ const { privateKey, publicKey } = await generateKeyPair(CLOUD_IDENTITY_ALGORITHM, { extractable: true, modulusLength: 2048 });
125
+ const kid = crypto.common.uuid();
126
+ const [rawPrivate, rawPublic] = await Promise.all([exportJWK(privateKey), exportJWK(publicKey)]);
127
+ const privateJwk: JWK = { ...rawPrivate, alg: CLOUD_IDENTITY_ALGORITHM, kid, use: "sig" };
128
+ const exposedPublicJwk: JWK = { ...rawPublic, alg: CLOUD_IDENTITY_ALGORITHM, kid, use: "sig" };
129
+ assertRsaJwk(privateJwk, true);
130
+ assertRsaJwk(exposedPublicJwk, false);
131
+ if (serializedSize(exposedPublicJwk) > 16_384 || serializedSize(privateJwk) > 16_384) {
132
+ throw new Error("Generated Cloud identity JWK exceeds its size bound");
133
+ }
134
+ const encryptedPrivateJwk = await crypto.symmetric.encrypt({
135
+ payload: JSON.stringify(privateJwk),
136
+ key: keys.current.key,
137
+ stretched: false,
138
+ });
139
+ if (encoder.encode(encryptedPrivateJwk).byteLength > 32_768) {
140
+ throw new Error("Encrypted Cloud identity private JWK exceeds its size bound");
141
+ }
142
+ return { kid, publicJwk: exposedPublicJwk, encryptedPrivateJwk, encryptionKeyId: keys.current.id, privateKey };
143
+ };
144
+
145
+ const signingDeadline = (activateAt: Date): Date =>
146
+ new Date(activateAt.getTime() + IDENTITY_ROTATION_AGE_MS + IDENTITY_ACTIVATION_LEAD_MS + IDENTITY_SIGNING_CACHE_MS);
147
+
148
+ const cache = new Map<SigningKeyPurpose, { signer: PreparedIdentitySigner; expiresAt: number }>();
149
+ const signerRefreshes = new SignerRefreshes<SigningKeyPurpose, PreparedIdentitySigner>();
150
+ let maintenanceTimer: ReturnType<typeof setInterval> | null = null;
151
+
152
+ const importRow = async (row: SigningKeyRow, keys: IdentityKeyEncryptionConfig): Promise<PreparedIdentitySigner> => {
153
+ if (row.alg !== CLOUD_IDENTITY_ALGORITHM || row.state !== "active") throw new Error("Cloud identity signer row is not active RS256");
154
+ const privateJwk = await decryptPrivateJwk(row, keys);
155
+ const key = await verifyPair(privateJwk, publicJwk(row.public_jwk));
156
+ return { kid: row.kid, key, signUntil: date(row.sign_until) };
157
+ };
158
+
159
+ const selectRows = async (purpose: SigningKeyPurpose): Promise<SigningKeyRow[]> =>
160
+ sql<SigningKeyRow[]>`
161
+ SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
162
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
163
+ FROM auth.signing_keys
164
+ WHERE purpose = ${purpose} AND state IN ('pending', 'active')
165
+ ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, created_at DESC
166
+ `;
167
+
168
+ const insertKey = async (
169
+ db: typeof sql,
170
+ purpose: SigningKeyPurpose,
171
+ generated: GeneratedKey,
172
+ state: "pending" | "active",
173
+ now: Date,
174
+ ): Promise<void> => {
175
+ const activateAt = state === "active" ? now : new Date(now.getTime() + IDENTITY_ACTIVATION_LEAD_MS);
176
+ const signUntil = signingDeadline(activateAt);
177
+ const purposeTtl = PURPOSE_FALLBACK_TOKEN_TTL_MS[purpose];
178
+ const verifyUntil = new Date(signUntil.getTime() + purposeTtl + IDENTITY_CLOCK_TOLERANCE_SECONDS * 1_000 + IDENTITY_ROLLOUT_MARGIN_MS);
179
+ await db`
180
+ INSERT INTO auth.signing_keys (
181
+ purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
182
+ created_at, activate_at, activated_at, sign_until, verify_until
183
+ ) VALUES (
184
+ ${purpose}, ${state}, ${generated.kid}, ${CLOUD_IDENTITY_ALGORITHM}, ${JSON.stringify(generated.publicJwk)}::jsonb,
185
+ ${generated.encryptedPrivateJwk}, ${generated.encryptionKeyId}, ${now}, ${activateAt},
186
+ ${state === "active" ? now : null}, ${signUntil}, ${verifyUntil}
187
+ )
188
+ `;
189
+ };
190
+
191
+ const maintainPurpose = async (purpose: SigningKeyPurpose): Promise<SigningKeyRow> => {
192
+ const keys = await readIdentityKeyEncryptionConfig();
193
+ const before = await selectRows(purpose);
194
+ const beforeActive = before.find((row) => row.state === "active");
195
+ const beforePending = before.find((row) => row.state === "pending");
196
+ const nowMs = Date.now();
197
+ const activeIsFresh =
198
+ beforeActive &&
199
+ nowMs < date(beforeActive.sign_until).getTime() - IDENTITY_ACTIVATION_LEAD_MS &&
200
+ nowMs < date(beforeActive.created_at).getTime() + IDENTITY_ROTATION_AGE_MS;
201
+ if (activeIsFresh && (!beforePending || date(beforePending.activate_at).getTime() > nowMs)) return beforeActive;
202
+
203
+ const generated =
204
+ !beforeActive || date(beforeActive.sign_until).getTime() <= nowMs || (!beforePending && !activeIsFresh)
205
+ ? await generateEncryptedKey(keys)
206
+ : null;
207
+ const active = await sql.begin(async (tx) => {
208
+ await tx`SELECT pg_advisory_xact_lock(hashtextextended(${`cloud:identity-keyring:${purpose}`}, 0))`;
209
+ const rows = await tx<SigningKeyRow[]>`
210
+ SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
211
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
212
+ FROM auth.signing_keys
213
+ WHERE purpose = ${purpose} AND state IN ('pending', 'active')
214
+ ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, created_at DESC
215
+ FOR UPDATE
216
+ `;
217
+ let current = rows.find((row) => row.state === "active");
218
+ let pending = rows.find((row) => row.state === "pending");
219
+ const now = new Date();
220
+
221
+ // Long downtime can outlive both signing deadlines. Preserve their public
222
+ // verification windows, but never promote or return an expired signer.
223
+ for (const row of [current, pending]) {
224
+ if (!row || date(row.sign_until).getTime() > now.getTime()) continue;
225
+ await tx`UPDATE auth.signing_keys SET state = 'retired', retired_at = ${now} WHERE id = ${row.id}`;
226
+ if (row === current) current = undefined;
227
+ if (row === pending) pending = undefined;
228
+ }
229
+
230
+ if (pending && date(pending.activate_at).getTime() <= now.getTime()) {
231
+ if (current) {
232
+ const overlapSignUntil = new Date(now.getTime() + IDENTITY_SIGNING_CACHE_MS);
233
+ await tx`
234
+ UPDATE auth.signing_keys
235
+ SET state = 'retired', retired_at = ${now}, sign_until = ${overlapSignUntil},
236
+ verify_until = GREATEST(verify_until, ${new Date(
237
+ overlapSignUntil.getTime() +
238
+ PURPOSE_FALLBACK_TOKEN_TTL_MS[purpose] +
239
+ IDENTITY_CLOCK_TOLERANCE_SECONDS * 1_000 +
240
+ IDENTITY_ROLLOUT_MARGIN_MS,
241
+ )})
242
+ WHERE id = ${current.id}
243
+ `;
244
+ }
245
+ const [promoted] = await tx<SigningKeyRow[]>`
246
+ UPDATE auth.signing_keys
247
+ SET state = 'active', activated_at = ${now}
248
+ WHERE id = ${pending.id}
249
+ RETURNING id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
250
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
251
+ `;
252
+ if (!promoted) throw new Error(`Failed to promote ${purpose} signing key`);
253
+ current = promoted;
254
+ identityMetrics.increment("rotation_success");
255
+ log.info("Cloud identity signing key promoted", { purpose, kid: promoted.kid });
256
+ }
257
+
258
+ if (!current) {
259
+ const candidate = generated ?? (await generateEncryptedKey(keys));
260
+ await insertKey(tx as typeof sql, purpose, candidate, "active", now);
261
+ const [created] = await tx<SigningKeyRow[]>`
262
+ SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
263
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
264
+ FROM auth.signing_keys WHERE purpose = ${purpose} AND state = 'active'
265
+ `;
266
+ if (!created) throw new Error(`Failed to create initial ${purpose} signing key`);
267
+ current = created;
268
+ identityMetrics.increment("rotation_success");
269
+ log.info("Initial Cloud identity signing key created", { purpose, kid: created.kid });
270
+ }
271
+
272
+ const currentDue =
273
+ date(current.created_at).getTime() + IDENTITY_ROTATION_AGE_MS <= now.getTime() ||
274
+ date(current.sign_until).getTime() - IDENTITY_ACTIVATION_LEAD_MS <= now.getTime();
275
+ if (!pending && currentDue) {
276
+ const candidate = generated ?? (await generateEncryptedKey(keys));
277
+ await insertKey(tx as typeof sql, purpose, candidate, "pending", now);
278
+ log.info("Cloud identity signing key pre-published", { purpose, kid: candidate.kid });
279
+ }
280
+ return current;
281
+ });
282
+ return active;
283
+ };
284
+
285
+ export const prepareIdentitySigner = async (purpose: SigningKeyPurpose): Promise<PreparedIdentitySigner> => {
286
+ const now = Date.now();
287
+ const cached = cache.get(purpose);
288
+ if (cached && cached.expiresAt > now && cached.signer.signUntil.getTime() > now) return cached.signer;
289
+ return signerRefreshes.run(
290
+ purpose,
291
+ async () => {
292
+ try {
293
+ const keys = await readIdentityKeyEncryptionConfig();
294
+ const row = await maintainPurpose(purpose);
295
+ const signer = await importRow(row, keys);
296
+ if (signer.signUntil.getTime() <= now) throw new Error(`Active ${purpose} signing key is past sign_until`);
297
+ return signer;
298
+ } catch (error) {
299
+ identityMetrics.increment("rotation_failure");
300
+ log.error("Cloud identity signer refresh failed", { purpose, error: error instanceof Error ? error.message : String(error) });
301
+ if (cached && cached.signer.signUntil.getTime() > now) return cached.signer;
302
+ throw error;
303
+ }
304
+ },
305
+ (signer) => {
306
+ cache.set(purpose, { signer, expiresAt: Math.min(Date.now() + IDENTITY_SIGNING_CACHE_MS, signer.signUntil.getTime()) });
307
+ },
308
+ );
309
+ };
310
+
311
+ /**
312
+ * Derives a local result while the selected key is still active in Postgres.
313
+ * The shared row lock makes emergency revocation wait for already-started
314
+ * issuance and prevents replicas from releasing a new result under that key
315
+ * after the revocation commits.
316
+ */
317
+ export const withActiveIdentitySigner = async <T>(
318
+ purpose: SigningKeyPurpose,
319
+ callback: (signer: PreparedIdentitySigner & { issuer: string }, db: typeof sql) => Promise<T>,
320
+ options: { pool?: typeof sql; signal?: AbortSignal; timeoutMs?: number } = {},
321
+ ): Promise<T> => {
322
+ const timeoutMs = options.timeoutMs ?? 30_000;
323
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Identity issuance timeout must be positive");
324
+ const deadline = Date.now() + timeoutMs;
325
+ const checkDeadline = () => {
326
+ options.signal?.throwIfAborted();
327
+ if (Date.now() >= deadline) throw new DOMException("Identity issuance deadline exceeded", "TimeoutError");
328
+ };
329
+ // Preparation can refresh/rotate keys using the pool. Never do it while
330
+ // holding a caller's transaction: every issuance needs just one connection.
331
+ for (let attempt = 0; attempt < 2; attempt += 1) {
332
+ checkDeadline();
333
+ const signer = await prepareIdentitySigner(purpose);
334
+ const { issuer } = await getIdentityRuntimeConfig();
335
+ checkDeadline();
336
+ const run = async (db: typeof sql): Promise<{ active: false } | { active: true; result: T }> => {
337
+ checkDeadline();
338
+ // A JS deadline alone cannot cancel a blocked PostgreSQL query. Bound
339
+ // each statement (including lock waits) on this connection, transaction-local.
340
+ await db`SELECT set_config('statement_timeout', ${`${Math.max(1, Math.floor(deadline - Date.now()))}ms`}, true)`;
341
+ const [active] = await db<Array<{ kid: string }>>`
342
+ SELECT kid
343
+ FROM auth.signing_keys
344
+ WHERE kid = ${signer.kid}
345
+ AND purpose = ${purpose}
346
+ AND state = 'active'
347
+ AND sign_until > now()
348
+ FOR SHARE
349
+ `;
350
+ checkDeadline();
351
+ if (!active) return { active: false };
352
+ const result = await callback({ ...signer, issuer }, db);
353
+ return { active: true, result };
354
+ };
355
+ const checked = await (options.pool ?? sql).begin(run);
356
+ checkDeadline();
357
+ if (checked.active) return checked.result;
358
+ invalidateIdentitySignerCache(purpose, signer.kid);
359
+ }
360
+ throw new Error(`No active ${purpose} identity signer is available`);
361
+ };
362
+
363
+ export const initializeIdentityAuthority = async (): Promise<void> => {
364
+ for (const purpose of SIGNING_KEY_PURPOSES) await prepareIdentitySigner(purpose);
365
+ const keys = await readIdentityKeyEncryptionConfig();
366
+ if (keys.previous) await rewrapIdentitySigningKeys();
367
+ };
368
+
369
+ export const listIdentityJwks = async (purpose: "session" | "invocation"): Promise<{ keys: JWK[]; etag: string }> => {
370
+ const rows = await sql<Array<{ public_jwk: JWK | string }>>`
371
+ SELECT public_jwk
372
+ FROM auth.signing_keys
373
+ WHERE purpose = ${purpose}
374
+ AND state IN ('pending', 'active', 'retired')
375
+ AND verify_until > now()
376
+ ORDER BY created_at
377
+ `;
378
+ const keys = rows.map((row) => publicJwk(row.public_jwk));
379
+ return { keys, etag: `"${(await crypto.common.hash(JSON.stringify({ purpose, keys }))).slice(0, 32)}"` };
380
+ };
381
+
382
+ export const revokeIdentitySigningKey = async (params: { kid: string; reason: string }): Promise<boolean> => {
383
+ const reason = params.reason.trim().slice(0, 500);
384
+ if (!reason) throw new Error("Emergency key revocation requires a reason");
385
+ const rows = await sql<Array<{ purpose: SigningKeyPurpose }>>`
386
+ UPDATE auth.signing_keys
387
+ SET state = 'revoked', revoked_at = now(), revoke_reason = ${reason}
388
+ WHERE kid = ${params.kid} AND state <> 'revoked'
389
+ RETURNING purpose
390
+ `;
391
+ for (const row of rows) invalidateIdentitySignerCache(row.purpose);
392
+ if (rows.length > 0) {
393
+ const purpose = rows[0]!.purpose;
394
+ log.warn("Cloud identity signing key revoked", { kid: params.kid, purpose, reason });
395
+ await prepareIdentitySigner(purpose);
396
+ }
397
+ return rows.length > 0;
398
+ };
399
+
400
+ export const getIdentitySigningKeyStatus = async (): Promise<IdentitySigningKeyStatus[]> => {
401
+ const rows = await sql<SigningKeyRow[]>`
402
+ SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
403
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
404
+ FROM auth.signing_keys
405
+ ORDER BY purpose, created_at DESC
406
+ `;
407
+ return rows.map((row) => ({
408
+ purpose: row.purpose,
409
+ state: row.state,
410
+ kid: row.kid,
411
+ alg: row.alg,
412
+ encryptionKeyId: row.encryption_key_id,
413
+ createdAt: date(row.created_at).toISOString(),
414
+ activateAt: date(row.activate_at).toISOString(),
415
+ signUntil: date(row.sign_until).toISOString(),
416
+ verifyUntil: date(row.verify_until).toISOString(),
417
+ revokedAt: row.revoked_at ? date(row.revoked_at).toISOString() : null,
418
+ }));
419
+ };
420
+
421
+ export const rewrapIdentitySigningKeys = async (): Promise<number> => {
422
+ const keys = await readIdentityKeyEncryptionConfig();
423
+ const rows = await sql<SigningKeyRow[]>`
424
+ SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
425
+ created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
426
+ FROM auth.signing_keys
427
+ ORDER BY created_at
428
+ `;
429
+ const replacements: Array<{ id: string; encrypted: string }> = [];
430
+ for (const row of rows) {
431
+ const privateJwk = await decryptPrivateJwk(row, keys);
432
+ await verifyPair(privateJwk, publicJwk(row.public_jwk));
433
+ if (row.encryption_key_id === keys.current.id) continue;
434
+ const encrypted = await crypto.symmetric.encrypt({ payload: JSON.stringify(privateJwk), key: keys.current.key, stretched: false });
435
+ const verifiedPlaintext = await crypto.symmetric.decrypt({ payload: encrypted, key: keys.current.key });
436
+ await verifyPair(JSON.parse(verifiedPlaintext) as JWK, publicJwk(row.public_jwk));
437
+ replacements.push({
438
+ id: row.id,
439
+ encrypted,
440
+ });
441
+ }
442
+ if (replacements.length === 0) return 0;
443
+ await sql.begin(async (tx) => {
444
+ for (const replacement of replacements) {
445
+ await tx`
446
+ UPDATE auth.signing_keys
447
+ SET encrypted_private_jwk = ${replacement.encrypted}, encryption_key_id = ${keys.current.id}
448
+ WHERE id = ${replacement.id}
449
+ `;
450
+ }
451
+ });
452
+ cache.clear();
453
+ signerRefreshes.invalidateAll();
454
+ log.info("Cloud identity signing keys rewrapped", { count: replacements.length, encryptionKeyId: keys.current.id });
455
+ return replacements.length;
456
+ };
457
+
458
+ const maintainVerificationWindows = async (): Promise<void> => {
459
+ await sql`
460
+ UPDATE auth.signing_keys key
461
+ SET verify_until = GREATEST(
462
+ key.verify_until,
463
+ family.latest_expiry + (${IDENTITY_CLOCK_TOLERANCE_SECONDS}::int * INTERVAL '1 second') + (${Math.floor(
464
+ IDENTITY_ROLLOUT_MARGIN_MS / 1_000,
465
+ )}::int * INTERVAL '1 second')
466
+ )
467
+ FROM (
468
+ SELECT signing_kid, max(expires_at) AS latest_expiry
469
+ FROM auth.session_families
470
+ GROUP BY signing_kid
471
+ ) family
472
+ WHERE key.kid = family.signing_kid AND key.purpose = 'session'
473
+ `;
474
+ };
475
+
476
+ export const runIdentityKeyMaintenance = async (): Promise<void> => {
477
+ for (const purpose of SIGNING_KEY_PURPOSES) await maintainPurpose(purpose);
478
+ await maintainVerificationWindows();
479
+ await sql`
480
+ DELETE FROM auth.session_families
481
+ WHERE sid IN (
482
+ SELECT sid FROM auth.session_families
483
+ WHERE expires_at < now() - INTERVAL '24 hours'
484
+ OR revoked_at < now() - INTERVAL '24 hours'
485
+ ORDER BY COALESCE(revoked_at, expires_at)
486
+ LIMIT 500
487
+ )
488
+ `;
489
+ await sql`
490
+ DELETE FROM auth.signing_keys
491
+ WHERE id IN (
492
+ SELECT key.id FROM auth.signing_keys key
493
+ WHERE key.state = 'retired' AND key.verify_until < now()
494
+ AND NOT EXISTS (
495
+ SELECT 1 FROM auth.session_families family
496
+ WHERE family.signing_kid = key.kid
497
+ )
498
+ ORDER BY verify_until
499
+ LIMIT 100
500
+ )
501
+ `;
502
+ };
503
+
504
+ export const startIdentityKeyMaintenance = (): (() => void) => {
505
+ if (maintenanceTimer) return () => undefined;
506
+ maintenanceTimer = setInterval(() => {
507
+ void runIdentityKeyMaintenance().catch((error) => {
508
+ identityMetrics.increment("rotation_failure");
509
+ log.error("Cloud identity key maintenance failed", { error: error instanceof Error ? error.message : String(error) });
510
+ });
511
+ }, IDENTITY_SIGNING_CACHE_MS);
512
+ maintenanceTimer.unref?.();
513
+ return () => {
514
+ if (maintenanceTimer) clearInterval(maintenanceTimer);
515
+ maintenanceTimer = null;
516
+ };
517
+ };
518
+
519
+ export const clearIdentityKeyCachesForTest = (): void => {
520
+ cache.clear();
521
+ signerRefreshes.invalidateAll();
522
+ };
523
+
524
+ export const invalidateIdentitySignerCache = (purpose: SigningKeyPurpose, kid?: string): void => {
525
+ const cached = cache.get(purpose);
526
+ if (!kid || cached?.signer.kid === kid) {
527
+ cache.delete(purpose);
528
+ signerRefreshes.invalidate(purpose);
529
+ }
530
+ };
@@ -0,0 +1,29 @@
1
+ export type IdentityMetricName =
2
+ | "sign_success"
3
+ | "sign_failure"
4
+ | "verify_success"
5
+ | "verify_failure"
6
+ | "unknown_kid_refresh"
7
+ | "rotation_success"
8
+ | "rotation_failure";
9
+
10
+ const names: IdentityMetricName[] = [
11
+ "sign_success",
12
+ "sign_failure",
13
+ "verify_success",
14
+ "verify_failure",
15
+ "unknown_kid_refresh",
16
+ "rotation_success",
17
+ "rotation_failure",
18
+ ];
19
+
20
+ const counters = new Map<IdentityMetricName, number>(names.map((name) => [name, 0]));
21
+
22
+ export const identityMetrics = {
23
+ increment(name: IdentityMetricName): void {
24
+ counters.set(name, (counters.get(name) ?? 0) + 1);
25
+ },
26
+ snapshot(): Readonly<Record<IdentityMetricName, number>> {
27
+ return Object.freeze(Object.fromEntries(names.map((name) => [name, counters.get(name) ?? 0])) as Record<IdentityMetricName, number>);
28
+ },
29
+ };
@@ -0,0 +1,18 @@
1
+ import { type Context, Hono } from "hono";
2
+ import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_SESSION_JWKS_PATH, IDENTITY_JWKS_MAX_AGE_SECONDS } from "./constants";
3
+ import { listIdentityJwks } from "./key-ring";
4
+
5
+ const serveJwks = (purpose: "session" | "invocation") => async (c: Context) => {
6
+ const jwks = await listIdentityJwks(purpose);
7
+ if (c.req.header("if-none-match") === jwks.etag) {
8
+ c.header("ETag", jwks.etag);
9
+ c.header("Cache-Control", `public, max-age=${IDENTITY_JWKS_MAX_AGE_SECONDS}, must-revalidate`);
10
+ return c.body(null, 304);
11
+ }
12
+ c.header("ETag", jwks.etag);
13
+ c.header("Cache-Control", `public, max-age=${IDENTITY_JWKS_MAX_AGE_SECONDS}, must-revalidate`);
14
+ return c.json({ keys: jwks.keys });
15
+ };
16
+
17
+ export const createIdentityPublicRoutes = (): Hono =>
18
+ new Hono().get(CLOUD_SESSION_JWKS_PATH, serveJwks("session")).get(CLOUD_INVOCATION_JWKS_PATH, serveJwks("invocation"));
@@ -0,0 +1,88 @@
1
+ import { publicCloudOrigin } from "../../shared/app-url";
2
+ import { get as getSetting } from "../settings";
3
+ import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_OAUTH_JWKS_PATH, CLOUD_SESSION_JWKS_PATH } from "./constants";
4
+
5
+ const RUNTIME_CONFIG_TTL_MS = 60_000;
6
+
7
+ export type IdentityRuntimeConfig = {
8
+ issuer: string;
9
+ sessionJwksUrl: URL;
10
+ invocationJwksUrl: URL;
11
+ oauthJwksUrl: URL;
12
+ groupsAdmin: string[];
13
+ };
14
+
15
+ let cached: { value: IdentityRuntimeConfig; expiresAt: number } | null = null;
16
+ let loading: Promise<IdentityRuntimeConfig> | null = null;
17
+
18
+ const normalizeGroups = (value: unknown): string[] => {
19
+ if (!Array.isArray(value)) return ["admins"];
20
+ const groups = [
21
+ ...new Set(
22
+ value
23
+ .filter((item): item is string => typeof item === "string")
24
+ .map((item) => item.trim())
25
+ .filter(Boolean),
26
+ ),
27
+ ];
28
+ return groups.length > 0 ? groups : ["admins"];
29
+ };
30
+
31
+ const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL => {
32
+ if (!transportOrigin) return new URL(path, issuer);
33
+ const origin = new URL(transportOrigin);
34
+ if (origin.protocol !== "http:" && origin.protocol !== "https:") {
35
+ throw new Error("CLOUD_IDENTITY_JWKS_ORIGIN must use http or https");
36
+ }
37
+ return new URL(path, origin);
38
+ };
39
+
40
+ export const resolveSessionJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
41
+ resolveCoreJwksUrl(issuer, CLOUD_SESSION_JWKS_PATH, transportOrigin);
42
+
43
+ export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
44
+ resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
45
+
46
+ export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_OAUTH_JWKS_ORIGIN?.trim()): URL => {
47
+ if (!transportOrigin) return new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
48
+
49
+ const url = new URL(CLOUD_OAUTH_JWKS_PATH, transportOrigin);
50
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
51
+ throw new Error("CLOUD_OAUTH_JWKS_ORIGIN must use http or https");
52
+ }
53
+ return url;
54
+ };
55
+
56
+ const load = async (): Promise<IdentityRuntimeConfig> => {
57
+ const [appUrl, rawGroupsAdmin] = await Promise.all([getSetting<string>("app.url"), getSetting<unknown>("freeipa.groups.admin")]);
58
+ const issuer = publicCloudOrigin(appUrl);
59
+ if (process.env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
60
+ throw new Error("Cloud identity issuer must use HTTPS in production");
61
+ }
62
+ return {
63
+ issuer,
64
+ sessionJwksUrl: resolveSessionJwksUrl(issuer),
65
+ invocationJwksUrl: resolveInvocationJwksUrl(issuer),
66
+ oauthJwksUrl: resolveOAuthJwksUrl(issuer),
67
+ groupsAdmin: normalizeGroups(rawGroupsAdmin),
68
+ };
69
+ };
70
+
71
+ export const getIdentityRuntimeConfig = async (): Promise<IdentityRuntimeConfig> => {
72
+ const now = Date.now();
73
+ if (cached && cached.expiresAt > now) return cached.value;
74
+ if (loading) return loading;
75
+ loading = load()
76
+ .then((value) => {
77
+ cached = { value, expiresAt: Date.now() + RUNTIME_CONFIG_TTL_MS };
78
+ return value;
79
+ })
80
+ .finally(() => {
81
+ loading = null;
82
+ });
83
+ return loading;
84
+ };
85
+
86
+ export const invalidateIdentityRuntimeConfig = (): void => {
87
+ cached = null;
88
+ };