@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,806 @@
1
+ import { password } from "@k2b/stdlib";
2
+ import { sql } from "bun";
3
+ import type { MutationResult, UserProfile } from "../../contracts/shared";
4
+ import { freeipa } from "../../server/services";
5
+ import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
6
+ import { resolveProviderProfile } from "../accounts/base-user";
7
+ import { logger } from "../logging";
8
+ import { session } from "../session";
9
+ import * as settings from "../settings";
10
+ import { ensureFreeIpaMutationAvailable, getIpaUrl } from "./guard";
11
+ import { mirrorIpaPosix } from "./posix";
12
+
13
+ type CreateUser = {
14
+ email: string;
15
+ givenname: string;
16
+ sn: string;
17
+ displayName?: string;
18
+ autoSendNotification?: boolean;
19
+ requestId?: string;
20
+ };
21
+
22
+ type IpaPatchData = {
23
+ givenname?: string;
24
+ sn?: string;
25
+ displayName?: string;
26
+ mail?: string;
27
+ ipa?: {
28
+ phone?: string;
29
+ address?: {
30
+ street?: string;
31
+ postalCode?: string;
32
+ city?: string;
33
+ state?: string;
34
+ };
35
+ sshPublicKeys?: string[];
36
+ };
37
+ };
38
+
39
+ type DbRow = Record<string, unknown>;
40
+
41
+ const log = logger("auth:ipa");
42
+
43
+ /**
44
+ * Full-row replacement upsert. Every column is written on UPDATE; omitted
45
+ * fields become NULL (or empty arrays). Use this only for the initial IPA-data
46
+ * insert after user creation or after a full user_show payload. For partial
47
+ * profile patches call `patchUserIpaData` — otherwise SSH keys, uid_number,
48
+ * password expiry, etc. get wiped.
49
+ */
50
+ const upsertUserIpaData = async (params: {
51
+ userId: string;
52
+ uidNumber?: number | null;
53
+ primaryGidNumber?: number | null;
54
+ homeDirectory?: string | null;
55
+ loginShell?: string | null;
56
+ phone?: string | null;
57
+ employeeType?: string | null;
58
+ mobile?: string | null;
59
+ street?: string | null;
60
+ postalCode?: string | null;
61
+ city?: string | null;
62
+ state?: string | null;
63
+ passwordExpires?: Date | null;
64
+ lastLoginIpa?: Date | null;
65
+ syncedAt?: Date | null;
66
+ sshPublicKeys?: string[];
67
+ sshFingerprints?: string[];
68
+ }) => {
69
+ await sql.begin(async (tx) => {
70
+ await mirrorIpaPosix(tx, {
71
+ userId: params.userId,
72
+ uidNumber: params.uidNumber ?? null,
73
+ primaryGidNumber: params.primaryGidNumber ?? null,
74
+ homeDirectory: params.homeDirectory ?? null,
75
+ loginShell: params.loginShell ?? null,
76
+ });
77
+ await tx`
78
+ INSERT INTO auth.user_ipa_data (
79
+ user_id, uid_number, phone, employee_type, mobile, addr_street, addr_postal_code,
80
+ addr_city, addr_state, ipa_password_expires, last_login_ipa, synced_at, ssh_public_keys, ssh_fingerprints
81
+ )
82
+ VALUES (
83
+ ${params.userId},
84
+ ${params.uidNumber ?? null},
85
+ ${params.phone ?? null},
86
+ ${params.employeeType ?? null},
87
+ ${params.mobile ?? null},
88
+ ${params.street ?? null},
89
+ ${params.postalCode ?? null},
90
+ ${params.city ?? null},
91
+ ${params.state ?? null},
92
+ ${params.passwordExpires ?? null},
93
+ ${params.lastLoginIpa ?? null},
94
+ ${params.syncedAt ?? null},
95
+ ${freeipa.util.toPgTextArray(params.sshPublicKeys ?? [])}::text[],
96
+ ${freeipa.util.toPgTextArray(params.sshFingerprints ?? [])}::text[]
97
+ )
98
+ ON CONFLICT (user_id) DO UPDATE SET
99
+ uid_number = EXCLUDED.uid_number,
100
+ phone = EXCLUDED.phone,
101
+ employee_type = EXCLUDED.employee_type,
102
+ mobile = EXCLUDED.mobile,
103
+ addr_street = EXCLUDED.addr_street,
104
+ addr_postal_code = EXCLUDED.addr_postal_code,
105
+ addr_city = EXCLUDED.addr_city,
106
+ addr_state = EXCLUDED.addr_state,
107
+ ipa_password_expires = EXCLUDED.ipa_password_expires,
108
+ last_login_ipa = EXCLUDED.last_login_ipa,
109
+ synced_at = EXCLUDED.synced_at,
110
+ ssh_public_keys = EXCLUDED.ssh_public_keys,
111
+ ssh_fingerprints = EXCLUDED.ssh_fingerprints
112
+ `;
113
+ });
114
+ };
115
+
116
+ /**
117
+ * Partial patch for an existing IPA-data row. Only columns whose value is not
118
+ * `undefined` are updated — `null` is still treated as an explicit clear.
119
+ * If no row exists yet the UPDATE is a no-op; callers that need to guarantee a
120
+ * row must use `upsertUserIpaData` first.
121
+ */
122
+ const patchUserIpaData = async (params: {
123
+ userId: string;
124
+ phone?: string | null;
125
+ street?: string | null;
126
+ postalCode?: string | null;
127
+ city?: string | null;
128
+ state?: string | null;
129
+ sshPublicKeys?: string[];
130
+ sshFingerprints?: string[];
131
+ syncedAt?: Date | null;
132
+ }) => {
133
+ const has = (v: unknown) => v !== undefined;
134
+ await sql`
135
+ UPDATE auth.user_ipa_data SET
136
+ phone = CASE WHEN ${has(params.phone)} THEN ${params.phone ?? null} ELSE phone END,
137
+ addr_street = CASE WHEN ${has(params.street)} THEN ${params.street ?? null} ELSE addr_street END,
138
+ addr_postal_code = CASE WHEN ${has(params.postalCode)} THEN ${params.postalCode ?? null} ELSE addr_postal_code END,
139
+ addr_city = CASE WHEN ${has(params.city)} THEN ${params.city ?? null} ELSE addr_city END,
140
+ addr_state = CASE WHEN ${has(params.state)} THEN ${params.state ?? null} ELSE addr_state END,
141
+ ssh_public_keys = CASE WHEN ${has(params.sshPublicKeys)}
142
+ THEN ${freeipa.util.toPgTextArray(params.sshPublicKeys ?? [])}::text[]
143
+ ELSE ssh_public_keys END,
144
+ ssh_fingerprints = CASE WHEN ${has(params.sshFingerprints)}
145
+ THEN ${freeipa.util.toPgTextArray(params.sshFingerprints ?? [])}::text[]
146
+ ELSE ssh_fingerprints END,
147
+ synced_at = CASE WHEN ${has(params.syncedAt)} THEN ${params.syncedAt ?? null} ELSE synced_at END
148
+ WHERE user_id = ${params.userId}
149
+ `;
150
+ };
151
+
152
+ // ==========================
153
+ // MUTATION: addIpa (create FreeIPA user)
154
+ // ==========================
155
+
156
+ /** Generate random lowercase abbreviation of specified length */
157
+ export const generateAbbreviation = (length: number): string => {
158
+ const chars = "abcdefghijklmnopqrstuvwxyz";
159
+ const limit = Math.floor(256 / chars.length) * chars.length;
160
+ let value = "";
161
+ while (value.length < length) {
162
+ const bytes = new Uint8Array(length - value.length);
163
+ crypto.getRandomValues(bytes);
164
+ for (const byte of bytes) {
165
+ if (byte >= limit) continue;
166
+ value += chars[byte % chars.length]!;
167
+ if (value.length === length) break;
168
+ }
169
+ }
170
+ return value;
171
+ };
172
+
173
+ /** Check if a UID exists in auth.users */
174
+ const uidExists = async (uid: string): Promise<boolean> => {
175
+ const rows: DbRow[] = await sql`
176
+ SELECT 1 FROM auth.users
177
+ WHERE uid = ${uid}
178
+ LIMIT 1
179
+ `;
180
+ return rows.length > 0;
181
+ };
182
+
183
+ /** Generate a unique abbreviation that doesn't exist in the database */
184
+ export const generateUniqueAbbreviation = async (length: number, maxAttempts = 100): Promise<string> => {
185
+ for (let i = 0; i < maxAttempts; i++) {
186
+ const abbr = generateAbbreviation(length);
187
+ if (!(await uidExists(abbr))) {
188
+ return abbr;
189
+ }
190
+ }
191
+ throw new Error(`Failed to generate unique abbreviation after ${maxAttempts} attempts`);
192
+ };
193
+
194
+ /**
195
+ * Generate a secure random password that meets FreeIPA policy requirements.
196
+ * Delegates to @k2b/stdlib `password.random` which uses rejection
197
+ * sampling over `crypto.getRandomValues`. A length of 20 with all 4 classes
198
+ * makes the probability of any one class being entirely absent astronomically
199
+ * small (~4 × (3/4)^20 ≈ 0.4%) — acceptable for a temporary admin-generated
200
+ * password; operators can retry if FreeIPA rejects on policy.
201
+ */
202
+ const generateFreeIpaPassword = (): string => password.random({ length: 20, uppercase: true, numbers: true, symbols: true });
203
+
204
+ /** Calculate account expiration date based on config */
205
+ const calculateAccountExpiration = async (): Promise<Date | null> => {
206
+ const expiresDays = await settings.get<number | null>("user.account.ipa_expires_days");
207
+ const days = expiresDays;
208
+
209
+ if (days && days > 0) {
210
+ const expiry = new Date();
211
+ expiry.setDate(expiry.getDate() + days);
212
+ return expiry;
213
+ }
214
+
215
+ return null;
216
+ };
217
+
218
+ export type AddIpaResult = {
219
+ id: string;
220
+ uid: string;
221
+ accountExpires: string | null;
222
+ /** The temporary password - only for internal use (email sending), not exposed to client */
223
+ _temporaryPassword: string;
224
+ };
225
+
226
+ /**
227
+ * Create a new user in FreeIPA and the local database.
228
+ * UID = existing guest UID (if promoting) or new abbreviation.
229
+ */
230
+ export const addIpa = async (params: {
231
+ ipaSession: string;
232
+ data: CreateUser;
233
+ profile?: UserProfile;
234
+ accountExpires?: Date | null;
235
+ }): Promise<MutationResult<AddIpaResult>> => {
236
+ const unavailable = await ensureFreeIpaMutationAvailable();
237
+ if (unavailable) return unavailable;
238
+ const { ipaSession, data } = params;
239
+ const { email, givenname, sn } = data;
240
+ const targetProfile = params.profile ?? "user";
241
+
242
+ const displayName = data.displayName || `${givenname} ${sn}`;
243
+
244
+ // Check if a local account with this email already exists (provider switch case)
245
+ const existingLocalRows: DbRow[] = await sql`
246
+ SELECT id, uid FROM auth.users WHERE mail = ${email} AND provider = 'local'
247
+ `;
248
+
249
+ // Use existing guest UID or generate a new one
250
+ let uid: string;
251
+ if (existingLocalRows.length > 0) {
252
+ uid = existingLocalRows[0]!.uid as string;
253
+ } else {
254
+ try {
255
+ const abbrLen = await settings.get<number>("user.abbr_length");
256
+ uid = await generateUniqueAbbreviation(abbrLen);
257
+ } catch (e) {
258
+ return {
259
+ ok: false,
260
+ error: e instanceof Error ? e.message : "Failed to generate UID",
261
+ status: 500,
262
+ };
263
+ }
264
+ }
265
+
266
+ if ((await uidExists(uid)) && existingLocalRows.length === 0) {
267
+ return { ok: false, error: `UID '${uid}' already exists`, status: 400 };
268
+ }
269
+
270
+ const temporaryPassword = generateFreeIpaPassword();
271
+ const accountExpiry = params.accountExpires === undefined ? await calculateAccountExpiration() : params.accountExpires;
272
+ const now = new Date();
273
+
274
+ const ipaOpts: Record<string, unknown> = {
275
+ givenname,
276
+ sn,
277
+ cn: displayName,
278
+ displayname: displayName,
279
+ mail: email,
280
+ userpassword: temporaryPassword,
281
+ };
282
+ if (accountExpiry) {
283
+ ipaOpts.krbprincipalexpiration = freeipa.util.toGeneralizedTime(accountExpiry);
284
+ }
285
+
286
+ const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession, method: "user_add", args: [uid], options: ipaOpts });
287
+ if (response.error) {
288
+ const code = response.error.code;
289
+ if (code === 4001)
290
+ return {
291
+ ok: false,
292
+ error: "IPA session expired. Please log in again.",
293
+ status: 401,
294
+ };
295
+ if (code === 4301)
296
+ return {
297
+ ok: false,
298
+ error: "You don't have permission to create users.",
299
+ status: 403,
300
+ };
301
+ return {
302
+ ok: false,
303
+ error: response.error.message || "Failed to create user.",
304
+ status: 400,
305
+ };
306
+ }
307
+
308
+ // Extract uidNumber from IPA response
309
+ const ipaResult = response.result?.result as Record<string, unknown> | undefined;
310
+ const uidNumber = ipaResult ? freeipa.util.num(ipaResult.uidnumber) : null;
311
+
312
+ let id: string;
313
+ try {
314
+ if (existingLocalRows.length > 0) {
315
+ // Provider switch: update existing local account to IPA user
316
+ const guestId = existingLocalRows[0]!.id as string;
317
+ const updateRows: DbRow[] = await sql`
318
+ UPDATE auth.users SET
319
+ provider = 'ipa',
320
+ profile = ${targetProfile},
321
+ admin = false,
322
+ given_name = ${givenname},
323
+ sn = ${sn},
324
+ display_name = ${displayName},
325
+ mail = ${email},
326
+ account_expires = ${accountExpiry}
327
+ WHERE id = ${guestId}
328
+ RETURNING id
329
+ `;
330
+ id = updateRows[0]!.id as string;
331
+ await upsertUserIpaData({
332
+ userId: id,
333
+ uidNumber,
334
+ primaryGidNumber: ipaResult ? freeipa.util.num(ipaResult.gidnumber) : null,
335
+ homeDirectory: ipaResult ? freeipa.util.str(ipaResult.homedirectory) || null : null,
336
+ loginShell: ipaResult ? freeipa.util.str(ipaResult.loginshell) || null : null,
337
+ passwordExpires: now,
338
+ syncedAt: now,
339
+ });
340
+ await session.revokeAllForUser(guestId);
341
+ } else {
342
+ // New user: insert
343
+ const insertRows: DbRow[] = await sql`
344
+ INSERT INTO auth.users (uid, provider, profile, admin, given_name, sn, display_name, mail, account_expires)
345
+ VALUES (${uid}, 'ipa', ${targetProfile}, false, ${givenname}, ${sn}, ${displayName}, ${email}, ${accountExpiry})
346
+ ON CONFLICT (uid) DO UPDATE SET
347
+ provider = EXCLUDED.provider,
348
+ profile = EXCLUDED.profile,
349
+ admin = false,
350
+ given_name = EXCLUDED.given_name,
351
+ sn = EXCLUDED.sn,
352
+ display_name = EXCLUDED.display_name,
353
+ mail = EXCLUDED.mail,
354
+ account_expires = EXCLUDED.account_expires
355
+ RETURNING id
356
+ `;
357
+ id = insertRows[0]!.id as string;
358
+ await upsertUserIpaData({
359
+ userId: id,
360
+ uidNumber,
361
+ primaryGidNumber: ipaResult ? freeipa.util.num(ipaResult.gidnumber) : null,
362
+ homeDirectory: ipaResult ? freeipa.util.str(ipaResult.homedirectory) || null : null,
363
+ loginShell: ipaResult ? freeipa.util.str(ipaResult.loginshell) || null : null,
364
+ passwordExpires: now,
365
+ syncedAt: now,
366
+ });
367
+ }
368
+ } catch (dbError) {
369
+ log.error("CRITICAL: FreeIPA user created but local DB update failed. Manual reconciliation needed.", {
370
+ uid,
371
+ email,
372
+ userId: existingLocalRows.length > 0 ? (existingLocalRows[0]!.id as string) : null,
373
+ uidNumber,
374
+ targetProfile,
375
+ error: dbError instanceof Error ? dbError.message : String(dbError),
376
+ });
377
+ throw dbError;
378
+ }
379
+
380
+ return {
381
+ ok: true,
382
+ data: {
383
+ id,
384
+ uid,
385
+ accountExpires: accountExpiry ? accountExpiry.toISOString() : null,
386
+ _temporaryPassword: temporaryPassword,
387
+ },
388
+ };
389
+ };
390
+
391
+ // ==========================
392
+ // MUTATION: updateProfile
393
+ // ==========================
394
+
395
+ /**
396
+ * Update user profile. Handles all realms internally.
397
+ */
398
+ export const updateProfile = async (params: {
399
+ ipaSession?: string | null;
400
+ id: string;
401
+ data: IpaPatchData;
402
+ }): Promise<MutationResult<void>> => {
403
+ const { ipaSession, id, data } = params;
404
+
405
+ const userRows: DbRow[] = await sql`SELECT uid, provider, profile FROM auth.users WHERE id = ${id}`;
406
+ if (userRows.length === 0) {
407
+ return { ok: false, error: "User not found", status: 404 };
408
+ }
409
+ const uid = userRows[0]!.uid as string;
410
+ const { provider } = resolveProviderProfile(userRows[0]!);
411
+
412
+ if (provider === "ipa") {
413
+ const unavailable = await ensureFreeIpaMutationAvailable();
414
+ if (unavailable) return unavailable;
415
+ if (!ipaSession) {
416
+ return {
417
+ ok: false,
418
+ error: "IPA session required to update IPA user",
419
+ status: 400,
420
+ };
421
+ }
422
+
423
+ const ipaOptions: Record<string, unknown> = {};
424
+ if (data.givenname !== undefined) ipaOptions.givenname = data.givenname;
425
+ if (data.sn !== undefined) ipaOptions.sn = data.sn;
426
+ if (data.displayName !== undefined) ipaOptions.displayname = data.displayName;
427
+ if (data.mail !== undefined) ipaOptions.mail = data.mail || "";
428
+ if (data.ipa?.phone !== undefined) ipaOptions.telephonenumber = data.ipa.phone || "";
429
+ if (data.ipa?.address?.street !== undefined) ipaOptions.street = data.ipa.address.street || "";
430
+ if (data.ipa?.address?.postalCode !== undefined) ipaOptions.postalcode = data.ipa.address.postalCode || "";
431
+ if (data.ipa?.address?.city !== undefined) ipaOptions.l = data.ipa.address.city || "";
432
+ if (data.ipa?.address?.state !== undefined) ipaOptions.st = data.ipa.address.state || "";
433
+ if (data.ipa?.sshPublicKeys !== undefined) {
434
+ ipaOptions.ipasshpubkey = data.ipa.sshPublicKeys.length > 0 ? data.ipa.sshPublicKeys : "";
435
+ }
436
+
437
+ const response = await freeipa.client.call({
438
+ url: await getIpaUrl(),
439
+ ipaSession,
440
+ method: "user_mod",
441
+ args: [uid],
442
+ options: ipaOptions,
443
+ });
444
+ if (response.error) {
445
+ return {
446
+ ok: false,
447
+ error: response.error.message ?? "Failed to update user in FreeIPA",
448
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
449
+ };
450
+ }
451
+
452
+ const result = response.result?.result as Record<string, unknown> | undefined;
453
+ // Partial patch: never touch uid_number, password expiry, last-login, or
454
+ // other full-sync fields when the user is only editing profile attributes.
455
+ await patchUserIpaData({
456
+ userId: id,
457
+ phone: data.ipa?.phone,
458
+ street: data.ipa?.address?.street,
459
+ postalCode: data.ipa?.address?.postalCode,
460
+ city: data.ipa?.address?.city,
461
+ state: data.ipa?.address?.state,
462
+ sshPublicKeys:
463
+ data.ipa?.sshPublicKeys !== undefined ? (Array.isArray(result?.ipasshpubkey) ? (result?.ipasshpubkey as string[]) : []) : undefined,
464
+ sshFingerprints:
465
+ data.ipa?.sshPublicKeys !== undefined ? (Array.isArray(result?.sshpubkeyfp) ? (result?.sshpubkeyfp as string[]) : []) : undefined,
466
+ syncedAt: new Date(),
467
+ });
468
+ }
469
+
470
+ await sql`
471
+ UPDATE auth.users
472
+ SET given_name = CASE WHEN ${data.givenname !== undefined} THEN ${data.givenname ?? ""} ELSE given_name END,
473
+ sn = CASE WHEN ${data.sn !== undefined} THEN ${data.sn ?? ""} ELSE sn END,
474
+ display_name = CASE WHEN ${data.displayName !== undefined} THEN ${data.displayName ?? ""} ELSE display_name END
475
+ WHERE id = ${id}
476
+ `;
477
+
478
+ if (data.mail !== undefined) {
479
+ await sql`UPDATE auth.users SET mail = ${data.mail || null} WHERE id = ${id}`;
480
+ }
481
+
482
+ return { ok: true, data: undefined };
483
+ };
484
+
485
+ // ==========================
486
+ // MUTATION: resetPassword
487
+ // ==========================
488
+
489
+ /**
490
+ * Reset a user's password to a new random password.
491
+ */
492
+ export const resetPassword = async (params: {
493
+ ipaSession: string;
494
+ /** User ID (database UUID) */
495
+ id: string;
496
+ }): Promise<MutationResult<{ password: string }>> => {
497
+ const unavailable = await ensureFreeIpaMutationAvailable();
498
+ if (unavailable) return unavailable;
499
+ const { ipaSession, id } = params;
500
+
501
+ // Look up uid from database
502
+ const userRows: DbRow[] = await sql`SELECT uid FROM auth.users WHERE id = ${id}`;
503
+ if (userRows.length === 0) {
504
+ return { ok: false, error: "User not found", status: 404 };
505
+ }
506
+ const uid = userRows[0]!.uid as string;
507
+
508
+ const newPassword = generateFreeIpaPassword();
509
+
510
+ const response = await freeipa.client.call({
511
+ url: await getIpaUrl(),
512
+ ipaSession,
513
+ method: "user_mod",
514
+ args: [uid],
515
+ options: { userpassword: newPassword },
516
+ });
517
+
518
+ if (response.error) {
519
+ return {
520
+ ok: false,
521
+ error: response.error.message ?? "Failed to reset password",
522
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
523
+ };
524
+ }
525
+
526
+ // Update password expiry in local DB (password is now "expired" / temporary)
527
+ await sql`
528
+ INSERT INTO auth.user_ipa_data (user_id, ipa_password_expires, synced_at)
529
+ VALUES (${id}, now(), now())
530
+ ON CONFLICT (user_id) DO UPDATE SET
531
+ ipa_password_expires = EXCLUDED.ipa_password_expires,
532
+ synced_at = EXCLUDED.synced_at
533
+ `;
534
+
535
+ return { ok: true, data: { password: newPassword } };
536
+ };
537
+
538
+ // ==========================
539
+ // MUTATION: setExpiry
540
+ // ==========================
541
+
542
+ /**
543
+ * Set or remove account expiration date.
544
+ */
545
+ export const setExpiry = async (params: {
546
+ ipaSession: string;
547
+ /** User ID (database UUID) */
548
+ id: string;
549
+ expiryDate: string | null;
550
+ }): Promise<MutationResult<void>> => {
551
+ const unavailable = await ensureFreeIpaMutationAvailable();
552
+ if (unavailable) return unavailable;
553
+ const { ipaSession, id, expiryDate } = params;
554
+
555
+ // Look up uid from database
556
+ const userRows: DbRow[] = await sql`SELECT uid, provider, profile FROM auth.users WHERE id = ${id}`;
557
+ if (userRows.length === 0) {
558
+ return { ok: false, error: "User not found", status: 404 };
559
+ }
560
+ const uid = userRows[0]!.uid as string;
561
+ const { provider, profile } = resolveProviderProfile(userRows[0]!);
562
+
563
+ if (provider === "local" && profile === "guest") {
564
+ const guestExpiry = expiryDate ? new Date(expiryDate) : null;
565
+ if (guestExpiry) guestExpiry.setUTCHours(23, 59, 59, 0);
566
+ await sql`
567
+ UPDATE auth.users
568
+ SET account_expires = ${guestExpiry}
569
+ WHERE id = ${id}
570
+ `;
571
+ return { ok: true, data: undefined };
572
+ }
573
+
574
+ let dbExpiry: Date | null = null;
575
+ const response = await (async () => {
576
+ if (expiryDate) {
577
+ const date = new Date(expiryDate);
578
+ date.setUTCHours(23, 59, 59, 0);
579
+ const ipaExpiry = date.toISOString().replace(/[-:T]/g, "").slice(0, 14) + "Z";
580
+ dbExpiry = date;
581
+ return freeipa.client.call({
582
+ url: await getIpaUrl(),
583
+ ipaSession,
584
+ method: "user_mod",
585
+ args: [uid],
586
+ options: { krbprincipalexpiration: ipaExpiry },
587
+ });
588
+ }
589
+ return freeipa.client.call({
590
+ url: await getIpaUrl(),
591
+ ipaSession,
592
+ method: "user_mod",
593
+ args: [uid],
594
+ options: { krbprincipalexpiration: null },
595
+ });
596
+ })();
597
+
598
+ if (response.error) {
599
+ return {
600
+ ok: false,
601
+ error: response.error.message ?? "Failed to update account expiry",
602
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
603
+ };
604
+ }
605
+
606
+ await sql`
607
+ UPDATE auth.users
608
+ SET account_expires = ${dbExpiry}
609
+ WHERE id = ${id}
610
+ `;
611
+ await sql`
612
+ INSERT INTO auth.user_ipa_data (user_id, synced_at)
613
+ VALUES (${id}, now())
614
+ ON CONFLICT (user_id) DO UPDATE SET synced_at = EXCLUDED.synced_at
615
+ `;
616
+
617
+ return { ok: true, data: undefined };
618
+ };
619
+
620
+ // ==========================
621
+ // MUTATION: demoteToGuest
622
+ // ==========================
623
+
624
+ /**
625
+ * Demote IPA user to guest (removes from FreeIPA, keeps in local DB as guest).
626
+ */
627
+ export const demoteToGuest = async (params: {
628
+ ipaSession: string;
629
+ /** User ID (database UUID) */
630
+ id: string;
631
+ actor: { userId: string; uid: string };
632
+ }): Promise<MutationResult<void>> => {
633
+ const unavailable = await ensureFreeIpaMutationAvailable();
634
+ if (unavailable) return unavailable;
635
+ const { ipaSession, id, actor } = params;
636
+
637
+ const userRows: DbRow[] = await sql`
638
+ SELECT uid, mail, display_name, provider, profile
639
+ FROM auth.users
640
+ WHERE id = ${id} AND provider = 'ipa'
641
+ `;
642
+ if (userRows.length === 0) {
643
+ return {
644
+ ok: false,
645
+ error: "User not found or not an IPA user",
646
+ status: 404,
647
+ };
648
+ }
649
+ const { provider: previousProvider, profile: previousProfile } = resolveProviderProfile(userRows[0]!);
650
+ const uid = userRows[0]!.uid as string;
651
+ const mail = (userRows[0]!.mail as string) ?? null;
652
+ const displayName = (userRows[0]!.display_name as string) ?? null;
653
+ const guestExpiresDays = await settings.get<number | null>("user.account.local_guest_expires_days");
654
+ const accountExpires = guestExpiresDays && guestExpiresDays > 0 ? new Date(Date.now() + guestExpiresDays * 24 * 60 * 60 * 1000) : null;
655
+
656
+ log.warn("About to delete IPA user, this cannot be undone", { uid, userId: id });
657
+ const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession, method: "user_del", args: [uid], options: {} });
658
+ const ipaDeleteMessage = (response.error?.message ?? "").toLowerCase();
659
+ const ipaDeleteNotFound = ipaDeleteMessage.includes("not found") || ipaDeleteMessage.includes("does not exist");
660
+ if (response.error && !ipaDeleteNotFound) {
661
+ return {
662
+ ok: false,
663
+ error: response.error.message ?? "Failed to delete user from FreeIPA",
664
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
665
+ };
666
+ }
667
+
668
+ log.info("FreeIPA user deleted, updating local DB", { uid, userId: id });
669
+ try {
670
+ await sql.begin(async (tx) => {
671
+ await tx`
672
+ UPDATE auth.users
673
+ SET provider = 'local', profile = 'guest', admin = false,
674
+ account_expires = ${accountExpires}
675
+ WHERE id = ${id}
676
+ `;
677
+ await tx`DELETE FROM auth.user_ipa_data WHERE user_id = ${id}`;
678
+
679
+ await tx`
680
+ DELETE FROM auth.user_groups_v2
681
+ WHERE user_id = ${id}
682
+ AND group_id IN (SELECT id FROM auth.groups WHERE provider = 'ipa')
683
+ `;
684
+ await tx`
685
+ DELETE FROM auth.group_manager_users_v2
686
+ WHERE user_id = ${id}
687
+ AND group_id IN (SELECT id FROM auth.groups WHERE provider = 'ipa')
688
+ `;
689
+ await writeDeletedAccountAudit({
690
+ db: tx,
691
+ userId: id,
692
+ uid,
693
+ mail,
694
+ displayName,
695
+ previousProvider,
696
+ previousProfile,
697
+ reason: "manual_demote",
698
+ meta: {
699
+ actorUserId: actor.userId,
700
+ actorUid: actor.uid,
701
+ guestExpiresAt: accountExpires?.toISOString() ?? null,
702
+ freeIpaUserAlreadyMissing: ipaDeleteNotFound,
703
+ },
704
+ });
705
+ });
706
+ } catch (dbError) {
707
+ log.error("CRITICAL: FreeIPA user was deleted but local DB update failed. Manual reconciliation needed.", {
708
+ uid,
709
+ userId: id,
710
+ mail,
711
+ error: dbError instanceof Error ? dbError.message : String(dbError),
712
+ });
713
+ throw dbError;
714
+ }
715
+ await session.revokeAllForUser(id);
716
+
717
+ return { ok: true, data: undefined };
718
+ };
719
+
720
+ // ==========================
721
+ // MUTATION: delete
722
+ // ==========================
723
+
724
+ /**
725
+ * Permanently delete user (from FreeIPA if applicable, and from local DB).
726
+ */
727
+ export const deleteUser = async (params: {
728
+ ipaSession?: string | null;
729
+ /** User ID (database UUID) */
730
+ id: string;
731
+ actor: { userId: string; uid: string };
732
+ }): Promise<MutationResult<void>> => {
733
+ const { ipaSession, id, actor } = params;
734
+
735
+ const userRows: DbRow[] = await sql`
736
+ SELECT uid, provider, profile, mail, display_name
737
+ FROM auth.users
738
+ WHERE id = ${id}
739
+ `;
740
+ if (userRows.length === 0) {
741
+ return { ok: false, error: "User not found", status: 404 };
742
+ }
743
+ const uid = userRows[0]!.uid as string;
744
+ const { provider, profile } = resolveProviderProfile(userRows[0]!);
745
+ const mail = (userRows[0]!.mail as string) ?? null;
746
+ const displayName = (userRows[0]!.display_name as string) ?? null;
747
+ let ipaDeleteNotFound = false;
748
+
749
+ if (provider === "ipa") {
750
+ const unavailable = await ensureFreeIpaMutationAvailable();
751
+ if (unavailable) return unavailable;
752
+ if (!ipaSession) {
753
+ return {
754
+ ok: false,
755
+ error: "IPA session required to delete IPA user",
756
+ status: 400,
757
+ };
758
+ }
759
+ log.warn("About to delete IPA user, this cannot be undone", { uid, userId: id });
760
+ const response = await freeipa.client.call({ url: await getIpaUrl(), ipaSession, method: "user_del", args: [uid], options: {} });
761
+ const ipaDeleteMessage = (response.error?.message ?? "").toLowerCase();
762
+ ipaDeleteNotFound = ipaDeleteMessage.includes("not found") || ipaDeleteMessage.includes("does not exist");
763
+ if (response.error && !ipaDeleteNotFound) {
764
+ return {
765
+ ok: false,
766
+ error: response.error.message ?? "Failed to delete user from FreeIPA",
767
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
768
+ };
769
+ }
770
+ log.info("FreeIPA user deleted, updating local DB", { uid, userId: id });
771
+ }
772
+
773
+ try {
774
+ await sql.begin(async (tx) => {
775
+ await writeDeletedAccountAudit({
776
+ db: tx,
777
+ userId: id,
778
+ uid,
779
+ mail,
780
+ displayName,
781
+ previousProvider: provider,
782
+ previousProfile: profile,
783
+ reason: "manual_delete",
784
+ meta: {
785
+ actorUserId: actor.userId,
786
+ actorUid: actor.uid,
787
+ deletedFromFreeIpa: provider === "ipa",
788
+ freeIpaUserAlreadyMissing: provider === "ipa" ? ipaDeleteNotFound : false,
789
+ },
790
+ });
791
+ await tx`DELETE FROM auth.users WHERE id = ${id}`;
792
+ });
793
+ } catch (dbError) {
794
+ log.error("CRITICAL: FreeIPA user was deleted but local DB update failed. Manual reconciliation needed.", {
795
+ uid,
796
+ userId: id,
797
+ mail,
798
+ provider,
799
+ error: dbError instanceof Error ? dbError.message : String(dbError),
800
+ });
801
+ throw dbError;
802
+ }
803
+ await session.revokeAllForUser(id);
804
+
805
+ return { ok: true, data: undefined };
806
+ };