@flowdular/sdk 0.1.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 (883) hide show
  1. package/.ai/agents/README.md +27 -0
  2. package/.ai/agents/module-executor.md +36 -0
  3. package/.ai/agents/reviewer.md +23 -0
  4. package/.ai/agents/sandbox/agentic-engineer.md +31 -0
  5. package/.ai/agents/sandbox/backend-engineer.md +36 -0
  6. package/.ai/agents/sandbox/business-manager.md +23 -0
  7. package/.ai/agents/sandbox/frontend-engineer.md +27 -0
  8. package/.ai/agents/sandbox/ux-designer.md +23 -0
  9. package/.ai/agents/spec-author.md +29 -0
  10. package/.ai/blueprints/add-migration/README.md +5 -0
  11. package/.ai/blueprints/add-migration/allowed-paths.yaml +23 -0
  12. package/.ai/blueprints/add-migration/blueprint.json +14 -0
  13. package/.ai/blueprints/add-migration/examples/invalid/input-destructive.json +6 -0
  14. package/.ai/blueprints/add-migration/examples/invalid/plan-unnumbered-file.json +9 -0
  15. package/.ai/blueprints/add-migration/examples/valid/input.json +6 -0
  16. package/.ai/blueprints/add-migration/examples/valid/plan.json +9 -0
  17. package/.ai/blueprints/add-migration/gates.yaml +30 -0
  18. package/.ai/blueprints/add-migration/input.schema.json +23 -0
  19. package/.ai/blueprints/add-migration/plan.schema.json +54 -0
  20. package/.ai/blueprints/add-migration/required-files.yaml +18 -0
  21. package/.ai/blueprints/add-migration/spec-requirements.yaml +13 -0
  22. package/.ai/blueprints/add-migration/steps.yaml +62 -0
  23. package/.ai/blueprints/author-spec/README.md +5 -0
  24. package/.ai/blueprints/author-spec/allowed-paths.yaml +7 -0
  25. package/.ai/blueprints/author-spec/blueprint.json +14 -0
  26. package/.ai/blueprints/author-spec/examples/invalid/input-missing-outcome.json +5 -0
  27. package/.ai/blueprints/author-spec/examples/valid/input.json +6 -0
  28. package/.ai/blueprints/author-spec/gates.yaml +13 -0
  29. package/.ai/blueprints/author-spec/input.schema.json +20 -0
  30. package/.ai/blueprints/author-spec/plan.schema.json +14 -0
  31. package/.ai/blueprints/author-spec/required-files.yaml +6 -0
  32. package/.ai/blueprints/author-spec/spec-requirements.yaml +35 -0
  33. package/.ai/blueprints/author-spec/steps.yaml +28 -0
  34. package/.ai/blueprints/author-spec/templates/module.yaml +45 -0
  35. package/.ai/blueprints/bug-fix/README.md +5 -0
  36. package/.ai/blueprints/bug-fix/allowed-paths.yaml +29 -0
  37. package/.ai/blueprints/bug-fix/blueprint.json +14 -0
  38. package/.ai/blueprints/bug-fix/examples/invalid/input-no-symptom.json +4 -0
  39. package/.ai/blueprints/bug-fix/examples/invalid/plan-no-test.json +8 -0
  40. package/.ai/blueprints/bug-fix/examples/valid/input.json +6 -0
  41. package/.ai/blueprints/bug-fix/examples/valid/plan.json +8 -0
  42. package/.ai/blueprints/bug-fix/gates.yaml +30 -0
  43. package/.ai/blueprints/bug-fix/input.schema.json +25 -0
  44. package/.ai/blueprints/bug-fix/plan.schema.json +53 -0
  45. package/.ai/blueprints/bug-fix/required-files.yaml +7 -0
  46. package/.ai/blueprints/bug-fix/spec-requirements.yaml +7 -0
  47. package/.ai/blueprints/bug-fix/steps.yaml +51 -0
  48. package/.ai/blueprints/core-extend/README.md +5 -0
  49. package/.ai/blueprints/core-extend/allowed-paths.yaml +49 -0
  50. package/.ai/blueprints/core-extend/blueprint.json +14 -0
  51. package/.ai/blueprints/core-extend/examples/invalid/input-unknown-package.json +5 -0
  52. package/.ai/blueprints/core-extend/examples/invalid/plan-missing-gates.json +7 -0
  53. package/.ai/blueprints/core-extend/examples/valid/input.json +6 -0
  54. package/.ai/blueprints/core-extend/examples/valid/plan.json +10 -0
  55. package/.ai/blueprints/core-extend/gates.yaml +16 -0
  56. package/.ai/blueprints/core-extend/input.schema.json +54 -0
  57. package/.ai/blueprints/core-extend/plan.schema.json +39 -0
  58. package/.ai/blueprints/core-extend/required-files.yaml +36 -0
  59. package/.ai/blueprints/core-extend/spec-requirements.yaml +17 -0
  60. package/.ai/blueprints/core-extend/steps.yaml +54 -0
  61. package/.ai/blueprints/edit-module/README.md +9 -0
  62. package/.ai/blueprints/edit-module/allowed-paths.yaml +27 -0
  63. package/.ai/blueprints/edit-module/blueprint.json +20 -0
  64. package/.ai/blueprints/edit-module/examples/invalid/input-unknown-change.json +5 -0
  65. package/.ai/blueprints/edit-module/examples/invalid/plan-touches-platform.json +15 -0
  66. package/.ai/blueprints/edit-module/examples/valid/input.json +5 -0
  67. package/.ai/blueprints/edit-module/examples/valid/plan.json +25 -0
  68. package/.ai/blueprints/edit-module/gates.yaml +30 -0
  69. package/.ai/blueprints/edit-module/input.schema.json +31 -0
  70. package/.ai/blueprints/edit-module/plan.schema.json +65 -0
  71. package/.ai/blueprints/edit-module/required-files.yaml +80 -0
  72. package/.ai/blueprints/edit-module/spec-requirements.yaml +15 -0
  73. package/.ai/blueprints/edit-module/steps.yaml +115 -0
  74. package/.ai/blueprints/new-module/README.md +7 -0
  75. package/.ai/blueprints/new-module/allowed-paths.yaml +27 -0
  76. package/.ai/blueprints/new-module/blueprint.json +20 -0
  77. package/.ai/blueprints/new-module/examples/invalid/input-spec-outside-modules.json +4 -0
  78. package/.ai/blueprints/new-module/examples/invalid/plan-unknown-gate.json +8 -0
  79. package/.ai/blueprints/new-module/examples/valid/input.json +5 -0
  80. package/.ai/blueprints/new-module/examples/valid/plan.json +22 -0
  81. package/.ai/blueprints/new-module/gates.yaml +30 -0
  82. package/.ai/blueprints/new-module/input.schema.json +21 -0
  83. package/.ai/blueprints/new-module/plan.schema.json +58 -0
  84. package/.ai/blueprints/new-module/required-files.yaml +73 -0
  85. package/.ai/blueprints/new-module/spec-requirements.yaml +30 -0
  86. package/.ai/blueprints/new-module/steps.yaml +138 -0
  87. package/.ai/blueprints/release/README.md +5 -0
  88. package/.ai/blueprints/release/allowed-paths.yaml +19 -0
  89. package/.ai/blueprints/release/blueprint.json +14 -0
  90. package/.ai/blueprints/release/examples/invalid/input-bad-version.json +4 -0
  91. package/.ai/blueprints/release/examples/invalid/plan-bad-branch.json +7 -0
  92. package/.ai/blueprints/release/examples/valid/input.json +5 -0
  93. package/.ai/blueprints/release/examples/valid/plan.json +20 -0
  94. package/.ai/blueprints/release/gates.yaml +20 -0
  95. package/.ai/blueprints/release/input.schema.json +24 -0
  96. package/.ai/blueprints/release/plan.schema.json +46 -0
  97. package/.ai/blueprints/release/required-files.yaml +19 -0
  98. package/.ai/blueprints/release/spec-requirements.yaml +8 -0
  99. package/.ai/blueprints/release/steps.yaml +47 -0
  100. package/.ai/blueprints/security-review/README.md +5 -0
  101. package/.ai/blueprints/security-review/allowed-paths.yaml +6 -0
  102. package/.ai/blueprints/security-review/blueprint.json +14 -0
  103. package/.ai/blueprints/security-review/examples/invalid/input-unknown-kind.json +4 -0
  104. package/.ai/blueprints/security-review/examples/invalid/plan-finding-without-scenario.json +14 -0
  105. package/.ai/blueprints/security-review/examples/valid/input.json +4 -0
  106. package/.ai/blueprints/security-review/examples/valid/plan.json +19 -0
  107. package/.ai/blueprints/security-review/gates.yaml +22 -0
  108. package/.ai/blueprints/security-review/input.schema.json +20 -0
  109. package/.ai/blueprints/security-review/plan.schema.json +65 -0
  110. package/.ai/blueprints/security-review/required-files.yaml +6 -0
  111. package/.ai/blueprints/security-review/spec-requirements.yaml +9 -0
  112. package/.ai/blueprints/security-review/steps.yaml +38 -0
  113. package/.ai/policies/capabilities.yaml +164 -0
  114. package/.ai/policies/model-routing.yaml +72 -0
  115. package/.ai/policies/path-ownership.yaml +65 -0
  116. package/.ai/policies/task-budgets.yaml +37 -0
  117. package/.ai/references/catalog/LICENSE +21 -0
  118. package/.ai/references/catalog/migrations/0001_catalog_core.down.sql +2 -0
  119. package/.ai/references/catalog/migrations/0001_catalog_core.up.sql +21 -0
  120. package/.ai/references/catalog/migrations/0002_catalog_history.down.sql +3 -0
  121. package/.ai/references/catalog/migrations/0002_catalog_history.up.sql +20 -0
  122. package/.ai/references/catalog/migrations/0003_catalog_history_service_actors.down.sql +3 -0
  123. package/.ai/references/catalog/migrations/0003_catalog_history_service_actors.up.sql +36 -0
  124. package/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.down.sql +3 -0
  125. package/.ai/references/catalog/migrations/0004_catalog_idempotency_ledger.up.sql +19 -0
  126. package/.ai/references/catalog/migrations/README.md +3 -0
  127. package/.ai/references/catalog/module.json +27 -0
  128. package/.ai/references/catalog/package.json +49 -0
  129. package/.ai/references/catalog/spec/module.yaml +86 -0
  130. package/.ai/references/catalog/src/acl/permissions.ts +6 -0
  131. package/.ai/references/catalog/src/agent/tools.ts +164 -0
  132. package/.ai/references/catalog/src/api/endpoints.ts +243 -0
  133. package/.ai/references/catalog/src/client/CatalogHistoryDrawer.tsrx +123 -0
  134. package/.ai/references/catalog/src/client/CatalogItemForm.tsrx +190 -0
  135. package/.ai/references/catalog/src/client/CatalogView.tsrx +473 -0
  136. package/.ai/references/catalog/src/client/api.ts +111 -0
  137. package/.ai/references/catalog/src/client/contribution.tsrx +61 -0
  138. package/.ai/references/catalog/src/client/index.ts +18 -0
  139. package/.ai/references/catalog/src/client/navigation-copy.ts +9 -0
  140. package/.ai/references/catalog/src/client/state.ts +24 -0
  141. package/.ai/references/catalog/src/domain/types.ts +32 -0
  142. package/.ai/references/catalog/src/domain/variables.ts +111 -0
  143. package/.ai/references/catalog/src/index.ts +31 -0
  144. package/.ai/references/catalog/src/platform.ts +35 -0
  145. package/.ai/references/catalog/src/server/index.ts +4 -0
  146. package/.ai/references/catalog/src/server/runtime.ts +86 -0
  147. package/.ai/references/catalog/src/services/catalog-service.ts +306 -0
  148. package/.ai/references/catalog/src/services/database-repository.ts +440 -0
  149. package/.ai/references/catalog/src/services/index.ts +4 -0
  150. package/.ai/references/catalog/src/services/migration.ts +171 -0
  151. package/.ai/references/catalog/src/services/repository.ts +36 -0
  152. package/.ai/references/catalog/src/services/target-idempotency.ts +59 -0
  153. package/.ai/references/catalog/tests/agent-tools.test.ts +277 -0
  154. package/.ai/references/catalog/tests/endpoints.test.ts +320 -0
  155. package/.ai/references/catalog/tests/idempotency.test.ts +297 -0
  156. package/.ai/references/catalog/tests/migrations.test.ts +149 -0
  157. package/.ai/references/catalog/tests/module.test.ts +271 -0
  158. package/.ai/references/catalog/tests/support/database.ts +76 -0
  159. package/.ai/references/catalog/translations/en.json +101 -0
  160. package/.ai/references/catalog/translations/pl.json +101 -0
  161. package/.ai/references/catalog/tsconfig.json +15 -0
  162. package/.ai/references/catalog/vitest.config.ts +16 -0
  163. package/.ai/references/catalog.provenance.json +55 -0
  164. package/.ai/skills/README.md +36 -0
  165. package/.ai/skills/agent-tool-design/SKILL.md +209 -0
  166. package/.ai/skills/auth-security-review/SKILL.md +96 -0
  167. package/.ai/skills/auto-review/SKILL.md +112 -0
  168. package/.ai/skills/bug-hunt/SKILL.md +110 -0
  169. package/.ai/skills/business-agent-design/SKILL.md +188 -0
  170. package/.ai/skills/cli-extension/SKILL.md +114 -0
  171. package/.ai/skills/core-extend/SKILL.md +104 -0
  172. package/.ai/skills/database-adapter/SKILL.md +204 -0
  173. package/.ai/skills/database-adapter/references/first-run-and-matrix.md +105 -0
  174. package/.ai/skills/migration-authoring/SKILL.md +167 -0
  175. package/.ai/skills/module-new/SKILL.md +180 -0
  176. package/.ai/skills/module-update/SKILL.md +100 -0
  177. package/.ai/skills/perf-audit/SKILL.md +105 -0
  178. package/.ai/skills/release-eject-pr/SKILL.md +113 -0
  179. package/.ai/skills/spec-approval/SKILL.md +112 -0
  180. package/.ai/skills/test-hardening/SKILL.md +86 -0
  181. package/.ai/skills/translations-i18n/SKILL.md +85 -0
  182. package/.ai/skills/ux-design/SKILL.md +97 -0
  183. package/.ai/skills/variables/SKILL.md +164 -0
  184. package/.ai/skills/workflow-development/SKILL.md +199 -0
  185. package/AGENTS.md +59 -0
  186. package/LICENSE +21 -0
  187. package/README.md +24 -0
  188. package/docs/agent-contract.md +45 -0
  189. package/docs/design-system.md +217 -0
  190. package/modules/agents/migrations/0001_agents_core.down.sql +4 -0
  191. package/modules/agents/migrations/0001_agents_core.up.sql +106 -0
  192. package/modules/agents/migrations/0002_provider_connections.down.sql +1 -0
  193. package/modules/agents/migrations/0002_provider_connections.up.sql +34 -0
  194. package/modules/agents/migrations/0003_resource_audit.down.sql +1 -0
  195. package/modules/agents/migrations/0003_resource_audit.up.sql +25 -0
  196. package/modules/agents/migrations/0004_run_grants.down.sql +1 -0
  197. package/modules/agents/migrations/0004_run_grants.up.sql +19 -0
  198. package/modules/agents/migrations/0005_long_running_limits.down.sql +2 -0
  199. package/modules/agents/migrations/0005_long_running_limits.up.sql +14 -0
  200. package/modules/agents/migrations/0006_agent_skills.down.sql +3 -0
  201. package/modules/agents/migrations/0006_agent_skills.up.sql +44 -0
  202. package/modules/agents/migrations/0007_model_readiness.down.sql +1 -0
  203. package/modules/agents/migrations/0007_model_readiness.up.sql +18 -0
  204. package/modules/agents/migrations/0008_output_limits.down.sql +2 -0
  205. package/modules/agents/migrations/0008_output_limits.up.sql +8 -0
  206. package/modules/agents/migrations/0009_agent_audit_v3.down.sql +1 -0
  207. package/modules/agents/migrations/0009_agent_audit_v3.up.sql +25 -0
  208. package/modules/agents/migrations/0012_agent_run_costs.down.sql +3 -0
  209. package/modules/agents/migrations/0012_agent_run_costs.up.sql +32 -0
  210. package/modules/agents/migrations/0013_workflow_prerequisites.down.sql +3 -0
  211. package/modules/agents/migrations/0013_workflow_prerequisites.up.sql +97 -0
  212. package/modules/agents/migrations/0014_agent_action_audit.down.sql +1 -0
  213. package/modules/agents/migrations/0014_agent_action_audit.up.sql +25 -0
  214. package/modules/agents/migrations/0015_agent_run_actors.down.sql +1 -0
  215. package/modules/agents/migrations/0015_agent_run_actors.up.sql +17 -0
  216. package/modules/agents/migrations/0016_module_owned_agents.down.sql +6 -0
  217. package/modules/agents/migrations/0016_module_owned_agents.up.sql +68 -0
  218. package/modules/agents/migrations/0017_agent_authorization_subjects.down.sql +2 -0
  219. package/modules/agents/migrations/0017_agent_authorization_subjects.up.sql +18 -0
  220. package/modules/agents/migrations/0018_agent_child_tenant_isolation.down.sql +12 -0
  221. package/modules/agents/migrations/0018_agent_child_tenant_isolation.up.sql +88 -0
  222. package/modules/agents/migrations/0019_background_recovery_grants.down.sql +6 -0
  223. package/modules/agents/migrations/0019_background_recovery_grants.up.sql +17 -0
  224. package/modules/agents/migrations/0020_provider_summary_role.down.sql +2 -0
  225. package/modules/agents/migrations/0020_provider_summary_role.up.sql +17 -0
  226. package/modules/agents/migrations/0021_agents_agent_reconciliation_role.down.sql +4 -0
  227. package/modules/agents/migrations/0021_agents_agent_reconciliation_role.up.sql +7 -0
  228. package/modules/agents/migrations/README.md +39 -0
  229. package/modules/agents/module.json +38 -0
  230. package/modules/agents/package.json +56 -0
  231. package/modules/agents/spec/module.yaml +271 -0
  232. package/modules/agents/spec/procedures-compatibility.md +63 -0
  233. package/modules/agents/src/acl/permissions.ts +13 -0
  234. package/modules/agents/src/api/endpoints.ts +1122 -0
  235. package/modules/agents/src/cli/commands.json +29 -0
  236. package/modules/agents/src/cli/index.ts +162 -0
  237. package/modules/agents/src/client/AgentDashboardWidget.tsrx +32 -0
  238. package/modules/agents/src/client/AgentDefinitionForm.tsrx +362 -0
  239. package/modules/agents/src/client/AgentDefinitionList.tsrx +127 -0
  240. package/modules/agents/src/client/AgentPlaygroundView.tsrx +246 -0
  241. package/modules/agents/src/client/AgentProcedureForm.tsrx +182 -0
  242. package/modules/agents/src/client/AgentProcedureList.tsrx +118 -0
  243. package/modules/agents/src/client/AgentProceduresView.tsrx +253 -0
  244. package/modules/agents/src/client/AgentProvidersView.tsrx +248 -0
  245. package/modules/agents/src/client/AgentRunsView.tsrx +304 -0
  246. package/modules/agents/src/client/AgentUsageView.tsrx +233 -0
  247. package/modules/agents/src/client/AgentsView.tsrx +339 -0
  248. package/modules/agents/src/client/ModuleAgentBindingForm.tsrx +239 -0
  249. package/modules/agents/src/client/ModuleAgentList.tsrx +122 -0
  250. package/modules/agents/src/client/ProviderConnectionForm.tsrx +273 -0
  251. package/modules/agents/src/client/ProviderConnectionList.tsrx +229 -0
  252. package/modules/agents/src/client/ProviderModelEditor.tsrx +313 -0
  253. package/modules/agents/src/client/RunTimeline.tsrx +212 -0
  254. package/modules/agents/src/client/agents.css +349 -0
  255. package/modules/agents/src/client/api.ts +373 -0
  256. package/modules/agents/src/client/contribution.tsrx +181 -0
  257. package/modules/agents/src/client/index.ts +21 -0
  258. package/modules/agents/src/client/presentation.ts +162 -0
  259. package/modules/agents/src/client/state.ts +100 -0
  260. package/modules/agents/src/domain/context-variables.ts +58 -0
  261. package/modules/agents/src/domain/run-timeline.ts +145 -0
  262. package/modules/agents/src/domain/types.ts +481 -0
  263. package/modules/agents/src/index.ts +88 -0
  264. package/modules/agents/src/platform.ts +82 -0
  265. package/modules/agents/src/server/action-execution.ts +811 -0
  266. package/modules/agents/src/server/define-agent.ts +170 -0
  267. package/modules/agents/src/server/index.ts +67 -0
  268. package/modules/agents/src/server/run-execution.ts +231 -0
  269. package/modules/agents/src/server/run-queue.ts +96 -0
  270. package/modules/agents/src/server/runtime.ts +448 -0
  271. package/modules/agents/src/server/tools.ts +18 -0
  272. package/modules/agents/src/services/agent-service.ts +2167 -0
  273. package/modules/agents/src/services/credential-vault.ts +133 -0
  274. package/modules/agents/src/services/database-repository.ts +2786 -0
  275. package/modules/agents/src/services/index.ts +33 -0
  276. package/modules/agents/src/services/migration.ts +1152 -0
  277. package/modules/agents/src/services/module-agent-preflight.ts +76 -0
  278. package/modules/agents/src/services/outbound-policy.ts +136 -0
  279. package/modules/agents/src/services/provider-broker.ts +80 -0
  280. package/modules/agents/src/services/provider-repository.ts +589 -0
  281. package/modules/agents/src/services/provider-service.ts +968 -0
  282. package/modules/agents/src/services/repository.ts +294 -0
  283. package/modules/agents/src/services/run-grant.ts +305 -0
  284. package/modules/agents/src/services/usage-service.ts +119 -0
  285. package/modules/agents/src/services/worker.ts +442 -0
  286. package/modules/agents/src/settings.ts +244 -0
  287. package/modules/agents/translations/en.json +403 -0
  288. package/modules/agents/translations/pl.json +403 -0
  289. package/modules/auth/README.md +215 -0
  290. package/modules/auth/migrations/0001_auth_core.up.sql +108 -0
  291. package/modules/auth/migrations/0002_bundled_module_scopes.up.sql +26 -0
  292. package/modules/auth/migrations/0003_agent_core_scopes.up.sql +12 -0
  293. package/modules/auth/migrations/0004_owner_only_development_scopes.up.sql +8 -0
  294. package/modules/auth/migrations/0005_agent_provider_scopes.up.sql +13 -0
  295. package/modules/auth/migrations/0006_tenant_slug.up.sql +6 -0
  296. package/modules/auth/migrations/0007_sandbox_scopes.up.sql +13 -0
  297. package/modules/auth/migrations/0008_api_tokens.up.sql +43 -0
  298. package/modules/auth/migrations/0009_module_settings.up.sql +24 -0
  299. package/modules/auth/migrations/0010_sign_in_lockout.up.sql +11 -0
  300. package/modules/auth/migrations/0011_session_ids_and_password_reset.up.sql +7 -0
  301. package/modules/auth/migrations/0012_roles.up.sql +31 -0
  302. package/modules/auth/migrations/0013_audit.up.sql +27 -0
  303. package/modules/auth/migrations/0014_audit_actor.up.sql +4 -0
  304. package/modules/auth/migrations/0015_account_security.up.sql +81 -0
  305. package/modules/auth/module.json +26 -0
  306. package/modules/auth/package.json +61 -0
  307. package/modules/auth/spec/module.yaml +190 -0
  308. package/modules/auth/src/acl/scopes.ts +86 -0
  309. package/modules/auth/src/api/attempt-limiter.ts +42 -0
  310. package/modules/auth/src/api/client-address.ts +14 -0
  311. package/modules/auth/src/api/cookies.ts +52 -0
  312. package/modules/auth/src/api/origin.ts +40 -0
  313. package/modules/auth/src/cli/commands.json +74 -0
  314. package/modules/auth/src/cli/database.ts +51 -0
  315. package/modules/auth/src/cli/greenfield.ts +173 -0
  316. package/modules/auth/src/cli/index.ts +184 -0
  317. package/modules/auth/src/cli/provisioning.ts +211 -0
  318. package/modules/auth/src/client/AuthBackdrop.tsrx +137 -0
  319. package/modules/auth/src/client/AuthenticationCore.tsrx +630 -0
  320. package/modules/auth/src/client/PasswordChangeRequired.tsrx +145 -0
  321. package/modules/auth/src/client/PublicAuthFlows.tsrx +379 -0
  322. package/modules/auth/src/client/api.ts +464 -0
  323. package/modules/auth/src/client/audit/AuditView.tsrx +357 -0
  324. package/modules/auth/src/client/audit/api.ts +164 -0
  325. package/modules/auth/src/client/audit/audit-columns.tsrx +47 -0
  326. package/modules/auth/src/client/audit/audit-row.ts +38 -0
  327. package/modules/auth/src/client/auth.css +483 -0
  328. package/modules/auth/src/client/contribution.tsrx +100 -0
  329. package/modules/auth/src/client/index.ts +32 -0
  330. package/modules/auth/src/client/security/SecurityView.tsrx +253 -0
  331. package/modules/auth/src/client/settings/PreferencesCard.tsrx +65 -0
  332. package/modules/auth/src/client/settings/SettingsView.tsrx +212 -0
  333. package/modules/auth/src/client/settings/WorkspaceCard.tsrx +59 -0
  334. package/modules/auth/src/client/settings/api.ts +115 -0
  335. package/modules/auth/src/client/slug.ts +26 -0
  336. package/modules/auth/src/client/state.ts +115 -0
  337. package/modules/auth/src/client/tokens/ApiTokenForm.tsrx +153 -0
  338. package/modules/auth/src/client/tokens/ApiTokenSecret.tsrx +35 -0
  339. package/modules/auth/src/client/tokens/ApiTokensView.tsrx +216 -0
  340. package/modules/auth/src/client/tokens/api.ts +59 -0
  341. package/modules/auth/src/client/tokens/state.ts +26 -0
  342. package/modules/auth/src/client/tokens/token-columns.tsrx +64 -0
  343. package/modules/auth/src/client/translations.ts +23 -0
  344. package/modules/auth/src/domain/types.ts +152 -0
  345. package/modules/auth/src/index.ts +42 -0
  346. package/modules/auth/src/middleware/authentication.ts +97 -0
  347. package/modules/auth/src/platform.ts +0 -0
  348. package/modules/auth/src/server/actor.ts +12 -0
  349. package/modules/auth/src/server/audit-endpoints.ts +51 -0
  350. package/modules/auth/src/server/composition.ts +9 -0
  351. package/modules/auth/src/server/endpoints.ts +893 -0
  352. package/modules/auth/src/server/http.ts +153 -0
  353. package/modules/auth/src/server/index.ts +37 -0
  354. package/modules/auth/src/server/roles-endpoints.ts +120 -0
  355. package/modules/auth/src/server/runtime.ts +705 -0
  356. package/modules/auth/src/server/session-endpoints.ts +86 -0
  357. package/modules/auth/src/server/session-security.ts +62 -0
  358. package/modules/auth/src/server/token-endpoints.ts +106 -0
  359. package/modules/auth/src/services/auth-service-error.ts +11 -0
  360. package/modules/auth/src/services/auth-service.ts +2309 -0
  361. package/modules/auth/src/services/database-repository.ts +1825 -0
  362. package/modules/auth/src/services/mail-delivery.ts +27 -0
  363. package/modules/auth/src/services/migration.ts +591 -0
  364. package/modules/auth/src/services/password.ts +115 -0
  365. package/modules/auth/src/services/repository.ts +395 -0
  366. package/modules/auth/src/services/settings-store.ts +92 -0
  367. package/modules/auth/src/services/totp.ts +135 -0
  368. package/modules/auth/src/services/validation.ts +186 -0
  369. package/modules/auth/src/settings.ts +150 -0
  370. package/modules/auth/translations/en.json +263 -0
  371. package/modules/auth/translations/pl.json +263 -0
  372. package/modules/automations/migrations/0001_automations_core.down.sql +10 -0
  373. package/modules/automations/migrations/0001_automations_core.up.sql +76 -0
  374. package/modules/automations/migrations/0002_automations_targets.down.sql +9 -0
  375. package/modules/automations/migrations/0002_automations_targets.up.sql +37 -0
  376. package/modules/automations/migrations/0003_automations_scheduler_role.down.sql +3 -0
  377. package/modules/automations/migrations/0003_automations_scheduler_role.up.sql +19 -0
  378. package/modules/automations/migrations/0004_automations_trigger_routing_role.down.sql +3 -0
  379. package/modules/automations/migrations/0004_automations_trigger_routing_role.up.sql +18 -0
  380. package/modules/automations/migrations/README.md +3 -0
  381. package/modules/automations/module.json +31 -0
  382. package/modules/automations/package.json +55 -0
  383. package/modules/automations/spec/module.yaml +93 -0
  384. package/modules/automations/src/acl/permissions.ts +10 -0
  385. package/modules/automations/src/api/endpoints.ts +480 -0
  386. package/modules/automations/src/client/AutomationScheduleForm.tsrx +218 -0
  387. package/modules/automations/src/client/AutomationSchedulesView.tsrx +339 -0
  388. package/modules/automations/src/client/AutomationTriggerForm.tsrx +190 -0
  389. package/modules/automations/src/client/AutomationTriggersView.tsrx +329 -0
  390. package/modules/automations/src/client/api.ts +148 -0
  391. package/modules/automations/src/client/contribution.tsrx +70 -0
  392. package/modules/automations/src/client/index.ts +20 -0
  393. package/modules/automations/src/client/presentation.ts +38 -0
  394. package/modules/automations/src/client/state.ts +40 -0
  395. package/modules/automations/src/client/target-selection.ts +44 -0
  396. package/modules/automations/src/domain/cadence.ts +44 -0
  397. package/modules/automations/src/domain/types.ts +110 -0
  398. package/modules/automations/src/domain/variables.ts +178 -0
  399. package/modules/automations/src/index.ts +39 -0
  400. package/modules/automations/src/platform.ts +73 -0
  401. package/modules/automations/src/server/execution.ts +77 -0
  402. package/modules/automations/src/server/index.ts +39 -0
  403. package/modules/automations/src/server/runtime.ts +218 -0
  404. package/modules/automations/src/server/targets.ts +116 -0
  405. package/modules/automations/src/services/automations-service.ts +10 -0
  406. package/modules/automations/src/services/database-repository.ts +763 -0
  407. package/modules/automations/src/services/index.ts +5 -0
  408. package/modules/automations/src/services/migration.ts +223 -0
  409. package/modules/automations/src/services/repository.ts +119 -0
  410. package/modules/automations/src/services/schedule-service.ts +837 -0
  411. package/modules/automations/src/services/secret-vault.ts +124 -0
  412. package/modules/automations/src/services/trigger-service.ts +648 -0
  413. package/modules/automations/src/settings.ts +69 -0
  414. package/modules/automations/translations/en.json +126 -0
  415. package/modules/automations/translations/pl.json +126 -0
  416. package/modules/automations-workflows-integration/module.json +27 -0
  417. package/modules/automations-workflows-integration/package.json +48 -0
  418. package/modules/automations-workflows-integration/spec/module.yaml +93 -0
  419. package/modules/automations-workflows-integration/src/index.ts +14 -0
  420. package/modules/automations-workflows-integration/src/platform.ts +24 -0
  421. package/modules/automations-workflows-integration/src/server/adapter.ts +272 -0
  422. package/modules/automations-workflows-integration/src/server/index.ts +11 -0
  423. package/modules/automations-workflows-integration/src/server/tools.ts +0 -0
  424. package/modules/automations-workflows-integration/translations/en.json +21 -0
  425. package/modules/automations-workflows-integration/translations/pl.json +21 -0
  426. package/modules/profile/migrations/0001_profile_core.down.sql +1 -0
  427. package/modules/profile/migrations/0001_profile_core.up.sql +13 -0
  428. package/modules/profile/migrations/0002_profile_language.down.sql +1 -0
  429. package/modules/profile/migrations/0002_profile_language.up.sql +12 -0
  430. package/modules/profile/migrations/README.md +3 -0
  431. package/modules/profile/module.json +23 -0
  432. package/modules/profile/package.json +55 -0
  433. package/modules/profile/spec/module.yaml +136 -0
  434. package/modules/profile/src/acl/permissions.ts +5 -0
  435. package/modules/profile/src/api/endpoints.ts +184 -0
  436. package/modules/profile/src/client/DisplayNameCard.tsrx +59 -0
  437. package/modules/profile/src/client/LanguageCard.tsrx +89 -0
  438. package/modules/profile/src/client/PasswordCard.tsrx +79 -0
  439. package/modules/profile/src/client/ProfileView.tsrx +192 -0
  440. package/modules/profile/src/client/SessionsCard.tsrx +158 -0
  441. package/modules/profile/src/client/api.ts +134 -0
  442. package/modules/profile/src/client/contribution.tsrx +61 -0
  443. package/modules/profile/src/client/index.ts +15 -0
  444. package/modules/profile/src/client/password.ts +41 -0
  445. package/modules/profile/src/client/sessions-api.ts +52 -0
  446. package/modules/profile/src/client/state.ts +23 -0
  447. package/modules/profile/src/domain/types.ts +21 -0
  448. package/modules/profile/src/index.ts +21 -0
  449. package/modules/profile/src/platform.ts +23 -0
  450. package/modules/profile/src/server/index.ts +7 -0
  451. package/modules/profile/src/server/runtime.ts +84 -0
  452. package/modules/profile/src/services/database-repository.ts +154 -0
  453. package/modules/profile/src/services/index.ts +6 -0
  454. package/modules/profile/src/services/migration.ts +58 -0
  455. package/modules/profile/src/services/profile-service.ts +114 -0
  456. package/modules/profile/src/services/repository.ts +13 -0
  457. package/modules/profile/translations/en.json +58 -0
  458. package/modules/profile/translations/pl.json +58 -0
  459. package/modules/sandbox/migrations/0001_sandbox_core.down.sql +6 -0
  460. package/modules/sandbox/migrations/0001_sandbox_core.up.sql +70 -0
  461. package/modules/sandbox/migrations/0002_sandbox_session_lifecycle.down.sql +36 -0
  462. package/modules/sandbox/migrations/0002_sandbox_session_lifecycle.up.sql +37 -0
  463. package/modules/sandbox/migrations/README.md +7 -0
  464. package/modules/sandbox/module.json +31 -0
  465. package/modules/sandbox/package.json +56 -0
  466. package/modules/sandbox/spec/module.yaml +111 -0
  467. package/modules/sandbox/src/acl/permissions.ts +18 -0
  468. package/modules/sandbox/src/api/endpoints.ts +439 -0
  469. package/modules/sandbox/src/cli/commands.json +84 -0
  470. package/modules/sandbox/src/cli/index.ts +450 -0
  471. package/modules/sandbox/src/client/SandboxAccessView.tsrx +191 -0
  472. package/modules/sandbox/src/client/SandboxGrantForm.tsrx +134 -0
  473. package/modules/sandbox/src/client/SandboxGrantList.tsrx +83 -0
  474. package/modules/sandbox/src/client/SandboxSessionList.tsrx +56 -0
  475. package/modules/sandbox/src/client/api.ts +87 -0
  476. package/modules/sandbox/src/client/contribution.tsrx +47 -0
  477. package/modules/sandbox/src/client/index.ts +18 -0
  478. package/modules/sandbox/src/client/presentation.ts +47 -0
  479. package/modules/sandbox/src/client/state.ts +20 -0
  480. package/modules/sandbox/src/domain/types.ts +125 -0
  481. package/modules/sandbox/src/index.ts +33 -0
  482. package/modules/sandbox/src/platform.ts +28 -0
  483. package/modules/sandbox/src/server/index.ts +20 -0
  484. package/modules/sandbox/src/server/runtime.ts +119 -0
  485. package/modules/sandbox/src/services/database-repository.ts +567 -0
  486. package/modules/sandbox/src/services/directory.ts +33 -0
  487. package/modules/sandbox/src/services/index.ts +19 -0
  488. package/modules/sandbox/src/services/migration.ts +145 -0
  489. package/modules/sandbox/src/services/repository.ts +55 -0
  490. package/modules/sandbox/src/services/sandbox-service-error.ts +10 -0
  491. package/modules/sandbox/src/services/sandbox-service.ts +533 -0
  492. package/modules/sandbox/translations/en.json +77 -0
  493. package/modules/sandbox/translations/pl.json +77 -0
  494. package/modules/system/module.json +18 -0
  495. package/modules/system/package.json +56 -0
  496. package/modules/system/spec/module.yaml +104 -0
  497. package/modules/system/src/acl/permissions.ts +10 -0
  498. package/modules/system/src/client/ModuleSettingRow.tsrx +191 -0
  499. package/modules/system/src/client/ModuleSettingsSection.tsrx +155 -0
  500. package/modules/system/src/client/ModulesView.tsrx +241 -0
  501. package/modules/system/src/client/api.ts +36 -0
  502. package/modules/system/src/client/contribution.tsrx +48 -0
  503. package/modules/system/src/client/index.ts +26 -0
  504. package/modules/system/src/client/module-columns.tsrx +66 -0
  505. package/modules/system/src/client/overview-api.ts +30 -0
  506. package/modules/system/src/client/setting-copy.ts +30 -0
  507. package/modules/system/src/client/settings-api.ts +57 -0
  508. package/modules/system/src/client/state.ts +16 -0
  509. package/modules/system/src/index.ts +11 -0
  510. package/modules/system/src/platform.ts +11 -0
  511. package/modules/system/src/server/endpoints.ts +372 -0
  512. package/modules/system/src/server/index.ts +11 -0
  513. package/modules/system/src/server/module-catalog.ts +117 -0
  514. package/modules/system/translations/en.json +64 -0
  515. package/modules/system/translations/pl.json +64 -0
  516. package/modules/users/migrations/README.md +3 -0
  517. package/modules/users/module.json +27 -0
  518. package/modules/users/package.json +54 -0
  519. package/modules/users/spec/module.yaml +79 -0
  520. package/modules/users/src/acl/permissions.ts +6 -0
  521. package/modules/users/src/api/endpoints.ts +222 -0
  522. package/modules/users/src/client/InviteMemberForm.tsrx +62 -0
  523. package/modules/users/src/client/MemberDrawer.tsrx +298 -0
  524. package/modules/users/src/client/RoleForm.tsrx +140 -0
  525. package/modules/users/src/client/RolesView.tsrx +288 -0
  526. package/modules/users/src/client/UserAccountForm.tsrx +86 -0
  527. package/modules/users/src/client/UsersView.tsrx +486 -0
  528. package/modules/users/src/client/api.ts +172 -0
  529. package/modules/users/src/client/contribution.tsrx +92 -0
  530. package/modules/users/src/client/index.ts +19 -0
  531. package/modules/users/src/client/member-columns.tsrx +52 -0
  532. package/modules/users/src/client/roles-api.ts +90 -0
  533. package/modules/users/src/client/state.ts +42 -0
  534. package/modules/users/src/index.ts +27 -0
  535. package/modules/users/src/platform.ts +11 -0
  536. package/modules/users/src/services/index.ts +2 -0
  537. package/modules/users/src/services/users-service.ts +142 -0
  538. package/modules/users/translations/en.json +133 -0
  539. package/modules/users/translations/pl.json +133 -0
  540. package/modules/workflows/migrations/0001_workflows_core.down.sql +19 -0
  541. package/modules/workflows/migrations/0001_workflows_core.up.sql +228 -0
  542. package/modules/workflows/migrations/0002_workflows_authorization_subject.down.sql +1 -0
  543. package/modules/workflows/migrations/0002_workflows_authorization_subject.up.sql +9 -0
  544. package/modules/workflows/migrations/0003_workflows_worker_role.up.sql +34 -0
  545. package/modules/workflows/migrations/README.md +7 -0
  546. package/modules/workflows/module.json +31 -0
  547. package/modules/workflows/package.json +55 -0
  548. package/modules/workflows/spec/module.yaml +227 -0
  549. package/modules/workflows/src/acl/permissions.ts +10 -0
  550. package/modules/workflows/src/api/endpoints.ts +852 -0
  551. package/modules/workflows/src/client/WorkflowCanvas.tsrx +819 -0
  552. package/modules/workflows/src/client/WorkflowCreateForm.tsrx +111 -0
  553. package/modules/workflows/src/client/WorkflowEditor.tsrx +443 -0
  554. package/modules/workflows/src/client/WorkflowExecutionHistory.tsrx +169 -0
  555. package/modules/workflows/src/client/WorkflowFixtureDrawer.tsrx +206 -0
  556. package/modules/workflows/src/client/WorkflowInputForm.tsrx +125 -0
  557. package/modules/workflows/src/client/WorkflowInputSchemaEditor.tsrx +185 -0
  558. package/modules/workflows/src/client/WorkflowInspector.tsrx +471 -0
  559. package/modules/workflows/src/client/WorkflowLiveConfirmation.tsrx +67 -0
  560. package/modules/workflows/src/client/WorkflowMappingEditor.tsrx +283 -0
  561. package/modules/workflows/src/client/WorkflowNodePalette.tsrx +50 -0
  562. package/modules/workflows/src/client/WorkflowPointerField.tsrx +178 -0
  563. package/modules/workflows/src/client/WorkflowRevisionReview.tsrx +49 -0
  564. package/modules/workflows/src/client/WorkflowRunInspector.tsrx +143 -0
  565. package/modules/workflows/src/client/WorkflowTestPanel.tsrx +109 -0
  566. package/modules/workflows/src/client/WorkflowValidationPanel.tsrx +139 -0
  567. package/modules/workflows/src/client/WorkflowsView.tsrx +1060 -0
  568. package/modules/workflows/src/client/api.ts +312 -0
  569. package/modules/workflows/src/client/canvas-model.ts +472 -0
  570. package/modules/workflows/src/client/connected-node.ts +71 -0
  571. package/modules/workflows/src/client/contribution.tsrx +57 -0
  572. package/modules/workflows/src/client/create-model.ts +15 -0
  573. package/modules/workflows/src/client/draft-repair.ts +86 -0
  574. package/modules/workflows/src/client/index.ts +19 -0
  575. package/modules/workflows/src/client/input-model.ts +124 -0
  576. package/modules/workflows/src/client/mapping-model.ts +110 -0
  577. package/modules/workflows/src/client/pointer-model.ts +123 -0
  578. package/modules/workflows/src/client/presentation.ts +62 -0
  579. package/modules/workflows/src/client/run-actions.ts +19 -0
  580. package/modules/workflows/src/client/state.ts +57 -0
  581. package/modules/workflows/src/client/viewport.ts +107 -0
  582. package/modules/workflows/src/client/workflows.css +899 -0
  583. package/modules/workflows/src/domain/events.ts +399 -0
  584. package/modules/workflows/src/domain/graph.ts +1136 -0
  585. package/modules/workflows/src/domain/types.ts +563 -0
  586. package/modules/workflows/src/index.ts +27 -0
  587. package/modules/workflows/src/platform.ts +49 -0
  588. package/modules/workflows/src/server/index.ts +13 -0
  589. package/modules/workflows/src/server/runtime.ts +266 -0
  590. package/modules/workflows/src/services/cursor-codec.ts +62 -0
  591. package/modules/workflows/src/services/database-repository.ts +2407 -0
  592. package/modules/workflows/src/services/index.ts +6 -0
  593. package/modules/workflows/src/services/migration.ts +375 -0
  594. package/modules/workflows/src/services/payload-codec.ts +264 -0
  595. package/modules/workflows/src/services/repository.ts +285 -0
  596. package/modules/workflows/src/services/simulation.ts +359 -0
  597. package/modules/workflows/src/services/worker.ts +1196 -0
  598. package/modules/workflows/src/services/workflows-service.ts +1395 -0
  599. package/modules/workflows/translations/en.json +348 -0
  600. package/modules/workflows/translations/pl.json +348 -0
  601. package/modules.json +41 -0
  602. package/package.json +125 -0
  603. package/packages/ai-provider/package.json +46 -0
  604. package/packages/ai-provider/src/catalog.ts +102 -0
  605. package/packages/ai-provider/src/errors.ts +110 -0
  606. package/packages/ai-provider/src/index.ts +33 -0
  607. package/packages/ai-provider/src/model.ts +198 -0
  608. package/packages/ai-provider/src/pricing.ts +73 -0
  609. package/packages/cli-protocol/package.json +40 -0
  610. package/packages/cli-protocol/src/index.ts +113 -0
  611. package/packages/client/package.json +46 -0
  612. package/packages/client/src/ApplicationShell.tsrx +374 -0
  613. package/packages/client/src/contributions.ts +172 -0
  614. package/packages/client/src/i18n/index.ts +30 -0
  615. package/packages/client/src/i18n/locale.ts +25 -0
  616. package/packages/client/src/i18n/locales/en.json +66 -0
  617. package/packages/client/src/i18n/locales/pl.json +66 -0
  618. package/packages/client/src/i18n/runtime.ts +201 -0
  619. package/packages/client/src/i18n/translations.ts +88 -0
  620. package/packages/client/src/index.ts +55 -0
  621. package/packages/client/src/routing.ts +9 -0
  622. package/packages/client/src/shell/BrandMark.tsrx +10 -0
  623. package/packages/client/src/shell/CommandPalette.tsrx +79 -0
  624. package/packages/client/src/shell/ContributionOutlets.tsrx +31 -0
  625. package/packages/client/src/shell/DashboardView.tsrx +60 -0
  626. package/packages/client/src/shell/MobileHeader.tsrx +24 -0
  627. package/packages/client/src/shell/OrganizationSwitcher.tsrx +100 -0
  628. package/packages/client/src/shell/OverviewCharts.tsrx +167 -0
  629. package/packages/client/src/shell/PrimaryNavigation.tsrx +75 -0
  630. package/packages/client/src/shell/Sidebar.tsrx +76 -0
  631. package/packages/client/src/shell/Topbar.tsrx +44 -0
  632. package/packages/client/src/shell/UserMenu.tsrx +103 -0
  633. package/packages/client/src/shell/WorkspaceContent.tsrx +30 -0
  634. package/packages/client/src/shell/identity.ts +12 -0
  635. package/packages/client/src/shell/navigation.ts +68 -0
  636. package/packages/client/src/shell/shell.css +734 -0
  637. package/packages/client/src/shell/types.ts +29 -0
  638. package/packages/client/src/state.ts +63 -0
  639. package/packages/client/src/web.ts +39 -0
  640. package/packages/coding-agent/package.json +44 -0
  641. package/packages/coding-agent/src/drivers/byok.ts +322 -0
  642. package/packages/coding-agent/src/drivers/claude-code.ts +290 -0
  643. package/packages/coding-agent/src/drivers/codex.ts +281 -0
  644. package/packages/coding-agent/src/index.ts +56 -0
  645. package/packages/coding-agent/src/registry.ts +119 -0
  646. package/packages/coding-agent/src/roles/contract.ts +60 -0
  647. package/packages/coding-agent/src/roles/defaults.ts +134 -0
  648. package/packages/coding-agent/src/roles/handoff.ts +41 -0
  649. package/packages/coding-agent/src/roles/registry.ts +156 -0
  650. package/packages/coding-agent/src/roles/skills.ts +72 -0
  651. package/packages/coding-agent/src/roles/sync.ts +88 -0
  652. package/packages/coding-agent/src/types.ts +102 -0
  653. package/packages/coding-agent/src/workspace.ts +275 -0
  654. package/packages/contracts/package.json +38 -0
  655. package/packages/contracts/schemas/blueprint.schema.json +48 -0
  656. package/packages/contracts/schemas/cli-extension.schema.json +90 -0
  657. package/packages/contracts/schemas/module-artifact.schema.json +82 -0
  658. package/packages/contracts/schemas/module-catalog.schema.json +48 -0
  659. package/packages/contracts/schemas/module-spec.schema.json +111 -0
  660. package/packages/contracts/schemas/module.schema.json +156 -0
  661. package/packages/contracts/schemas/platform-spec.schema.json +17 -0
  662. package/packages/contracts/schemas/project.schema.json +270 -0
  663. package/packages/contracts/src/index.ts +191 -0
  664. package/packages/contracts/src/module-distribution.ts +47 -0
  665. package/packages/contracts/src/schemas.ts +18 -0
  666. package/packages/contracts/src/variables.ts +156 -0
  667. package/packages/contracts/src/web.ts +60 -0
  668. package/packages/database/package.json +43 -0
  669. package/packages/database/src/contracts.ts +399 -0
  670. package/packages/database/src/index.ts +112 -0
  671. package/packages/database/src/migrations.ts +444 -0
  672. package/packages/database/src/postgresql.ts +517 -0
  673. package/packages/database/src/provider.ts +639 -0
  674. package/packages/database/src/record-history.ts +201 -0
  675. package/packages/database/src/registry.ts +265 -0
  676. package/packages/database/src/reset.ts +87 -0
  677. package/packages/database-pglite/package.json +42 -0
  678. package/packages/database-pglite/src/driver.ts +168 -0
  679. package/packages/database-pglite/src/index.ts +2 -0
  680. package/packages/database-testing/package.json +44 -0
  681. package/packages/database-testing/src/index.ts +8 -0
  682. package/packages/database-testing/src/pglite.ts +124 -0
  683. package/packages/database-testing/src/postgres.ts +184 -0
  684. package/packages/database-testing/src/provider.ts +43 -0
  685. package/packages/database-testing/src/vitest-preset.ts +23 -0
  686. package/packages/dev-console/README.md +6 -0
  687. package/packages/dev-console/package.json +38 -0
  688. package/packages/dev-console/src/index.mjs +220 -0
  689. package/packages/harness/package.json +47 -0
  690. package/packages/harness/schemas/task-packet.schema.json +48 -0
  691. package/packages/harness/src/catalog.ts +24 -0
  692. package/packages/harness/src/errors.ts +9 -0
  693. package/packages/harness/src/index.ts +53 -0
  694. package/packages/harness/src/preamble.ts +31 -0
  695. package/packages/harness/src/runtime.ts +833 -0
  696. package/packages/harness/src/task-packet.ts +86 -0
  697. package/packages/harness/src/tool-adapters.ts +106 -0
  698. package/packages/harness/src/tool-contract.ts +242 -0
  699. package/packages/harness/src/vercel-ai-provider.ts +198 -0
  700. package/packages/kernel/package.json +44 -0
  701. package/packages/kernel/src/acl.ts +20 -0
  702. package/packages/kernel/src/actor.ts +139 -0
  703. package/packages/kernel/src/agent-registry.ts +81 -0
  704. package/packages/kernel/src/capability-registry.ts +28 -0
  705. package/packages/kernel/src/errors.ts +9 -0
  706. package/packages/kernel/src/index.ts +90 -0
  707. package/packages/kernel/src/legacy-local-state.ts +103 -0
  708. package/packages/kernel/src/module-compatibility.ts +70 -0
  709. package/packages/kernel/src/module-registry.ts +124 -0
  710. package/packages/kernel/src/module-settings.ts +403 -0
  711. package/packages/kernel/src/record-history.ts +146 -0
  712. package/packages/kernel/src/runtime-config.ts +51 -0
  713. package/packages/kernel/src/tool-registry.ts +25 -0
  714. package/packages/kernel/src/variable-registry.ts +339 -0
  715. package/packages/sandbox/README.md +691 -0
  716. package/packages/sandbox/bin/flowdular-sandbox.mjs +393 -0
  717. package/packages/sandbox/dist/client/assets/App-ClMOrpyg.js +11 -0
  718. package/packages/sandbox/dist/client/assets/PreviewHost-BHiHrfbQ.js +1 -0
  719. package/packages/sandbox/dist/client/assets/auto-buGEq6de.js +3 -0
  720. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  721. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  722. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  723. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  724. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  725. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  726. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  727. package/packages/sandbox/dist/client/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  728. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  729. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  730. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  731. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  732. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  733. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  734. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  735. package/packages/sandbox/dist/client/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  736. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  737. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  738. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  739. package/packages/sandbox/dist/client/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  740. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-ext-wght-normal-d45eAU9y.woff2 +0 -0
  741. package/packages/sandbox/dist/client/assets/ibm-plex-sans-cyrillic-wght-normal-BAAhND-U.woff2 +0 -0
  742. package/packages/sandbox/dist/client/assets/ibm-plex-sans-greek-wght-normal-CmyJS8uq.woff2 +0 -0
  743. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-ext-wght-normal-CIII54If.woff2 +0 -0
  744. package/packages/sandbox/dist/client/assets/ibm-plex-sans-latin-wght-normal-IvpUvPa2.woff2 +0 -0
  745. package/packages/sandbox/dist/client/assets/ibm-plex-sans-vietnamese-wght-normal-Dg1JeJN0.woff2 +0 -0
  746. package/packages/sandbox/dist/client/assets/index-CvwhlQ87.js +4 -0
  747. package/packages/sandbox/dist/client/assets/styles-BPWaGf55.js +3 -0
  748. package/packages/sandbox/dist/client/assets/styles-Dx1_KOag.css +1 -0
  749. package/packages/sandbox/dist/client/favicon.svg +1 -0
  750. package/packages/sandbox/dist/server/assets/auto-CVGNYyb3.js +12785 -0
  751. package/packages/sandbox/dist/server/assets/chunk-DDJLRBDX-3OUNo51g.js +4125 -0
  752. package/packages/sandbox/dist/server/assets/nodefs-DVSI7kG0.js +24 -0
  753. package/packages/sandbox/dist/server/assets/opfs-ahp-D56iokt1.js +388 -0
  754. package/packages/sandbox/dist/server/assets/token-Bvaq7Anz.js +55 -0
  755. package/packages/sandbox/dist/server/assets/token-util-BqSvdIPQ.js +3 -0
  756. package/packages/sandbox/dist/server/assets/token-util-CktFFT4z.js +494 -0
  757. package/packages/sandbox/dist/server/entry.js +91671 -0
  758. package/packages/sandbox/dist/server/favicon.svg +1 -0
  759. package/packages/sandbox/dist/server/index.html +161 -0
  760. package/packages/sandbox/index.html +159 -0
  761. package/packages/sandbox/octane.config.ts +36 -0
  762. package/packages/sandbox/package.json +83 -0
  763. package/packages/sandbox/public/favicon.svg +1 -0
  764. package/packages/sandbox/public/og.png +0 -0
  765. package/packages/sandbox/src/App.tsrx +924 -0
  766. package/packages/sandbox/src/client/ApprovalHandoff.tsrx +39 -0
  767. package/packages/sandbox/src/client/ChatPane.tsrx +694 -0
  768. package/packages/sandbox/src/client/ConnectView.tsrx +86 -0
  769. package/packages/sandbox/src/client/DiffPane.tsrx +150 -0
  770. package/packages/sandbox/src/client/EjectModal.tsrx +571 -0
  771. package/packages/sandbox/src/client/GateResults.tsrx +60 -0
  772. package/packages/sandbox/src/client/GitHubSettingsModal.tsrx +219 -0
  773. package/packages/sandbox/src/client/NewSessionForm.tsrx +214 -0
  774. package/packages/sandbox/src/client/SessionBar.tsrx +151 -0
  775. package/packages/sandbox/src/client/SessionPicker.tsrx +472 -0
  776. package/packages/sandbox/src/client/SpecEditor.tsrx +109 -0
  777. package/packages/sandbox/src/client/SpecReviewCard.tsrx +321 -0
  778. package/packages/sandbox/src/client/WorkCard.tsrx +128 -0
  779. package/packages/sandbox/src/client/WorkbenchModal.tsrx +161 -0
  780. package/packages/sandbox/src/client/WorkspaceMenu.tsrx +100 -0
  781. package/packages/sandbox/src/client/api.ts +797 -0
  782. package/packages/sandbox/src/client/attachments.ts +73 -0
  783. package/packages/sandbox/src/client/dashboard.ts +46 -0
  784. package/packages/sandbox/src/client/github-settings.ts +48 -0
  785. package/packages/sandbox/src/client/highlight.ts +105 -0
  786. package/packages/sandbox/src/client/i18n.ts +32 -0
  787. package/packages/sandbox/src/client/locales/en.json +466 -0
  788. package/packages/sandbox/src/client/locales/pl.json +466 -0
  789. package/packages/sandbox/src/client/session-labels.ts +77 -0
  790. package/packages/sandbox/src/client/state.ts +108 -0
  791. package/packages/sandbox/src/index.ts +10 -0
  792. package/packages/sandbox/src/preview/PreviewHost.tsrx +373 -0
  793. package/packages/sandbox/src/preview/load-module.ts +16 -0
  794. package/packages/sandbox/src/server/attachments.ts +308 -0
  795. package/packages/sandbox/src/server/auto-review.ts +181 -0
  796. package/packages/sandbox/src/server/checkpoints.ts +90 -0
  797. package/packages/sandbox/src/server/config.ts +460 -0
  798. package/packages/sandbox/src/server/dashboard.ts +245 -0
  799. package/packages/sandbox/src/server/delivery/configuration.ts +193 -0
  800. package/packages/sandbox/src/server/delivery/git-pr.ts +1473 -0
  801. package/packages/sandbox/src/server/delivery/index.ts +83 -0
  802. package/packages/sandbox/src/server/delivery/local.ts +151 -0
  803. package/packages/sandbox/src/server/delivery/official-modules.ts +516 -0
  804. package/packages/sandbox/src/server/delivery/plan.ts +218 -0
  805. package/packages/sandbox/src/server/delivery/policies.ts +239 -0
  806. package/packages/sandbox/src/server/delivery/record.ts +41 -0
  807. package/packages/sandbox/src/server/delivery/steps.ts +464 -0
  808. package/packages/sandbox/src/server/delivery/types.ts +158 -0
  809. package/packages/sandbox/src/server/dependencies.ts +88 -0
  810. package/packages/sandbox/src/server/diff.ts +230 -0
  811. package/packages/sandbox/src/server/gates.ts +388 -0
  812. package/packages/sandbox/src/server/index.ts +139 -0
  813. package/packages/sandbox/src/server/path-guard.ts +282 -0
  814. package/packages/sandbox/src/server/planning.ts +749 -0
  815. package/packages/sandbox/src/server/platform-client.ts +189 -0
  816. package/packages/sandbox/src/server/preview-database-host.ts +340 -0
  817. package/packages/sandbox/src/server/preview-database-protocol.ts +163 -0
  818. package/packages/sandbox/src/server/preview-database-proxy.ts +356 -0
  819. package/packages/sandbox/src/server/preview-database.ts +109 -0
  820. package/packages/sandbox/src/server/preview-ipc.ts +15 -0
  821. package/packages/sandbox/src/server/preview-modules.ts +83 -0
  822. package/packages/sandbox/src/server/preview-revision.ts +44 -0
  823. package/packages/sandbox/src/server/preview-runtime.ts +467 -0
  824. package/packages/sandbox/src/server/preview-worker-manager.ts +436 -0
  825. package/packages/sandbox/src/server/preview-worker.ts +168 -0
  826. package/packages/sandbox/src/server/reference.ts +251 -0
  827. package/packages/sandbox/src/server/routes.ts +2258 -0
  828. package/packages/sandbox/src/server/runtime.ts +188 -0
  829. package/packages/sandbox/src/server/session-owner.ts +23 -0
  830. package/packages/sandbox/src/server/sessions.ts +849 -0
  831. package/packages/sandbox/src/server/spec.ts +471 -0
  832. package/packages/sandbox/src/server/turn-lifecycle.ts +56 -0
  833. package/packages/sandbox/src/server/turns.ts +993 -0
  834. package/packages/sandbox/src/server/workspace-install.ts +266 -0
  835. package/packages/sandbox/src/server/workspace-root.ts +73 -0
  836. package/packages/sandbox/src/styles.css +1573 -0
  837. package/packages/sandbox/vite.config.ts +133 -0
  838. package/packages/server/package.json +43 -0
  839. package/packages/server/src/application-routes.ts +21 -0
  840. package/packages/server/src/composition.ts +48 -0
  841. package/packages/server/src/endpoint.ts +142 -0
  842. package/packages/server/src/http.ts +128 -0
  843. package/packages/server/src/index.ts +46 -0
  844. package/packages/server/src/response-lifetime.ts +56 -0
  845. package/packages/server/src/security-headers.ts +130 -0
  846. package/packages/server/src/web-html.ts +55 -0
  847. package/packages/server/src/web.ts +361 -0
  848. package/packages/ui/package.json +46 -0
  849. package/packages/ui/src/brand/BrandMark.tsrx +66 -0
  850. package/packages/ui/src/brand/mark.ts +44 -0
  851. package/packages/ui/src/components/Alert.tsrx +17 -0
  852. package/packages/ui/src/components/Avatar.tsrx +18 -0
  853. package/packages/ui/src/components/Button.tsrx +29 -0
  854. package/packages/ui/src/components/Chart.tsrx +274 -0
  855. package/packages/ui/src/components/CheckGrid.tsrx +93 -0
  856. package/packages/ui/src/components/ConfirmDialog.tsrx +63 -0
  857. package/packages/ui/src/components/Drawer.tsrx +71 -0
  858. package/packages/ui/src/components/EmptyState.tsrx +26 -0
  859. package/packages/ui/src/components/Filters.tsrx +39 -0
  860. package/packages/ui/src/components/FormField.tsrx +27 -0
  861. package/packages/ui/src/components/Kpi.tsrx +36 -0
  862. package/packages/ui/src/components/PageHeader.tsrx +26 -0
  863. package/packages/ui/src/components/ScopeSummary.tsrx +76 -0
  864. package/packages/ui/src/components/SearchField.tsrx +24 -0
  865. package/packages/ui/src/components/SettingRow.tsrx +50 -0
  866. package/packages/ui/src/components/Switch.tsrx +21 -0
  867. package/packages/ui/src/components/Table.tsrx +267 -0
  868. package/packages/ui/src/components/TableCard.tsrx +52 -0
  869. package/packages/ui/src/components/Tag.tsrx +28 -0
  870. package/packages/ui/src/components/VariableField.tsrx +284 -0
  871. package/packages/ui/src/components/VariableInput.tsrx +7 -0
  872. package/packages/ui/src/components/VariableSelect.tsrx +72 -0
  873. package/packages/ui/src/components/VariableTextarea.tsrx +9 -0
  874. package/packages/ui/src/components/variable-field.ts +61 -0
  875. package/packages/ui/src/components/variable-select.ts +69 -0
  876. package/packages/ui/src/icons/Icon.tsrx +56 -0
  877. package/packages/ui/src/index.ts +81 -0
  878. package/packages/ui/src/initials.ts +10 -0
  879. package/packages/ui/src/styles/base.css +71 -0
  880. package/packages/ui/src/styles/components.css +1859 -0
  881. package/packages/ui/src/styles/index.css +3 -0
  882. package/packages/ui/src/styles/tokens.css +124 -0
  883. package/tsconfig.base.json +18 -0
@@ -0,0 +1,1825 @@
1
+ import {
2
+ runDatabaseMigrations,
3
+ type DatabaseHandle,
4
+ type DatabaseParameter,
5
+ type DatabaseStatement,
6
+ type DatabaseTransaction,
7
+ } from '@flowdular/sdk/database';
8
+ import type {
9
+ ActorKind,
10
+ ModuleSettingRecord,
11
+ ModuleSettingValue,
12
+ } from '@flowdular/sdk/kernel';
13
+ import { BUILTIN_ROLES } from '../acl/scopes.ts';
14
+ import type {
15
+ ApiTokenRecord,
16
+ AuditQuery,
17
+ AuthPrincipal,
18
+ AuthSession,
19
+ AuthTenantAccess,
20
+ SessionSummary,
21
+ TenantRole,
22
+ } from '../domain/types.ts';
23
+ import { databaseMigrations } from './migration.ts';
24
+ import {
25
+ DuplicateAccountError,
26
+ DuplicateRoleKeyError,
27
+ DuplicateTenantSlugError,
28
+ type AccountCredential,
29
+ type AuditActorEvent,
30
+ type AuditRecord,
31
+ type AuthRepository,
32
+ type CreateAccountInTenantRecord,
33
+ type CreateAccountRecord,
34
+ type CreateApiTokenRecord,
35
+ type CreateRoleRecord,
36
+ type CreateSessionRecord,
37
+ type CreateTenantMembershipRecord,
38
+ type MfaChallengeRecord,
39
+ type PasswordResetTokenRecord,
40
+ type SignInFailureRecord,
41
+ type TenantInvitationRecord,
42
+ type TenantMember,
43
+ type TenantSummary,
44
+ } from './repository.ts';
45
+
46
+ /**
47
+ * The tenant context an identity table is read and written under. Accounts,
48
+ * sign-in failures, reset tokens and enrolled factors belong to a person, not
49
+ * to a workspace, so they carry no tenant column and no policy binds this
50
+ * value. The runtime handle still requires one, and no workspace can claim it:
51
+ * a tenant id is a UUID.
52
+ */
53
+ export const IDENTITY_TENANT_CONTEXT = 'auth.core:identity';
54
+
55
+ /**
56
+ * Storage tenant of a platform-scoped setting. The kernel addresses it as the
57
+ * empty string, which is not a tenant id a policy can be bound to, so the row
58
+ * carries this reserved id instead.
59
+ */
60
+ export const PLATFORM_SETTINGS_STORAGE_TENANT = 'auth.core:platform';
61
+
62
+ interface ApiTokenRow {
63
+ id: string;
64
+ tenant_id: string;
65
+ account_id: string;
66
+ label: string;
67
+ prefix: string;
68
+ token_hash: string;
69
+ scopes_json: string;
70
+ created_by: string;
71
+ created_at: number | bigint | string;
72
+ expires_at: number | bigint | string | null;
73
+ last_used_at: number | bigint | string | null;
74
+ revoked_at: number | bigint | string | null;
75
+ revoked_by: string | null;
76
+ }
77
+
78
+ interface AccountRow {
79
+ account_id: string;
80
+ tenant_id: string;
81
+ email: string;
82
+ display_name: string;
83
+ password_hash: string;
84
+ role: string;
85
+ role_id: string | null;
86
+ status: 'active' | 'disabled';
87
+ password_change_required: number | bigint | string;
88
+ }
89
+
90
+ interface ScopeRow {
91
+ scope: string;
92
+ }
93
+
94
+ interface TenantAccessRow {
95
+ tenant_id: string;
96
+ name: string;
97
+ slug: string;
98
+ role: string;
99
+ }
100
+
101
+ interface TenantMemberRow {
102
+ account_id: string;
103
+ email: string;
104
+ display_name: string;
105
+ role: string;
106
+ role_id: string | null;
107
+ status: 'active' | 'disabled';
108
+ password_change_required: number | bigint | string;
109
+ scopes: string | null;
110
+ created_at: number | bigint | string;
111
+ }
112
+
113
+ interface SessionRow extends AccountRow {
114
+ session_id: string;
115
+ csrf_token: string;
116
+ expires_at: number | bigint | string;
117
+ last_seen_at: number | bigint | string;
118
+ }
119
+
120
+ interface SessionSummaryRow {
121
+ id: string;
122
+ tenant_id: string;
123
+ tenant_name: string;
124
+ created_at: number | bigint | string;
125
+ last_seen_at: number | bigint | string;
126
+ expires_at: number | bigint | string;
127
+ }
128
+
129
+ interface RoleRow {
130
+ id: string;
131
+ tenant_id: string;
132
+ key: string;
133
+ name: string;
134
+ description: string;
135
+ scopes_json: string;
136
+ builtin: number | bigint | string;
137
+ created_at: number | bigint | string;
138
+ updated_at: number | bigint | string;
139
+ }
140
+
141
+ interface AuditRow {
142
+ id: number | bigint | string;
143
+ tenant_id: string;
144
+ actor_account_id: string | null;
145
+ actor_label: string;
146
+ actor_kind: ActorKind;
147
+ actor_run_id: string | null;
148
+ action: string;
149
+ subject_type: string;
150
+ subject_id: string;
151
+ metadata_json: string;
152
+ occurred_at: number | bigint | string;
153
+ }
154
+
155
+ interface TenantRow {
156
+ id: string;
157
+ name: string;
158
+ slug: string | null;
159
+ }
160
+
161
+ interface CountRow {
162
+ total: number | bigint | string;
163
+ }
164
+
165
+ /* PostgreSQL returns BIGINT as a string, and every timestamp, expiry and
166
+ counter here is one. An unnormalized read would compare and serialize as
167
+ text: a session would never look expired and a role would never look
168
+ built in. */
169
+ function integer(value: number | bigint | string): number {
170
+ const normalized = Number(value);
171
+ if (!Number.isSafeInteger(normalized)) {
172
+ throw new Error('The auth database returned an invalid integer.');
173
+ }
174
+ return normalized;
175
+ }
176
+
177
+ function optionalInteger(
178
+ value: number | bigint | string | null,
179
+ ): number | null {
180
+ return value === null ? null : integer(value);
181
+ }
182
+
183
+ const ACCOUNT_COLUMNS = `a.id AS account_id, m.tenant_id, a.email, a.display_name,
184
+ a.password_hash, m.role, m.role_id, a.status, a.password_change_required`;
185
+
186
+ export function builtinRoleId(tenantId: string, key: string): string {
187
+ return `${tenantId}:${key}`;
188
+ }
189
+
190
+ function fromApiTokenRow(row: ApiTokenRow): ApiTokenRecord {
191
+ return {
192
+ id: row.id,
193
+ tenantId: row.tenant_id,
194
+ accountId: row.account_id,
195
+ label: row.label,
196
+ prefix: row.prefix,
197
+ scopes: JSON.parse(row.scopes_json) as readonly string[],
198
+ createdBy: row.created_by,
199
+ createdAt: integer(row.created_at),
200
+ expiresAt: optionalInteger(row.expires_at),
201
+ lastUsedAt: optionalInteger(row.last_used_at),
202
+ revokedAt: optionalInteger(row.revoked_at),
203
+ revokedBy: row.revoked_by,
204
+ };
205
+ }
206
+
207
+ function fromRoleRow(row: RoleRow): TenantRole {
208
+ return {
209
+ id: row.id,
210
+ tenantId: row.tenant_id,
211
+ key: row.key,
212
+ name: row.name,
213
+ description: row.description,
214
+ scopes: JSON.parse(row.scopes_json) as readonly string[],
215
+ builtin: integer(row.builtin) === 1,
216
+ createdAt: integer(row.created_at),
217
+ updatedAt: integer(row.updated_at),
218
+ };
219
+ }
220
+
221
+ function tenantSummary(row: TenantRow): TenantSummary {
222
+ return { tenantId: row.id, name: row.name, slug: row.slug ?? row.id };
223
+ }
224
+
225
+ /* A unique violation arrives as the driver's own error. The constraint name is
226
+ the only stable part of it, so the duplicate a caller can act on is
227
+ recognized by name and everything else is rethrown untouched. */
228
+ function duplicate(error: unknown): 'email' | 'role-key' | 'slug' | null {
229
+ const text = String(error);
230
+ if (text.includes('auth_accounts_email_normalized_key')) return 'email';
231
+ if (text.includes('auth_tenants_slug_idx')) return 'slug';
232
+ if (text.includes('auth_roles_tenant_id_key_key')) return 'role-key';
233
+ return null;
234
+ }
235
+
236
+ /** Values always travel as parameters; only the marker count is assembled. */
237
+ function scopeRows(
238
+ accountId: string,
239
+ tenantId: string,
240
+ scopes: readonly string[],
241
+ ): DatabaseStatement | null {
242
+ const unique = [...new Set(scopes)];
243
+ if (unique.length === 0) return null;
244
+ const parameters: DatabaseParameter[] = [accountId, tenantId];
245
+ const rows = unique.map((scope) => {
246
+ parameters.push(scope);
247
+ return `($1, $2, $${parameters.length})`;
248
+ });
249
+ return {
250
+ text: `INSERT INTO auth_membership_scopes (account_id, tenant_id, scope)
251
+ VALUES ${rows.join(', ')} ON CONFLICT DO NOTHING`,
252
+ parameters,
253
+ };
254
+ }
255
+
256
+ export async function migrateAuthDatabase(
257
+ database: DatabaseHandle,
258
+ ): Promise<void> {
259
+ await runDatabaseMigrations(database, 'auth.core', databaseMigrations);
260
+ }
261
+
262
+ export interface AuthDatabaseHandles {
263
+ /** Tenant-scoped handle: every request-time read and write runs on it. */
264
+ readonly runtime: DatabaseHandle;
265
+ /**
266
+ * Read-only cross-tenant handle. A session cookie, a bearer token, an
267
+ * invitation link and an email address name no workspace, so this handle
268
+ * answers one question only: which tenant owns the row. Everything the
269
+ * answer is used for is read and written again under that tenant.
270
+ */
271
+ readonly background: DatabaseHandle;
272
+ }
273
+
274
+ interface RoutedTenantRow {
275
+ tenant_id: string;
276
+ }
277
+
278
+ /** auth.core over platform-owned PostgreSQL handles. */
279
+ export class DatabaseAuthRepository implements AuthRepository {
280
+ readonly #runtime: DatabaseHandle;
281
+ readonly #background: DatabaseHandle;
282
+
283
+ constructor(handles: AuthDatabaseHandles) {
284
+ this.#runtime = handles.runtime;
285
+ this.#background = handles.background;
286
+ }
287
+
288
+ #tx<T>(
289
+ tenantId: string,
290
+ access: 'read' | 'write',
291
+ body: (transaction: DatabaseTransaction) => Promise<T>,
292
+ ): Promise<T> {
293
+ return this.#runtime.transaction(body, { access, tenantId });
294
+ }
295
+
296
+ async #query<Row extends object>(
297
+ tenantId: string,
298
+ statement: DatabaseStatement,
299
+ ): Promise<readonly Row[]> {
300
+ return this.#tx(tenantId, 'read', async (transaction) => {
301
+ const result = await transaction.query<Row>(statement);
302
+ return result.rows;
303
+ });
304
+ }
305
+
306
+ async #exec(tenantId: string, statement: DatabaseStatement): Promise<number> {
307
+ return this.#tx(tenantId, 'write', async (transaction) => {
308
+ const result = await transaction.execute(statement);
309
+ return result.affectedRows;
310
+ });
311
+ }
312
+
313
+ /** A cross-tenant routing read. It returns which tenant owns a key, never a record. */
314
+ async #route<Row extends object>(
315
+ statement: DatabaseStatement,
316
+ ): Promise<readonly Row[]> {
317
+ const result = await this.#background.query<Row>(statement);
318
+ return result.rows;
319
+ }
320
+
321
+ async #tenantOfAccount(accountId: string): Promise<string | null> {
322
+ const rows = await this.#route<RoutedTenantRow>({
323
+ text: `SELECT tenant_id FROM auth_memberships WHERE account_id = $1
324
+ ORDER BY created_at, tenant_id LIMIT 1`,
325
+ parameters: [accountId],
326
+ });
327
+ return rows[0]?.tenant_id ?? null;
328
+ }
329
+
330
+ async #scopes(
331
+ transaction: DatabaseTransaction,
332
+ accountId: string,
333
+ tenantId: string,
334
+ ): Promise<string[]> {
335
+ const result = await transaction.query<ScopeRow>({
336
+ text: `SELECT scope FROM auth_membership_scopes
337
+ WHERE account_id = $1 AND tenant_id = $2 ORDER BY scope`,
338
+ parameters: [accountId, tenantId],
339
+ });
340
+ return result.rows.map((row) => row.scope);
341
+ }
342
+
343
+ async #credential(
344
+ transaction: DatabaseTransaction,
345
+ row: AccountRow,
346
+ ): Promise<AccountCredential> {
347
+ return {
348
+ accountId: row.account_id,
349
+ tenantId: row.tenant_id,
350
+ email: row.email,
351
+ displayName: row.display_name,
352
+ passwordHash: row.password_hash,
353
+ role: row.role,
354
+ roleId: row.role_id,
355
+ status: row.status,
356
+ passwordChangeRequired: integer(row.password_change_required) === 1,
357
+ scopes: await this.#scopes(transaction, row.account_id, row.tenant_id),
358
+ };
359
+ }
360
+
361
+ /* Every workspace carries the built-in roles as rows, so a custom role and a
362
+ built-in one are listed, assigned and audited the same way. They are
363
+ written with the workspace itself, inside its own transaction. */
364
+ async #seedBuiltinRoles(
365
+ transaction: DatabaseTransaction,
366
+ tenantId: string,
367
+ createdAt: number,
368
+ ): Promise<void> {
369
+ for (const role of BUILTIN_ROLES) {
370
+ await transaction.execute({
371
+ text: `INSERT INTO auth_roles
372
+ (id, tenant_id, key, name, description, scopes_json, builtin, created_at, updated_at)
373
+ VALUES ($1, $2, $3, $4, $5, $6, 1, $7, $7)
374
+ ON CONFLICT DO NOTHING`,
375
+ parameters: [
376
+ builtinRoleId(tenantId, role.key),
377
+ tenantId,
378
+ role.key,
379
+ role.name,
380
+ role.description,
381
+ JSON.stringify(role.scopes),
382
+ createdAt,
383
+ ],
384
+ });
385
+ }
386
+ }
387
+
388
+ async #insertScopes(
389
+ transaction: DatabaseTransaction,
390
+ accountId: string,
391
+ tenantId: string,
392
+ scopes: readonly string[],
393
+ ): Promise<void> {
394
+ const statement = scopeRows(accountId, tenantId, scopes);
395
+ if (statement) await transaction.execute(statement);
396
+ }
397
+
398
+ /* The service may have read the role before hashing a password or preparing
399
+ an invitation. Reread owners inside the write transaction and hold the
400
+ role lock until insertion commits, so a concurrent module grant either
401
+ precedes this snapshot or includes the newly committed membership. */
402
+ async #assignedScopes(
403
+ transaction: DatabaseTransaction,
404
+ tenantId: string,
405
+ role: string,
406
+ scopes: readonly string[],
407
+ ): Promise<readonly string[]> {
408
+ if (role !== 'owner') return scopes;
409
+ const roles = await transaction.query<RoleRow>({
410
+ text: `SELECT * FROM auth_roles
411
+ WHERE tenant_id = $1 AND key = 'owner' AND builtin = 1 FOR SHARE`,
412
+ parameters: [tenantId],
413
+ });
414
+ if (!roles.rows[0])
415
+ throw new Error('The workspace has no built-in owner role.');
416
+ return fromRoleRow(roles.rows[0]).scopes;
417
+ }
418
+
419
+ async #membership(
420
+ transaction: DatabaseTransaction,
421
+ accountId: string,
422
+ tenantId: string,
423
+ ): Promise<AccountCredential | null> {
424
+ const result = await transaction.query<AccountRow>({
425
+ text: `SELECT ${ACCOUNT_COLUMNS}
426
+ FROM auth_accounts a
427
+ JOIN auth_memberships m ON m.account_id = a.id
428
+ WHERE a.id = $1 AND m.tenant_id = $2`,
429
+ parameters: [accountId, tenantId],
430
+ });
431
+ const row = result.rows[0];
432
+ return row ? this.#credential(transaction, row) : null;
433
+ }
434
+
435
+ async isTenantSlugTaken(slug: string): Promise<boolean> {
436
+ const rows = await this.#route<{ id: string }>({
437
+ text: 'SELECT id FROM auth_tenants WHERE slug = $1 LIMIT 1',
438
+ parameters: [slug],
439
+ });
440
+ return rows.length > 0;
441
+ }
442
+
443
+ async findTenant(reference: string): Promise<TenantSummary | null> {
444
+ const rows = await this.#route<TenantRow>({
445
+ text: `SELECT id, name, slug FROM auth_tenants
446
+ WHERE id = $1 OR slug = $1 LIMIT 1`,
447
+ parameters: [reference],
448
+ });
449
+ return rows[0] ? tenantSummary(rows[0]) : null;
450
+ }
451
+
452
+ async listTenants(): Promise<readonly TenantSummary[]> {
453
+ const rows = await this.#route<TenantRow>({
454
+ text: 'SELECT id, name, slug FROM auth_tenants ORDER BY name, id',
455
+ });
456
+ return rows.map(tenantSummary);
457
+ }
458
+
459
+ async renameTenant(
460
+ tenantId: string,
461
+ name: string,
462
+ ): Promise<TenantSummary | null> {
463
+ const rows = await this.#tx(tenantId, 'write', async (transaction) => {
464
+ const result = await transaction.query<TenantRow>({
465
+ text: `UPDATE auth_tenants SET name = $1 WHERE id = $2
466
+ RETURNING id, name, slug`,
467
+ parameters: [name, tenantId],
468
+ });
469
+ return result.rows;
470
+ });
471
+ return rows[0] ? tenantSummary(rows[0]) : null;
472
+ }
473
+
474
+ async listOwnerMemberships(): Promise<
475
+ readonly { readonly accountId: string; readonly tenantId: string }[]
476
+ > {
477
+ const rows = await this.#route<{ account_id: string; tenant_id: string }>({
478
+ text: `SELECT account_id, tenant_id FROM auth_memberships
479
+ WHERE role = 'owner' ORDER BY tenant_id, account_id`,
480
+ });
481
+ return rows.map((row) => ({
482
+ accountId: row.account_id,
483
+ tenantId: row.tenant_id,
484
+ }));
485
+ }
486
+
487
+ /* Sign-in knows an email address and nothing else. The account row is
488
+ identity, not workspace data, so it is read under the identity context;
489
+ only the workspace the credential belongs to needs the routing read. */
490
+ async findAccountByEmail(
491
+ normalizedEmail: string,
492
+ ): Promise<AccountCredential | null> {
493
+ const accounts = await this.#query<{ id: string }>(
494
+ IDENTITY_TENANT_CONTEXT,
495
+ {
496
+ text: 'SELECT id FROM auth_accounts WHERE email_normalized = $1',
497
+ parameters: [normalizedEmail],
498
+ },
499
+ );
500
+ const accountId = accounts[0]?.id;
501
+ return accountId ? this.findAccountCredentialById(accountId) : null;
502
+ }
503
+
504
+ async findAccountCredentialById(
505
+ accountId: string,
506
+ ): Promise<AccountCredential | null> {
507
+ const tenantId = await this.#tenantOfAccount(accountId);
508
+ return tenantId ? this.findAccountMembership(accountId, tenantId) : null;
509
+ }
510
+
511
+ async findAccountMembership(
512
+ accountId: string,
513
+ tenantId: string,
514
+ ): Promise<AccountCredential | null> {
515
+ return this.#tx(tenantId, 'read', (transaction) =>
516
+ this.#membership(transaction, accountId, tenantId),
517
+ );
518
+ }
519
+
520
+ async updatePasswordHash(
521
+ accountId: string,
522
+ passwordHash: string,
523
+ changeRequired: boolean,
524
+ ): Promise<void> {
525
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
526
+ text: `UPDATE auth_accounts SET password_hash = $1,
527
+ password_change_required = $2 WHERE id = $3`,
528
+ parameters: [passwordHash, changeRequired ? 1 : 0, accountId],
529
+ });
530
+ }
531
+
532
+ async updateAccountDisplayName(
533
+ accountId: string,
534
+ displayName: string,
535
+ ): Promise<void> {
536
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
537
+ text: 'UPDATE auth_accounts SET display_name = $1 WHERE id = $2',
538
+ parameters: [displayName, accountId],
539
+ });
540
+ }
541
+
542
+ async updateAccountStatus(
543
+ accountId: string,
544
+ status: 'active' | 'disabled',
545
+ ): Promise<void> {
546
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
547
+ text: 'UPDATE auth_accounts SET status = $1 WHERE id = $2',
548
+ parameters: [status, accountId],
549
+ });
550
+ }
551
+
552
+ /* A credential change invalidates every other session of that account, in
553
+ every workspace it can enter. Row security scopes a delete to one
554
+ workspace, so the sessions are routed first and removed per tenant. */
555
+ async deleteAccountSessions(
556
+ accountId: string,
557
+ exceptTokenHash: string | null,
558
+ ): Promise<void> {
559
+ const tenants = await this.#route<RoutedTenantRow>({
560
+ text: 'SELECT DISTINCT tenant_id FROM auth_sessions WHERE account_id = $1',
561
+ parameters: [accountId],
562
+ });
563
+ for (const row of tenants) {
564
+ await this.#exec(
565
+ row.tenant_id,
566
+ exceptTokenHash
567
+ ? {
568
+ text: `DELETE FROM auth_sessions
569
+ WHERE account_id = $1 AND token_hash <> $2`,
570
+ parameters: [accountId, exceptTokenHash],
571
+ }
572
+ : {
573
+ text: 'DELETE FROM auth_sessions WHERE account_id = $1',
574
+ parameters: [accountId],
575
+ },
576
+ );
577
+ }
578
+ }
579
+
580
+ async deleteMembershipSessions(
581
+ accountId: string,
582
+ tenantId: string,
583
+ ): Promise<void> {
584
+ await this.#exec(tenantId, {
585
+ text: 'DELETE FROM auth_sessions WHERE account_id = $1 AND tenant_id = $2',
586
+ parameters: [accountId, tenantId],
587
+ });
588
+ }
589
+
590
+ async deleteMembership(
591
+ accountId: string,
592
+ tenantId: string,
593
+ ): Promise<boolean> {
594
+ return this.#tx(tenantId, 'write', async (transaction) => {
595
+ await transaction.execute({
596
+ text: `DELETE FROM auth_sessions
597
+ WHERE account_id = $1 AND tenant_id = $2`,
598
+ parameters: [accountId, tenantId],
599
+ });
600
+ const result = await transaction.execute({
601
+ text: `DELETE FROM auth_memberships
602
+ WHERE account_id = $1 AND tenant_id = $2`,
603
+ parameters: [accountId, tenantId],
604
+ });
605
+ return result.affectedRows > 0;
606
+ });
607
+ }
608
+
609
+ async countMemberships(accountId: string): Promise<number> {
610
+ const rows = await this.#route<CountRow>({
611
+ text: 'SELECT count(*) AS total FROM auth_memberships WHERE account_id = $1',
612
+ parameters: [accountId],
613
+ });
614
+ return rows[0] ? integer(rows[0].total) : 0;
615
+ }
616
+
617
+ /* The account row is the identity. Its memberships, sessions and tokens go
618
+ with it: referential actions are not subject to row security, so the
619
+ cascade reaches every workspace the account belonged to. */
620
+ async deleteAccount(accountId: string): Promise<void> {
621
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
622
+ text: 'DELETE FROM auth_accounts WHERE id = $1',
623
+ parameters: [accountId],
624
+ });
625
+ }
626
+
627
+ async countActiveOwners(tenantId: string): Promise<number> {
628
+ const rows = await this.#query<CountRow>(tenantId, {
629
+ text: `SELECT count(*) AS total FROM auth_memberships m
630
+ JOIN auth_accounts a ON a.id = m.account_id
631
+ WHERE m.tenant_id = $1 AND m.role = 'owner' AND a.status = 'active'`,
632
+ parameters: [tenantId],
633
+ });
634
+ return rows[0] ? integer(rows[0].total) : 0;
635
+ }
636
+
637
+ /* Which workspaces an account may enter is the one question that spans
638
+ them all, so it is answered on the cross-tenant handle. */
639
+ async listTenantAccess(
640
+ accountId: string,
641
+ ): Promise<readonly AuthTenantAccess[]> {
642
+ const rows = await this.#route<TenantAccessRow>({
643
+ text: `SELECT t.id AS tenant_id, t.name, t.slug, m.role
644
+ FROM auth_memberships m
645
+ JOIN auth_tenants t ON t.id = m.tenant_id
646
+ WHERE m.account_id = $1
647
+ ORDER BY m.created_at, t.id`,
648
+ parameters: [accountId],
649
+ });
650
+ return rows.map((row) => ({
651
+ tenantId: row.tenant_id,
652
+ name: row.name,
653
+ slug: row.slug,
654
+ role: row.role,
655
+ }));
656
+ }
657
+
658
+ async listTenantMembers(tenantId: string): Promise<readonly TenantMember[]> {
659
+ const rows = await this.#query<TenantMemberRow>(tenantId, {
660
+ text: `SELECT a.id AS account_id, a.email, a.display_name, m.role, m.role_id,
661
+ a.status, a.password_change_required, m.created_at,
662
+ (SELECT string_agg(s.scope, ' ' ORDER BY s.scope)
663
+ FROM auth_membership_scopes s
664
+ WHERE s.account_id = m.account_id AND s.tenant_id = m.tenant_id) AS scopes
665
+ FROM auth_memberships m
666
+ JOIN auth_accounts a ON a.id = m.account_id
667
+ WHERE m.tenant_id = $1
668
+ ORDER BY lower(a.display_name), a.id`,
669
+ parameters: [tenantId],
670
+ });
671
+ return rows.map((row) => ({
672
+ accountId: row.account_id,
673
+ email: row.email,
674
+ displayName: row.display_name,
675
+ role: row.role,
676
+ roleId: row.role_id,
677
+ status: row.status,
678
+ scopes: row.scopes ? row.scopes.split(' ') : [],
679
+ passwordChangeRequired: integer(row.password_change_required) === 1,
680
+ createdAt: integer(row.created_at),
681
+ }));
682
+ }
683
+
684
+ async listTenantScopes(tenantId: string): Promise<readonly string[]> {
685
+ const rows = await this.#query<ScopeRow>(tenantId, {
686
+ text: `SELECT DISTINCT scope FROM auth_membership_scopes
687
+ WHERE tenant_id = $1 ORDER BY scope`,
688
+ parameters: [tenantId],
689
+ });
690
+ return rows.map((row) => row.scope);
691
+ }
692
+
693
+ async createAccountWithTenant(
694
+ record: CreateAccountRecord,
695
+ ): Promise<AccountCredential> {
696
+ try {
697
+ return await this.#tx(record.tenantId, 'write', async (transaction) => {
698
+ await transaction.execute({
699
+ text: `INSERT INTO auth_tenants (id, name, slug, created_at)
700
+ VALUES ($1, $2, $3, $4)`,
701
+ parameters: [
702
+ record.tenantId,
703
+ record.organizationName,
704
+ record.organizationSlug,
705
+ record.createdAt,
706
+ ],
707
+ });
708
+ await this.#seedBuiltinRoles(
709
+ transaction,
710
+ record.tenantId,
711
+ record.createdAt,
712
+ );
713
+ await transaction.execute({
714
+ text: `INSERT INTO auth_accounts
715
+ (id, email, email_normalized, password_hash, display_name, status, created_at)
716
+ VALUES ($1, $2, $3, $4, $5, 'active', $6)`,
717
+ parameters: [
718
+ record.accountId,
719
+ record.email,
720
+ record.normalizedEmail,
721
+ record.passwordHash,
722
+ record.displayName,
723
+ record.createdAt,
724
+ ],
725
+ });
726
+ await transaction.execute({
727
+ text: `INSERT INTO auth_memberships
728
+ (account_id, tenant_id, role, role_id, created_at)
729
+ VALUES ($1, $2, $3, $4, $5)`,
730
+ parameters: [
731
+ record.accountId,
732
+ record.tenantId,
733
+ record.role,
734
+ builtinRoleId(record.tenantId, record.role),
735
+ record.createdAt,
736
+ ],
737
+ });
738
+ await this.#insertScopes(
739
+ transaction,
740
+ record.accountId,
741
+ record.tenantId,
742
+ record.scopes,
743
+ );
744
+ return (await this.#membership(
745
+ transaction,
746
+ record.accountId,
747
+ record.tenantId,
748
+ ))!;
749
+ });
750
+ } catch (error) {
751
+ const conflict = duplicate(error);
752
+ if (conflict === 'email') throw new DuplicateAccountError();
753
+ if (conflict === 'slug') throw new DuplicateTenantSlugError();
754
+ throw error;
755
+ }
756
+ }
757
+
758
+ async createAccountInTenant(
759
+ record: CreateAccountInTenantRecord,
760
+ ): Promise<AccountCredential> {
761
+ try {
762
+ return await this.#tx(record.tenantId, 'write', async (transaction) => {
763
+ const scopes = await this.#assignedScopes(
764
+ transaction,
765
+ record.tenantId,
766
+ record.role,
767
+ record.scopes,
768
+ );
769
+ await transaction.execute({
770
+ text: `INSERT INTO auth_accounts
771
+ (id, email, email_normalized, password_hash, display_name, status, created_at)
772
+ VALUES ($1, $2, $3, $4, $5, 'active', $6)`,
773
+ parameters: [
774
+ record.accountId,
775
+ record.email,
776
+ record.normalizedEmail,
777
+ record.passwordHash,
778
+ record.displayName,
779
+ record.createdAt,
780
+ ],
781
+ });
782
+ await transaction.execute({
783
+ text: `INSERT INTO auth_memberships
784
+ (account_id, tenant_id, role, role_id, created_at)
785
+ VALUES ($1, $2, $3, $4, $5)`,
786
+ parameters: [
787
+ record.accountId,
788
+ record.tenantId,
789
+ record.role,
790
+ record.roleId,
791
+ record.createdAt,
792
+ ],
793
+ });
794
+ await this.#insertScopes(
795
+ transaction,
796
+ record.accountId,
797
+ record.tenantId,
798
+ scopes,
799
+ );
800
+ return (await this.#membership(
801
+ transaction,
802
+ record.accountId,
803
+ record.tenantId,
804
+ ))!;
805
+ });
806
+ } catch (error) {
807
+ if (duplicate(error) === 'email') throw new DuplicateAccountError();
808
+ throw error;
809
+ }
810
+ }
811
+
812
+ async createTenantMembership(
813
+ record: CreateTenantMembershipRecord,
814
+ ): Promise<AccountCredential> {
815
+ try {
816
+ return await this.#tx(record.tenantId, 'write', async (transaction) => {
817
+ await transaction.execute({
818
+ text: `INSERT INTO auth_tenants (id, name, slug, created_at)
819
+ VALUES ($1, $2, $3, $4)`,
820
+ parameters: [
821
+ record.tenantId,
822
+ record.organizationName,
823
+ record.organizationSlug,
824
+ record.createdAt,
825
+ ],
826
+ });
827
+ await this.#seedBuiltinRoles(
828
+ transaction,
829
+ record.tenantId,
830
+ record.createdAt,
831
+ );
832
+ await transaction.execute({
833
+ text: `INSERT INTO auth_memberships
834
+ (account_id, tenant_id, role, role_id, created_at)
835
+ VALUES ($1, $2, $3, $4, $5)`,
836
+ parameters: [
837
+ record.accountId,
838
+ record.tenantId,
839
+ record.role,
840
+ builtinRoleId(record.tenantId, record.role),
841
+ record.createdAt,
842
+ ],
843
+ });
844
+ await this.#insertScopes(
845
+ transaction,
846
+ record.accountId,
847
+ record.tenantId,
848
+ record.scopes,
849
+ );
850
+ return (await this.#membership(
851
+ transaction,
852
+ record.accountId,
853
+ record.tenantId,
854
+ ))!;
855
+ });
856
+ } catch (error) {
857
+ if (duplicate(error) === 'slug') throw new DuplicateTenantSlugError();
858
+ throw error;
859
+ }
860
+ }
861
+
862
+ async createMembershipInTenant(record: {
863
+ readonly accountId: string;
864
+ readonly tenantId: string;
865
+ readonly role: string;
866
+ readonly roleId: string | null;
867
+ readonly scopes: readonly string[];
868
+ readonly createdAt: number;
869
+ }): Promise<AccountCredential> {
870
+ return this.#tx(record.tenantId, 'write', async (transaction) => {
871
+ const scopes = await this.#assignedScopes(
872
+ transaction,
873
+ record.tenantId,
874
+ record.role,
875
+ record.scopes,
876
+ );
877
+ await transaction.execute({
878
+ text: `INSERT INTO auth_memberships
879
+ (account_id, tenant_id, role, role_id, created_at)
880
+ VALUES ($1, $2, $3, $4, $5)`,
881
+ parameters: [
882
+ record.accountId,
883
+ record.tenantId,
884
+ record.role,
885
+ record.roleId,
886
+ record.createdAt,
887
+ ],
888
+ });
889
+ await this.#insertScopes(
890
+ transaction,
891
+ record.accountId,
892
+ record.tenantId,
893
+ scopes,
894
+ );
895
+ return (await this.#membership(
896
+ transaction,
897
+ record.accountId,
898
+ record.tenantId,
899
+ ))!;
900
+ });
901
+ }
902
+
903
+ async createApiToken(record: CreateApiTokenRecord): Promise<ApiTokenRecord> {
904
+ const rows = await this.#tx(
905
+ record.tenantId,
906
+ 'write',
907
+ async (transaction) => {
908
+ const result = await transaction.query<ApiTokenRow>({
909
+ text: `INSERT INTO auth_api_tokens
910
+ (id, tenant_id, account_id, label, prefix, token_hash, scopes_json,
911
+ created_by, created_at, expires_at, last_used_at, revoked_at, revoked_by)
912
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NULL, NULL, NULL)
913
+ RETURNING *`,
914
+ parameters: [
915
+ record.id,
916
+ record.tenantId,
917
+ record.accountId,
918
+ record.label,
919
+ record.prefix,
920
+ record.tokenHash,
921
+ JSON.stringify(record.scopes),
922
+ record.createdBy,
923
+ record.createdAt,
924
+ record.expiresAt,
925
+ ],
926
+ });
927
+ return result.rows;
928
+ },
929
+ );
930
+ return fromApiTokenRow(rows[0]!);
931
+ }
932
+
933
+ async listApiTokens(tenantId: string): Promise<readonly ApiTokenRecord[]> {
934
+ const rows = await this.#query<ApiTokenRow>(tenantId, {
935
+ text: `SELECT * FROM auth_api_tokens WHERE tenant_id = $1
936
+ ORDER BY (revoked_at IS NOT NULL), created_at DESC, id`,
937
+ parameters: [tenantId],
938
+ });
939
+ return rows.map(fromApiTokenRow);
940
+ }
941
+
942
+ /* A bearer token names no workspace. The routing read hands back the tenant
943
+ that owns it, and the record itself is read under that tenant. A revoked
944
+ token routes nowhere, which is the same answer an unknown token gets. */
945
+ async findApiTokenByHash(tokenHash: string): Promise<ApiTokenRecord | null> {
946
+ const routed = await this.#route<RoutedTenantRow>({
947
+ text: `SELECT tenant_id FROM auth_api_tokens
948
+ WHERE token_hash = $1 AND revoked_at IS NULL`,
949
+ parameters: [tokenHash],
950
+ });
951
+ const tenantId = routed[0]?.tenant_id;
952
+ if (!tenantId) return null;
953
+ const rows = await this.#query<ApiTokenRow>(tenantId, {
954
+ text: 'SELECT * FROM auth_api_tokens WHERE token_hash = $1',
955
+ parameters: [tokenHash],
956
+ });
957
+ return rows[0] ? fromApiTokenRow(rows[0]) : null;
958
+ }
959
+
960
+ async touchApiToken(
961
+ tenantId: string,
962
+ id: string,
963
+ usedAt: number,
964
+ ): Promise<void> {
965
+ await this.#exec(tenantId, {
966
+ text: 'UPDATE auth_api_tokens SET last_used_at = $1 WHERE id = $2',
967
+ parameters: [usedAt, id],
968
+ });
969
+ }
970
+
971
+ async revokeApiToken(
972
+ tenantId: string,
973
+ id: string,
974
+ revokedAt: number,
975
+ revokedBy: string,
976
+ ): Promise<ApiTokenRecord | null> {
977
+ const rows = await this.#tx(tenantId, 'write', async (transaction) => {
978
+ await transaction.execute({
979
+ text: `UPDATE auth_api_tokens SET revoked_at = $1, revoked_by = $2
980
+ WHERE tenant_id = $3 AND id = $4 AND revoked_at IS NULL`,
981
+ parameters: [revokedAt, revokedBy, tenantId, id],
982
+ });
983
+ const result = await transaction.query<ApiTokenRow>({
984
+ text: 'SELECT * FROM auth_api_tokens WHERE tenant_id = $1 AND id = $2',
985
+ parameters: [tenantId, id],
986
+ });
987
+ return result.rows;
988
+ });
989
+ return rows[0] ? fromApiTokenRow(rows[0]) : null;
990
+ }
991
+
992
+ async insertMembershipScopes(
993
+ accountId: string,
994
+ tenantId: string,
995
+ scopes: readonly string[],
996
+ ): Promise<void> {
997
+ const statement = scopeRows(accountId, tenantId, scopes);
998
+ if (statement) await this.#exec(tenantId, statement);
999
+ }
1000
+
1001
+ async grantTenantOwnerScopes(
1002
+ tenantId: string,
1003
+ scopes: readonly string[],
1004
+ updatedAt: number,
1005
+ ): Promise<
1006
+ readonly {
1007
+ readonly accountId: string;
1008
+ readonly granted: readonly string[];
1009
+ }[]
1010
+ > {
1011
+ if (scopes.length === 0) return [];
1012
+ return this.#tx(tenantId, 'write', async (transaction) => {
1013
+ /* Serialize concurrent module grants before reading the role ceiling. */
1014
+ const roles = await transaction.query<RoleRow>({
1015
+ text: `SELECT * FROM auth_roles
1016
+ WHERE tenant_id = $1 AND key = 'owner' AND builtin = 1 FOR UPDATE`,
1017
+ parameters: [tenantId],
1018
+ });
1019
+ const role = roles.rows[0];
1020
+ if (!role) throw new Error('The workspace has no built-in owner role.');
1021
+ const held = fromRoleRow(role).scopes;
1022
+ const combined = [...new Set([...held, ...scopes])].sort();
1023
+ if (combined.length !== new Set(held).size) {
1024
+ await transaction.execute({
1025
+ text: `UPDATE auth_roles SET scopes_json = $1, updated_at = $2
1026
+ WHERE tenant_id = $3 AND id = $4`,
1027
+ parameters: [JSON.stringify(combined), updatedAt, tenantId, role.id],
1028
+ });
1029
+ }
1030
+ const inserted = await transaction.query<{
1031
+ account_id: string;
1032
+ scope: string;
1033
+ }>({
1034
+ text: `INSERT INTO auth_membership_scopes (account_id, tenant_id, scope)
1035
+ SELECT m.account_id, m.tenant_id, s.scope
1036
+ FROM auth_memberships m
1037
+ CROSS JOIN jsonb_array_elements_text($2::jsonb) AS s(scope)
1038
+ WHERE m.tenant_id = $1 AND m.role = 'owner'
1039
+ ON CONFLICT DO NOTHING RETURNING account_id, scope`,
1040
+ parameters: [tenantId, JSON.stringify([...new Set(scopes)])],
1041
+ });
1042
+ const granted = new Map<string, string[]>();
1043
+ for (const row of inserted.rows) {
1044
+ const values = granted.get(row.account_id) ?? [];
1045
+ values.push(row.scope);
1046
+ granted.set(row.account_id, values);
1047
+ }
1048
+ return [...granted]
1049
+ .sort(([a], [b]) => a.localeCompare(b))
1050
+ .map(([accountId, values]) => ({ accountId, granted: values.sort() }));
1051
+ });
1052
+ }
1053
+
1054
+ async replaceMembershipScopes(
1055
+ accountId: string,
1056
+ tenantId: string,
1057
+ scopes: readonly string[],
1058
+ ): Promise<void> {
1059
+ await this.#tx(tenantId, 'write', async (transaction) => {
1060
+ await transaction.execute({
1061
+ text: `DELETE FROM auth_membership_scopes
1062
+ WHERE account_id = $1 AND tenant_id = $2`,
1063
+ parameters: [accountId, tenantId],
1064
+ });
1065
+ await this.#insertScopes(transaction, accountId, tenantId, scopes);
1066
+ });
1067
+ }
1068
+
1069
+ async updateMembershipRole(
1070
+ accountId: string,
1071
+ tenantId: string,
1072
+ role: string,
1073
+ roleId: string | null,
1074
+ scopes: readonly string[],
1075
+ ): Promise<void> {
1076
+ await this.#tx(tenantId, 'write', async (transaction) => {
1077
+ const assignedScopes = await this.#assignedScopes(
1078
+ transaction,
1079
+ tenantId,
1080
+ role,
1081
+ scopes,
1082
+ );
1083
+ await transaction.execute({
1084
+ text: `UPDATE auth_memberships SET role = $1, role_id = $2
1085
+ WHERE account_id = $3 AND tenant_id = $4`,
1086
+ parameters: [role, roleId, accountId, tenantId],
1087
+ });
1088
+ await transaction.execute({
1089
+ text: `DELETE FROM auth_membership_scopes
1090
+ WHERE account_id = $1 AND tenant_id = $2`,
1091
+ parameters: [accountId, tenantId],
1092
+ });
1093
+ await this.#insertScopes(
1094
+ transaction,
1095
+ accountId,
1096
+ tenantId,
1097
+ assignedScopes,
1098
+ );
1099
+ });
1100
+ }
1101
+
1102
+ async createSession(record: CreateSessionRecord): Promise<void> {
1103
+ await this.#exec(record.tenantId, {
1104
+ text: `INSERT INTO auth_sessions
1105
+ (id, token_hash, account_id, tenant_id, csrf_token, created_at, expires_at, last_seen_at)
1106
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $6)`,
1107
+ parameters: [
1108
+ record.id,
1109
+ record.tokenHash,
1110
+ record.accountId,
1111
+ record.tenantId,
1112
+ record.csrfToken,
1113
+ record.createdAt,
1114
+ record.expiresAt,
1115
+ ],
1116
+ });
1117
+ }
1118
+
1119
+ /* The cookie names its workspace only through the session row, so the
1120
+ tenant is routed first and the session, the account and the scopes are
1121
+ then read in one transaction under it. */
1122
+ async findSession(
1123
+ tokenHash: string,
1124
+ now: number,
1125
+ idleMs: number,
1126
+ touchIntervalMs: number,
1127
+ ): Promise<AuthSession | null> {
1128
+ const routed = await this.#route<RoutedTenantRow>({
1129
+ text: 'SELECT tenant_id FROM auth_sessions WHERE token_hash = $1',
1130
+ parameters: [tokenHash],
1131
+ });
1132
+ const tenantId = routed[0]?.tenant_id;
1133
+ if (!tenantId) return null;
1134
+ const session = await this.#tx(tenantId, 'write', async (transaction) => {
1135
+ const result = await transaction.query<SessionRow>({
1136
+ text: `SELECT ${ACCOUNT_COLUMNS}, s.id AS session_id, s.csrf_token,
1137
+ s.expires_at, s.last_seen_at
1138
+ FROM auth_sessions s
1139
+ JOIN auth_accounts a ON a.id = s.account_id
1140
+ JOIN auth_memberships m
1141
+ ON m.account_id = s.account_id AND m.tenant_id = s.tenant_id
1142
+ WHERE s.token_hash = $1 AND s.expires_at > $2 AND a.status = 'active'`,
1143
+ parameters: [tokenHash, now],
1144
+ });
1145
+ const row = result.rows[0];
1146
+ if (!row) return null;
1147
+ const lastSeenAt = integer(row.last_seen_at);
1148
+ if (now - lastSeenAt > idleMs) {
1149
+ await transaction.execute({
1150
+ text: 'DELETE FROM auth_sessions WHERE token_hash = $1',
1151
+ parameters: [tokenHash],
1152
+ });
1153
+ return null;
1154
+ }
1155
+ if (now - lastSeenAt >= touchIntervalMs) {
1156
+ await transaction.execute({
1157
+ text: 'UPDATE auth_sessions SET last_seen_at = $1 WHERE token_hash = $2',
1158
+ parameters: [now, tokenHash],
1159
+ });
1160
+ }
1161
+ return {
1162
+ row,
1163
+ scopes: await this.#scopes(transaction, row.account_id, tenantId),
1164
+ };
1165
+ });
1166
+ if (!session) return null;
1167
+ const principal: AuthPrincipal = {
1168
+ accountId: session.row.account_id,
1169
+ tenantId: session.row.tenant_id,
1170
+ email: session.row.email,
1171
+ displayName: session.row.display_name,
1172
+ role: session.row.role,
1173
+ scopes: session.scopes,
1174
+ tenants: await this.listTenantAccess(session.row.account_id),
1175
+ };
1176
+ return {
1177
+ principal,
1178
+ csrfToken: session.row.csrf_token,
1179
+ expiresAt: integer(session.row.expires_at),
1180
+ sessionId: session.row.session_id,
1181
+ passwordChangeRequired:
1182
+ integer(session.row.password_change_required) === 1,
1183
+ };
1184
+ }
1185
+
1186
+ /* An account sees its sessions in every workspace it can enter, and row
1187
+ security scopes each read to one, so the workspaces are routed first. */
1188
+ async listAccountSessions(
1189
+ accountId: string,
1190
+ now: number,
1191
+ ): Promise<readonly SessionSummary[]> {
1192
+ const tenants = await this.#route<RoutedTenantRow>({
1193
+ text: `SELECT DISTINCT tenant_id FROM auth_sessions
1194
+ WHERE account_id = $1 AND expires_at > $2`,
1195
+ parameters: [accountId, now],
1196
+ });
1197
+ const summaries: SessionSummary[] = [];
1198
+ for (const tenant of tenants) {
1199
+ const rows = await this.#query<SessionSummaryRow>(tenant.tenant_id, {
1200
+ text: `SELECT s.id, s.tenant_id, t.name AS tenant_name, s.created_at,
1201
+ s.last_seen_at, s.expires_at
1202
+ FROM auth_sessions s
1203
+ JOIN auth_tenants t ON t.id = s.tenant_id
1204
+ WHERE s.account_id = $1 AND s.expires_at > $2`,
1205
+ parameters: [accountId, now],
1206
+ });
1207
+ for (const row of rows) {
1208
+ summaries.push({
1209
+ id: row.id,
1210
+ tenantId: row.tenant_id,
1211
+ tenantName: row.tenant_name,
1212
+ createdAt: integer(row.created_at),
1213
+ lastSeenAt: integer(row.last_seen_at),
1214
+ expiresAt: integer(row.expires_at),
1215
+ });
1216
+ }
1217
+ }
1218
+ return summaries.sort(
1219
+ (left, right) =>
1220
+ right.lastSeenAt - left.lastSeenAt || left.id.localeCompare(right.id),
1221
+ );
1222
+ }
1223
+
1224
+ async deleteSession(tokenHash: string): Promise<void> {
1225
+ const routed = await this.#route<RoutedTenantRow>({
1226
+ text: 'SELECT tenant_id FROM auth_sessions WHERE token_hash = $1',
1227
+ parameters: [tokenHash],
1228
+ });
1229
+ const tenantId = routed[0]?.tenant_id;
1230
+ if (!tenantId) return;
1231
+ await this.#exec(tenantId, {
1232
+ text: 'DELETE FROM auth_sessions WHERE token_hash = $1',
1233
+ parameters: [tokenHash],
1234
+ });
1235
+ }
1236
+
1237
+ async deleteSessionById(accountId: string, id: string): Promise<boolean> {
1238
+ const routed = await this.#route<RoutedTenantRow>({
1239
+ text: `SELECT tenant_id FROM auth_sessions
1240
+ WHERE account_id = $1 AND id = $2`,
1241
+ parameters: [accountId, id],
1242
+ });
1243
+ const tenantId = routed[0]?.tenant_id;
1244
+ if (!tenantId) return false;
1245
+ const affected = await this.#exec(tenantId, {
1246
+ text: 'DELETE FROM auth_sessions WHERE account_id = $1 AND id = $2',
1247
+ parameters: [accountId, id],
1248
+ });
1249
+ return affected > 0;
1250
+ }
1251
+
1252
+ async deleteExpiredSessions(now: number): Promise<number> {
1253
+ const tenants = await this.#route<RoutedTenantRow>({
1254
+ text: 'SELECT DISTINCT tenant_id FROM auth_sessions WHERE expires_at <= $1',
1255
+ parameters: [now],
1256
+ });
1257
+ let removed = 0;
1258
+ for (const tenant of tenants) {
1259
+ removed += await this.#exec(tenant.tenant_id, {
1260
+ text: 'DELETE FROM auth_sessions WHERE expires_at <= $1',
1261
+ parameters: [now],
1262
+ });
1263
+ }
1264
+ return removed;
1265
+ }
1266
+
1267
+ async createPasswordResetToken(
1268
+ record: PasswordResetTokenRecord,
1269
+ ): Promise<void> {
1270
+ await this.#tx(IDENTITY_TENANT_CONTEXT, 'write', async (transaction) => {
1271
+ await transaction.execute({
1272
+ text: `DELETE FROM auth_password_reset_tokens
1273
+ WHERE account_id = $1 OR expires_at <= $2`,
1274
+ parameters: [record.accountId, record.createdAt],
1275
+ });
1276
+ await transaction.execute({
1277
+ text: `INSERT INTO auth_password_reset_tokens
1278
+ (token_hash, account_id, expires_at, used_at, created_at)
1279
+ VALUES ($1, $2, $3, NULL, $4)`,
1280
+ parameters: [
1281
+ record.tokenHash,
1282
+ record.accountId,
1283
+ record.expiresAt,
1284
+ record.createdAt,
1285
+ ],
1286
+ });
1287
+ });
1288
+ }
1289
+
1290
+ /* One statement claims the token: a second caller with the same link finds
1291
+ used_at already set and gets nothing. */
1292
+ async consumePasswordResetToken(
1293
+ tokenHash: string,
1294
+ now: number,
1295
+ ): Promise<string | null> {
1296
+ const rows = await this.#tx(
1297
+ IDENTITY_TENANT_CONTEXT,
1298
+ 'write',
1299
+ async (transaction) => {
1300
+ const result = await transaction.query<{ account_id: string }>({
1301
+ text: `UPDATE auth_password_reset_tokens SET used_at = $1
1302
+ WHERE token_hash = $2 AND used_at IS NULL AND expires_at > $1
1303
+ RETURNING account_id`,
1304
+ parameters: [now, tokenHash],
1305
+ });
1306
+ return result.rows;
1307
+ },
1308
+ );
1309
+ return rows[0]?.account_id ?? null;
1310
+ }
1311
+
1312
+ async createTenantInvitation(record: TenantInvitationRecord): Promise<void> {
1313
+ await this.#tx(record.tenantId, 'write', async (transaction) => {
1314
+ await transaction.execute({
1315
+ text: `DELETE FROM auth_tenant_invitations
1316
+ WHERE tenant_id = $1 AND email_normalized = $2`,
1317
+ parameters: [record.tenantId, record.normalizedEmail],
1318
+ });
1319
+ await transaction.execute({
1320
+ text: `INSERT INTO auth_tenant_invitations
1321
+ (id, tenant_id, email, email_normalized, role_key, token_hash,
1322
+ expires_at, accepted_at, created_by, created_at)
1323
+ VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $9)`,
1324
+ parameters: [
1325
+ record.id,
1326
+ record.tenantId,
1327
+ record.email,
1328
+ record.normalizedEmail,
1329
+ record.roleKey,
1330
+ record.tokenHash,
1331
+ record.expiresAt,
1332
+ record.createdBy,
1333
+ record.createdAt,
1334
+ ],
1335
+ });
1336
+ });
1337
+ }
1338
+
1339
+ async consumeTenantInvitation(
1340
+ tokenHash: string,
1341
+ now: number,
1342
+ ): Promise<{
1343
+ readonly tenantId: string;
1344
+ readonly email: string;
1345
+ readonly normalizedEmail: string;
1346
+ readonly roleKey: string;
1347
+ } | null> {
1348
+ const routed = await this.#route<RoutedTenantRow>({
1349
+ text: 'SELECT tenant_id FROM auth_tenant_invitations WHERE token_hash = $1',
1350
+ parameters: [tokenHash],
1351
+ });
1352
+ const tenantId = routed[0]?.tenant_id;
1353
+ if (!tenantId) return null;
1354
+ const rows = await this.#tx(tenantId, 'write', async (transaction) => {
1355
+ const result = await transaction.query<{
1356
+ tenant_id: string;
1357
+ email: string;
1358
+ email_normalized: string;
1359
+ role_key: string;
1360
+ }>({
1361
+ text: `UPDATE auth_tenant_invitations SET accepted_at = $1
1362
+ WHERE token_hash = $2 AND accepted_at IS NULL AND expires_at > $1
1363
+ RETURNING tenant_id, email, email_normalized, role_key`,
1364
+ parameters: [now, tokenHash],
1365
+ });
1366
+ return result.rows;
1367
+ });
1368
+ const row = rows[0];
1369
+ return row
1370
+ ? {
1371
+ tenantId: row.tenant_id,
1372
+ email: row.email,
1373
+ normalizedEmail: row.email_normalized,
1374
+ roleKey: row.role_key,
1375
+ }
1376
+ : null;
1377
+ }
1378
+
1379
+ async upsertMfaTotp(
1380
+ accountId: string,
1381
+ secretCiphertext: string,
1382
+ createdAt: number,
1383
+ ): Promise<void> {
1384
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
1385
+ text: `INSERT INTO auth_mfa_totp
1386
+ (account_id, secret_ciphertext, confirmed_at, created_at)
1387
+ VALUES ($1, $2, NULL, $3)
1388
+ ON CONFLICT (account_id) DO UPDATE SET
1389
+ secret_ciphertext = excluded.secret_ciphertext,
1390
+ confirmed_at = NULL,
1391
+ created_at = excluded.created_at`,
1392
+ parameters: [accountId, secretCiphertext, createdAt],
1393
+ });
1394
+ }
1395
+
1396
+ async findMfaTotp(accountId: string): Promise<{
1397
+ readonly secretCiphertext: string;
1398
+ readonly confirmedAt: number | null;
1399
+ } | null> {
1400
+ const rows = await this.#query<{
1401
+ secret_ciphertext: string;
1402
+ confirmed_at: number | bigint | string | null;
1403
+ }>(IDENTITY_TENANT_CONTEXT, {
1404
+ text: `SELECT secret_ciphertext, confirmed_at FROM auth_mfa_totp
1405
+ WHERE account_id = $1`,
1406
+ parameters: [accountId],
1407
+ });
1408
+ const row = rows[0];
1409
+ return row
1410
+ ? {
1411
+ secretCiphertext: row.secret_ciphertext,
1412
+ confirmedAt: optionalInteger(row.confirmed_at),
1413
+ }
1414
+ : null;
1415
+ }
1416
+
1417
+ async confirmMfaTotp(accountId: string, confirmedAt: number): Promise<void> {
1418
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
1419
+ text: 'UPDATE auth_mfa_totp SET confirmed_at = $1 WHERE account_id = $2',
1420
+ parameters: [confirmedAt, accountId],
1421
+ });
1422
+ }
1423
+
1424
+ async replaceMfaRecoveryCodes(
1425
+ accountId: string,
1426
+ codeHashes: readonly string[],
1427
+ createdAt: number,
1428
+ ): Promise<void> {
1429
+ await this.#tx(IDENTITY_TENANT_CONTEXT, 'write', async (transaction) => {
1430
+ await transaction.execute({
1431
+ text: 'DELETE FROM auth_mfa_recovery_codes WHERE account_id = $1',
1432
+ parameters: [accountId],
1433
+ });
1434
+ for (const codeHash of codeHashes) {
1435
+ await transaction.execute({
1436
+ text: `INSERT INTO auth_mfa_recovery_codes
1437
+ (code_hash, account_id, used_at, created_at)
1438
+ VALUES ($1, $2, NULL, $3)`,
1439
+ parameters: [codeHash, accountId, createdAt],
1440
+ });
1441
+ }
1442
+ });
1443
+ }
1444
+
1445
+ async consumeMfaRecoveryCode(
1446
+ accountId: string,
1447
+ codeHash: string,
1448
+ ): Promise<boolean> {
1449
+ const affected = await this.#exec(IDENTITY_TENANT_CONTEXT, {
1450
+ text: `UPDATE auth_mfa_recovery_codes
1451
+ SET used_at = (extract(epoch FROM now()) * 1000)::bigint
1452
+ WHERE account_id = $1 AND code_hash = $2 AND used_at IS NULL`,
1453
+ parameters: [accountId, codeHash],
1454
+ });
1455
+ return affected === 1;
1456
+ }
1457
+
1458
+ /* Expired challenges of the same workspace are dropped on the way in, so
1459
+ the table stays bounded by recent sign-in activity. */
1460
+ async createMfaChallenge(record: MfaChallengeRecord): Promise<void> {
1461
+ await this.#tx(record.tenantId, 'write', async (transaction) => {
1462
+ await transaction.execute({
1463
+ text: 'DELETE FROM auth_mfa_challenges WHERE expires_at <= $1',
1464
+ parameters: [record.createdAt],
1465
+ });
1466
+ await transaction.execute({
1467
+ text: `INSERT INTO auth_mfa_challenges
1468
+ (token_hash, account_id, tenant_id, expires_at, used_at, created_at)
1469
+ VALUES ($1, $2, $3, $4, NULL, $5)`,
1470
+ parameters: [
1471
+ record.tokenHash,
1472
+ record.accountId,
1473
+ record.tenantId,
1474
+ record.expiresAt,
1475
+ record.createdAt,
1476
+ ],
1477
+ });
1478
+ });
1479
+ }
1480
+
1481
+ async consumeMfaChallenge(
1482
+ tokenHash: string,
1483
+ now: number,
1484
+ ): Promise<{ readonly accountId: string; readonly tenantId: string } | null> {
1485
+ const routed = await this.#route<RoutedTenantRow>({
1486
+ text: 'SELECT tenant_id FROM auth_mfa_challenges WHERE token_hash = $1',
1487
+ parameters: [tokenHash],
1488
+ });
1489
+ const tenantId = routed[0]?.tenant_id;
1490
+ if (!tenantId) return null;
1491
+ const rows = await this.#tx(tenantId, 'write', async (transaction) => {
1492
+ const result = await transaction.query<{
1493
+ account_id: string;
1494
+ tenant_id: string;
1495
+ }>({
1496
+ text: `UPDATE auth_mfa_challenges SET used_at = $1
1497
+ WHERE token_hash = $2 AND used_at IS NULL AND expires_at > $1
1498
+ RETURNING account_id, tenant_id`,
1499
+ parameters: [now, tokenHash],
1500
+ });
1501
+ return result.rows;
1502
+ });
1503
+ const row = rows[0];
1504
+ return row ? { accountId: row.account_id, tenantId: row.tenant_id } : null;
1505
+ }
1506
+
1507
+ async findSignInFailure(
1508
+ normalizedEmail: string,
1509
+ ): Promise<SignInFailureRecord | null> {
1510
+ const rows = await this.#query<{
1511
+ failures: number | bigint | string;
1512
+ locked_until: number | bigint | string | null;
1513
+ }>(IDENTITY_TENANT_CONTEXT, {
1514
+ text: `SELECT failures, locked_until FROM auth_sign_in_failures
1515
+ WHERE email_normalized = $1`,
1516
+ parameters: [normalizedEmail],
1517
+ });
1518
+ const row = rows[0];
1519
+ return row
1520
+ ? {
1521
+ failures: integer(row.failures),
1522
+ lockedUntil: optionalInteger(row.locked_until),
1523
+ }
1524
+ : null;
1525
+ }
1526
+
1527
+ /* One row per address that failed recently; rows idle longer than the
1528
+ retention window are dropped on the way in, so the table stays bounded
1529
+ by recent activity instead of growing with every guessed address. The
1530
+ read and the write share one transaction, so two attempts cannot both
1531
+ count the same failure. */
1532
+ async recordSignInFailure(
1533
+ normalizedEmail: string,
1534
+ now: number,
1535
+ lockThreshold: number,
1536
+ lockMs: number,
1537
+ retentionMs: number,
1538
+ ): Promise<SignInFailureRecord> {
1539
+ return this.#tx(IDENTITY_TENANT_CONTEXT, 'write', async (transaction) => {
1540
+ await transaction.execute({
1541
+ text: 'DELETE FROM auth_sign_in_failures WHERE updated_at < $1',
1542
+ parameters: [now - retentionMs],
1543
+ });
1544
+ const current = await transaction.query<{
1545
+ failures: number | bigint | string;
1546
+ locked_until: number | bigint | string | null;
1547
+ }>({
1548
+ text: `SELECT failures, locked_until FROM auth_sign_in_failures
1549
+ WHERE email_normalized = $1`,
1550
+ parameters: [normalizedEmail],
1551
+ });
1552
+ const previous = current.rows[0];
1553
+ const failures = (previous ? integer(previous.failures) : 0) + 1;
1554
+ const lockedUntil =
1555
+ failures >= lockThreshold
1556
+ ? now + lockMs
1557
+ : previous
1558
+ ? optionalInteger(previous.locked_until)
1559
+ : null;
1560
+ await transaction.execute({
1561
+ text: `INSERT INTO auth_sign_in_failures
1562
+ (email_normalized, failures, locked_until, updated_at)
1563
+ VALUES ($1, $2, $3, $4)
1564
+ ON CONFLICT (email_normalized) DO UPDATE SET
1565
+ failures = excluded.failures,
1566
+ locked_until = excluded.locked_until,
1567
+ updated_at = excluded.updated_at`,
1568
+ parameters: [
1569
+ normalizedEmail,
1570
+ failures >= lockThreshold ? 0 : failures,
1571
+ lockedUntil,
1572
+ now,
1573
+ ],
1574
+ });
1575
+ return { failures, lockedUntil };
1576
+ });
1577
+ }
1578
+
1579
+ async clearSignInFailures(normalizedEmail: string): Promise<void> {
1580
+ await this.#exec(IDENTITY_TENANT_CONTEXT, {
1581
+ text: 'DELETE FROM auth_sign_in_failures WHERE email_normalized = $1',
1582
+ parameters: [normalizedEmail],
1583
+ });
1584
+ }
1585
+
1586
+ async listRoles(tenantId: string): Promise<readonly TenantRole[]> {
1587
+ const rows = await this.#query<RoleRow>(tenantId, {
1588
+ text: `SELECT * FROM auth_roles WHERE tenant_id = $1
1589
+ ORDER BY builtin DESC,
1590
+ CASE key WHEN 'owner' THEN 0 WHEN 'member' THEN 1 ELSE 2 END,
1591
+ lower(name), id`,
1592
+ parameters: [tenantId],
1593
+ });
1594
+ return rows.map(fromRoleRow);
1595
+ }
1596
+
1597
+ async findRole(tenantId: string, id: string): Promise<TenantRole | null> {
1598
+ const rows = await this.#query<RoleRow>(tenantId, {
1599
+ text: 'SELECT * FROM auth_roles WHERE tenant_id = $1 AND id = $2',
1600
+ parameters: [tenantId, id],
1601
+ });
1602
+ return rows[0] ? fromRoleRow(rows[0]) : null;
1603
+ }
1604
+
1605
+ async findRoleByKey(
1606
+ tenantId: string,
1607
+ key: string,
1608
+ ): Promise<TenantRole | null> {
1609
+ const rows = await this.#query<RoleRow>(tenantId, {
1610
+ text: 'SELECT * FROM auth_roles WHERE tenant_id = $1 AND key = $2',
1611
+ parameters: [tenantId, key],
1612
+ });
1613
+ return rows[0] ? fromRoleRow(rows[0]) : null;
1614
+ }
1615
+
1616
+ async createRole(record: CreateRoleRecord): Promise<TenantRole> {
1617
+ try {
1618
+ const rows = await this.#tx(
1619
+ record.tenantId,
1620
+ 'write',
1621
+ async (transaction) => {
1622
+ const result = await transaction.query<RoleRow>({
1623
+ text: `INSERT INTO auth_roles
1624
+ (id, tenant_id, key, name, description, scopes_json, builtin,
1625
+ created_at, updated_at)
1626
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8)
1627
+ RETURNING *`,
1628
+ parameters: [
1629
+ record.id,
1630
+ record.tenantId,
1631
+ record.key,
1632
+ record.name,
1633
+ record.description,
1634
+ JSON.stringify(record.scopes),
1635
+ record.builtin ? 1 : 0,
1636
+ record.createdAt,
1637
+ ],
1638
+ });
1639
+ return result.rows;
1640
+ },
1641
+ );
1642
+ return fromRoleRow(rows[0]!);
1643
+ } catch (error) {
1644
+ if (duplicate(error) === 'role-key') throw new DuplicateRoleKeyError();
1645
+ throw error;
1646
+ }
1647
+ }
1648
+
1649
+ async updateRole(
1650
+ tenantId: string,
1651
+ id: string,
1652
+ patch: {
1653
+ readonly name: string;
1654
+ readonly description: string;
1655
+ readonly scopes: readonly string[];
1656
+ },
1657
+ updatedAt: number,
1658
+ ): Promise<TenantRole | null> {
1659
+ const rows = await this.#tx(tenantId, 'write', async (transaction) => {
1660
+ const result = await transaction.query<RoleRow>({
1661
+ text: `UPDATE auth_roles SET name = $1, description = $2,
1662
+ scopes_json = $3, updated_at = $4
1663
+ WHERE tenant_id = $5 AND id = $6 RETURNING *`,
1664
+ parameters: [
1665
+ patch.name,
1666
+ patch.description,
1667
+ JSON.stringify(patch.scopes),
1668
+ updatedAt,
1669
+ tenantId,
1670
+ id,
1671
+ ],
1672
+ });
1673
+ return result.rows;
1674
+ });
1675
+ return rows[0] ? fromRoleRow(rows[0]) : null;
1676
+ }
1677
+
1678
+ async deleteRole(tenantId: string, id: string): Promise<boolean> {
1679
+ const affected = await this.#exec(tenantId, {
1680
+ text: `DELETE FROM auth_roles
1681
+ WHERE tenant_id = $1 AND id = $2 AND builtin = 0`,
1682
+ parameters: [tenantId, id],
1683
+ });
1684
+ return affected > 0;
1685
+ }
1686
+
1687
+ async countRoleMemberships(
1688
+ tenantId: string,
1689
+ roleId: string,
1690
+ ): Promise<number> {
1691
+ const rows = await this.#query<CountRow>(tenantId, {
1692
+ text: `SELECT count(*) AS total FROM auth_memberships
1693
+ WHERE tenant_id = $1 AND role_id = $2`,
1694
+ parameters: [tenantId, roleId],
1695
+ });
1696
+ return rows[0] ? integer(rows[0].total) : 0;
1697
+ }
1698
+
1699
+ async appendAudit(record: AuditRecord): Promise<void> {
1700
+ await this.#exec(record.tenantId, {
1701
+ text: `INSERT INTO auth_audit
1702
+ (tenant_id, actor_account_id, actor_label, actor_kind, actor_run_id,
1703
+ action, subject_type, subject_id, metadata_json, occurred_at)
1704
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
1705
+ parameters: [
1706
+ record.tenantId,
1707
+ record.actorAccountId,
1708
+ record.actorLabel,
1709
+ record.actorKind,
1710
+ record.actorRunId,
1711
+ record.action,
1712
+ record.subjectType,
1713
+ record.subjectId,
1714
+ JSON.stringify(record.metadata),
1715
+ record.occurredAt,
1716
+ ],
1717
+ });
1718
+ }
1719
+
1720
+ async queryAudit(query: AuditQuery): Promise<readonly AuditActorEvent[]> {
1721
+ const parameters: DatabaseParameter[] = [query.tenantId];
1722
+ const conditions = ['tenant_id = $1'];
1723
+ const marker = (value: DatabaseParameter): string => {
1724
+ parameters.push(value);
1725
+ return `$${parameters.length}`;
1726
+ };
1727
+ if (query.action) conditions.push(`action = ${marker(query.action)}`);
1728
+ if (query.actor) {
1729
+ conditions.push(
1730
+ `(actor_account_id = ${marker(query.actor)} OR actor_label = ${marker(query.actor)})`,
1731
+ );
1732
+ }
1733
+ if (query.cursor) {
1734
+ const [occurredAt = NaN, id = NaN] = query.cursor.split(':').map(Number);
1735
+ if (Number.isSafeInteger(occurredAt) && Number.isSafeInteger(id)) {
1736
+ conditions.push(
1737
+ `(occurred_at < ${marker(occurredAt)} OR (occurred_at = ${marker(occurredAt)} AND id < ${marker(id)}))`,
1738
+ );
1739
+ }
1740
+ }
1741
+ const rows = await this.#query<AuditRow>(query.tenantId, {
1742
+ text: `SELECT * FROM auth_audit WHERE ${conditions.join(' AND ')}
1743
+ ORDER BY occurred_at DESC, id DESC LIMIT ${marker(query.limit)}`,
1744
+ parameters,
1745
+ });
1746
+ return rows.map((row) => ({
1747
+ id: integer(row.id),
1748
+ tenantId: row.tenant_id,
1749
+ actorAccountId: row.actor_account_id,
1750
+ actorLabel: row.actor_label,
1751
+ actorKind: row.actor_kind,
1752
+ actorRunId: row.actor_run_id,
1753
+ action: row.action,
1754
+ subjectType: row.subject_type,
1755
+ subjectId: row.subject_id,
1756
+ metadata: JSON.parse(row.metadata_json) as Record<string, unknown>,
1757
+ occurredAt: integer(row.occurred_at),
1758
+ }));
1759
+ }
1760
+
1761
+ async loadSettings(
1762
+ tenantId: string,
1763
+ moduleId: string,
1764
+ ): Promise<Readonly<Record<string, ModuleSettingValue>>> {
1765
+ const storage = settingsTenant(tenantId);
1766
+ const rows = await this.#query<{ key: string; value_json: string }>(
1767
+ storage,
1768
+ {
1769
+ text: `SELECT key, value_json FROM module_settings
1770
+ WHERE tenant_id = $1 AND module_id = $2`,
1771
+ parameters: [storage, moduleId],
1772
+ },
1773
+ );
1774
+ const values: Record<string, ModuleSettingValue> = {};
1775
+ for (const row of rows) {
1776
+ const value = JSON.parse(row.value_json) as unknown;
1777
+ if (
1778
+ typeof value === 'string' ||
1779
+ typeof value === 'number' ||
1780
+ typeof value === 'boolean'
1781
+ ) {
1782
+ values[row.key] = value;
1783
+ }
1784
+ }
1785
+ return values;
1786
+ }
1787
+
1788
+ async saveSetting(record: ModuleSettingRecord): Promise<void> {
1789
+ const storage = settingsTenant(record.tenantId);
1790
+ await this.#exec(storage, {
1791
+ text: `INSERT INTO module_settings
1792
+ (tenant_id, module_id, key, value_json, updated_at, updated_by)
1793
+ VALUES ($1, $2, $3, $4, $5, $6)
1794
+ ON CONFLICT (tenant_id, module_id, key) DO UPDATE SET
1795
+ value_json = excluded.value_json,
1796
+ updated_at = excluded.updated_at,
1797
+ updated_by = excluded.updated_by`,
1798
+ parameters: [
1799
+ storage,
1800
+ record.moduleId,
1801
+ record.key,
1802
+ JSON.stringify(record.value),
1803
+ record.updatedAt,
1804
+ record.updatedBy,
1805
+ ],
1806
+ });
1807
+ }
1808
+
1809
+ async clearSetting(
1810
+ tenantId: string,
1811
+ moduleId: string,
1812
+ key: string,
1813
+ ): Promise<void> {
1814
+ const storage = settingsTenant(tenantId);
1815
+ await this.#exec(storage, {
1816
+ text: `DELETE FROM module_settings
1817
+ WHERE tenant_id = $1 AND module_id = $2 AND key = $3`,
1818
+ parameters: [storage, moduleId, key],
1819
+ });
1820
+ }
1821
+ }
1822
+
1823
+ function settingsTenant(tenantId: string): string {
1824
+ return tenantId === '' ? PLATFORM_SETTINGS_STORAGE_TENANT : tenantId;
1825
+ }