@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,891 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { crypto, err, fail, ok, type PageParams, type Paginated, type Result } from "@k2b/stdlib";
3
+ import { type SQL, sql } from "bun";
4
+ import type { User } from "../contracts/shared";
5
+ import { accounts } from "./accounts";
6
+ import { audit } from "./audit";
7
+ import { isUniqueViolation, toPgTextArray } from "./postgres";
8
+ import { type ServiceAccount, serviceAccounts } from "./service-accounts";
9
+
10
+ export type ServiceAccountCredentialStatus = "active" | "revoked";
11
+ export type ServiceAccountCredentialKind = "api_token";
12
+
13
+ export type ServiceAccountCredential = {
14
+ id: string;
15
+ serviceAccountId: string;
16
+ name: string;
17
+ kind: ServiceAccountCredentialKind;
18
+ status: ServiceAccountCredentialStatus;
19
+ tokenPrefix: string;
20
+ scopes: string[];
21
+ expiresAt: string | null;
22
+ lastUsedAt: string | null;
23
+ createdBy: string | null;
24
+ createdAt: string;
25
+ revokedAt: string | null;
26
+ revokedBy: string | null;
27
+ };
28
+
29
+ export type AuthenticatedServiceAccountCredential = {
30
+ credential: ServiceAccountCredential;
31
+ serviceAccount: ServiceAccount;
32
+ delegatedUser: User | null;
33
+ };
34
+
35
+ export type ServiceAccountCredentialOwner =
36
+ | {
37
+ type: "user";
38
+ userId: string;
39
+ uid: string;
40
+ displayName: string;
41
+ mail: string | null;
42
+ avatarHash: string | null;
43
+ }
44
+ | {
45
+ type: "resource";
46
+ appId: string;
47
+ resourceType: string;
48
+ resourceId: string;
49
+ };
50
+
51
+ export type ServiceAccountCredentialOverview = ServiceAccountCredential & {
52
+ serviceAccount: ServiceAccount;
53
+ owner: ServiceAccountCredentialOwner;
54
+ };
55
+
56
+ type DbCredentialRow = {
57
+ id: string;
58
+ service_account_id: string;
59
+ name: string;
60
+ kind: ServiceAccountCredentialKind;
61
+ status: ServiceAccountCredentialStatus;
62
+ token_prefix: string;
63
+ scopes: string[];
64
+ expires_at: Date | null;
65
+ last_used_at: Date | null;
66
+ created_by: string | null;
67
+ created_at: Date;
68
+ revoked_at: Date | null;
69
+ revoked_by: string | null;
70
+ };
71
+
72
+ type DbCredentialWithSecretRow = DbCredentialRow & {
73
+ activity_due: boolean;
74
+ secret_hash: string;
75
+ } & DbCredentialServiceAccountFields;
76
+
77
+ type DbCredentialServiceAccountFields = {
78
+ service_account_id: string;
79
+ service_account_name: string;
80
+ service_account_kind: ServiceAccount["kind"];
81
+ service_account_status: ServiceAccount["status"];
82
+ delegated_user_id: string | null;
83
+ app_id: string | null;
84
+ resource_type: string | null;
85
+ resource_id: string | null;
86
+ service_account_created_by: string | null;
87
+ service_account_created_at: Date;
88
+ };
89
+
90
+ type SqlRunner = SQL;
91
+ type DbCredentialOverviewRow = DbCredentialRow &
92
+ DbCredentialServiceAccountFields & {
93
+ delegated_uid: string | null;
94
+ delegated_display_name: string | null;
95
+ delegated_mail: string | null;
96
+ delegated_avatar_hash: string | null;
97
+ };
98
+
99
+ const TOKEN_PREFIX = "cld";
100
+ const TOKEN_PATTERN = /^cld_([0-9a-f]{24})_([0-9a-f]{64})$/i;
101
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
102
+ const SECRET_HASH_PATTERN = /^sha256:([0-9a-f]{64})$/;
103
+ const SUCCESS_ACTIVITY_INTERVAL_MS = 60_000;
104
+
105
+ const isForeignKeyViolation = (error: unknown): boolean => {
106
+ if (!error || typeof error !== "object") return false;
107
+ const e = error as { code?: string; errno?: string };
108
+ return e.code === "23503" || e.errno === "23503";
109
+ };
110
+
111
+ const USER_DELEGATED_UNIQUE_CONSTRAINT = "uniq_service_accounts_user_delegated";
112
+
113
+ const mapCredential = (row: DbCredentialRow): ServiceAccountCredential => ({
114
+ id: row.id,
115
+ serviceAccountId: row.service_account_id,
116
+ name: row.name,
117
+ kind: row.kind,
118
+ status: row.status,
119
+ tokenPrefix: row.token_prefix,
120
+ scopes: row.scopes ?? [],
121
+ expiresAt: row.expires_at?.toISOString() ?? null,
122
+ lastUsedAt: row.last_used_at?.toISOString() ?? null,
123
+ createdBy: row.created_by,
124
+ createdAt: row.created_at.toISOString(),
125
+ revokedAt: row.revoked_at?.toISOString() ?? null,
126
+ revokedBy: row.revoked_by,
127
+ });
128
+
129
+ const mapServiceAccount = (row: DbCredentialServiceAccountFields): ServiceAccount => ({
130
+ id: row.service_account_id,
131
+ name: row.service_account_name,
132
+ kind: row.service_account_kind,
133
+ status: row.service_account_status,
134
+ delegatedUserId: row.delegated_user_id,
135
+ appId: row.app_id,
136
+ resourceType: row.resource_type,
137
+ resourceId: row.resource_id,
138
+ createdBy: row.service_account_created_by,
139
+ createdAt: row.service_account_created_at.toISOString(),
140
+ });
141
+
142
+ const mapCredentialOverview = (row: DbCredentialOverviewRow): ServiceAccountCredentialOverview => {
143
+ const serviceAccount = mapServiceAccount(row);
144
+ return {
145
+ ...mapCredential(row),
146
+ serviceAccount,
147
+ owner:
148
+ serviceAccount.kind === "user_delegated" && serviceAccount.delegatedUserId
149
+ ? {
150
+ type: "user",
151
+ userId: serviceAccount.delegatedUserId,
152
+ uid: row.delegated_uid ?? serviceAccount.delegatedUserId,
153
+ displayName: row.delegated_display_name ?? "",
154
+ mail: row.delegated_mail,
155
+ avatarHash: row.delegated_avatar_hash,
156
+ }
157
+ : {
158
+ type: "resource",
159
+ appId: serviceAccount.appId ?? "",
160
+ resourceType: serviceAccount.resourceType ?? "",
161
+ resourceId: serviceAccount.resourceId ?? "",
162
+ },
163
+ };
164
+ };
165
+
166
+ const actorForUser = (user: Pick<User, "id" | "uid" | "provider" | "roles">) => ({
167
+ userId: user.id,
168
+ uid: user.uid,
169
+ provider: user.provider,
170
+ roles: user.roles,
171
+ });
172
+
173
+ const normalizeName = (value: string): string => value.trim();
174
+
175
+ const parseToken = (token: string): { tokenPrefix: string; secret: string } | null => {
176
+ const match = token.match(TOKEN_PATTERN);
177
+ if (!match) return null;
178
+ return { tokenPrefix: match[1]!.toLowerCase(), secret: match[2]!.toLowerCase() };
179
+ };
180
+
181
+ const generateTokenParts = (): { tokenPrefix: string; secret: string; token: string } => {
182
+ const tokenPrefix = crypto.common.generateKey(12);
183
+ const secret = crypto.common.generateKey(32);
184
+ return { tokenPrefix, secret, token: `${TOKEN_PREFIX}_${tokenPrefix}_${secret}` };
185
+ };
186
+
187
+ // API token secrets carry 256 bits of entropy, so a fast digest is sufficient and avoids password-KDF cost per request.
188
+ const hashApiTokenSecret = (secret: string): string => `sha256:${createHash("sha256").update(secret).digest("hex")}`;
189
+
190
+ const verifyApiTokenSecret = async (secret: string, storedHash: string): Promise<boolean> => {
191
+ const match = storedHash.match(SECRET_HASH_PATTERN);
192
+ if (match) {
193
+ const actual = Buffer.from(hashApiTokenSecret(secret).slice("sha256:".length), "hex");
194
+ const expected = Buffer.from(match[1]!, "hex");
195
+ return timingSafeEqual(actual, expected);
196
+ }
197
+
198
+ try {
199
+ return await Bun.password.verify(secret, storedHash);
200
+ } catch {
201
+ return false;
202
+ }
203
+ };
204
+
205
+ const generateUniqueTokenParts = async (): Promise<{ tokenPrefix: string; secret: string; token: string }> => {
206
+ for (let i = 0; i < 5; i += 1) {
207
+ const parts = generateTokenParts();
208
+ const [row] = await sql<{ exists: boolean }[]>`
209
+ SELECT EXISTS(
210
+ SELECT 1 FROM auth.service_account_credentials WHERE token_prefix = ${parts.tokenPrefix}
211
+ ) AS exists
212
+ `;
213
+ if (!row?.exists) return parts;
214
+ }
215
+ throw new Error("Failed to generate unique API token prefix");
216
+ };
217
+
218
+ export const isApiToken = (token: string | null | undefined): boolean => Boolean(token && TOKEN_PATTERN.test(token));
219
+
220
+ export const getOrCreateUserDelegatedServiceAccount = async (params: {
221
+ userId: string;
222
+ createdBy?: string | null;
223
+ }): Promise<Result<ServiceAccount>> => {
224
+ const [existing] = await sql<
225
+ {
226
+ id: string;
227
+ name: string;
228
+ kind: ServiceAccount["kind"];
229
+ status: ServiceAccount["status"];
230
+ delegated_user_id: string | null;
231
+ app_id: string | null;
232
+ resource_type: string | null;
233
+ resource_id: string | null;
234
+ created_by: string | null;
235
+ created_at: Date;
236
+ }[]
237
+ >`
238
+ SELECT id, name, kind, status, delegated_user_id, app_id, resource_type, resource_id, created_by, created_at
239
+ FROM auth.service_accounts
240
+ WHERE kind = 'user_delegated'
241
+ AND delegated_user_id = ${params.userId}::uuid
242
+ ORDER BY created_at ASC
243
+ LIMIT 1
244
+ `;
245
+ if (existing) {
246
+ return ok({
247
+ id: existing.id,
248
+ name: existing.name,
249
+ kind: existing.kind,
250
+ status: existing.status,
251
+ delegatedUserId: existing.delegated_user_id,
252
+ appId: existing.app_id,
253
+ resourceType: existing.resource_type,
254
+ resourceId: existing.resource_id,
255
+ createdBy: existing.created_by,
256
+ createdAt: existing.created_at.toISOString(),
257
+ });
258
+ }
259
+
260
+ try {
261
+ return await serviceAccounts.createUserDelegated({
262
+ name: "Personal API keys",
263
+ delegatedUserId: params.userId,
264
+ createdBy: params.createdBy ?? params.userId,
265
+ });
266
+ } catch (error) {
267
+ if (!isUniqueViolation(error, USER_DELEGATED_UNIQUE_CONSTRAINT)) throw error;
268
+ const [row] = await sql<
269
+ {
270
+ id: string;
271
+ name: string;
272
+ kind: ServiceAccount["kind"];
273
+ status: ServiceAccount["status"];
274
+ delegated_user_id: string | null;
275
+ app_id: string | null;
276
+ resource_type: string | null;
277
+ resource_id: string | null;
278
+ created_by: string | null;
279
+ created_at: Date;
280
+ }[]
281
+ >`
282
+ SELECT id, name, kind, status, delegated_user_id, app_id, resource_type, resource_id, created_by, created_at
283
+ FROM auth.service_accounts
284
+ WHERE kind = 'user_delegated'
285
+ AND delegated_user_id = ${params.userId}::uuid
286
+ LIMIT 1
287
+ `;
288
+ if (!row) return fail(err.internal("Failed to load user service account"));
289
+ return ok({
290
+ id: row.id,
291
+ name: row.name,
292
+ kind: row.kind,
293
+ status: row.status,
294
+ delegatedUserId: row.delegated_user_id,
295
+ appId: row.app_id,
296
+ resourceType: row.resource_type,
297
+ resourceId: row.resource_id,
298
+ createdBy: row.created_by,
299
+ createdAt: row.created_at.toISOString(),
300
+ });
301
+ }
302
+ };
303
+
304
+ const insertApiToken = async (
305
+ db: SqlRunner,
306
+ params: {
307
+ serviceAccountId: string;
308
+ name: string;
309
+ createdBy?: string | null;
310
+ expiresAt?: string | null;
311
+ scopes?: string[];
312
+ },
313
+ ): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
314
+ if (!UUID_PATTERN.test(params.serviceAccountId)) return fail(err.notFound("Service account"));
315
+ const name = normalizeName(params.name);
316
+ if (!name) return fail(err.badInput("API key name is required"));
317
+ if (name.length > 120) return fail(err.badInput("API key name must be 120 characters or fewer"));
318
+
319
+ let expiresAt: Date | null = null;
320
+ if (params.expiresAt) {
321
+ expiresAt = new Date(params.expiresAt);
322
+ if (Number.isNaN(expiresAt.getTime())) return fail(err.badInput("Invalid expiry date"));
323
+ if (expiresAt.getTime() <= Date.now()) return fail(err.badInput("Expiry must be in the future"));
324
+ }
325
+
326
+ const parts = await generateUniqueTokenParts();
327
+ const secretHash = hashApiTokenSecret(parts.secret);
328
+
329
+ try {
330
+ const [row] = await db<DbCredentialRow[]>`
331
+ INSERT INTO auth.service_account_credentials (
332
+ service_account_id,
333
+ name,
334
+ token_prefix,
335
+ secret_hash,
336
+ scopes,
337
+ expires_at,
338
+ created_by
339
+ )
340
+ VALUES (
341
+ ${params.serviceAccountId}::uuid,
342
+ ${name},
343
+ ${parts.tokenPrefix},
344
+ ${secretHash},
345
+ ${toPgTextArray(params.scopes ?? [])}::text[],
346
+ ${expiresAt},
347
+ ${params.createdBy ?? null}::uuid
348
+ )
349
+ RETURNING id, service_account_id, name, kind, status, token_prefix, scopes, expires_at, last_used_at, created_by, created_at, revoked_at, revoked_by
350
+ `;
351
+ if (!row) return fail(err.internal("Failed to create API key"));
352
+ return ok({ credential: mapCredential(row), token: parts.token });
353
+ } catch (error) {
354
+ if (isForeignKeyViolation(error)) return fail(err.notFound("Service account"));
355
+ if (isUniqueViolation(error)) return fail(err.conflict("API key"));
356
+ throw error;
357
+ }
358
+ };
359
+
360
+ export const createApiToken = (params: {
361
+ serviceAccountId: string;
362
+ name: string;
363
+ createdBy?: string | null;
364
+ expiresAt?: string | null;
365
+ scopes?: string[];
366
+ }): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => insertApiToken(sql, params);
367
+
368
+ export const createUserApiToken = async (params: {
369
+ user: User;
370
+ name: string;
371
+ expiresAt?: string | null;
372
+ }): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
373
+ const serviceAccountResult = await getOrCreateUserDelegatedServiceAccount({
374
+ userId: params.user.id,
375
+ createdBy: params.user.id,
376
+ });
377
+ if (!serviceAccountResult.ok) return fail(serviceAccountResult.error);
378
+
379
+ return sql.begin(async (tx) => {
380
+ const result = await insertApiToken(tx, {
381
+ serviceAccountId: serviceAccountResult.data.id,
382
+ name: params.name,
383
+ expiresAt: params.expiresAt,
384
+ createdBy: params.user.id,
385
+ });
386
+
387
+ return audit.recordResult({
388
+ action: "service_account_credential.create",
389
+ actor: actorForUser(params.user),
390
+ target: { type: "service_account_credential", id: result.ok ? result.data.credential.id : null, label: params.name },
391
+ metadata: {
392
+ serviceAccountId: serviceAccountResult.data.id,
393
+ kind: "api_token",
394
+ expiresAt: params.expiresAt ?? null,
395
+ },
396
+ result,
397
+ db: tx,
398
+ });
399
+ });
400
+ };
401
+
402
+ export const createResourceApiToken = async (params: {
403
+ serviceAccountId: string;
404
+ actor: User;
405
+ name: string;
406
+ expiresAt?: string | null;
407
+ scopes?: string[];
408
+ }): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
409
+ const serviceAccount = await serviceAccounts.get({ id: params.serviceAccountId });
410
+ if (!serviceAccount || serviceAccount.kind !== "resource_bound") return fail(err.notFound("Resource service account"));
411
+ if (serviceAccount.status !== "active") return fail(err.badInput("Resource service account is disabled"));
412
+
413
+ return sql.begin(async (tx) => {
414
+ const result = await insertApiToken(tx, {
415
+ serviceAccountId: serviceAccount.id,
416
+ name: params.name,
417
+ expiresAt: params.expiresAt,
418
+ createdBy: params.actor.id,
419
+ scopes: params.scopes,
420
+ });
421
+
422
+ return audit.recordResult({
423
+ action: "service_account_credential.create",
424
+ actor: actorForUser(params.actor),
425
+ target: { type: "service_account_credential", id: result.ok ? result.data.credential.id : null, label: params.name },
426
+ metadata: {
427
+ serviceAccountId: serviceAccount.id,
428
+ kind: "api_token",
429
+ serviceAccountKind: serviceAccount.kind,
430
+ appId: serviceAccount.appId,
431
+ resourceType: serviceAccount.resourceType,
432
+ resourceId: serviceAccount.resourceId,
433
+ expiresAt: params.expiresAt ?? null,
434
+ },
435
+ result,
436
+ db: tx,
437
+ });
438
+ });
439
+ };
440
+
441
+ export const listForDelegatedUser = async (params: { userId: string }): Promise<ServiceAccountCredential[]> => {
442
+ const rows = await sql<DbCredentialRow[]>`
443
+ SELECT c.id, c.service_account_id, c.name, c.kind, c.status, c.token_prefix, c.scopes, c.expires_at,
444
+ c.last_used_at, c.created_by, c.created_at, c.revoked_at, c.revoked_by
445
+ FROM auth.service_account_credentials c
446
+ JOIN auth.service_accounts sa ON sa.id = c.service_account_id
447
+ WHERE sa.kind = 'user_delegated'
448
+ AND sa.delegated_user_id = ${params.userId}::uuid
449
+ AND c.status = 'active'
450
+ ORDER BY c.created_at DESC
451
+ `;
452
+ return rows.map(mapCredential);
453
+ };
454
+
455
+ export const listOverview = async (config?: {
456
+ pagination?: PageParams;
457
+ filter?: {
458
+ search?: string;
459
+ serviceAccountKind?: ServiceAccount["kind"];
460
+ credentialStatus?: ServiceAccountCredentialStatus;
461
+ userId?: string;
462
+ appId?: string;
463
+ resourceType?: string;
464
+ resourceId?: string;
465
+ serviceAccountId?: string;
466
+ };
467
+ }): Promise<Paginated<ServiceAccountCredentialOverview>> => {
468
+ const page = Math.max(1, config?.pagination?.page ?? 1);
469
+ const perPage = Math.max(1, Math.min(config?.pagination?.perPage ?? 100, 500));
470
+ const offset = (page - 1) * perPage;
471
+ const search = config?.filter?.search?.trim() || null;
472
+ const serviceAccountKind = config?.filter?.serviceAccountKind ?? null;
473
+ const credentialStatus = config?.filter?.credentialStatus ?? null;
474
+ const userId = config?.filter?.userId ?? null;
475
+ const appId = config?.filter?.appId ?? null;
476
+ const resourceType = config?.filter?.resourceType ?? null;
477
+ const resourceId = config?.filter?.resourceId ?? null;
478
+ const serviceAccountId = config?.filter?.serviceAccountId ?? null;
479
+
480
+ const [countRow] = await sql<{ count: string }[]>`
481
+ SELECT COUNT(*)::text AS count
482
+ FROM auth.service_account_credentials c
483
+ JOIN auth.service_accounts sa ON sa.id = c.service_account_id
484
+ LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
485
+ WHERE (${serviceAccountKind}::text IS NULL OR sa.kind = ${serviceAccountKind})
486
+ AND (${credentialStatus}::text IS NULL OR c.status = ${credentialStatus})
487
+ AND (${userId}::uuid IS NULL OR sa.delegated_user_id = ${userId}::uuid)
488
+ AND (${serviceAccountId}::uuid IS NULL OR sa.id = ${serviceAccountId}::uuid)
489
+ AND (${appId}::text IS NULL OR sa.app_id = ${appId})
490
+ AND (${resourceType}::text IS NULL OR sa.resource_type = ${resourceType})
491
+ AND (${resourceId}::text IS NULL OR sa.resource_id = ${resourceId})
492
+ AND (
493
+ ${search}::text IS NULL
494
+ OR c.name ILIKE '%' || ${search} || '%'
495
+ OR c.token_prefix ILIKE '%' || ${search} || '%'
496
+ OR sa.name ILIKE '%' || ${search} || '%'
497
+ OR du.uid ILIKE '%' || ${search} || '%'
498
+ OR du.display_name ILIKE '%' || ${search} || '%'
499
+ OR du.mail ILIKE '%' || ${search} || '%'
500
+ OR sa.app_id ILIKE '%' || ${search} || '%'
501
+ OR sa.resource_type ILIKE '%' || ${search} || '%'
502
+ OR sa.resource_id ILIKE '%' || ${search} || '%'
503
+ )
504
+ `;
505
+
506
+ const rows = await sql<DbCredentialOverviewRow[]>`
507
+ SELECT
508
+ c.id,
509
+ c.service_account_id,
510
+ c.name,
511
+ c.kind,
512
+ c.status,
513
+ c.token_prefix,
514
+ c.scopes,
515
+ c.expires_at,
516
+ c.last_used_at,
517
+ c.created_by,
518
+ c.created_at,
519
+ c.revoked_at,
520
+ c.revoked_by,
521
+ sa.name AS service_account_name,
522
+ sa.kind AS service_account_kind,
523
+ sa.status AS service_account_status,
524
+ sa.delegated_user_id,
525
+ sa.app_id,
526
+ sa.resource_type,
527
+ sa.resource_id,
528
+ sa.created_by AS service_account_created_by,
529
+ sa.created_at AS service_account_created_at,
530
+ du.uid AS delegated_uid,
531
+ du.display_name AS delegated_display_name,
532
+ du.mail AS delegated_mail,
533
+ du.avatar_hash AS delegated_avatar_hash
534
+ FROM auth.service_account_credentials c
535
+ JOIN auth.service_accounts sa ON sa.id = c.service_account_id
536
+ LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
537
+ WHERE (${serviceAccountKind}::text IS NULL OR sa.kind = ${serviceAccountKind})
538
+ AND (${credentialStatus}::text IS NULL OR c.status = ${credentialStatus})
539
+ AND (${userId}::uuid IS NULL OR sa.delegated_user_id = ${userId}::uuid)
540
+ AND (${serviceAccountId}::uuid IS NULL OR sa.id = ${serviceAccountId}::uuid)
541
+ AND (${appId}::text IS NULL OR sa.app_id = ${appId})
542
+ AND (${resourceType}::text IS NULL OR sa.resource_type = ${resourceType})
543
+ AND (${resourceId}::text IS NULL OR sa.resource_id = ${resourceId})
544
+ AND (
545
+ ${search}::text IS NULL
546
+ OR c.name ILIKE '%' || ${search} || '%'
547
+ OR c.token_prefix ILIKE '%' || ${search} || '%'
548
+ OR sa.name ILIKE '%' || ${search} || '%'
549
+ OR du.uid ILIKE '%' || ${search} || '%'
550
+ OR du.display_name ILIKE '%' || ${search} || '%'
551
+ OR du.mail ILIKE '%' || ${search} || '%'
552
+ OR sa.app_id ILIKE '%' || ${search} || '%'
553
+ OR sa.resource_type ILIKE '%' || ${search} || '%'
554
+ OR sa.resource_id ILIKE '%' || ${search} || '%'
555
+ )
556
+ ORDER BY c.created_at DESC
557
+ LIMIT ${perPage}
558
+ OFFSET ${offset}
559
+ `;
560
+
561
+ const total = Number.parseInt(countRow?.count ?? "0", 10);
562
+ return {
563
+ items: rows.map(mapCredentialOverview),
564
+ page,
565
+ perPage,
566
+ total,
567
+ hasNext: page * perPage < total,
568
+ };
569
+ };
570
+
571
+ export const getOverview = async (params: { id: string }): Promise<ServiceAccountCredentialOverview | null> => {
572
+ if (!UUID_PATTERN.test(params.id)) return null;
573
+ const [row] = await sql<DbCredentialOverviewRow[]>`
574
+ SELECT
575
+ c.id,
576
+ c.service_account_id,
577
+ c.name,
578
+ c.kind,
579
+ c.status,
580
+ c.token_prefix,
581
+ c.scopes,
582
+ c.expires_at,
583
+ c.last_used_at,
584
+ c.created_by,
585
+ c.created_at,
586
+ c.revoked_at,
587
+ c.revoked_by,
588
+ sa.name AS service_account_name,
589
+ sa.kind AS service_account_kind,
590
+ sa.status AS service_account_status,
591
+ sa.delegated_user_id,
592
+ sa.app_id,
593
+ sa.resource_type,
594
+ sa.resource_id,
595
+ sa.created_by AS service_account_created_by,
596
+ sa.created_at AS service_account_created_at,
597
+ du.uid AS delegated_uid,
598
+ du.display_name AS delegated_display_name,
599
+ du.mail AS delegated_mail,
600
+ du.avatar_hash AS delegated_avatar_hash
601
+ FROM auth.service_account_credentials c
602
+ JOIN auth.service_accounts sa ON sa.id = c.service_account_id
603
+ LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
604
+ WHERE c.id = ${params.id}::uuid
605
+ LIMIT 1
606
+ `;
607
+ return row ? mapCredentialOverview(row) : null;
608
+ };
609
+
610
+ export const revokeForDelegatedUser = async (params: { credentialId: string; user: User }): Promise<Result<void>> => {
611
+ if (!UUID_PATTERN.test(params.credentialId)) return fail(err.notFound("API key"));
612
+
613
+ return sql.begin(async (tx) => {
614
+ const [row] = await tx<DbCredentialRow[]>`
615
+ UPDATE auth.service_account_credentials c
616
+ SET status = 'revoked',
617
+ revoked_at = now(),
618
+ revoked_by = ${params.user.id}::uuid
619
+ FROM auth.service_accounts sa
620
+ WHERE c.id = ${params.credentialId}::uuid
621
+ AND c.service_account_id = sa.id
622
+ AND sa.kind = 'user_delegated'
623
+ AND sa.delegated_user_id = ${params.user.id}::uuid
624
+ AND c.status = 'active'
625
+ RETURNING c.id, c.service_account_id, c.name, c.kind, c.status, c.token_prefix, c.scopes, c.expires_at,
626
+ c.last_used_at, c.created_by, c.created_at, c.revoked_at, c.revoked_by
627
+ `;
628
+
629
+ const result = row ? ok() : fail(err.notFound("API key"));
630
+ return audit.recordResult({
631
+ action: "service_account_credential.revoke",
632
+ actor: actorForUser(params.user),
633
+ target: { type: "service_account_credential", id: params.credentialId, label: row?.name ?? null },
634
+ metadata: { serviceAccountId: row?.service_account_id ?? null },
635
+ result,
636
+ db: tx,
637
+ });
638
+ });
639
+ };
640
+
641
+ type RevokeCredentialInput =
642
+ | { credentialId: string; actor: User; system?: never }
643
+ | { credentialId: string; actor?: never; system: { service: string; reason: string } };
644
+
645
+ export const revoke = async (params: RevokeCredentialInput, options: { db?: SQL } = {}): Promise<Result<void>> => {
646
+ if (!UUID_PATTERN.test(params.credentialId)) return fail(err.notFound("API key"));
647
+
648
+ const run = async (db: SQL): Promise<Result<void>> => {
649
+ const [row] = await db<DbCredentialRow[]>`
650
+ UPDATE auth.service_account_credentials
651
+ SET status = 'revoked',
652
+ revoked_at = now(),
653
+ revoked_by = ${params.actor?.id ?? null}::uuid
654
+ WHERE id = ${params.credentialId}::uuid
655
+ AND status = 'active'
656
+ RETURNING id, service_account_id, name, kind, status, token_prefix, scopes, expires_at,
657
+ last_used_at, created_by, created_at, revoked_at, revoked_by
658
+ `;
659
+
660
+ const [existing] = row
661
+ ? [row]
662
+ : await db<DbCredentialRow[]>`
663
+ SELECT id, service_account_id, name, kind, status, token_prefix, scopes, expires_at,
664
+ last_used_at, created_by, created_at, revoked_at, revoked_by
665
+ FROM auth.service_account_credentials
666
+ WHERE id = ${params.credentialId}::uuid
667
+ LIMIT 1
668
+ `;
669
+ const result = row || existing?.status === "revoked" ? ok() : fail(err.notFound("API key"));
670
+ return audit.recordResult({
671
+ action: "service_account_credential.revoke",
672
+ actor: params.actor ? actorForUser(params.actor) : null,
673
+ target: { type: "service_account_credential", id: params.credentialId, label: row?.name ?? existing?.name ?? null },
674
+ metadata: {
675
+ serviceAccountId: row?.service_account_id ?? existing?.service_account_id ?? null,
676
+ ...(params.actor ? { adminAction: true } : { provenance: "system", service: params.system.service, reason: params.system.reason }),
677
+ },
678
+ result,
679
+ db,
680
+ });
681
+ };
682
+ return options.db ? run(options.db) : sql.begin(run);
683
+ };
684
+
685
+ const findActiveByTokenPrefix = async (tokenPrefix: string): Promise<DbCredentialWithSecretRow | null> => {
686
+ const [row] = await sql<DbCredentialWithSecretRow[]>`
687
+ SELECT
688
+ c.id,
689
+ c.service_account_id,
690
+ c.name,
691
+ c.kind,
692
+ c.status,
693
+ c.token_prefix,
694
+ c.secret_hash,
695
+ c.scopes,
696
+ c.expires_at,
697
+ c.last_used_at,
698
+ (
699
+ c.last_used_at IS NULL
700
+ OR c.last_used_at < now() - (${SUCCESS_ACTIVITY_INTERVAL_MS} * interval '1 millisecond')
701
+ ) AS activity_due,
702
+ c.created_by,
703
+ c.created_at,
704
+ c.revoked_at,
705
+ c.revoked_by,
706
+ sa.name AS service_account_name,
707
+ sa.kind AS service_account_kind,
708
+ sa.status AS service_account_status,
709
+ sa.delegated_user_id,
710
+ sa.app_id,
711
+ sa.resource_type,
712
+ sa.resource_id,
713
+ sa.created_by AS service_account_created_by,
714
+ sa.created_at AS service_account_created_at
715
+ FROM auth.service_account_credentials c
716
+ JOIN auth.service_accounts sa ON sa.id = c.service_account_id
717
+ WHERE c.token_prefix = ${tokenPrefix}
718
+ AND c.status = 'active'
719
+ AND sa.status = 'active'
720
+ AND (c.expires_at IS NULL OR c.expires_at > now())
721
+ LIMIT 1
722
+ `;
723
+ return row ?? null;
724
+ };
725
+
726
+ const recordSuccessfulAuthentication = async (params: {
727
+ row: DbCredentialWithSecretRow;
728
+ secret: string;
729
+ serviceAccount: ServiceAccount;
730
+ delegatedUser: User | null;
731
+ }): Promise<void> => {
732
+ const upgradedSecretHash = SECRET_HASH_PATTERN.test(params.row.secret_hash) ? null : hashApiTokenSecret(params.secret);
733
+ if (!upgradedSecretHash && !params.row.activity_due) return;
734
+
735
+ if (upgradedSecretHash) {
736
+ await sql`
737
+ UPDATE auth.service_account_credentials
738
+ SET secret_hash = ${upgradedSecretHash}
739
+ WHERE id = ${params.row.id}::uuid
740
+ AND status = 'active'
741
+ AND secret_hash = ${params.row.secret_hash}
742
+ `;
743
+ }
744
+ if (!params.row.activity_due) return;
745
+
746
+ await sql.begin(async (tx) => {
747
+ // Authentication telemetry is best-effort under contention. SKIP LOCKED
748
+ // keeps one replica from queuing live requests behind another replica.
749
+ const [activity] = await tx<{ last_used_at: Date }[]>`
750
+ WITH claimed AS (
751
+ SELECT id
752
+ FROM auth.service_account_credentials
753
+ WHERE id = ${params.row.id}::uuid
754
+ AND status = 'active'
755
+ AND (
756
+ last_used_at IS NULL
757
+ OR last_used_at < now() - (${SUCCESS_ACTIVITY_INTERVAL_MS} * interval '1 millisecond')
758
+ )
759
+ FOR UPDATE SKIP LOCKED
760
+ )
761
+ UPDATE auth.service_account_credentials
762
+ SET last_used_at = now()
763
+ FROM claimed
764
+ WHERE auth.service_account_credentials.id = claimed.id
765
+ RETURNING auth.service_account_credentials.last_used_at
766
+ `;
767
+ if (!activity) return;
768
+
769
+ await audit.record(
770
+ {
771
+ action: "service_account_credential.authenticate",
772
+ outcome: "allowed",
773
+ actor: params.delegatedUser ? actorForUser(params.delegatedUser) : null,
774
+ target: { type: "service_account_credential", id: params.row.id, label: params.row.name },
775
+ metadata: {
776
+ serviceAccountId: params.row.service_account_id,
777
+ serviceAccountKind: params.serviceAccount.kind,
778
+ },
779
+ },
780
+ tx,
781
+ );
782
+ });
783
+ };
784
+
785
+ const pendingSecretVerifications = new Map<string, Promise<boolean>>();
786
+
787
+ const verifyApiTokenSecretOnce = async (secret: string, row: DbCredentialWithSecretRow): Promise<boolean> => {
788
+ const key = `${row.id}:${row.secret_hash}:${hashApiTokenSecret(secret)}`;
789
+ let pending = pendingSecretVerifications.get(key);
790
+ if (!pending) {
791
+ pending = verifyApiTokenSecret(secret, row.secret_hash);
792
+ pendingSecretVerifications.set(key, pending);
793
+ }
794
+ try {
795
+ return await pending;
796
+ } finally {
797
+ if (pendingSecretVerifications.get(key) === pending) pendingSecretVerifications.delete(key);
798
+ }
799
+ };
800
+
801
+ const recordDeniedAuthentication = async (params: {
802
+ reason: string;
803
+ tokenPrefix: string;
804
+ row?: DbCredentialWithSecretRow;
805
+ }): Promise<null> => {
806
+ await audit.record({
807
+ action: "service_account_credential.authenticate",
808
+ outcome: "denied",
809
+ ...(params.row ? { target: { type: "service_account_credential" as const, id: params.row.id, label: params.row.name } } : {}),
810
+ reason: params.reason,
811
+ metadata: {
812
+ tokenPrefix: params.tokenPrefix,
813
+ ...(params.row ? { serviceAccountId: params.row.service_account_id } : {}),
814
+ },
815
+ });
816
+ return null;
817
+ };
818
+
819
+ export const authenticateApiToken = async (token: string): Promise<AuthenticatedServiceAccountCredential | null> => {
820
+ const parsed = parseToken(token);
821
+ if (!parsed) return null;
822
+
823
+ let row = await findActiveByTokenPrefix(parsed.tokenPrefix);
824
+ if (!row) {
825
+ return recordDeniedAuthentication({
826
+ reason: "API key not found, inactive, or expired",
827
+ tokenPrefix: parsed.tokenPrefix,
828
+ });
829
+ }
830
+ if (!(await verifyApiTokenSecretOnce(parsed.secret, row))) {
831
+ return recordDeniedAuthentication({
832
+ reason: "Invalid API key secret",
833
+ tokenPrefix: parsed.tokenPrefix,
834
+ row,
835
+ });
836
+ }
837
+
838
+ // Verification may be expensive. Refresh authorization afterwards so a
839
+ // request cannot join work that began before a credential was revoked.
840
+ const current = await findActiveByTokenPrefix(parsed.tokenPrefix);
841
+ if (!current || current.id !== row.id) {
842
+ return recordDeniedAuthentication({
843
+ reason: "API key not found, inactive, or expired",
844
+ tokenPrefix: parsed.tokenPrefix,
845
+ row,
846
+ });
847
+ }
848
+ if (current.secret_hash !== row.secret_hash && !(await verifyApiTokenSecretOnce(parsed.secret, current))) {
849
+ return recordDeniedAuthentication({
850
+ reason: "Invalid API key secret",
851
+ tokenPrefix: parsed.tokenPrefix,
852
+ row: current,
853
+ });
854
+ }
855
+ row = current;
856
+
857
+ const serviceAccount = mapServiceAccount(row);
858
+ const delegatedUser = serviceAccount.delegatedUserId ? await accounts.users.get({ id: serviceAccount.delegatedUserId }) : null;
859
+ if (delegatedUser && !(await isAccountCategoryAllowed(delegatedUser))) return null;
860
+ if (serviceAccount.kind === "user_delegated" && !delegatedUser) {
861
+ return recordDeniedAuthentication({
862
+ reason: "Delegated user is missing",
863
+ tokenPrefix: parsed.tokenPrefix,
864
+ row,
865
+ });
866
+ }
867
+
868
+ await recordSuccessfulAuthentication({ row, secret: parsed.secret, serviceAccount, delegatedUser });
869
+
870
+ return {
871
+ credential: mapCredential({ ...row, last_used_at: new Date() }),
872
+ serviceAccount,
873
+ delegatedUser,
874
+ };
875
+ };
876
+
877
+ export const serviceAccountCredentials = {
878
+ isApiToken,
879
+ getOrCreateUserDelegatedServiceAccount,
880
+ createApiToken,
881
+ createUserApiToken,
882
+ createResourceApiToken,
883
+ listForDelegatedUser,
884
+ listOverview,
885
+ getOverview,
886
+ revokeForDelegatedUser,
887
+ revoke,
888
+ authenticateApiToken,
889
+ };
890
+
891
+ import { isAccountCategoryAllowed } from "./account-category-policy";