@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,745 @@
1
+ import { type SQL, sql } from "bun";
2
+ import type { BaseUser, MutationResult, PaginationResponse, Role, User, UserProfile, UserProvider } from "../../contracts/shared";
3
+ import { freeipa } from "../../server/services";
4
+ import { createAuthLoginUrl } from "../../shared/redirect";
5
+ import { isAccountCategoryAllowed } from "../account-category-policy";
6
+ import type { AuditActor } from "../audit";
7
+ import { getFreeIpaConfig } from "../freeipa-config";
8
+ import { getServiceIpaSession } from "../ipa/service-account";
9
+ import { toPgTextArray, toPgUuidArray } from "../postgres";
10
+ import { providers } from "../providers";
11
+ import { session } from "../session";
12
+ import * as settings from "../settings";
13
+ import { buildRoles } from "./authz";
14
+ import { buildBaseUser, resolveProviderProfile } from "./base-user";
15
+ import { managedGroupIdsSubquery, managedGroupsNamesSubquery, recursiveGroupIdsSubquery, recursiveGroupNamesSubquery } from "./group-sql";
16
+ import { buildIpaUserData, emptyIpaUserData, userIpaDataColumns, userIpaDataJoin } from "./ipa-data";
17
+ import {
18
+ canPersistStoredAdmin,
19
+ getDefaultAccountExpiry,
20
+ parseManualAccountExpiry,
21
+ resolveAccountExpires,
22
+ resolveEffectiveAdminState,
23
+ resolveTargetAccountExpiry,
24
+ } from "./model";
25
+ import type { AccountsNotificationSender } from "./notification-sender";
26
+ import { transitionIpaUserToLocal } from "./switching";
27
+
28
+ export { clearAvatar, getAvatar, parseAvatarDataUrl, setAvatar } from "./avatar";
29
+
30
+ type DbRow = Record<string, unknown>;
31
+ type UserMutationTarget = BaseUser & { accountExpires: string | null; storedAdmin: boolean };
32
+
33
+ type CreateUserData = {
34
+ provider: UserProvider;
35
+ profile: UserProfile;
36
+ admin?: boolean;
37
+ email: string;
38
+ givenname: string;
39
+ sn: string;
40
+ displayName?: string;
41
+ autoSendNotification?: boolean;
42
+ requestId?: string;
43
+ accountExpires?: string | null;
44
+ };
45
+
46
+ type UpdateUserData = {
47
+ givenname?: string;
48
+ sn?: string;
49
+ displayName?: string;
50
+ mail?: string;
51
+ ipa?: {
52
+ phone?: string;
53
+ address?: {
54
+ street?: string;
55
+ postalCode?: string;
56
+ city?: string;
57
+ state?: string;
58
+ };
59
+ sshPublicKeys?: string[];
60
+ };
61
+ };
62
+
63
+ const buildUserMutationTarget = (row: DbRow): UserMutationTarget => ({
64
+ ...buildBaseUser(row),
65
+ accountExpires: resolveAccountExpires(row)?.toISOString() ?? null,
66
+ storedAdmin: Boolean(row.admin),
67
+ });
68
+
69
+ const pgArrayLiteralToStrings = (value: string): string[] => {
70
+ if (value === "{}") return [];
71
+ if (!value.startsWith("{") || !value.endsWith("}")) return [];
72
+ const items: string[] = [];
73
+ let item = "";
74
+ let quoted = false;
75
+ let escaped = false;
76
+ let wasQuoted = false;
77
+
78
+ for (const char of value.slice(1, -1)) {
79
+ if (escaped) {
80
+ item += char;
81
+ escaped = false;
82
+ continue;
83
+ }
84
+ if (quoted && char === "\\") {
85
+ escaped = true;
86
+ continue;
87
+ }
88
+ if (char === '"') {
89
+ quoted = !quoted;
90
+ wasQuoted = true;
91
+ continue;
92
+ }
93
+ if (!quoted && char === ",") {
94
+ if (wasQuoted || (item !== "" && item !== "NULL")) items.push(item);
95
+ item = "";
96
+ wasQuoted = false;
97
+ continue;
98
+ }
99
+ item += char;
100
+ }
101
+
102
+ if (wasQuoted || (item !== "" && item !== "NULL")) items.push(item);
103
+ return items;
104
+ };
105
+
106
+ const stringArray = (value: unknown): string[] => {
107
+ if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string");
108
+ if (typeof value === "string") return pgArrayLiteralToStrings(value);
109
+ return [];
110
+ };
111
+
112
+ const buildUser = (row: DbRow, groupsAdmin: string[]): User => {
113
+ const { provider, profile } = resolveProviderProfile(row);
114
+ const displayName = (row.display_name as string) ?? "";
115
+ const mail = (row.mail as string) ?? null;
116
+ const memberofGroup = stringArray(row.member_groups);
117
+ const memberofGroupIds = stringArray(row.member_group_ids);
118
+ const manages = stringArray(row.manages);
119
+ const managesGroupIds = stringArray(row.manages_group_ids);
120
+ const effectiveAdmin =
121
+ row.effective_admin !== undefined
122
+ ? Boolean(row.effective_admin)
123
+ : resolveEffectiveAdminState({
124
+ provider,
125
+ storedAdmin: Boolean(row.admin),
126
+ memberofGroup,
127
+ groupsAdmin,
128
+ });
129
+ const roles = buildRoles({
130
+ provider,
131
+ profile,
132
+ memberofGroup,
133
+ manages,
134
+ admin: effectiveAdmin,
135
+ });
136
+ const common = {
137
+ id: row.id as string,
138
+ uid: row.uid as string,
139
+ roles,
140
+ profile,
141
+ givenname: (row.given_name as string) ?? "",
142
+ sn: (row.sn as string) ?? "",
143
+ displayName: displayName || (profile === "guest" && mail ? mail : ""),
144
+ mail,
145
+ avatarHash: (row.avatar_hash as string | null | undefined) ?? null,
146
+ accountExpires: resolveAccountExpires(row)?.toISOString() ?? null,
147
+ lastLoginLocal: row.last_login_local ? (row.last_login_local as Date).toISOString() : null,
148
+ memberofGroup,
149
+ memberofGroupIds,
150
+ manages,
151
+ managesGroupIds,
152
+ };
153
+
154
+ if (provider === "ipa") {
155
+ return {
156
+ ...common,
157
+ provider: "ipa",
158
+ ipa: buildIpaUserData(row) ?? emptyIpaUserData(),
159
+ };
160
+ }
161
+
162
+ return {
163
+ ...common,
164
+ provider: "local",
165
+ ipa: null,
166
+ };
167
+ };
168
+
169
+ export const get = async (params: { id: string } | { uid: string }): Promise<User | null> => {
170
+ const whereClause = "id" in params ? sql`u.id = ${params.id}` : sql`u.uid = ${params.uid}`;
171
+ const userIdExpr = "id" in params ? sql`${params.id}` : sql`u.id`;
172
+ const { groupsAdmin } = await getFreeIpaConfig();
173
+ const rows = await sql<DbRow[]>`
174
+ SELECT u.*,
175
+ ${userIpaDataColumns},
176
+ CASE
177
+ WHEN u.provider = 'local' THEN u.admin
178
+ ELSE EXISTS(
179
+ SELECT 1
180
+ FROM auth.ipa_user_effective_groups eg
181
+ WHERE eg.user_id = u.id
182
+ AND eg.group_name = ANY(${toPgTextArray(groupsAdmin)}::text[])
183
+ )
184
+ END AS effective_admin,
185
+ COALESCE(ARRAY(
186
+ SELECT g.name
187
+ FROM auth.user_groups_v2 ug
188
+ JOIN auth.groups g ON g.id = ug.group_id
189
+ WHERE ug.user_id = u.id
190
+ ORDER BY g.name
191
+ ), '{}') AS member_groups,
192
+ COALESCE(ARRAY(
193
+ SELECT ug.group_id
194
+ FROM auth.user_groups_v2 ug
195
+ JOIN auth.groups g ON g.id = ug.group_id
196
+ WHERE ug.user_id = u.id
197
+ ORDER BY g.name
198
+ ), '{}') AS member_group_ids,
199
+ COALESCE(ARRAY(
200
+ ${managedGroupsNamesSubquery(userIdExpr)}
201
+ ), '{}') AS manages,
202
+ COALESCE(ARRAY(
203
+ ${managedGroupIdsSubquery(userIdExpr)}
204
+ ), '{}') AS manages_group_ids
205
+ FROM auth.users u
206
+ ${userIpaDataJoin}
207
+ WHERE ${whereClause}
208
+ `;
209
+ if (rows.length === 0) return null;
210
+ return buildUser(rows[0]!, groupsAdmin);
211
+ };
212
+
213
+ export const getMinimal = async (params: { id: string } | { uid: string }): Promise<UserMutationTarget | null> => {
214
+ const whereClause = "id" in params ? sql`id = ${params.id}` : sql`uid = ${params.uid}`;
215
+ const rows = await sql<DbRow[]>`
216
+ SELECT id, uid, provider, profile, admin, given_name, sn, display_name, mail, avatar_hash, account_expires
217
+ FROM auth.users
218
+ WHERE ${whereClause}
219
+ `;
220
+ if (rows.length === 0) return null;
221
+ return buildUserMutationTarget(rows[0]!);
222
+ };
223
+
224
+ export const getByUid = async (params: { uid: string }): Promise<{ id: string; roles: Role[] } | null> => {
225
+ const { groupsAdmin } = await getFreeIpaConfig();
226
+ const rows = await sql<DbRow[]>`
227
+ SELECT u.id, u.provider, u.profile, u.admin,
228
+ CASE
229
+ WHEN u.provider = 'local' THEN u.admin
230
+ ELSE EXISTS(
231
+ SELECT 1
232
+ FROM auth.ipa_user_effective_groups eg
233
+ WHERE eg.user_id = u.id
234
+ AND eg.group_name = ANY(${toPgTextArray(groupsAdmin)}::text[])
235
+ )
236
+ END AS effective_admin
237
+ FROM auth.users u
238
+ WHERE u.uid = ${params.uid}
239
+ `;
240
+ if (rows.length === 0) return null;
241
+ const { provider, profile } = resolveProviderProfile(rows[0]!);
242
+ const roles = buildRoles({
243
+ provider,
244
+ profile,
245
+ memberofGroup: [],
246
+ manages: [],
247
+ admin: Boolean(rows[0]!.effective_admin),
248
+ });
249
+ return { id: rows[0]!.id as string, roles };
250
+ };
251
+
252
+ export const list = async (params: {
253
+ ids?: string[];
254
+ uids?: string[];
255
+ search?: string;
256
+ provider?: UserProvider;
257
+ profile?: UserProfile;
258
+ page?: number;
259
+ perPage?: number;
260
+ }) => {
261
+ const page = params.page ?? 1;
262
+ const perPage = params.perPage ?? 100;
263
+ const offset = (page - 1) * perPage;
264
+ const search = params.search ? `%${freeipa.util.escapeLike(params.search.toLowerCase())}%` : null;
265
+ const ids = params.ids;
266
+ const uids = params.uids;
267
+
268
+ if ((ids && ids.length === 0) || (uids && uids.length === 0)) {
269
+ return {
270
+ users: [],
271
+ total: 0,
272
+ pagination: {
273
+ page,
274
+ per_page: perPage,
275
+ total: 0,
276
+ total_pages: 0,
277
+ has_next: false,
278
+ } satisfies PaginationResponse,
279
+ };
280
+ }
281
+
282
+ const conditions: SQL.Query<unknown>[] = [sql`TRUE`];
283
+ if (ids) conditions.push(sql`id = ANY(${toPgUuidArray(ids)}::uuid[])`);
284
+ if (uids) conditions.push(sql`uid = ANY(${toPgTextArray(uids)}::text[])`);
285
+ if (params.provider) conditions.push(sql`provider = ${params.provider}`);
286
+ if (params.profile) conditions.push(sql`profile = ${params.profile}`);
287
+ if (search) {
288
+ conditions.push(sql`(
289
+ LOWER(uid) LIKE ${search} ESCAPE '\\' OR
290
+ LOWER(display_name) LIKE ${search} ESCAPE '\\' OR
291
+ LOWER(given_name) LIKE ${search} ESCAPE '\\' OR
292
+ LOWER(sn) LIKE ${search} ESCAPE '\\' OR
293
+ LOWER(mail) LIKE ${search} ESCAPE '\\'
294
+ )`);
295
+ }
296
+
297
+ const where = conditions.reduce((acc, cond) => sql`${acc} AND ${cond}`);
298
+ const countRows = await sql<DbRow[]>`SELECT COUNT(*)::int AS count FROM auth.users WHERE ${where}`;
299
+ const total = (countRows[0]?.count as number) ?? 0;
300
+ const totalPages = Math.ceil(total / perPage);
301
+ const groupsAdmin = (await getFreeIpaConfig()).groupsAdmin;
302
+ const rows = await sql<DbRow[]>`
303
+ SELECT u.*,
304
+ CASE
305
+ WHEN u.provider = 'local' THEN u.admin
306
+ ELSE EXISTS(
307
+ SELECT 1
308
+ FROM auth.ipa_user_effective_groups eg
309
+ WHERE eg.user_id = u.id
310
+ AND eg.group_name = ANY(${toPgTextArray(groupsAdmin)}::text[])
311
+ )
312
+ END AS effective_admin
313
+ FROM auth.users u
314
+ WHERE ${where}
315
+ ORDER BY uid
316
+ LIMIT ${perPage} OFFSET ${offset}
317
+ `;
318
+
319
+ return {
320
+ users: rows.map(buildBaseUser),
321
+ total,
322
+ pagination: {
323
+ page,
324
+ per_page: perPage,
325
+ total,
326
+ total_pages: totalPages,
327
+ has_next: page < totalPages,
328
+ } satisfies PaginationResponse,
329
+ };
330
+ };
331
+
332
+ export const getGroups = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
333
+ if (params.recursive) {
334
+ const rows = await sql<DbRow[]>`${recursiveGroupNamesSubquery(params.id)}`;
335
+ return rows.map((row) => row.name as string);
336
+ }
337
+
338
+ const rows = await sql<DbRow[]>`
339
+ SELECT g.name
340
+ FROM auth.user_groups_v2 ug
341
+ JOIN auth.groups g ON g.id = ug.group_id
342
+ WHERE ug.user_id = ${params.id}::uuid
343
+ ORDER BY g.name
344
+ `;
345
+ return rows.map((row) => row.name as string);
346
+ };
347
+
348
+ export const getGroupIds = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
349
+ if (params.recursive) {
350
+ const rows = await sql<DbRow[]>`${recursiveGroupIdsSubquery(params.id)}`;
351
+ return rows.map((row) => row.group_id as string);
352
+ }
353
+
354
+ const rows = await sql<DbRow[]>`
355
+ SELECT ug.group_id
356
+ FROM auth.user_groups_v2 ug
357
+ JOIN auth.groups g ON g.id = ug.group_id
358
+ WHERE ug.user_id = ${params.id}::uuid
359
+ ORDER BY g.name
360
+ `;
361
+ return rows.map((row) => row.group_id as string);
362
+ };
363
+
364
+ export const getManagedGroups = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
365
+ if (params.recursive === false) {
366
+ const rows = await sql<DbRow[]>`
367
+ SELECT DISTINCT g.name
368
+ FROM auth.groups g
369
+ LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${params.id}::uuid
370
+ LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
371
+ LEFT JOIN auth.user_groups_v2 ug ON ug.group_id = gmg.manager_group_id AND ug.user_id = ${params.id}::uuid
372
+ LEFT JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
373
+ WHERE gmu.user_id IS NOT NULL OR (ug.user_id IS NOT NULL AND g_manager.provider = g.provider)
374
+ ORDER BY g.name
375
+ `;
376
+ return rows.map((row) => row.name as string);
377
+ }
378
+
379
+ const rows = await sql<DbRow[]>`${managedGroupsNamesSubquery(params.id)}`;
380
+ return rows.map((row) => row.name as string);
381
+ };
382
+
383
+ /**
384
+ * Same as `getManagedGroups` but returns group IDs. Prefer this for
385
+ * authorization checks — group names are unique only per provider, so
386
+ * comparing by name can authorize a local group based on a same-named
387
+ * IPA group membership (or vice versa).
388
+ */
389
+ export const getManagedGroupIds = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
390
+ if (params.recursive === false) {
391
+ const rows = await sql<DbRow[]>`
392
+ SELECT DISTINCT g.id
393
+ FROM auth.groups g
394
+ LEFT JOIN auth.group_manager_users_v2 gmu ON gmu.group_id = g.id AND gmu.user_id = ${params.id}::uuid
395
+ LEFT JOIN auth.group_manager_groups_v2 gmg ON gmg.group_id = g.id
396
+ LEFT JOIN auth.user_groups_v2 ug ON ug.group_id = gmg.manager_group_id AND ug.user_id = ${params.id}::uuid
397
+ LEFT JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
398
+ WHERE gmu.user_id IS NOT NULL OR (ug.user_id IS NOT NULL AND g_manager.provider = g.provider)
399
+ ORDER BY g.id
400
+ `;
401
+ return rows.map((row) => row.id as string);
402
+ }
403
+
404
+ const rows = await sql<DbRow[]>`${managedGroupIdsSubquery(params.id)}`;
405
+ return rows.map((row) => row.id as string);
406
+ };
407
+
408
+ export const demoteToGuest = async (params: { id: string; actor: { userId: string; uid: string } }): Promise<MutationResult<void>> => {
409
+ const user = await getMinimal({ id: params.id });
410
+ if (!user) return { ok: false, error: "User not found", status: 404 };
411
+ if (user.provider !== "ipa") {
412
+ return { ok: false, error: "Only IPA-backed accounts can be demoted to local guests", status: 400 };
413
+ }
414
+ const serviceSession = await getServiceIpaSession();
415
+ if (!serviceSession.ok) return serviceSession;
416
+
417
+ return providers.ipa.users.demoteToGuest({
418
+ ipaSession: serviceSession.data,
419
+ id: params.id,
420
+ actor: params.actor,
421
+ });
422
+ };
423
+
424
+ export const create = async (params: {
425
+ data: CreateUserData;
426
+ actor?: AuditActor;
427
+ }): Promise<MutationResult<{ user: User; temporaryPassword?: string }>> => {
428
+ if (!(await isAccountCategoryAllowed(params.data)))
429
+ return { ok: false, error: "This account category is disabled. Contact an administrator.", status: 403 };
430
+ if (params.data.provider === "local" && params.data.admin && !canPersistStoredAdmin("local", params.data.profile)) {
431
+ return { ok: false, error: "Only local full accounts can be created as admins", status: 400 };
432
+ }
433
+
434
+ const accountExpires = await resolveTargetAccountExpiry({
435
+ provider: params.data.provider,
436
+ profile: params.data.profile,
437
+ requested: params.data.accountExpires,
438
+ });
439
+
440
+ if (params.data.provider === "local") {
441
+ const created = await providers.local.users.create({
442
+ actor: params.actor,
443
+ data: {
444
+ email: params.data.email,
445
+ givenname: params.data.givenname,
446
+ sn: params.data.sn,
447
+ displayName: params.data.displayName,
448
+ },
449
+ profile: params.data.profile,
450
+ accountExpires,
451
+ admin: params.data.admin,
452
+ });
453
+ if (!created.ok) return created;
454
+ const user = await get({ id: created.data.id });
455
+ if (!user) return { ok: false, error: "Created user not found", status: 500 };
456
+ return { ok: true, data: { user } };
457
+ }
458
+
459
+ const serviceSession = await getServiceIpaSession();
460
+ if (!serviceSession.ok) return serviceSession;
461
+
462
+ const created = await providers.ipa.users.create({
463
+ ipaSession: serviceSession.data,
464
+ profile: params.data.profile,
465
+ accountExpires,
466
+ data: {
467
+ email: params.data.email,
468
+ givenname: params.data.givenname,
469
+ sn: params.data.sn,
470
+ displayName: params.data.displayName,
471
+ autoSendNotification: params.data.autoSendNotification,
472
+ requestId: params.data.requestId,
473
+ },
474
+ });
475
+ if (!created.ok) return created;
476
+ const user = await get({ id: created.data.id });
477
+ if (!user) return { ok: false, error: "Created user not found", status: 500 };
478
+ return {
479
+ ok: true,
480
+ data: {
481
+ user,
482
+ temporaryPassword: created.data._temporaryPassword,
483
+ },
484
+ };
485
+ };
486
+
487
+ export const update = async (params: { id: string; data: UpdateUserData }): Promise<MutationResult<void>> => {
488
+ const user = await getMinimal({ id: params.id });
489
+ if (!user) return { ok: false, error: "User not found", status: 404 };
490
+
491
+ if (user.provider === "ipa") {
492
+ const serviceSession = await getServiceIpaSession();
493
+ if (!serviceSession.ok) return serviceSession;
494
+ return providers.ipa.users.update({
495
+ ipaSession: serviceSession.data,
496
+ id: params.id,
497
+ data: params.data,
498
+ });
499
+ }
500
+
501
+ if (params.data.ipa) {
502
+ return { ok: false, error: "IPA-only fields can only be updated for IPA-backed users", status: 400 };
503
+ }
504
+
505
+ return providers.local.users.update({
506
+ id: params.id,
507
+ data: params.data,
508
+ });
509
+ };
510
+
511
+ export const setProfile = async (params: { id: string; profile: UserProfile; actor?: AuditActor }): Promise<MutationResult<void>> => {
512
+ const user = await getMinimal({ id: params.id });
513
+ if (!user) return { ok: false, error: "User not found", status: 404 };
514
+ if (user.provider !== "local") {
515
+ return { ok: false, error: "IPA profile is derived from IPA groups and cannot be set directly", status: 400 };
516
+ }
517
+
518
+ const accountExpires = user.accountExpires ? new Date(user.accountExpires) : await getDefaultAccountExpiry("local", params.profile);
519
+ return providers.local.users.setProfile({
520
+ actor: params.actor,
521
+ id: params.id,
522
+ profile: params.profile,
523
+ accountExpires,
524
+ });
525
+ };
526
+
527
+ export const setAdmin = async (params: { id: string; admin: boolean }): Promise<MutationResult<void>> => {
528
+ const user = await getMinimal({ id: params.id });
529
+ if (!user) return { ok: false, error: "User not found", status: 404 };
530
+ if (!canPersistStoredAdmin(user.provider, user.profile)) {
531
+ return {
532
+ ok: false,
533
+ error:
534
+ user.provider === "ipa"
535
+ ? "FreeIPA admin access is managed through FreeIPA groups"
536
+ : "Guest accounts cannot be granted admin access",
537
+ status: 400,
538
+ };
539
+ }
540
+
541
+ return providers.local.users.setAdmin(params);
542
+ };
543
+
544
+ export const setExpiry = async (params: {
545
+ actor?: { userId: string; uid: string; roles: string[] };
546
+ id: string;
547
+ expiryDate: string | null;
548
+ }): Promise<MutationResult<void>> => {
549
+ const user = await getMinimal({ id: params.id });
550
+ if (!user) return { ok: false, error: "User not found", status: 404 };
551
+
552
+ const selfTarget = params.actor?.userId === params.id;
553
+ if (selfTarget && !params.actor?.roles.includes("admin")) {
554
+ return { ok: false, error: "Only admins can change their own account expiry.", status: 403 };
555
+ }
556
+
557
+ // Explicit account-expiry management is allowed to target the acting admin
558
+ // as well. Automatic self-extension remains handled separately by the
559
+ // account-lifecycle service and must not turn non-expiring accounts back into
560
+ // expiring ones implicitly.
561
+ if (user.provider === "ipa") {
562
+ const serviceSession = await getServiceIpaSession();
563
+ if (!serviceSession.ok) return serviceSession;
564
+ return providers.ipa.users.setExpiry({
565
+ ipaSession: serviceSession.data,
566
+ id: params.id,
567
+ expiryDate: params.expiryDate,
568
+ });
569
+ }
570
+
571
+ const parsed = parseManualAccountExpiry(params.expiryDate);
572
+ if (!parsed.ok) return { ok: false, error: parsed.error, status: 400 };
573
+
574
+ return providers.local.users.setExpiry({
575
+ id: params.id,
576
+ profile: user.profile,
577
+ accountExpires: parsed.date,
578
+ });
579
+ };
580
+
581
+ export const sendLoginLink = async (params: {
582
+ id: string;
583
+ notificationSender: AccountsNotificationSender;
584
+ locale?: string;
585
+ }): Promise<MutationResult<void>> => {
586
+ const user = await getMinimal({ id: params.id });
587
+ if (!user) return { ok: false, error: "User not found", status: 404 };
588
+ if (user.provider !== "local") {
589
+ return { ok: false, error: "Login links are only available for local accounts", status: 400 };
590
+ }
591
+ if (!user.mail) return { ok: false, error: "A local account requires an email address to receive a login link", status: 400 };
592
+
593
+ const token = await providers.local.auth.createMagicLinkToken({ email: user.mail, ttlSeconds: 300 });
594
+ const rawAppUrl = await settings.get<string>("app.url");
595
+ const appUrl = rawAppUrl.startsWith("http") ? rawAppUrl : `https://${rawAppUrl}`;
596
+ try {
597
+ const delivery = await params.notificationSender.sendLoginLink({
598
+ email: user.mail,
599
+ token,
600
+ magicLink: createAuthLoginUrl(appUrl, { token }),
601
+ locale: params.locale,
602
+ });
603
+ if (delivery.status !== "error" && delivery.status !== "suppressed") return { ok: true, data: undefined };
604
+ } catch {
605
+ // Keep the service contract result-based even when rendering or persistence fails.
606
+ }
607
+ return { ok: false, error: "The login link could not be delivered", status: 500 };
608
+ };
609
+
610
+ export const createLoginToken = async (params: {
611
+ id: string;
612
+ }): Promise<MutationResult<{ token: string; magicLink: string; expiresInSeconds: number }>> => {
613
+ const user = await getMinimal({ id: params.id });
614
+ if (!user) return { ok: false, error: "User not found", status: 404 };
615
+ if (user.provider !== "local") {
616
+ return { ok: false, error: "Login tokens are only available for local accounts", status: 400 };
617
+ }
618
+ if (!user.mail) {
619
+ return { ok: false, error: "A local account requires an email address before a login token can be created", status: 400 };
620
+ }
621
+
622
+ const expiresInSeconds = 300;
623
+ const token = await providers.local.auth.createMagicLinkToken({
624
+ email: user.mail,
625
+ ttlSeconds: expiresInSeconds,
626
+ });
627
+ const rawAppUrl = await settings.get<string>("app.url");
628
+ const appUrl = rawAppUrl.startsWith("http") ? rawAppUrl : `https://${rawAppUrl}`;
629
+
630
+ return {
631
+ ok: true,
632
+ data: {
633
+ token,
634
+ magicLink: createAuthLoginUrl(appUrl, { token }),
635
+ expiresInSeconds,
636
+ },
637
+ };
638
+ };
639
+
640
+ export const resetPassword = async (params: { id: string }): Promise<MutationResult<{ password: string }>> => {
641
+ const user = await getMinimal({ id: params.id });
642
+ if (!user) return { ok: false, error: "User not found", status: 404 };
643
+ if (user.provider !== "ipa") {
644
+ return { ok: false, error: "Password resets are only available for IPA-backed accounts", status: 400 };
645
+ }
646
+ const serviceSession = await getServiceIpaSession();
647
+ if (!serviceSession.ok) return serviceSession;
648
+
649
+ return providers.ipa.users.resetPassword({
650
+ ipaSession: serviceSession.data,
651
+ id: params.id,
652
+ });
653
+ };
654
+
655
+ export const switchProvider = async (params: { id: string; provider: UserProvider }): Promise<MutationResult<void>> => {
656
+ const user = await getMinimal({ id: params.id });
657
+ if (!user) return { ok: false, error: "User not found", status: 404 };
658
+ const freeIpaConfig = await getFreeIpaConfig();
659
+
660
+ const currentProvider = user.provider;
661
+ const currentProfile = user.profile;
662
+ const currentExpiry = user.accountExpires ? new Date(user.accountExpires) : null;
663
+
664
+ if (currentProvider === params.provider) {
665
+ return { ok: false, error: `Account already uses provider '${params.provider}'`, status: 400 };
666
+ }
667
+
668
+ if (!freeIpaConfig.enabled) {
669
+ return { ok: false, error: "FreeIPA is disabled.", status: 400 };
670
+ }
671
+
672
+ const serviceSession = await getServiceIpaSession();
673
+ if (!serviceSession.ok) return serviceSession;
674
+
675
+ if (params.provider === "ipa") {
676
+ if (!user.mail) {
677
+ return { ok: false, error: "A local account needs an email address before it can be switched to IPA", status: 400 };
678
+ }
679
+
680
+ const result = await providers.ipa.users.create({
681
+ ipaSession: serviceSession.data,
682
+ profile: currentProfile,
683
+ accountExpires: currentExpiry,
684
+ data: {
685
+ email: user.mail,
686
+ givenname: user.givenname,
687
+ sn: user.sn,
688
+ displayName: user.displayName || undefined,
689
+ autoSendNotification: false,
690
+ },
691
+ });
692
+ if (!result.ok) return result;
693
+ return { ok: true, data: undefined };
694
+ }
695
+
696
+ const response = await freeipa.client.call({
697
+ url: freeIpaConfig.url,
698
+ ipaSession: serviceSession.data,
699
+ method: "user_del",
700
+ args: [user.uid],
701
+ options: {},
702
+ });
703
+ const ipaDeleteMessage = (response.error?.message ?? "").toLowerCase();
704
+ const ipaDeleteNotFound = ipaDeleteMessage.includes("not found") || ipaDeleteMessage.includes("does not exist");
705
+ if (response.error && !ipaDeleteNotFound) {
706
+ return {
707
+ ok: false,
708
+ error: response.error.message ?? "Failed to delete user from FreeIPA",
709
+ status: freeipa.util.mapIpaErrorCode(response.error.code),
710
+ };
711
+ }
712
+
713
+ await sql.begin(async (tx) => {
714
+ await transitionIpaUserToLocal({
715
+ userId: params.id,
716
+ targetProfile: currentProfile,
717
+ accountExpires: currentExpiry,
718
+ db: tx,
719
+ });
720
+ });
721
+
722
+ await session.revokeAllForUser(params.id);
723
+
724
+ return { ok: true, data: undefined };
725
+ };
726
+
727
+ export const remove = async (params: { id: string; actor: { userId: string; uid: string } }): Promise<MutationResult<void>> => {
728
+ const user = await getMinimal({ id: params.id });
729
+ if (!user) return { ok: false, error: "User not found", status: 404 };
730
+
731
+ if (user.provider === "ipa") {
732
+ const serviceSession = await getServiceIpaSession();
733
+ if (!serviceSession.ok) return serviceSession;
734
+ return providers.ipa.users.remove({
735
+ ipaSession: serviceSession.data,
736
+ id: params.id,
737
+ actor: params.actor,
738
+ });
739
+ }
740
+
741
+ return providers.local.users.remove({
742
+ id: params.id,
743
+ actor: params.actor,
744
+ });
745
+ };