@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,45 @@
1
+ import { sql } from "bun";
2
+ import { backfillWeatherLocationShortIds } from "./short-id";
3
+
4
+ export const migrate = async (): Promise<void> => {
5
+ await sql`
6
+ CREATE TABLE IF NOT EXISTS weather_locations (
7
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
8
+ short_id TEXT,
9
+ user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
10
+ name TEXT NOT NULL,
11
+ state TEXT,
12
+ lat DOUBLE PRECISION NOT NULL,
13
+ lon DOUBLE PRECISION NOT NULL,
14
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
15
+ )
16
+ `.simple();
17
+ console.log(" ✓ weather_locations table");
18
+
19
+ await sql`ALTER TABLE weather_locations ADD COLUMN IF NOT EXISTS short_id TEXT`.simple();
20
+ await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_weather_locations_short_id ON weather_locations(short_id)`.simple();
21
+
22
+ await sql.begin(async (tx) => {
23
+ await tx`SELECT pg_advisory_xact_lock(hashtext('cloud.weather.location-short-id-backfill'))`;
24
+ const filled = await backfillWeatherLocationShortIds(tx);
25
+ if (filled > 0) console.log(` ✓ weather location short_id backfill: ${filled}`);
26
+ await tx`ALTER TABLE weather_locations ALTER COLUMN short_id SET NOT NULL`;
27
+ await tx`ALTER TABLE weather_locations DROP CONSTRAINT IF EXISTS weather_locations_short_id_format`;
28
+ await tx`
29
+ ALTER TABLE weather_locations
30
+ ADD CONSTRAINT weather_locations_short_id_format CHECK (short_id ~ '^[0-9A-Za-z]{6}$')
31
+ `;
32
+ });
33
+
34
+ await sql`
35
+ CREATE INDEX IF NOT EXISTS idx_weather_locations_user
36
+ ON weather_locations(user_id)
37
+ `.simple();
38
+ console.log(" ✓ weather_locations index");
39
+
40
+ await sql`
41
+ CREATE INDEX IF NOT EXISTS idx_weather_locations_user_created
42
+ ON weather_locations(user_id, created_at, id)
43
+ `.simple();
44
+ console.log(" ✓ weather_locations pagination index");
45
+ };
@@ -0,0 +1,66 @@
1
+ export const WEATHER_SETTINGS = {
2
+ "weather.default_lat": {
3
+ kind: "string",
4
+ label: "Default Latitude",
5
+ default: "",
6
+ description: "Default latitude shown in weather widgets",
7
+ placeholder: "e.g. 48.401082 (Ulm)",
8
+ presentation: {
9
+ translations: {
10
+ de: {
11
+ label: "Standardbreitengrad",
12
+ description: "In Wetter-Widgets angezeigter Standardbreitengrad",
13
+ placeholder: "z. B. 48.401082 (Ulm)",
14
+ },
15
+ },
16
+ },
17
+ },
18
+ "weather.default_lon": {
19
+ kind: "string",
20
+ label: "Default Longitude",
21
+ default: "",
22
+ description: "Default longitude shown in weather widgets",
23
+ placeholder: "e.g. 9.987608 (Ulm)",
24
+ presentation: {
25
+ translations: {
26
+ de: {
27
+ label: "Standardlängengrad",
28
+ description: "In Wetter-Widgets angezeigter Standardlängengrad",
29
+ placeholder: "z. B. 9.987608 (Ulm)",
30
+ },
31
+ },
32
+ },
33
+ },
34
+ "weather.cache_minutes": {
35
+ kind: "number",
36
+ label: "Cache TTL (minutes)",
37
+ default: 30,
38
+ min: 1,
39
+ max: 1440,
40
+ description: "How long weather data is cached before fetching fresh data (in minutes)",
41
+ presentation: {
42
+ translations: {
43
+ de: {
44
+ label: "Cache-TTL (Minuten)",
45
+ description: "Dauer in Minuten, für die Wetterdaten vor dem nächsten Abruf zwischengespeichert werden",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ "weather.geo_url": {
51
+ kind: "url",
52
+ label: "Geo API URL",
53
+ default: "",
54
+ description: "Geocoding API URL for the location search feature",
55
+ placeholder: "e.g. https://geocoding.example.com/search",
56
+ presentation: {
57
+ translations: {
58
+ de: {
59
+ label: "Geo-API-URL",
60
+ description: "Geocoding-API-URL für die Ortssuche",
61
+ placeholder: "z. B. https://geocoding.example.com/search",
62
+ },
63
+ },
64
+ },
65
+ },
66
+ } as const;
@@ -0,0 +1,57 @@
1
+ import { crypto } from "@k2b/stdlib";
2
+ import { sql } from "bun";
3
+ import { isUniqueViolation } from "../postgres";
4
+
5
+ export const WEATHER_LOCATION_SHORT_ID_PATTERN = /^[0-9A-Za-z]{6}$/;
6
+
7
+ const SHORT_ID_LENGTH = 6;
8
+ const MAX_ATTEMPTS = 10;
9
+ const BACKFILL_BATCH_SIZE = 500;
10
+ const UNIQUE_CONSTRAINT = "idx_weather_locations_short_id";
11
+
12
+ type SqlExecutor = typeof sql;
13
+
14
+ export const newWeatherLocationShortId = (): string => crypto.common.readableId(SHORT_ID_LENGTH);
15
+
16
+ export const withWeatherLocationShortId = async <T>(write: (shortId: string) => Promise<T | null>): Promise<T> => {
17
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
18
+ try {
19
+ const result = await write(newWeatherLocationShortId());
20
+ if (result !== null) return result;
21
+ } catch (error) {
22
+ if (!isUniqueViolation(error, UNIQUE_CONSTRAINT)) throw error;
23
+ }
24
+ }
25
+ throw new Error("Failed to allocate a Weather location short ID");
26
+ };
27
+
28
+ export const backfillWeatherLocationShortIds = async (db: SqlExecutor = sql): Promise<number> => {
29
+ let filled = 0;
30
+ for (;;) {
31
+ const rows = await db<{ id: string }[]>`
32
+ SELECT id
33
+ FROM weather_locations
34
+ WHERE short_id IS NULL
35
+ ORDER BY id
36
+ LIMIT ${BACKFILL_BATCH_SIZE}
37
+ FOR UPDATE
38
+ `;
39
+ if (rows.length === 0) return filled;
40
+
41
+ for (const row of rows) {
42
+ await withWeatherLocationShortId(async (shortId) => {
43
+ const updated = await db<{ id: string }[]>`
44
+ UPDATE weather_locations
45
+ SET short_id = ${shortId}
46
+ WHERE id = ${row.id}::uuid
47
+ AND short_id IS NULL
48
+ AND NOT EXISTS (SELECT 1 FROM weather_locations WHERE short_id = ${shortId})
49
+ RETURNING id
50
+ `;
51
+ if (updated.length === 0) return null;
52
+ filled++;
53
+ return updated[0]!;
54
+ });
55
+ }
56
+ }
57
+ };
@@ -0,0 +1,61 @@
1
+ /** Weather icon codes from Brightsky API */
2
+ export type WeatherIcon =
3
+ | "clear-day"
4
+ | "clear-night"
5
+ | "partly-cloudy-day"
6
+ | "partly-cloudy-night"
7
+ | "cloudy"
8
+ | "fog"
9
+ | "wind"
10
+ | "rain"
11
+ | "sleet"
12
+ | "snow"
13
+ | "hail"
14
+ | "thunderstorm";
15
+
16
+ /** Current weather data */
17
+ export type CurrentWeather = {
18
+ temperature: number; // deg C
19
+ icon: WeatherIcon;
20
+ cloudCover: number; // 0-100%
21
+ windSpeed: number; // km/h
22
+ windGust: number | null; // km/h
23
+ windDirection: number | null; // degrees
24
+ humidity: number | null; // 0-100%
25
+ precipitation: number; // mm in last hour
26
+ pressure: number | null; // hPa
27
+ visibility: number | null; // meters
28
+ dewPoint: number | null; // deg C
29
+ sunshine: number | null; // minutes in last hour
30
+ stationName: string;
31
+ timestamp: string; // ISO timestamp
32
+ };
33
+
34
+ /** Hourly forecast entry */
35
+ export type HourlyForecast = {
36
+ timestamp: string;
37
+ temperature: number;
38
+ icon: WeatherIcon;
39
+ precipitation: number;
40
+ precipitationProbability: number | null; // 0-100%
41
+ windSpeed: number;
42
+ cloudCover: number;
43
+ };
44
+
45
+ /** Daily forecast summary */
46
+ export type DailyForecast = {
47
+ date: string; // YYYY-MM-DD
48
+ tempMin: number;
49
+ tempMax: number;
50
+ icon: WeatherIcon;
51
+ precipitation: number; // total mm
52
+ precipitationProbability: number | null; // max probability for the day
53
+ sunshine: number; // total minutes
54
+ };
55
+
56
+ /** Full weather data with forecasts */
57
+ export type WeatherData = {
58
+ current: CurrentWeather;
59
+ hourly: HourlyForecast[];
60
+ daily: DailyForecast[];
61
+ };
@@ -0,0 +1,50 @@
1
+ import type { WeatherIcon } from "./types";
2
+
3
+ /** Map Brightsky icon to Tabler icon name. */
4
+ const getTablerIcon = (icon: WeatherIcon): string => {
5
+ const iconMap: Record<WeatherIcon, string> = {
6
+ "clear-day": "sun",
7
+ "clear-night": "moon",
8
+ "partly-cloudy-day": "sun-moon",
9
+ "partly-cloudy-night": "sun-moon",
10
+ cloudy: "cloud",
11
+ fog: "mist",
12
+ wind: "wind",
13
+ rain: "cloud-rain",
14
+ sleet: "cloud-snow",
15
+ snow: "snowflake",
16
+ hail: "cloud-snow",
17
+ thunderstorm: "cloud-storm",
18
+ };
19
+ return iconMap[icon] ?? "cloud";
20
+ };
21
+
22
+ /** Get Tailwind color class for temperature. */
23
+ const getTempColorClass = (temp: number): string => {
24
+ if (temp <= 0) return "text-blue-400";
25
+ if (temp <= 10) return "text-cyan-500";
26
+ if (temp <= 20) return "text-emerald-500";
27
+ if (temp <= 25) return "text-amber-500";
28
+ return "text-red-500";
29
+ };
30
+
31
+ /** Get temperature color class for an average of min/max. */
32
+ const getAvgTempColorClass = (tempMin: number, tempMax: number): string => {
33
+ return getTempColorClass((tempMin + tempMax) / 2);
34
+ };
35
+
36
+ /** Format temperature with degree symbol. */
37
+ const formatTemp = (temp: number): string => `${temp}°`;
38
+
39
+ /** Format temperature range (e.g., "12° / 5°"). */
40
+ const formatTempRange = (tempMax: number, tempMin: number): string => `${tempMax}° / ${tempMin}°`;
41
+
42
+ export const weatherUiService = {
43
+ getTablerIcon,
44
+ getTempColorClass,
45
+ getAvgTempColorClass,
46
+ formatTemp,
47
+ formatTempRange,
48
+ };
49
+
50
+ export type WeatherUiService = typeof weatherUiService;
@@ -0,0 +1,385 @@
1
+ import {
2
+ type AuthenticationResponseJSON,
3
+ type AuthenticatorTransportFuture,
4
+ generateAuthenticationOptions,
5
+ generateRegistrationOptions,
6
+ type PublicKeyCredentialCreationOptionsJSON,
7
+ type PublicKeyCredentialRequestOptionsJSON,
8
+ type RegistrationResponseJSON,
9
+ verifyAuthenticationResponse,
10
+ verifyRegistrationResponse,
11
+ type WebAuthnCredential,
12
+ } from "@simplewebauthn/server";
13
+ import { err, fail, ok, type Result } from "@k2b/stdlib";
14
+ import { redis, sql } from "bun";
15
+ import type { User, WebAuthnPasskey } from "../contracts/shared";
16
+ import { accounts } from "./accounts";
17
+ import { audit } from "./audit";
18
+ import { logger } from "./logging";
19
+ import { isUniqueViolation, toPgTextArray } from "./postgres";
20
+ import { coreSettings } from "./settings/api";
21
+
22
+ const CHALLENGE_TTL_SECONDS = 300;
23
+ const REGISTRATION_CHALLENGE_PREFIX = "webauthn:registration:";
24
+ const AUTHENTICATION_CHALLENGE_PREFIX = "webauthn:authentication:";
25
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;
26
+ const log = logger("auth:webauthn");
27
+
28
+ type DbPasskeyRow = {
29
+ id: string;
30
+ user_id: string;
31
+ name: string;
32
+ credential_id: string;
33
+ public_key: Uint8Array;
34
+ counter: string | number | bigint;
35
+ transports: string[];
36
+ device_type: string | null;
37
+ backed_up: boolean;
38
+ created_at: Date;
39
+ last_used_at: Date | null;
40
+ };
41
+
42
+ type DbPasskeyWithUserRow = DbPasskeyRow & {
43
+ user_account_expires: Date | null;
44
+ };
45
+
46
+ type StoredWebAuthnPasskey = WebAuthnPasskey & {
47
+ credentialId: string;
48
+ };
49
+
50
+ export type WebAuthnRp = {
51
+ rpName: string;
52
+ rpID: string;
53
+ origin: string;
54
+ };
55
+
56
+ export const resolveWebAuthnRp = (config: { appUrl: string; appName: string }): WebAuthnRp => {
57
+ const rawAppUrl = config.appUrl.trim();
58
+ const hasProtocol = /^https?:\/\//i.test(rawAppUrl);
59
+ const parseUrl = (value: string) => new URL(value);
60
+ const hostname = hasProtocol ? parseUrl(rawAppUrl).hostname : parseUrl(`https://${rawAppUrl}`).hostname;
61
+ const isBareLocalhost =
62
+ !hasProtocol && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]");
63
+ const withProtocol = hasProtocol ? rawAppUrl : `${isBareLocalhost ? "http" : "https"}://${rawAppUrl}`;
64
+ const url = new URL(withProtocol);
65
+ const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]";
66
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalhost)) {
67
+ throw new Error("WebAuthn requires an HTTPS app.url, except for localhost development.");
68
+ }
69
+ return {
70
+ rpName: config.appName.trim() || "Cloud",
71
+ rpID: url.hostname,
72
+ origin: url.origin,
73
+ };
74
+ };
75
+
76
+ const loadRp = async (): Promise<WebAuthnRp> =>
77
+ resolveWebAuthnRp({
78
+ appUrl: await coreSettings.get<string>("app.url"),
79
+ appName: await coreSettings.get<string>("app.name"),
80
+ });
81
+
82
+ const loadRpResult = async (): Promise<Result<WebAuthnRp>> => {
83
+ try {
84
+ return ok(await loadRp());
85
+ } catch (error) {
86
+ return fail(err.badInput(error instanceof Error ? error.message : "Invalid WebAuthn configuration."));
87
+ }
88
+ };
89
+
90
+ const mapStoredPasskey = (row: DbPasskeyRow): StoredWebAuthnPasskey => ({
91
+ id: row.id,
92
+ userId: row.user_id,
93
+ name: row.name,
94
+ credentialId: row.credential_id,
95
+ transports: row.transports ?? [],
96
+ deviceType: row.device_type,
97
+ backedUp: row.backed_up,
98
+ createdAt: row.created_at.toISOString(),
99
+ lastUsedAt: row.last_used_at?.toISOString() ?? null,
100
+ });
101
+
102
+ const mapPasskey = (row: DbPasskeyRow): WebAuthnPasskey => {
103
+ const { credentialId: _, ...passkey } = mapStoredPasskey(row);
104
+ return passkey;
105
+ };
106
+
107
+ const actorForUser = (user: Pick<User, "id" | "uid" | "provider" | "roles">) => ({
108
+ userId: user.id,
109
+ uid: user.uid,
110
+ provider: user.provider,
111
+ roles: user.roles,
112
+ });
113
+
114
+ const challengeKey = (prefix: string, id: string) => `${prefix}${id}`;
115
+
116
+ const storeRegistrationChallenge = async (userId: string, challenge: string): Promise<void> => {
117
+ await redis.set(challengeKey(REGISTRATION_CHALLENGE_PREFIX, userId), challenge, "EX", CHALLENGE_TTL_SECONDS);
118
+ };
119
+
120
+ const consumeRegistrationChallenge = async (userId: string): Promise<string | null> => {
121
+ return redis.getdel(challengeKey(REGISTRATION_CHALLENGE_PREFIX, userId));
122
+ };
123
+
124
+ const storeAuthenticationChallenge = async (challenge: string): Promise<void> => {
125
+ await redis.set(challengeKey(AUTHENTICATION_CHALLENGE_PREFIX, challenge), "1", "EX", CHALLENGE_TTL_SECONDS);
126
+ };
127
+
128
+ const consumeAuthenticationChallenge = async (challenge: string): Promise<boolean> => {
129
+ const value = await redis.getdel(challengeKey(AUTHENTICATION_CHALLENGE_PREFIX, challenge));
130
+ return value === "1";
131
+ };
132
+
133
+ const userIdToWebAuthnBytes = (userId: string): WebAuthnCredential["publicKey"] =>
134
+ new TextEncoder().encode(userId) as WebAuthnCredential["publicKey"];
135
+
136
+ const toCredential = (row: DbPasskeyRow): WebAuthnCredential => ({
137
+ id: row.credential_id,
138
+ publicKey: new Uint8Array(row.public_key) as WebAuthnCredential["publicKey"],
139
+ counter: Number(row.counter),
140
+ transports: (row.transports ?? []) as AuthenticatorTransportFuture[],
141
+ });
142
+
143
+ const isExpired = (date: Date | null): boolean => Boolean(date && date.getTime() <= Date.now());
144
+
145
+ const listStoredForUser = async (params: { userId: string }): Promise<StoredWebAuthnPasskey[]> => {
146
+ const rows = await sql<DbPasskeyRow[]>`
147
+ SELECT id, user_id, name, credential_id, public_key, counter, transports, device_type,
148
+ backed_up, created_at, last_used_at
149
+ FROM auth.webauthn_credentials
150
+ WHERE user_id = ${params.userId}::uuid
151
+ ORDER BY created_at DESC
152
+ `;
153
+ return rows.map(mapStoredPasskey);
154
+ };
155
+
156
+ export const listForUser = async (params: { userId: string }): Promise<WebAuthnPasskey[]> => {
157
+ const rows = await listStoredForUser(params);
158
+ return rows.map(({ credentialId: _, ...passkey }) => passkey);
159
+ };
160
+
161
+ export const beginRegistration = async (params: { user: User }): Promise<Result<PublicKeyCredentialCreationOptionsJSON>> => {
162
+ const rpResult = await loadRpResult();
163
+ if (!rpResult.ok) return rpResult;
164
+ const rp = rpResult.data;
165
+ const existing = await listStoredForUser({ userId: params.user.id });
166
+ const options = await generateRegistrationOptions({
167
+ rpName: rp.rpName,
168
+ rpID: rp.rpID,
169
+ userID: userIdToWebAuthnBytes(params.user.id),
170
+ userName: params.user.mail ?? params.user.uid,
171
+ userDisplayName: params.user.displayName || params.user.uid,
172
+ attestationType: "none",
173
+ authenticatorSelection: {
174
+ residentKey: "required",
175
+ userVerification: "required",
176
+ },
177
+ excludeCredentials: existing.map((credential) => ({
178
+ id: credential.credentialId,
179
+ transports: credential.transports as AuthenticatorTransportFuture[],
180
+ })),
181
+ });
182
+
183
+ await storeRegistrationChallenge(params.user.id, options.challenge);
184
+ return ok(options);
185
+ };
186
+
187
+ export const finishRegistration = async (params: {
188
+ user: User;
189
+ name: string;
190
+ response: RegistrationResponseJSON;
191
+ }): Promise<Result<WebAuthnPasskey>> => {
192
+ const name = params.name.trim();
193
+ if (!name) return fail(err.badInput("Passkey name is required."));
194
+ if (name.length > 120) return fail(err.badInput("Passkey name must be 120 characters or fewer."));
195
+
196
+ const expectedChallenge = await consumeRegistrationChallenge(params.user.id);
197
+ if (!expectedChallenge) return fail(err.badInput("Passkey registration expired. Please try again."));
198
+
199
+ const rpResult = await loadRpResult();
200
+ if (!rpResult.ok) return rpResult;
201
+ const rp = rpResult.data;
202
+ const verification = await verifyRegistrationResponse({
203
+ response: params.response,
204
+ expectedChallenge,
205
+ expectedOrigin: rp.origin,
206
+ expectedRPID: rp.rpID,
207
+ requireUserVerification: true,
208
+ }).catch((error) => {
209
+ log.warn("Passkey registration verification failed", {
210
+ userId: params.user.id,
211
+ rpID: rp.rpID,
212
+ origin: rp.origin,
213
+ error: error instanceof Error ? error.message : String(error),
214
+ });
215
+ return null;
216
+ });
217
+ if (!verification) return fail(err.badInput("Passkey registration could not be verified."));
218
+ if (!verification.verified) return fail(err.badInput("Passkey registration could not be verified."));
219
+
220
+ const { credential, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
221
+ const transports = params.response.response.transports ?? [];
222
+
223
+ try {
224
+ return sql.begin(async (tx) => {
225
+ const [row] = await tx<DbPasskeyRow[]>`
226
+ INSERT INTO auth.webauthn_credentials (
227
+ user_id,
228
+ name,
229
+ credential_id,
230
+ public_key,
231
+ counter,
232
+ transports,
233
+ device_type,
234
+ backed_up
235
+ )
236
+ VALUES (
237
+ ${params.user.id}::uuid,
238
+ ${name},
239
+ ${credential.id},
240
+ ${Buffer.from(credential.publicKey)},
241
+ ${credential.counter},
242
+ ${toPgTextArray(transports)}::text[],
243
+ ${credentialDeviceType},
244
+ ${credentialBackedUp}
245
+ )
246
+ RETURNING id, user_id, name, credential_id, public_key, counter, transports, device_type,
247
+ backed_up, created_at, last_used_at
248
+ `;
249
+ const result = row ? ok(mapPasskey(row)) : fail(err.badInput("Passkey could not be saved."));
250
+ return audit.recordResult({
251
+ action: "webauthn_credential.create",
252
+ actor: actorForUser(params.user),
253
+ target: { type: "webauthn_credential", id: row?.id ?? null, label: name },
254
+ metadata: {
255
+ deviceType: credentialDeviceType,
256
+ backedUp: credentialBackedUp,
257
+ transports,
258
+ },
259
+ result,
260
+ db: tx,
261
+ });
262
+ });
263
+ } catch (error) {
264
+ if (isUniqueViolation(error)) return fail(err.conflict("Passkey"));
265
+ throw error;
266
+ }
267
+ };
268
+
269
+ export const beginAuthentication = async (): Promise<Result<PublicKeyCredentialRequestOptionsJSON>> => {
270
+ const rpResult = await loadRpResult();
271
+ if (!rpResult.ok) return rpResult;
272
+ const rp = rpResult.data;
273
+ const options = await generateAuthenticationOptions({
274
+ rpID: rp.rpID,
275
+ userVerification: "required",
276
+ });
277
+ await storeAuthenticationChallenge(options.challenge);
278
+ return ok(options);
279
+ };
280
+
281
+ const findCredentialForAuthentication = async (credentialId: string): Promise<DbPasskeyWithUserRow | null> => {
282
+ const [row] = await sql<DbPasskeyWithUserRow[]>`
283
+ SELECT c.id, c.user_id, c.name, c.credential_id, c.public_key, c.counter, c.transports,
284
+ c.device_type, c.backed_up, c.created_at, c.last_used_at, u.account_expires AS user_account_expires
285
+ FROM auth.webauthn_credentials c
286
+ JOIN auth.users u ON u.id = c.user_id
287
+ WHERE c.credential_id = ${credentialId}
288
+ LIMIT 1
289
+ `;
290
+ return row ?? null;
291
+ };
292
+
293
+ export const finishAuthentication = async (params: {
294
+ response: AuthenticationResponseJSON;
295
+ }): Promise<Result<{ user: User; passkey: WebAuthnPasskey }>> => {
296
+ const row = await findCredentialForAuthentication(params.response.id);
297
+ if (!row) return fail(err.unauthenticated("Passkey could not be verified."));
298
+ if (isExpired(row.user_account_expires)) return fail(err.forbidden("Your account has expired. Contact an administrator."));
299
+
300
+ const rpResult = await loadRpResult();
301
+ if (!rpResult.ok) return rpResult;
302
+ const rp = rpResult.data;
303
+ const verification = await verifyAuthenticationResponse({
304
+ response: params.response,
305
+ expectedChallenge: consumeAuthenticationChallenge,
306
+ expectedOrigin: rp.origin,
307
+ expectedRPID: rp.rpID,
308
+ credential: toCredential(row),
309
+ requireUserVerification: true,
310
+ }).catch((error) => {
311
+ log.warn("Passkey authentication verification failed", {
312
+ credentialId: row.id,
313
+ userId: row.user_id,
314
+ rpID: rp.rpID,
315
+ origin: rp.origin,
316
+ error: error instanceof Error ? error.message : String(error),
317
+ });
318
+ return null;
319
+ });
320
+ if (!verification) return fail(err.unauthenticated("Passkey could not be verified."));
321
+ if (!verification.verified) return fail(err.unauthenticated("Passkey could not be verified."));
322
+
323
+ const user = await accounts.users.get({ id: row.user_id });
324
+ if (!user) return fail(err.unauthenticated("Passkey user not found."));
325
+ if (user.accountExpires && new Date(user.accountExpires).getTime() <= Date.now()) {
326
+ return fail(err.forbidden("Your account has expired. Contact an administrator."));
327
+ }
328
+
329
+ return sql.begin(async (tx) => {
330
+ const [updated] = await tx<DbPasskeyRow[]>`
331
+ UPDATE auth.webauthn_credentials
332
+ SET counter = ${verification.authenticationInfo.newCounter},
333
+ device_type = ${verification.authenticationInfo.credentialDeviceType},
334
+ backed_up = ${verification.authenticationInfo.credentialBackedUp},
335
+ last_used_at = now()
336
+ WHERE id = ${row.id}::uuid
337
+ RETURNING id, user_id, name, credential_id, public_key, counter, transports, device_type,
338
+ backed_up, created_at, last_used_at
339
+ `;
340
+ const passkey = mapPasskey(updated ?? row);
341
+ const result = ok({ user, passkey });
342
+ return audit.recordResult({
343
+ action: "webauthn_credential.authenticate",
344
+ actor: actorForUser(user),
345
+ target: { type: "webauthn_credential", id: row.id, label: row.name },
346
+ metadata: {
347
+ deviceType: verification.authenticationInfo.credentialDeviceType,
348
+ backedUp: verification.authenticationInfo.credentialBackedUp,
349
+ },
350
+ result,
351
+ db: tx,
352
+ });
353
+ });
354
+ };
355
+
356
+ export const deleteForUser = async (params: { user: User; id: string }): Promise<Result<void>> => {
357
+ if (!UUID_PATTERN.test(params.id)) return fail(err.notFound("Passkey"));
358
+
359
+ return sql.begin(async (tx) => {
360
+ const [row] = await tx<Pick<DbPasskeyRow, "id" | "name">[]>`
361
+ DELETE FROM auth.webauthn_credentials
362
+ WHERE id = ${params.id}::uuid
363
+ AND user_id = ${params.user.id}::uuid
364
+ RETURNING id, name
365
+ `;
366
+ const result = row ? ok() : fail(err.notFound("Passkey"));
367
+ return audit.recordResult({
368
+ action: "webauthn_credential.delete",
369
+ actor: actorForUser(params.user),
370
+ target: { type: "webauthn_credential", id: params.id, label: row?.name ?? null },
371
+ result,
372
+ db: tx,
373
+ });
374
+ });
375
+ };
376
+
377
+ export const webauthn = {
378
+ resolveWebAuthnRp,
379
+ listForUser,
380
+ beginRegistration,
381
+ finishRegistration,
382
+ beginAuthentication,
383
+ finishAuthentication,
384
+ deleteForUser,
385
+ };