@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,499 @@
1
+ import { sql } from "bun";
2
+ import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
3
+ import { escapeLikePattern, isUniqueViolation } from "../postgres";
4
+
5
+ type DbRow = Record<string, unknown>;
6
+
7
+ type LocalGroupRow = {
8
+ id: string;
9
+ provider: "local";
10
+ name: string;
11
+ description: string | null;
12
+ gidNumber: number | null;
13
+ };
14
+
15
+ const toBaseGroup = (row: LocalGroupRow): BaseGroup => ({
16
+ id: row.id,
17
+ provider: row.provider,
18
+ name: row.name,
19
+ description: row.description,
20
+ gidnumber: row.gidNumber,
21
+ });
22
+
23
+ const getLocalGroupById = async (id: string): Promise<LocalGroupRow | null> => {
24
+ const [row] = await sql<DbRow[]>`
25
+ SELECT id, provider, name, description, gid_number
26
+ FROM auth.groups
27
+ WHERE id = ${id}::uuid AND provider = 'local'
28
+ `;
29
+ if (!row) return null;
30
+ return {
31
+ id: row.id as string,
32
+ provider: row.provider as "local",
33
+ name: row.name as string,
34
+ description: row.description as string | null,
35
+ gidNumber: row.gid_number as number | null,
36
+ };
37
+ };
38
+
39
+ const getUserProvider = async (userId: string): Promise<UserProvider | null> => {
40
+ const [row] = await sql<DbRow[]>`SELECT provider FROM auth.users WHERE id = ${userId}`;
41
+ return (row?.provider as UserProvider | undefined) ?? null;
42
+ };
43
+
44
+ const ensureLocalGroupTreeMember = async (groupId: string): Promise<boolean> => {
45
+ const [row] = await sql<DbRow[]>`SELECT 1 FROM auth.groups WHERE id = ${groupId} AND provider = 'local'`;
46
+ return Boolean(row);
47
+ };
48
+
49
+ const wouldCreateLocalGroupCycle = async (params: { parentGroupId: string; childGroupId: string }): Promise<boolean> => {
50
+ if (params.parentGroupId === params.childGroupId) return true;
51
+
52
+ const [row] = await sql<DbRow[]>`
53
+ WITH RECURSIVE descendants AS (
54
+ SELECT gg.child_group_id
55
+ FROM auth.group_groups_v2 gg
56
+ JOIN auth.groups g ON g.id = gg.child_group_id
57
+ WHERE gg.parent_group_id = ${params.childGroupId}::uuid
58
+ AND g.provider = 'local'
59
+ UNION
60
+ SELECT gg.child_group_id
61
+ FROM auth.group_groups_v2 gg
62
+ JOIN auth.groups g ON g.id = gg.child_group_id
63
+ JOIN descendants d ON d.child_group_id = gg.parent_group_id
64
+ WHERE g.provider = 'local'
65
+ )
66
+ SELECT 1
67
+ FROM descendants
68
+ WHERE child_group_id = ${params.parentGroupId}::uuid
69
+ LIMIT 1
70
+ `;
71
+
72
+ return Boolean(row);
73
+ };
74
+
75
+ export const get = async (params: { id: string }): Promise<BaseGroup | null> => {
76
+ const row = await getLocalGroupById(params.id);
77
+ return row ? toBaseGroup(row) : null;
78
+ };
79
+
80
+ export const create = async (params: { name: string; description?: string }): Promise<MutationResult<BaseGroup>> => {
81
+ const storedCn = `local:${params.name}`;
82
+ try {
83
+ const rows = await sql<DbRow[]>`
84
+ INSERT INTO auth.groups (id, cn, provider, name, description, synced_at)
85
+ VALUES (gen_random_uuid(), ${storedCn}, 'local', ${params.name}, ${params.description ?? null}, now())
86
+ RETURNING id, provider, name, description, gid_number
87
+ `;
88
+
89
+ return {
90
+ ok: true,
91
+ data: toBaseGroup({
92
+ id: rows[0]!.id as string,
93
+ provider: "local",
94
+ name: rows[0]!.name as string,
95
+ description: rows[0]!.description as string | null,
96
+ gidNumber: rows[0]!.gid_number as number | null,
97
+ }),
98
+ };
99
+ } catch (error) {
100
+ if (isUniqueViolation(error, "groups_cn_key") || isUniqueViolation(error, "groups_provider_name_unique")) {
101
+ return { ok: false, error: "A local group with this name already exists.", status: 409 };
102
+ }
103
+ throw error;
104
+ }
105
+ };
106
+
107
+ export const list = async (params: { page?: number; perPage?: number; search?: string }) => {
108
+ const page = params.page ?? 1;
109
+ const perPage = params.perPage ?? 100;
110
+ const offset = (page - 1) * perPage;
111
+ const search = params.search?.trim().toLowerCase();
112
+ const pattern = search ? `%${escapeLikePattern(search)}%` : null;
113
+
114
+ const [countRow] = await sql<DbRow[]>`
115
+ SELECT COUNT(*)::int AS count
116
+ FROM auth.groups g
117
+ WHERE g.provider = 'local'
118
+ AND (${pattern}::text IS NULL OR LOWER(g.name) LIKE ${pattern} ESCAPE '\\' OR LOWER(g.description) LIKE ${pattern} ESCAPE '\\')
119
+ `;
120
+ const total = Number(countRow?.count ?? 0);
121
+ const rows = await sql<DbRow[]>`
122
+ SELECT id, provider, name, description, gid_number
123
+ FROM auth.groups g
124
+ WHERE g.provider = 'local'
125
+ AND (${pattern}::text IS NULL OR LOWER(g.name) LIKE ${pattern} ESCAPE '\\' OR LOWER(g.description) LIKE ${pattern} ESCAPE '\\')
126
+ ORDER BY g.name
127
+ LIMIT ${perPage} OFFSET ${offset}
128
+ `;
129
+
130
+ return {
131
+ groups: rows.map((row) =>
132
+ toBaseGroup({
133
+ id: row.id as string,
134
+ provider: row.provider as "local",
135
+ name: row.name as string,
136
+ description: row.description as string | null,
137
+ gidNumber: row.gid_number as number | null,
138
+ }),
139
+ ),
140
+ total,
141
+ pagination: {
142
+ page,
143
+ perPage,
144
+ totalPages: Math.ceil(total / perPage),
145
+ hasNext: page * perPage < total,
146
+ },
147
+ };
148
+ };
149
+
150
+ export const update = async (params: { id: string; description: string }): Promise<MutationResult<void>> => {
151
+ const group = await getLocalGroupById(params.id);
152
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
153
+
154
+ await sql`
155
+ UPDATE auth.groups
156
+ SET description = ${params.description}
157
+ WHERE id = ${params.id}::uuid
158
+ AND provider = 'local'
159
+ `;
160
+ return { ok: true, data: undefined };
161
+ };
162
+
163
+ export const remove = async (params: { id: string }): Promise<MutationResult<void>> => {
164
+ const group = await getLocalGroupById(params.id);
165
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
166
+
167
+ const [primary] = await sql`SELECT 1 FROM auth.user_posix WHERE primary_group_id = ${params.id}::uuid LIMIT 1`;
168
+ if (primary) return { ok: false, error: "This group is a user's primary Linux group and cannot be deleted.", status: 409 };
169
+ try {
170
+ await sql`DELETE FROM auth.groups WHERE id = ${params.id}::uuid AND provider = 'local'`;
171
+ } catch (error) {
172
+ if (error && typeof error === "object" && "errno" in error && error.errno === "23503") {
173
+ return { ok: false, error: "This group is still used by a Linux identity.", status: 409 };
174
+ }
175
+ throw error;
176
+ }
177
+ return { ok: true, data: undefined };
178
+ };
179
+
180
+ export const getMembers = async (params: { id: string; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
181
+ const group = await getLocalGroupById(params.id);
182
+ if (!group) return [];
183
+ const members: GroupMember[] = [];
184
+
185
+ if (!params.type || params.type === "user") {
186
+ const userRows = params.recursive
187
+ ? await sql<DbRow[]>`
188
+ WITH RECURSIVE local_group_tree AS (
189
+ SELECT ${group.id}::uuid AS group_id
190
+ UNION
191
+ SELECT gg.child_group_id
192
+ FROM auth.group_groups_v2 gg
193
+ JOIN auth.groups g ON g.id = gg.child_group_id
194
+ JOIN local_group_tree tree ON tree.group_id = gg.parent_group_id
195
+ WHERE g.provider = 'local'
196
+ )
197
+ SELECT DISTINCT u.id, u.uid, u.display_name
198
+ FROM local_group_tree tree
199
+ JOIN auth.user_groups_v2 ug ON ug.group_id = tree.group_id
200
+ JOIN auth.users u ON u.id = ug.user_id
201
+ ORDER BY u.uid
202
+ `
203
+ : await sql<DbRow[]>`
204
+ SELECT u.id, u.uid, u.display_name
205
+ FROM auth.user_groups_v2 ug
206
+ JOIN auth.users u ON u.id = ug.user_id
207
+ WHERE ug.group_id = ${group.id}
208
+ ORDER BY u.uid
209
+ `;
210
+ for (const row of userRows) {
211
+ members.push({ type: "user", id: row.id as string, displayName: (row.display_name as string | null) ?? (row.uid as string) });
212
+ }
213
+ }
214
+
215
+ if (!params.type || params.type === "group") {
216
+ const groupRows = params.recursive
217
+ ? await sql<DbRow[]>`
218
+ WITH RECURSIVE local_group_tree AS (
219
+ SELECT gg.child_group_id
220
+ FROM auth.group_groups_v2 gg
221
+ JOIN auth.groups g ON g.id = gg.child_group_id
222
+ WHERE gg.parent_group_id = ${group.id}::uuid
223
+ AND g.provider = 'local'
224
+ UNION
225
+ SELECT gg.child_group_id
226
+ FROM auth.group_groups_v2 gg
227
+ JOIN auth.groups g ON g.id = gg.child_group_id
228
+ JOIN local_group_tree tree ON tree.child_group_id = gg.parent_group_id
229
+ WHERE g.provider = 'local'
230
+ )
231
+ SELECT DISTINCT g.id, g.name
232
+ FROM local_group_tree tree
233
+ JOIN auth.groups g ON g.id = tree.child_group_id
234
+ ORDER BY g.name
235
+ `
236
+ : await sql<DbRow[]>`
237
+ SELECT g.id, g.name
238
+ FROM auth.group_groups_v2 gg
239
+ JOIN auth.groups g ON g.id = gg.child_group_id
240
+ WHERE gg.parent_group_id = ${group.id} AND g.provider = 'local'
241
+ ORDER BY g.name
242
+ `;
243
+ for (const row of groupRows) {
244
+ members.push({ type: "group", id: row.id as string, displayName: row.name as string });
245
+ }
246
+ }
247
+
248
+ return members;
249
+ };
250
+
251
+ export const getManagers = async (params: { id: string; type?: "user" | "group"; recursive?: boolean }): Promise<GroupMember[]> => {
252
+ const group = await getLocalGroupById(params.id);
253
+ if (!group) return [];
254
+
255
+ const managers: GroupMember[] = [];
256
+
257
+ if (!params.type || params.type === "user") {
258
+ // Match IPA semantics (see ipa/groups.ts getManagers): start from the
259
+ // target's *direct manager groups*, then walk UPWARDS via parent
260
+ // relations, then collect members of every group in that set plus direct
261
+ // user managers of the target. The previous implementation walked DOWN
262
+ // the target's *child* tree and treated their managers as managers of the
263
+ // parent, which over-broadens authorization (a manager of a sub-team
264
+ // would inherit rights on the parent team).
265
+ const userRows = params.recursive
266
+ ? await sql<DbRow[]>`
267
+ WITH RECURSIVE manager_groups AS (
268
+ SELECT gmg.manager_group_id AS group_id
269
+ FROM auth.group_manager_groups_v2 gmg
270
+ JOIN auth.groups g_manager ON g_manager.id = gmg.manager_group_id
271
+ WHERE gmg.group_id = ${group.id} AND g_manager.provider = 'local'
272
+ UNION
273
+ SELECT gg.parent_group_id AS group_id
274
+ FROM auth.group_groups_v2 gg
275
+ JOIN auth.groups g_parent ON g_parent.id = gg.parent_group_id
276
+ JOIN manager_groups mg ON gg.child_group_id = mg.group_id
277
+ WHERE g_parent.provider = 'local'
278
+ )
279
+ SELECT DISTINCT u.id, u.uid, u.display_name
280
+ FROM (
281
+ SELECT gmu.user_id
282
+ FROM auth.group_manager_users_v2 gmu
283
+ WHERE gmu.group_id = ${group.id}
284
+ UNION
285
+ SELECT ug.user_id
286
+ FROM auth.user_groups_v2 ug
287
+ JOIN manager_groups mg ON ug.group_id = mg.group_id
288
+ ) all_managers
289
+ JOIN auth.users u ON u.id = all_managers.user_id
290
+ ORDER BY u.uid
291
+ `
292
+ : await sql<DbRow[]>`
293
+ SELECT u.id, u.uid, u.display_name
294
+ FROM auth.group_manager_users_v2 gmu
295
+ JOIN auth.users u ON u.id = gmu.user_id
296
+ WHERE gmu.group_id = ${group.id}
297
+ ORDER BY u.uid
298
+ `;
299
+ for (const row of userRows) {
300
+ managers.push({ type: "user", id: row.id as string, displayName: (row.display_name as string | null) ?? (row.uid as string) });
301
+ }
302
+ }
303
+
304
+ if (!params.type || params.type === "group") {
305
+ // Direct manager groups only. Parents of manager groups are managers-by-transitivity
306
+ // at the user level (handled above), but we don't conflate them into "group managers"
307
+ // of the target — that would misrepresent the configured relations.
308
+ const groupRows = await sql<DbRow[]>`
309
+ SELECT g.id, g.name
310
+ FROM auth.group_manager_groups_v2 gmg
311
+ JOIN auth.groups g ON g.id = gmg.manager_group_id
312
+ WHERE gmg.group_id = ${group.id} AND g.provider = 'local'
313
+ ORDER BY g.name
314
+ `;
315
+ for (const row of groupRows) {
316
+ managers.push({ type: "group", id: row.id as string, displayName: row.name as string });
317
+ }
318
+ }
319
+
320
+ return managers;
321
+ };
322
+
323
+ export const getParents = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
324
+ const group = await getLocalGroupById(params.id);
325
+ if (!group) return [];
326
+
327
+ const rows = params.recursive
328
+ ? await sql<DbRow[]>`
329
+ WITH RECURSIVE local_parent_tree AS (
330
+ SELECT gg.parent_group_id
331
+ FROM auth.group_groups_v2 gg
332
+ JOIN auth.groups g ON g.id = gg.parent_group_id
333
+ WHERE gg.child_group_id = ${group.id}::uuid
334
+ AND g.provider = 'local'
335
+ UNION
336
+ SELECT gg.parent_group_id
337
+ FROM auth.group_groups_v2 gg
338
+ JOIN auth.groups g ON g.id = gg.parent_group_id
339
+ JOIN local_parent_tree tree ON tree.parent_group_id = gg.child_group_id
340
+ WHERE g.provider = 'local'
341
+ )
342
+ SELECT DISTINCT parent_group_id
343
+ FROM local_parent_tree
344
+ `
345
+ : await sql<DbRow[]>`
346
+ SELECT gg.parent_group_id
347
+ FROM auth.group_groups_v2 gg
348
+ JOIN auth.groups g ON g.id = gg.parent_group_id
349
+ WHERE gg.child_group_id = ${group.id}
350
+ AND g.provider = 'local'
351
+ ORDER BY g.name
352
+ `;
353
+
354
+ return rows.map((row) => row.parent_group_id as string);
355
+ };
356
+
357
+ export const getManagedGroups = async (params: { id: string; recursive?: boolean }): Promise<string[]> => {
358
+ const group = await getLocalGroupById(params.id);
359
+ if (!group) return [];
360
+
361
+ const rows = params.recursive
362
+ ? await sql<DbRow[]>`
363
+ WITH RECURSIVE local_manager_tree AS (
364
+ SELECT ${group.id}::uuid AS manager_group_id
365
+ UNION
366
+ SELECT gg.child_group_id
367
+ FROM auth.group_groups_v2 gg
368
+ JOIN auth.groups g ON g.id = gg.child_group_id
369
+ JOIN local_manager_tree tree ON tree.manager_group_id = gg.parent_group_id
370
+ WHERE g.provider = 'local'
371
+ )
372
+ SELECT DISTINCT gmg.group_id
373
+ FROM local_manager_tree tree
374
+ JOIN auth.group_manager_groups_v2 gmg ON gmg.manager_group_id = tree.manager_group_id
375
+ JOIN auth.groups g ON g.id = gmg.group_id
376
+ WHERE g.provider = 'local'
377
+ `
378
+ : await sql<DbRow[]>`
379
+ SELECT gmg.group_id
380
+ FROM auth.group_manager_groups_v2 gmg
381
+ JOIN auth.groups g ON g.id = gmg.group_id
382
+ WHERE gmg.manager_group_id = ${group.id}
383
+ AND g.provider = 'local'
384
+ ORDER BY g.name
385
+ `;
386
+
387
+ return rows.map((row) => row.group_id as string);
388
+ };
389
+
390
+ export const addMember = async (params: { id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
391
+ const group = await getLocalGroupById(params.id);
392
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
393
+
394
+ if (params.user) {
395
+ const provider = await getUserProvider(params.user);
396
+ if (!provider) return { ok: false, error: "User not found", status: 404 };
397
+ const [existing] = await sql<DbRow[]>`
398
+ SELECT 1
399
+ FROM auth.user_groups_v2
400
+ WHERE user_id = ${params.user}::uuid
401
+ AND group_id = ${group.id}::uuid
402
+ LIMIT 1
403
+ `;
404
+ if (existing) return { ok: false, error: "User is already a direct member of this group", status: 409 };
405
+ await sql`INSERT INTO auth.user_groups_v2 (user_id, group_id) VALUES (${params.user}, ${group.id}) ON CONFLICT DO NOTHING`;
406
+ return { ok: true, data: undefined };
407
+ }
408
+
409
+ if (params.group) {
410
+ const isLocal = await ensureLocalGroupTreeMember(params.group);
411
+ if (!isLocal) return { ok: false, error: "Only local groups can be nested into local groups", status: 400 };
412
+ const [existing] = await sql<DbRow[]>`
413
+ SELECT 1
414
+ FROM auth.group_groups_v2
415
+ WHERE parent_group_id = ${group.id}::uuid
416
+ AND child_group_id = ${params.group}::uuid
417
+ LIMIT 1
418
+ `;
419
+ if (existing) return { ok: false, error: "Group is already a direct member of this group", status: 409 };
420
+ if (await wouldCreateLocalGroupCycle({ parentGroupId: group.id, childGroupId: params.group })) {
421
+ return { ok: false, error: "Local group nesting cannot create cycles", status: 400 };
422
+ }
423
+ await sql`INSERT INTO auth.group_groups_v2 (parent_group_id, child_group_id) VALUES (${group.id}, ${params.group}) ON CONFLICT DO NOTHING`;
424
+ return { ok: true, data: undefined };
425
+ }
426
+
427
+ return { ok: false, error: "Missing member", status: 400 };
428
+ };
429
+
430
+ export const removeMember = async (params: { id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
431
+ const group = await getLocalGroupById(params.id);
432
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
433
+
434
+ if (params.user) {
435
+ await sql`DELETE FROM auth.user_groups_v2 WHERE user_id = ${params.user}::uuid AND group_id = ${group.id}::uuid`;
436
+ return { ok: true, data: undefined };
437
+ }
438
+
439
+ if (params.group) {
440
+ await sql`DELETE FROM auth.group_groups_v2 WHERE parent_group_id = ${group.id}::uuid AND child_group_id = ${params.group}::uuid`;
441
+ return { ok: true, data: undefined };
442
+ }
443
+
444
+ return { ok: false, error: "Missing member", status: 400 };
445
+ };
446
+
447
+ export const addManager = async (params: { id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
448
+ const group = await getLocalGroupById(params.id);
449
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
450
+
451
+ if (params.user) {
452
+ const provider = await getUserProvider(params.user);
453
+ if (!provider) return { ok: false, error: "User not found", status: 404 };
454
+ const [existing] = await sql<DbRow[]>`
455
+ SELECT 1
456
+ FROM auth.group_manager_users_v2
457
+ WHERE group_id = ${group.id}::uuid
458
+ AND user_id = ${params.user}::uuid
459
+ LIMIT 1
460
+ `;
461
+ if (existing) return { ok: false, error: "User is already a direct manager of this group", status: 409 };
462
+ await sql`INSERT INTO auth.group_manager_users_v2 (group_id, user_id) VALUES (${group.id}, ${params.user}) ON CONFLICT DO NOTHING`;
463
+ return { ok: true, data: undefined };
464
+ }
465
+
466
+ if (params.group) {
467
+ const isLocal = await ensureLocalGroupTreeMember(params.group);
468
+ if (!isLocal) return { ok: false, error: "Only local groups can manage local groups", status: 400 };
469
+ const [existing] = await sql<DbRow[]>`
470
+ SELECT 1
471
+ FROM auth.group_manager_groups_v2
472
+ WHERE group_id = ${group.id}::uuid
473
+ AND manager_group_id = ${params.group}::uuid
474
+ LIMIT 1
475
+ `;
476
+ if (existing) return { ok: false, error: "Group is already a direct manager of this group", status: 409 };
477
+ await sql`INSERT INTO auth.group_manager_groups_v2 (group_id, manager_group_id) VALUES (${group.id}, ${params.group}) ON CONFLICT DO NOTHING`;
478
+ return { ok: true, data: undefined };
479
+ }
480
+
481
+ return { ok: false, error: "Missing manager", status: 400 };
482
+ };
483
+
484
+ export const removeManager = async (params: { id: string; user?: string; group?: string }): Promise<MutationResult<void>> => {
485
+ const group = await getLocalGroupById(params.id);
486
+ if (!group) return { ok: false, error: "Group not found", status: 404 };
487
+
488
+ if (params.user) {
489
+ await sql`DELETE FROM auth.group_manager_users_v2 WHERE group_id = ${group.id}::uuid AND user_id = ${params.user}::uuid`;
490
+ return { ok: true, data: undefined };
491
+ }
492
+
493
+ if (params.group) {
494
+ await sql`DELETE FROM auth.group_manager_groups_v2 WHERE group_id = ${group.id}::uuid AND manager_group_id = ${params.group}::uuid`;
495
+ return { ok: true, data: undefined };
496
+ }
497
+
498
+ return { ok: false, error: "Missing manager", status: 400 };
499
+ };
@@ -0,0 +1,129 @@
1
+ import type { UserProfile, UserProvider } from "../../contracts/shared";
2
+ import * as settings from "../settings";
3
+
4
+ export type IpaMatchMode = "ignore" | "migrate";
5
+ export type IpaAccountTransitionPolicy = "delete" | "demote_to_local" | "demote_to_local_guest" | "demote_to_local_user";
6
+
7
+ export const isGuestProfile = (profile: UserProfile): boolean => profile === "guest";
8
+ export const isIpaProvider = (provider: UserProvider): boolean => provider === "ipa";
9
+ export const isLocalProvider = (provider: UserProvider): boolean => provider === "local";
10
+ export const canPersistStoredAdmin = (provider: UserProvider, profile: UserProfile): boolean => provider === "local" && profile === "user";
11
+
12
+ export const isAccountExpired = (accountExpires: string | null, now = Date.now()): boolean => {
13
+ if (accountExpires === null) return false;
14
+ const expiresAt = Date.parse(accountExpires);
15
+ return !Number.isFinite(expiresAt) || expiresAt <= now;
16
+ };
17
+
18
+ export const parseIpaMatchMode = (value: string | null | undefined): IpaMatchMode => (value === "migrate" ? "migrate" : "ignore");
19
+
20
+ export const parseIpaAccountTransitionPolicy = (value: string | null | undefined): IpaAccountTransitionPolicy => {
21
+ if (value === "delete" || value === "demote_to_local" || value === "demote_to_local_user") return value;
22
+ return "demote_to_local_guest";
23
+ };
24
+
25
+ /**
26
+ * Pure helpers — caller passes the relevant FreeIPA group lists in. Avoids
27
+ * an implicit settings dependency inside what is otherwise a pure data
28
+ * transformation; the caller already needs `getFreeIpaConfig()` for other
29
+ * fields, so reading both lists at once is a single roundtrip.
30
+ */
31
+ export const calculateIpaProfileFromGroupNames = (groupNames: string[], groupsBaseIpaRealm: string[]): UserProfile =>
32
+ groupsBaseIpaRealm.some((group) => groupNames.includes(group)) ? "user" : "guest";
33
+
34
+ export const deriveIpaAdminFromGroupNames = (groupNames: string[], groupsAdmin: string[]): boolean =>
35
+ groupsAdmin.some((group) => groupNames.includes(group));
36
+
37
+ export const resolveStoredAdminState = (params: {
38
+ provider: UserProvider;
39
+ profile: UserProfile;
40
+ currentAdmin?: boolean;
41
+ requestedAdmin?: boolean;
42
+ }): boolean => {
43
+ if (!canPersistStoredAdmin(params.provider, params.profile)) return false;
44
+ return params.requestedAdmin ?? params.currentAdmin ?? false;
45
+ };
46
+
47
+ /**
48
+ * `groupsAdmin` is required when `provider === "ipa"`; defaults to `[]` (no
49
+ * admin grant via group membership) when omitted, matching the previous sync
50
+ * behaviour where an unconfigured/disabled FreeIPA returned an empty list.
51
+ */
52
+ export const resolveEffectiveAdminState = (params: {
53
+ provider: UserProvider;
54
+ storedAdmin?: boolean;
55
+ memberofGroup?: string[];
56
+ groupsAdmin?: string[];
57
+ }): boolean => {
58
+ if (params.provider === "ipa") {
59
+ return deriveIpaAdminFromGroupNames(params.memberofGroup ?? [], params.groupsAdmin ?? []);
60
+ }
61
+ return params.storedAdmin ?? false;
62
+ };
63
+
64
+ export const resolveAccountExpires = (row: Record<string, unknown>): Date | null => {
65
+ return (row.account_expires as Date | null | undefined) ?? null;
66
+ };
67
+
68
+ /**
69
+ * Discriminated parse of a user-supplied expiry value. An explicit `null` or
70
+ * absent/empty string means "no expiry"; a malformed non-empty string is an
71
+ * error — callers must surface that as a 400 rather than silently wiping the
72
+ * expiry.
73
+ */
74
+ export type ParsedExpiry = { ok: true; date: Date | null } | { ok: false; error: string };
75
+
76
+ export const parseManualAccountExpiry = (value: string | null | undefined): ParsedExpiry => {
77
+ if (value === null || value === undefined || value === "") {
78
+ return { ok: true, date: null };
79
+ }
80
+ const date = new Date(value);
81
+ if (Number.isNaN(date.getTime())) {
82
+ return { ok: false, error: "Invalid expiry date" };
83
+ }
84
+ date.setUTCHours(23, 59, 59, 0);
85
+ return { ok: true, date };
86
+ };
87
+
88
+ /**
89
+ * Legacy name kept for internal defaulting path where the input is already
90
+ * known-valid (Zod has validated it). Prefer `parseManualAccountExpiry` for
91
+ * new code — it distinguishes "no expiry" from "invalid input".
92
+ */
93
+ export const normalizeManualAccountExpiry = (value: string | null | undefined): Date | null => {
94
+ const parsed = parseManualAccountExpiry(value);
95
+ return parsed.ok ? parsed.date : null;
96
+ };
97
+
98
+ export const getConfiguredExpiryDays = async (provider: UserProvider, profile: UserProfile): Promise<number> => {
99
+ if (provider === "ipa") {
100
+ const configured = await settings.get<number | null>("user.account.ipa_expires_days");
101
+ return typeof configured === "number" ? configured : 365;
102
+ }
103
+
104
+ if (profile === "guest") {
105
+ const configured = await settings.get<number | null>("user.account.local_guest_expires_days");
106
+ return typeof configured === "number" ? configured : 365;
107
+ }
108
+
109
+ const configured = await settings.get<number | null>("user.account.local_user_expires_days");
110
+ return typeof configured === "number" ? configured : 0;
111
+ };
112
+
113
+ export const getDefaultAccountExpiry = async (provider: UserProvider, profile: UserProfile): Promise<Date | null> => {
114
+ const days = await getConfiguredExpiryDays(provider, profile);
115
+ if (days <= 0) return null;
116
+ const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
117
+ if (provider === "ipa") expiresAt.setUTCHours(23, 59, 59, 0);
118
+ return expiresAt;
119
+ };
120
+
121
+ export const resolveTargetAccountExpiry = async (params: {
122
+ provider: UserProvider;
123
+ profile: UserProfile;
124
+ requested?: string | null;
125
+ }): Promise<Date | null> => {
126
+ const manual = normalizeManualAccountExpiry(params.requested);
127
+ if (params.requested !== undefined) return manual;
128
+ return getDefaultAccountExpiry(params.provider, params.profile);
129
+ };
@@ -0,0 +1,31 @@
1
+ import type { TypedNotificationSendResult } from "../notifications/platform";
2
+
3
+ export type AccountNotificationDeliveryResult = Pick<TypedNotificationSendResult, "id" | "status">;
4
+
5
+ export type AccountsNotificationSender = {
6
+ sendLoginLink: (input: { email: string; token: string; magicLink: string; locale?: string }) => Promise<AccountNotificationDeliveryResult>;
7
+ sendFreeIpaWelcome: (input: {
8
+ userId: string;
9
+ uid: string;
10
+ temporaryPassword: string;
11
+ accountExpires: string | null;
12
+ locale?: string;
13
+ }) => Promise<AccountNotificationDeliveryResult>;
14
+ sendLocalWelcome: (input: { userId: string; email: string; accountExpires: string | null; locale?: string }) => Promise<AccountNotificationDeliveryResult>;
15
+ sendRequestDenied: (input: {
16
+ requestId: string;
17
+ userId: string;
18
+ firstName: string;
19
+ reason: string;
20
+ sentBy: string;
21
+ locale?: string;
22
+ }) => Promise<AccountNotificationDeliveryResult>;
23
+ sendAdministrativeMessage: (input: {
24
+ idempotencyKey: string;
25
+ userId: string;
26
+ subject: string;
27
+ rawHtml: string;
28
+ sentBy: string;
29
+ locale?: string;
30
+ }) => Promise<AccountNotificationDeliveryResult>;
31
+ };