@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,152 @@
1
+ import type { JobContext, PumpConfig, Sync } from "@k2b/sync";
2
+ import { sql } from "bun";
3
+ import { freeipa } from "../../server/services/freeipa";
4
+ import { getConfiguredExpiryDays } from "../account-model";
5
+ import { getFreeIpaConfig } from "../freeipa-config";
6
+ import { logger } from "../logging";
7
+
8
+ const log = logger("auth:ipa:backfill");
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+
11
+ export type IpaBackfillInput = { runId: string; minimumExpiry: string; createdBefore: string } | null;
12
+ type IpaBackfillAccount = { userId: string; uid: string; minimumExpiry: string };
13
+ type IpaBackfillItem = { key: string; uid: string };
14
+
15
+ /** Capture one target before accepting work; retries never move it forward. */
16
+ export const prepareIpaBackfill = async (runId: string): Promise<IpaBackfillInput> => {
17
+ const config = await getFreeIpaConfig();
18
+ if (!config.enabled) return null;
19
+ if (!config.configured) {
20
+ throw new Error(`FreeIPA is enabled but not fully configured. Missing: ${config.missingSettings.join(", ")}.`);
21
+ }
22
+ const days = await getConfiguredExpiryDays("ipa", "user");
23
+ if (days <= 0) return null;
24
+ const minimumExpiry = new Date(Date.now() + Math.max(days, 7) * DAY_MS);
25
+ minimumExpiry.setUTCHours(23, 59, 59, 0);
26
+ // Preserve Postgres timestamp precision when bounding the finite scan.
27
+ const [snapshot] = await sql<{ created_before: string }[]>`SELECT CURRENT_TIMESTAMP::text AS created_before`;
28
+ if (!snapshot) throw new Error("Could not capture the IPA backfill cutoff");
29
+ return { runId, minimumExpiry: minimumExpiry.toISOString(), createdBefore: snapshot.created_before };
30
+ };
31
+
32
+ export const ipaBackfillConfig: Omit<PumpConfig<IpaBackfillInput, string, IpaBackfillItem>, "dispatch"> = {
33
+ id: "auth:ipa:backfill",
34
+ owner: "core",
35
+ // Keep one bounded producer run at a time; account jobs own directory writes.
36
+ maxActiveRuns: 1,
37
+ dispatchConcurrency: 1,
38
+ leaseMs: 300_000,
39
+ retry: { maxAttempts: 2, backoffMs: [2000, 4000] },
40
+ pull: async ({ input, cursor, limit, signal }) => {
41
+ signal.throwIfAborted();
42
+ if (!input) return { items: [], nextCursor: null };
43
+ const rows = await sql<{ id: string; uid: string }[]>`
44
+ SELECT id, uid
45
+ FROM auth.users
46
+ WHERE provider = 'ipa'
47
+ AND created_at <= ${input.createdBefore}::timestamptz
48
+ AND (${cursor}::uuid IS NULL OR id > ${cursor}::uuid)
49
+ AND (account_expires IS NULL OR account_expires < ${input.minimumExpiry}::timestamptz)
50
+ ORDER BY id
51
+ LIMIT ${limit}
52
+ `;
53
+ signal.throwIfAborted();
54
+ return {
55
+ items: rows.map((row) => ({ key: row.id, uid: row.uid })),
56
+ nextCursor: rows.length === limit ? (rows.at(-1)?.id ?? null) : null,
57
+ };
58
+ },
59
+ };
60
+
61
+ export const processIpaBackfillAccount = async ({
62
+ input,
63
+ signal,
64
+ }: Pick<JobContext<IpaBackfillAccount>, "input" | "signal">): Promise<void> => {
65
+ signal.throwIfAborted();
66
+ const config = await getFreeIpaConfig();
67
+ // Disabling the provider fails accepted work through the retry policy;
68
+ // it must not checkpoint unfinished directory effects as successful.
69
+ if (!config.enabled || !config.configured) throw new Error("FreeIPA backfill requires an enabled, configured provider");
70
+ const minimumExpiry = new Date(input.minimumExpiry);
71
+ await sql.begin(async (tx) => {
72
+ const [account] = await tx<{ account_expires: Date | null }[]>`
73
+ SELECT account_expires
74
+ FROM auth.users
75
+ WHERE id = ${input.userId}::uuid AND uid = ${input.uid} AND provider = 'ipa'
76
+ FOR UPDATE
77
+ `;
78
+ signal.throwIfAborted();
79
+ // The durable item identifies the original account, never a replacement
80
+ // with the same username. A later local expiry needs no backfill.
81
+ if (!account || (account.account_expires && account.account_expires >= minimumExpiry)) return;
82
+ const ipaSession = await freeipa.session.getServiceSession({ ...config, signal });
83
+ const remote = await freeipa.client.call({
84
+ url: config.url,
85
+ ipaSession,
86
+ method: "user_show",
87
+ args: [input.uid],
88
+ options: { all: true },
89
+ signal,
90
+ });
91
+ if (remote.error) throw new Error(`IPA backfill read failed for ${input.uid}: ${remote.error.message}`);
92
+ const record = remote.result?.result;
93
+ if (
94
+ !record ||
95
+ typeof record !== "object" ||
96
+ Array.isArray(record) ||
97
+ !("uid" in record) ||
98
+ freeipa.util.str(record.uid) !== input.uid
99
+ ) {
100
+ throw new Error(`IPA backfill received an invalid account for ${input.uid}`);
101
+ }
102
+ const expiryValue = "krbprincipalexpiration" in record ? record.krbprincipalexpiration : undefined;
103
+ const remoteExpiry = freeipa.util.parseGeneralizedTime(expiryValue);
104
+ if (freeipa.util.str(expiryValue) && !remoteExpiry) {
105
+ throw new Error(`IPA backfill received an invalid expiry for ${input.uid}`);
106
+ }
107
+ signal.throwIfAborted();
108
+ const target = remoteExpiry && remoteExpiry >= minimumExpiry ? remoteExpiry : minimumExpiry;
109
+ if (target === minimumExpiry) {
110
+ const response = await freeipa.client.call({
111
+ url: config.url,
112
+ ipaSession,
113
+ method: "user_mod",
114
+ args: [input.uid],
115
+ options: { krbprincipalexpiration: freeipa.util.toGeneralizedTime(target) },
116
+ signal,
117
+ });
118
+ if (response.error) throw new Error(`IPA backfill failed for ${input.uid}: ${response.error.message}`);
119
+ }
120
+ signal.throwIfAborted();
121
+ await tx`
122
+ UPDATE auth.users SET account_expires = ${target} WHERE id = ${input.userId}::uuid
123
+ `;
124
+ await tx`
125
+ INSERT INTO auth.user_ipa_data (user_id, synced_at)
126
+ VALUES (${input.userId}::uuid, now())
127
+ ON CONFLICT (user_id) DO UPDATE SET synced_at = EXCLUDED.synced_at
128
+ `;
129
+ signal.throwIfAborted();
130
+ });
131
+ log.info("IPA expiry backfill account complete", { userId: input.userId, uid: input.uid, minimumExpiry: input.minimumExpiry });
132
+ };
133
+
134
+ export const declareIpaBackfill = (sync: Sync) => {
135
+ const accounts = sync.job<IpaBackfillAccount>({
136
+ id: "auth:ipa:backfill:account",
137
+ owner: "core",
138
+ delivery: { maxInFlight: 1, ackWaitMs: 300_000, maxAttempts: 2, backoffMs: [2000, 4000] },
139
+ });
140
+ const pump = sync.pump({
141
+ ...ipaBackfillConfig,
142
+ dispatch: async ({ input, item, signal }) => {
143
+ signal.throwIfAborted();
144
+ if (!input) return;
145
+ await accounts.submit({
146
+ key: `${input.runId}:${item.key}`,
147
+ input: { userId: item.key, uid: item.uid, minimumExpiry: input.minimumExpiry },
148
+ });
149
+ },
150
+ });
151
+ return { pump, accounts };
152
+ };
@@ -0,0 +1,13 @@
1
+ import type { TypedNotificationSendResult } from "../notifications/platform";
2
+
3
+ export type AccountLifecycleNotificationSender = {
4
+ sendExpiryReminder: (input: {
5
+ reminderId: string;
6
+ userId: string;
7
+ firstName: string;
8
+ displayName: string;
9
+ expiresAt: string;
10
+ accountKind: "ipa" | "local-user" | "local-guest";
11
+ locale?: string;
12
+ }) => Promise<Pick<TypedNotificationSendResult, "id" | "status">>;
13
+ };
@@ -0,0 +1,506 @@
1
+ import type { DeliveryConfig, JobContext, Worker } from "@k2b/sync";
2
+ import { lazySync } from "../../_internal/process-sync";
3
+ import { logger, logging, trace } from "../logging";
4
+ import { providers } from "../providers";
5
+ import { get as getSetting } from "../settings";
6
+ import { accountLifecycle } from "./index";
7
+ import { declareIpaBackfill, prepareIpaBackfill, processIpaBackfillAccount } from "./ipa-backfill";
8
+ import type { AccountLifecycleNotificationSender } from "./notification-sender";
9
+
10
+ const log = logger("auth:lifecycle:scheduler");
11
+ const ipaSyncLog = logger("auth:ipa:sync");
12
+ const reminderLog = logger("auth:reminder:daily");
13
+ const guestCleanupLog = logger("auth:guest:cleanup");
14
+ const localUserCleanupLog = logger("auth:local-user:cleanup");
15
+ const auditCleanupLog = logger("auth:lifecycle:audit:cleanup");
16
+ const localUserBackfillLog = logger("auth:local-user:backfill");
17
+ const guestBackfillLog = logger("auth:guest:backfill");
18
+ const logCleanupLog = logger("logging");
19
+ const DEFAULT_IPA_SYNC_CRON = "*/5 * * * *";
20
+ const IPA_SYNC_LEASE_MS = 120_000;
21
+ // Scheduled scans retry in minutes: a short FreeIPA or database blip must not
22
+ // dead-letter a slot; anything longer surfaces once as a dead letter and the
23
+ // next slot retries anyway. Sync releases a coalesced key when it dead-letters
24
+ // the run (job `onDeadLetter`), so the stable `scheduled` key is not pinned
25
+ // for the dead letter's retention and the next slot can submit again.
26
+ const SCAN_RETRY = { maxAttempts: 3, backoffMs: [60_000, 300_000] } satisfies Partial<DeliveryConfig>;
27
+ const ipaSyncMutex = lazySync((sync) => sync.mutex({ id: "auth:ipa:sync", ttlMs: IPA_SYNC_LEASE_MS, retry: { maxAttempts: 1 } }));
28
+ let notificationSender: AccountLifecycleNotificationSender | null = null;
29
+
30
+ type JobSummary = {
31
+ scanned: number;
32
+ changed: number;
33
+ skipped: number;
34
+ failed: number;
35
+ };
36
+
37
+ const abortedSummary = (): JobSummary => ({ scanned: 0, changed: 0, skipped: 0, failed: 0 });
38
+
39
+ const toDemotionLog = (summary: JobSummary) => ({
40
+ expiredCandidates: summary.scanned,
41
+ demotedToGuest: summary.changed,
42
+ skipped: summary.skipped,
43
+ failed: summary.failed,
44
+ });
45
+
46
+ const toReminderLog = (summary: JobSummary) => ({
47
+ candidates: summary.scanned,
48
+ sent: summary.changed,
49
+ skipped: summary.skipped,
50
+ failed: summary.failed,
51
+ });
52
+
53
+ const toCleanupLog = (summary: JobSummary) => ({
54
+ candidates: summary.scanned,
55
+ deleted: summary.changed,
56
+ skipped: summary.skipped,
57
+ failed: summary.failed,
58
+ });
59
+
60
+ const toBackfillLog = (summary: JobSummary) => ({
61
+ candidates: summary.scanned,
62
+ updated: summary.changed,
63
+ skipped: summary.skipped,
64
+ failed: summary.failed,
65
+ });
66
+
67
+ /** Presentation of the run span Sync already opened for this job (`trace.syncSpanKey`). */
68
+ const jobSpan = (ctx: JobContext<null>, id: string, name: string, source = id) => ({
69
+ name,
70
+ source,
71
+ appId: "core",
72
+ category: "job" as const,
73
+ kind: "consumer" as const,
74
+ spanKey: trace.syncSpanKey("job", id, ctx.jobId),
75
+ });
76
+
77
+ const getCronSetting = async (key: string, fallback: string): Promise<string> => {
78
+ const value = String((await getSetting<string>(key)) || "").trim();
79
+ return value.length > 0 ? value : fallback;
80
+ };
81
+
82
+ const getTimezoneSetting = async (): Promise<string> => {
83
+ const value = String((await getSetting<string>("app.timezone")) || "").trim();
84
+ return value.length > 0 ? value : "Europe/Berlin";
85
+ };
86
+
87
+ // ── Jobs ───────────────────────────────────────────────────────────────
88
+
89
+ const ipaSyncJob = lazySync((sync) =>
90
+ sync.job<null>({
91
+ id: "auth:ipa:sync",
92
+ delivery: { ackWaitMs: IPA_SYNC_LEASE_MS, ...SCAN_RETRY },
93
+ }),
94
+ );
95
+ const processIpaSyncJob = async (ctx: JobContext<null>): Promise<void> => {
96
+ await trace.withSpan(
97
+ jobSpan(ctx, "auth:ipa:sync", "FreeIPA account sync"),
98
+ async () => {
99
+ if (ctx.signal.aborted) return abortedSummary();
100
+ const lock = await ipaSyncMutex().acquire({ resource: "global" });
101
+ if (!lock) {
102
+ ipaSyncLog.info("Sync skipped", { reason: "already_running" });
103
+ return abortedSummary();
104
+ }
105
+ const heartbeat = async () => {
106
+ await ctx.heartbeat();
107
+ if (!(await ipaSyncMutex().extend(lock, { ttlMs: IPA_SYNC_LEASE_MS }))) {
108
+ throw new Error("Lost FreeIPA synchronization lock");
109
+ }
110
+ };
111
+ try {
112
+ try {
113
+ await providers.ipa.sync.run({ signal: ctx.signal, heartbeat });
114
+ } catch (error) {
115
+ ipaSyncLog.error("Sync step failed", { step: "sync", error: error instanceof Error ? error.message : String(error) });
116
+ throw error;
117
+ }
118
+ await heartbeat();
119
+ try {
120
+ const summary = await accountLifecycle.demoteExpiredIpaUsers({ signal: ctx.signal, heartbeat });
121
+ ipaSyncLog.info("Expired IPA demotion complete", toDemotionLog(summary));
122
+ return summary;
123
+ } catch (error) {
124
+ ipaSyncLog.error("Expired IPA demotion step failed", {
125
+ step: "demote-expired",
126
+ error: error instanceof Error ? error.message : String(error),
127
+ });
128
+ throw error;
129
+ }
130
+ } finally {
131
+ await ipaSyncMutex()
132
+ .release(lock)
133
+ .catch((error) => {
134
+ ipaSyncLog.error("Failed to release synchronization lock", {
135
+ error: error instanceof Error ? error.message : String(error),
136
+ });
137
+ });
138
+ }
139
+ },
140
+ { summarize: (summary) => summary },
141
+ );
142
+ };
143
+
144
+ const reminderJob = lazySync((sync) =>
145
+ sync.job<null>({
146
+ id: "auth:reminder:daily",
147
+ delivery: { ackWaitMs: 180_000, ...SCAN_RETRY },
148
+ }),
149
+ );
150
+ const processReminderJob = async (ctx: JobContext<null>): Promise<void> => {
151
+ await trace.withSpan(
152
+ jobSpan(ctx, "auth:reminder:daily", "Account expiry reminders"),
153
+ async () => {
154
+ if (ctx.signal.aborted) return abortedSummary();
155
+ if (!notificationSender) throw new Error("Account lifecycle notification sender is not configured");
156
+ const summary = await accountLifecycle.sendExpiryReminders(notificationSender);
157
+ reminderLog.info("Reminder run complete", toReminderLog(summary));
158
+ return summary;
159
+ },
160
+ { summarize: (summary) => summary },
161
+ );
162
+ };
163
+
164
+ const guestCleanupJob = lazySync((sync) =>
165
+ sync.job<null>({
166
+ id: "auth:guest:cleanup",
167
+ delivery: { ackWaitMs: 120_000, ...SCAN_RETRY },
168
+ }),
169
+ );
170
+ const processGuestCleanupJob = async (ctx: JobContext<null>): Promise<void> => {
171
+ await trace.withSpan(
172
+ jobSpan(ctx, "auth:guest:cleanup", "Expired guest cleanup"),
173
+ async () => {
174
+ if (ctx.signal.aborted) return abortedSummary();
175
+ const summary = await accountLifecycle.cleanupExpiredGuests();
176
+ guestCleanupLog.info("Expired guest cleanup complete", toCleanupLog(summary));
177
+ return summary;
178
+ },
179
+ { summarize: (summary) => summary },
180
+ );
181
+ };
182
+
183
+ const localUserCleanupJob = lazySync((sync) =>
184
+ sync.job<null>({
185
+ id: "auth:local-user:cleanup",
186
+ delivery: { ackWaitMs: 120_000, ...SCAN_RETRY },
187
+ }),
188
+ );
189
+ const processLocalUserCleanupJob = async (ctx: JobContext<null>): Promise<void> => {
190
+ await trace.withSpan(
191
+ jobSpan(ctx, "auth:local-user:cleanup", "Expired local user cleanup"),
192
+ async () => {
193
+ if (ctx.signal.aborted) return abortedSummary();
194
+ const summary = await accountLifecycle.cleanupExpiredLocalUsers();
195
+ localUserCleanupLog.info("Expired local user cleanup complete", toCleanupLog(summary));
196
+ return summary;
197
+ },
198
+ { summarize: (summary) => summary },
199
+ );
200
+ };
201
+
202
+ const auditCleanupJob = lazySync((sync) =>
203
+ sync.job<null>({
204
+ id: "auth:lifecycle:audit:cleanup",
205
+ delivery: { ackWaitMs: 120_000, ...SCAN_RETRY },
206
+ }),
207
+ );
208
+ const processAuditCleanupJob = async (ctx: JobContext<null>): Promise<void> => {
209
+ await trace.withSpan(
210
+ jobSpan(ctx, "auth:lifecycle:audit:cleanup", "Lifecycle audit cleanup"),
211
+ async () => {
212
+ if (ctx.signal.aborted) return abortedSummary();
213
+ const summary = await accountLifecycle.cleanupLifecycleAudit();
214
+ auditCleanupLog.info("Lifecycle audit cleanup complete", toCleanupLog(summary));
215
+ return summary;
216
+ },
217
+ { summarize: (summary) => summary },
218
+ );
219
+ };
220
+
221
+ const logCleanupJob = lazySync((sync) =>
222
+ sync.job<null>({
223
+ id: "app:logs:cleanup",
224
+ delivery: { ackWaitMs: 120_000, ...SCAN_RETRY },
225
+ }),
226
+ );
227
+ const processLogCleanupJob = async (ctx: JobContext<null>): Promise<void> => {
228
+ await trace.withSpan(
229
+ jobSpan(ctx, "app:logs:cleanup", "Log cleanup", "logging"),
230
+ async () => {
231
+ if (ctx.signal.aborted) return { deleted: 0, retentionDays: 0 };
232
+ const configured = Number((await getSetting<number | string | null>("logs.retention_days")) ?? 30);
233
+ const retentionDays = Number.isFinite(configured) ? configured : 30;
234
+ const summary = await logging.cleanup(retentionDays);
235
+ logCleanupLog.info("Log cleanup complete", { deleted: summary.deleted, retentionDays });
236
+ return { deleted: summary.deleted, retentionDays };
237
+ },
238
+ { summarize: (summary) => summary },
239
+ );
240
+ };
241
+
242
+ const ipaBackfill = lazySync(declareIpaBackfill);
243
+
244
+ const guestBackfillJob = lazySync((sync) =>
245
+ sync.job<null>({
246
+ id: "auth:guest:backfill",
247
+ delivery: { ackWaitMs: 300_000, maxAttempts: 2, backoffMs: [2000, 4000] },
248
+ }),
249
+ );
250
+ const processGuestBackfillJob = async (ctx: JobContext<null>): Promise<void> => {
251
+ await trace.withSpan(
252
+ jobSpan(ctx, "auth:guest:backfill", "Guest expiry backfill"),
253
+ async () => {
254
+ if (ctx.signal.aborted) return abortedSummary();
255
+ const summary = await accountLifecycle.runGuestBackfill();
256
+ guestBackfillLog.info("Guest expiry backfill complete", toBackfillLog(summary));
257
+ return summary;
258
+ },
259
+ { summarize: (summary) => summary },
260
+ );
261
+ };
262
+
263
+ const localUserBackfillJob = lazySync((sync) =>
264
+ sync.job<null>({
265
+ id: "auth:local-user:backfill",
266
+ delivery: { ackWaitMs: 300_000, maxAttempts: 2, backoffMs: [2000, 4000] },
267
+ }),
268
+ );
269
+ const processLocalUserBackfillJob = async (ctx: JobContext<null>): Promise<void> => {
270
+ await trace.withSpan(
271
+ jobSpan(ctx, "auth:local-user:backfill", "Local user expiry backfill"),
272
+ async () => {
273
+ if (ctx.signal.aborted) return abortedSummary();
274
+ const summary = await accountLifecycle.runLocalUserBackfill();
275
+ localUserBackfillLog.info("Local user expiry backfill complete", toBackfillLog(summary));
276
+ return summary;
277
+ },
278
+ { summarize: (summary) => summary },
279
+ );
280
+ };
281
+
282
+ // ── Scheduler ──────────────────────────────────────────────────────────
283
+
284
+ // A tick that fails to submit its job retries once shortly after; with a single
285
+ // attempt the daily slot would be dropped until the next day.
286
+ const lifecycleScheduler = lazySync((sync) => sync.scheduler({ id: "auth-lifecycle", delivery: { maxAttempts: 2, backoffMs: [5_000] } }));
287
+
288
+ let started = false;
289
+ let workers: Worker[] = [];
290
+ let registered = false;
291
+ let registerPromise: Promise<void> | null = null;
292
+
293
+ /**
294
+ * Register (or update) a cron-triggered schedule that fans out to the given
295
+ * job. `scheduler.create` is idempotent by id — same cron/tz keeps `nextRunAt`
296
+ * intact; a change resets it. Each slot submits with the job's stable
297
+ * `scheduled` key and `coalesce`: at most one scan is queued or running, a slot
298
+ * that fires during a run joins it, and the key is released when the run
299
+ * settles. Scans read their whole work set, so a joined slot loses nothing.
300
+ */
301
+ const createSchedule = async (config: {
302
+ id: string;
303
+ cron: string;
304
+ tz: string;
305
+ family: string;
306
+ label: string;
307
+ source?: string;
308
+ submit: () => Promise<unknown>;
309
+ }): Promise<void> => {
310
+ const source = config.source ?? config.id;
311
+ await lifecycleScheduler().create({
312
+ id: config.id,
313
+ cron: config.cron,
314
+ timezone: config.tz,
315
+ misfire: "latest",
316
+ meta: {
317
+ appId: "core",
318
+ family: config.family,
319
+ label: config.label,
320
+ source,
321
+ },
322
+ process: async () => {
323
+ await config.submit();
324
+ },
325
+ });
326
+ };
327
+
328
+ const createScheduleWithFallback = async (config: {
329
+ id: string;
330
+ cron: string;
331
+ fallbackCron: string;
332
+ tz: string;
333
+ submit: () => Promise<unknown>;
334
+ settingsKey: string;
335
+ family: string;
336
+ label: string;
337
+ source?: string;
338
+ }): Promise<void> => {
339
+ try {
340
+ await createSchedule({
341
+ id: config.id,
342
+ cron: config.cron,
343
+ tz: config.tz,
344
+ family: config.family,
345
+ label: config.label,
346
+ source: config.source,
347
+ submit: config.submit,
348
+ });
349
+ } catch (error) {
350
+ if (config.cron === config.fallbackCron) throw error;
351
+ log.warn("Invalid configured cron, falling back to default", {
352
+ key: config.settingsKey,
353
+ configuredCron: config.cron,
354
+ fallbackCron: config.fallbackCron,
355
+ timezone: config.tz,
356
+ error: error instanceof Error ? error.message : String(error),
357
+ });
358
+ await createSchedule({
359
+ id: config.id,
360
+ cron: config.fallbackCron,
361
+ tz: config.tz,
362
+ family: config.family,
363
+ label: config.label,
364
+ source: config.source,
365
+ submit: config.submit,
366
+ });
367
+ }
368
+ };
369
+
370
+ const doRegister = async (): Promise<void> => {
371
+ const [scheduleTz, ipaSyncCron, reminderCron, cleanupCron] = await Promise.all([
372
+ getTimezoneSetting(),
373
+ getCronSetting("freeipa.sync_cron", DEFAULT_IPA_SYNC_CRON),
374
+ getCronSetting("user.account.reminder_cron", "0 9 * * *"),
375
+ getCronSetting("app.cleanup_schedule", "0 4 * * *"),
376
+ ]);
377
+
378
+ await createScheduleWithFallback({
379
+ id: "auth:ipa:sync",
380
+ cron: ipaSyncCron,
381
+ fallbackCron: DEFAULT_IPA_SYNC_CRON,
382
+ tz: scheduleTz,
383
+ settingsKey: "freeipa.sync_cron",
384
+ family: "auth:ipa",
385
+ label: "FreeIPA account sync",
386
+ submit: () => ipaSyncJob().submit({ key: "scheduled", input: null, coalesce: true }),
387
+ });
388
+
389
+ await createSchedule({
390
+ id: "auth:reminder:daily",
391
+ cron: reminderCron,
392
+ tz: scheduleTz,
393
+ family: "auth:reminders",
394
+ label: "Daily account reminders",
395
+ submit: () => reminderJob().submit({ key: "scheduled", input: null, coalesce: true }),
396
+ });
397
+
398
+ await createSchedule({
399
+ id: "auth:guest:cleanup",
400
+ cron: cleanupCron,
401
+ tz: scheduleTz,
402
+ family: "auth:cleanup",
403
+ label: "Guest account cleanup",
404
+ submit: () => guestCleanupJob().submit({ key: "scheduled", input: null, coalesce: true }),
405
+ });
406
+
407
+ await createSchedule({
408
+ id: "auth:local-user:cleanup",
409
+ cron: cleanupCron,
410
+ tz: scheduleTz,
411
+ family: "auth:cleanup",
412
+ label: "Local user cleanup",
413
+ submit: () => localUserCleanupJob().submit({ key: "scheduled", input: null, coalesce: true }),
414
+ });
415
+
416
+ await createSchedule({
417
+ id: "auth:lifecycle:audit:cleanup",
418
+ cron: cleanupCron,
419
+ tz: scheduleTz,
420
+ family: "auth:cleanup",
421
+ label: "Account lifecycle audit cleanup",
422
+ submit: () => auditCleanupJob().submit({ key: "scheduled", input: null, coalesce: true }),
423
+ });
424
+
425
+ await createSchedule({
426
+ id: "app:logs:cleanup",
427
+ cron: cleanupCron,
428
+ tz: scheduleTz,
429
+ family: "app:cleanup",
430
+ label: "Application log cleanup",
431
+ submit: () => logCleanupJob().submit({ key: "scheduled", input: null, coalesce: true }),
432
+ });
433
+
434
+ registered = true;
435
+ };
436
+
437
+ const ensureRegistered = async (): Promise<void> => {
438
+ if (registered) return;
439
+ if (!registerPromise) {
440
+ registerPromise = doRegister().finally(() => {
441
+ registerPromise = null;
442
+ });
443
+ }
444
+ await registerPromise;
445
+ };
446
+
447
+ export const lifecycleJobs = {
448
+ start: async (config: { notificationSender: AccountLifecycleNotificationSender }): Promise<void> => {
449
+ notificationSender = config.notificationSender;
450
+ if (started) return;
451
+ await ensureRegistered();
452
+ try {
453
+ workers.push(await ipaSyncJob().process({}, processIpaSyncJob));
454
+ workers.push(await reminderJob().process({}, processReminderJob));
455
+ workers.push(await guestCleanupJob().process({}, processGuestCleanupJob));
456
+ workers.push(await localUserCleanupJob().process({}, processLocalUserCleanupJob));
457
+ workers.push(await auditCleanupJob().process({}, processAuditCleanupJob));
458
+ workers.push(await logCleanupJob().process({}, processLogCleanupJob));
459
+ workers.push(await ipaBackfill().accounts.process({ concurrency: 1 }, processIpaBackfillAccount));
460
+ workers.push(await ipaBackfill().pump.process());
461
+ workers.push(await guestBackfillJob().process({}, processGuestBackfillJob));
462
+ workers.push(await localUserBackfillJob().process({}, processLocalUserBackfillJob));
463
+ workers.push(await lifecycleScheduler().process());
464
+ started = true;
465
+ } catch (error) {
466
+ for (const worker of workers) worker.stop();
467
+ await Promise.all(workers.map((worker) => worker.drain()));
468
+ workers = [];
469
+ throw error;
470
+ }
471
+ },
472
+
473
+ stop: async (): Promise<void> => {
474
+ if (!started) {
475
+ notificationSender = null;
476
+ return;
477
+ }
478
+ for (const worker of workers) worker.stop();
479
+ await Promise.all(workers.map((worker) => worker.drain()));
480
+ workers = [];
481
+ started = false;
482
+ registered = false;
483
+ registerPromise = null;
484
+ notificationSender = null;
485
+ },
486
+
487
+ // Manual-only backfill triggers. Scheduled jobs run from their broker schedules
488
+ // (`listSchedules`; Admin Observability Jobs can run a schedule now).
489
+ submitIpaBackfill: async (): Promise<string> => {
490
+ const key = crypto.randomUUID();
491
+ await ipaBackfill().pump.start({ key, input: await prepareIpaBackfill(key) });
492
+ return key;
493
+ },
494
+ submitLocalUserBackfill: async (): Promise<string> =>
495
+ (await localUserBackfillJob().submit({ key: `manual:${Date.now()}`, input: null })).jobId,
496
+ submitGuestBackfill: async (): Promise<string> => (await guestBackfillJob().submit({ key: `manual:${Date.now()}`, input: null })).jobId,
497
+
498
+ /** Process-local worker state; schedule state (next run, failures) comes from `listSchedules`. */
499
+ metrics: () => ({
500
+ started,
501
+ registered,
502
+ active: workers.reduce((total, worker) => total + worker.active, 0),
503
+ capacity: workers.reduce((total, worker) => total + worker.capacity, 0),
504
+ }),
505
+ listSchedules: async () => lifecycleScheduler().list(),
506
+ };
@@ -0,0 +1,22 @@
1
+ export { buildRoles } from "./accounts/authz";
2
+ export {
3
+ calculateIpaProfileFromGroupNames,
4
+ canPersistStoredAdmin,
5
+ deriveIpaAdminFromGroupNames,
6
+ getConfiguredExpiryDays,
7
+ getDefaultAccountExpiry,
8
+ type IpaAccountTransitionPolicy,
9
+ type IpaMatchMode,
10
+ isAccountExpired,
11
+ isGuestProfile,
12
+ isIpaProvider,
13
+ isLocalProvider,
14
+ normalizeManualAccountExpiry,
15
+ parseIpaAccountTransitionPolicy,
16
+ parseIpaMatchMode,
17
+ parseManualAccountExpiry,
18
+ resolveAccountExpires,
19
+ resolveEffectiveAdminState,
20
+ resolveStoredAdminState,
21
+ resolveTargetAccountExpiry,
22
+ } from "./accounts/model";