@maudecode/kanvil 0.2.1 → 0.4.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 (301) hide show
  1. package/apps/server/.env.example +19 -0
  2. package/apps/server/dist/auth-Cuo7ECLh.mjs +7770 -0
  3. package/apps/server/dist/client.mjs +884 -0
  4. package/apps/server/dist/{config-CbNqtJEj.mjs → config-DtNfVSnB.mjs} +28 -133
  5. package/apps/server/dist/index.mjs +2503 -295
  6. package/apps/server/dist/instance-CCeXRJ6m.mjs +133 -0
  7. package/apps/server/dist/migrate.mjs +1 -1
  8. package/apps/server/dist/recover.mjs +5 -4
  9. package/apps/server/dist/src-tQ82UI8k.mjs +4818 -0
  10. package/apps/server/drizzle/0014_inbox_dismissal.sql +1 -0
  11. package/apps/server/drizzle/0015_notification_categories.sql +14 -0
  12. package/apps/server/drizzle/0016_ticket_resources.sql +19 -0
  13. package/apps/server/drizzle/0017_project_backgrounds.sql +3 -0
  14. package/apps/server/drizzle/0018_delivery_outbox.sql +65 -0
  15. package/apps/server/drizzle/0019_retire_push_deliveries.sql +1 -0
  16. package/apps/server/drizzle/0020_calendar_feeds.sql +16 -0
  17. package/apps/server/drizzle/0021_staged_uploads.sql +14 -0
  18. package/apps/server/drizzle/0022_manual_order.sql +6 -0
  19. package/apps/server/drizzle/0023_workspace_invitations.sql +22 -0
  20. package/apps/server/drizzle/0024_workflow_states.sql +39 -0
  21. package/apps/server/drizzle/0025_workspace_labels.sql +10 -0
  22. package/apps/server/drizzle/0026_public_read.sql +1 -0
  23. package/apps/server/drizzle/0027_custom_fields.sql +16 -0
  24. package/apps/server/drizzle/0028_project_lifecycle.sql +11 -0
  25. package/apps/server/drizzle/0029_ticket_relocation.sql +16 -0
  26. package/apps/server/drizzle/0030_repository_connections.sql +39 -0
  27. package/apps/server/drizzle/0031_repository_providers.sql +3 -0
  28. package/apps/server/drizzle/0032_workspace_ownership.sql +11 -0
  29. package/apps/server/drizzle/0033_import_runs.sql +38 -0
  30. package/apps/server/drizzle/0034_issue_sync.sql +38 -0
  31. package/apps/server/drizzle/0035_account_deletion.sql +9 -0
  32. package/apps/server/drizzle/0036_workspace_roles.sql +16 -0
  33. package/apps/server/drizzle/0037_email_code_owner.sql +14 -0
  34. package/apps/server/drizzle/0038_time_entries.sql +19 -0
  35. package/apps/server/drizzle/meta/0014_snapshot.json +4246 -0
  36. package/apps/server/drizzle/meta/0015_snapshot.json +4297 -0
  37. package/apps/server/drizzle/meta/0016_snapshot.json +4443 -0
  38. package/apps/server/drizzle/meta/0017_snapshot.json +4468 -0
  39. package/apps/server/drizzle/meta/0018_snapshot.json +4948 -0
  40. package/apps/server/drizzle/meta/0019_snapshot.json +4826 -0
  41. package/apps/server/drizzle/meta/0020_snapshot.json +4947 -0
  42. package/apps/server/drizzle/meta/0021_snapshot.json +5059 -0
  43. package/apps/server/drizzle/meta/0022_snapshot.json +5094 -0
  44. package/apps/server/drizzle/meta/0023_snapshot.json +5267 -0
  45. package/apps/server/drizzle/meta/0024_snapshot.json +5405 -0
  46. package/apps/server/drizzle/meta/0025_snapshot.json +5474 -0
  47. package/apps/server/drizzle/meta/0026_snapshot.json +5481 -0
  48. package/apps/server/drizzle/meta/0027_snapshot.json +5605 -0
  49. package/apps/server/drizzle/meta/0028_snapshot.json +5699 -0
  50. package/apps/server/drizzle/meta/0029_snapshot.json +5809 -0
  51. package/apps/server/drizzle/meta/0030_snapshot.json +6066 -0
  52. package/apps/server/drizzle/meta/0031_snapshot.json +6074 -0
  53. package/apps/server/drizzle/meta/0032_snapshot.json +6100 -0
  54. package/apps/server/drizzle/meta/0033_snapshot.json +6406 -0
  55. package/apps/server/drizzle/meta/0034_snapshot.json +6698 -0
  56. package/apps/server/drizzle/meta/0035_snapshot.json +6704 -0
  57. package/apps/server/drizzle/meta/0036_snapshot.json +6825 -0
  58. package/apps/server/drizzle/meta/0037_snapshot.json +6825 -0
  59. package/apps/server/drizzle/meta/0038_snapshot.json +6998 -0
  60. package/apps/server/drizzle/meta/_journal.json +175 -0
  61. package/apps/web/dist/assets/CompositeItem-DgnfVvrF.js +1 -0
  62. package/apps/web/dist/assets/Trans-f2VYVS9z.js +1 -0
  63. package/apps/web/dist/assets/abnfDiagram-WEYOIA3C-DTOx8N4B.js +1 -0
  64. package/apps/web/dist/assets/admin-settings-DBZL4siY.js +1 -0
  65. package/apps/web/dist/assets/arc-Y1ARJVZ4.js +1 -0
  66. package/apps/web/dist/assets/architecture-WOLXFQ4H-DlC3p5z3.js +1 -0
  67. package/apps/web/dist/assets/architectureDiagram-ZHVYKT53-DooN3Z58.js +36 -0
  68. package/apps/web/dist/assets/archive-restore-CGuLxoHC.js +1 -0
  69. package/apps/web/dist/assets/array-BifhSqXX.js +1 -0
  70. package/apps/web/dist/assets/az-AZ-DesqXNg5.js +1 -0
  71. package/apps/web/dist/assets/az-BE5iqNIE.js +1 -0
  72. package/apps/web/dist/assets/az-D-kj4lmf.js +1 -0
  73. package/apps/web/dist/assets/blockDiagram-RJAE7QA3-CD5AgY4U.js +140 -0
  74. package/apps/web/dist/assets/c4Diagram-XDPPPE54-CTDOcLuN.js +38 -0
  75. package/apps/web/dist/assets/channel-BhHvU4I-.js +1 -0
  76. package/apps/web/dist/assets/chunk-2E7WHVMD-D9qnBqTV.js +88 -0
  77. package/apps/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  78. package/apps/web/dist/assets/chunk-4EMNKR2T-Kz5JiJ85.js +1 -0
  79. package/apps/web/dist/assets/chunk-55XCU76K-DnKiFpYM.js +1 -0
  80. package/apps/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  81. package/apps/web/dist/assets/chunk-7S6XSTFO-CpdNd6Ke.js +2 -0
  82. package/apps/web/dist/assets/chunk-AQDTSKZB-CpyuSWt8.js +72 -0
  83. package/apps/web/dist/assets/chunk-DSNLSHJZ-DTC3y9rb.js +129 -0
  84. package/apps/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  85. package/apps/web/dist/assets/chunk-HAGFOTED-Bv8PrMYK.js +62 -0
  86. package/apps/web/dist/assets/chunk-HNE4Q36P-CDDJ_yRa.js +1 -0
  87. package/apps/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  88. package/apps/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  89. package/apps/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  90. package/apps/web/dist/assets/chunk-NQOKRVIF-lNtnsWHS.js +213 -0
  91. package/apps/web/dist/assets/chunk-UHW7PFBP-D79ptlTN.js +1 -0
  92. package/apps/web/dist/assets/chunk-VQ5W62QI-B8hWFCiM.js +1 -0
  93. package/apps/web/dist/assets/chunk-WQQL4CDK-DAGuWzS3.js +10 -0
  94. package/apps/web/dist/assets/chunk-XXDRQBXY-CnbRkih_.js +1 -0
  95. package/apps/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  96. package/apps/web/dist/assets/chunk-YZO67HFG-C0ptzlKO.js +1 -0
  97. package/apps/web/dist/assets/chunk-ZAUX2LTX-ah8U5xoJ.js +1 -0
  98. package/apps/web/dist/assets/chunk-ZCHMQSOR-DYqATOIZ.js +2 -0
  99. package/apps/web/dist/assets/classDiagram-v2-4MYWR3UA-DYytA-ua.js +217 -0
  100. package/apps/web/dist/assets/core-jXc03Sty.js +1 -0
  101. package/apps/web/dist/assets/corner-left-up-BCro--Gl.js +1 -0
  102. package/apps/web/dist/assets/cose-bilkent-JH36ORCC-lWVj4msc.js +1 -0
  103. package/apps/web/dist/assets/cynefin-EF2NZ3EQ-Xy6e6m1E.js +1 -0
  104. package/apps/web/dist/assets/cynefinDiagram-WF24UDRP-BYarZJSm.js +62 -0
  105. package/apps/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  106. package/apps/web/dist/assets/dagre-NK4GIZVX-WLvvUY1o.js +4 -0
  107. package/apps/web/dist/assets/de-DAv77-wP.js +1 -0
  108. package/apps/web/dist/assets/de-DE-BIGY9SNm.js +1 -0
  109. package/apps/web/dist/assets/de-DLGjUGf_.js +1 -0
  110. package/apps/web/dist/assets/default-BgvtI7HK.js +56 -0
  111. package/apps/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  112. package/apps/web/dist/assets/delivery-settings-CZOtb4X8.js +1 -0
  113. package/apps/web/dist/assets/diagram-CBK3MWTC-D6PtxkLg.js +24 -0
  114. package/apps/web/dist/assets/diagram-I5XIIV2D-DTOKQwIY.js +24 -0
  115. package/apps/web/dist/assets/diagram-ODIL2UCW-BmERt0Xu.js +30 -0
  116. package/apps/web/dist/assets/diagram-PFBEJUXE-WDUe08eD.js +3 -0
  117. package/apps/web/dist/assets/diagram-RJA3I4VV-B2K-NLGG.js +41 -0
  118. package/apps/web/dist/assets/diagram-TYUNFSNR-BLf7PpV7.js +200 -0
  119. package/apps/web/dist/assets/dist-CumnGVmJ.js +1 -0
  120. package/apps/web/dist/assets/ebnfDiagram-V4P3TTPT-DwBUQs9p.js +1 -0
  121. package/apps/web/dist/assets/el-CG6Xy42v.js +1 -0
  122. package/apps/web/dist/assets/el-D2ypddWn.js +1 -0
  123. package/apps/web/dist/assets/el-GR-D2TjImt0.js +1 -0
  124. package/apps/web/dist/assets/elk-QONTBCTQ-AuT8PinL.js +27 -0
  125. package/apps/web/dist/assets/ellipsis-ZJM26JNS.js +1 -0
  126. package/apps/web/dist/assets/en-EnSRisMz.js +1 -0
  127. package/apps/web/dist/assets/en-US-DKYSujv3.js +1 -0
  128. package/apps/web/dist/assets/erDiagram-KYQIGQAA-h--u4Eo6.js +99 -0
  129. package/apps/web/dist/assets/es-B_49BMhR.js +1 -0
  130. package/apps/web/dist/assets/es-Bbj5WuMM.js +1 -0
  131. package/apps/web/dist/assets/es-ES-C_fRqLGU.js +1 -0
  132. package/apps/web/dist/assets/eventmodeling-K75KTNOO-CW4hDt1j.js +1 -0
  133. package/apps/web/dist/assets/flowDiagram-IUGLLWBU-BGUscnNP.js +1 -0
  134. package/apps/web/dist/assets/forms-C3pNy-qY.js +1 -0
  135. package/apps/web/dist/assets/fr-B-WmPGZD.js +1 -0
  136. package/apps/web/dist/assets/fr-CjuIKc1o.js +1 -0
  137. package/apps/web/dist/assets/fr-FR-_tnQsn4j.js +1 -0
  138. package/apps/web/dist/assets/ganttDiagram-R7GPNTJY-CB3lsQvj.js +292 -0
  139. package/apps/web/dist/assets/gitGraph-VSP46ZUC-Hu-kvdCR.js +1 -0
  140. package/apps/web/dist/assets/gitGraphDiagram-PDYL7XHY-DCuY3C1Q.js +106 -0
  141. package/apps/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  142. package/apps/web/dist/assets/hi-BVVrZ-zZ.js +1 -0
  143. package/apps/web/dist/assets/hi-DvoDcUpL.js +1 -0
  144. package/apps/web/dist/assets/hi-IN-CcmFsZYy.js +1 -0
  145. package/apps/web/dist/assets/highlight-CKyEM4jx.js +5 -0
  146. package/apps/web/dist/assets/id-BhNWOQnp.js +1 -0
  147. package/apps/web/dist/assets/id-CPASBZ2r.js +1 -0
  148. package/apps/web/dist/assets/id-ID-C59tC61D.js +1 -0
  149. package/apps/web/dist/assets/index-BIuqxnJn.js +17 -0
  150. package/apps/web/dist/assets/index-DyQNFS69.css +2 -0
  151. package/apps/web/dist/assets/info-OHQRW6UA-Cc8VwTsk.js +1 -0
  152. package/apps/web/dist/assets/infoDiagram-MVDRKAX5-KIMOx55J.js +2 -0
  153. package/apps/web/dist/assets/init-C-OQMol4.js +1 -0
  154. package/apps/web/dist/assets/invitations-D5unaga2.js +1 -0
  155. package/apps/web/dist/assets/isSameWeek-2ftYGvBT.js +1 -0
  156. package/apps/web/dist/assets/ishikawaDiagram-HPEHV2GE-CAou726L.js +70 -0
  157. package/apps/web/dist/assets/it-Cav3_-xd.js +1 -0
  158. package/apps/web/dist/assets/it-Cd7u97Jd.js +1 -0
  159. package/apps/web/dist/assets/it-IT-BAP5O3gK.js +1 -0
  160. package/apps/web/dist/assets/item-Cj8xpMLk.js +1 -0
  161. package/apps/web/dist/assets/ja-DTmZE0Qe.js +1 -0
  162. package/apps/web/dist/assets/ja-JP-vQa7mEdh.js +1 -0
  163. package/apps/web/dist/assets/ja-LkOrjegp.js +1 -0
  164. package/apps/web/dist/assets/journeyDiagram-BOSHW7O7-YbJuPYDH.js +139 -0
  165. package/apps/web/dist/assets/kanban-definition-FNSA4HC5-nuRnm9y8.js +89 -0
  166. package/apps/web/dist/assets/katex-B_JcIwqj.js +257 -0
  167. package/apps/web/dist/assets/ko-BuYCMOUj.js +1 -0
  168. package/apps/web/dist/assets/ko-DPpXw683.js +1 -0
  169. package/apps/web/dist/assets/ko-KR-CXlpmX0-.js +1 -0
  170. package/apps/web/dist/assets/label-settings-B89Ehf78.js +1 -0
  171. package/apps/web/dist/assets/line-WsxZYwZr.js +1 -0
  172. package/apps/web/dist/assets/linear-C9iwaMOi.js +1 -0
  173. package/apps/web/dist/assets/lucide-DIlszs-T.js +1 -0
  174. package/apps/web/dist/assets/markdown-blocks-DSEA68s5.js +2 -0
  175. package/apps/web/dist/assets/member-directory-48yD01yN.js +1 -0
  176. package/apps/web/dist/assets/mermaid-parser.core-B5c-1CnP.js +7 -0
  177. package/apps/web/dist/assets/mermaid.core-vGVkgzA3.js +44 -0
  178. package/apps/web/dist/assets/mindmap-definition-6OY57UG3-uooJyUak.js +98 -0
  179. package/apps/web/dist/assets/mk-BZ2Rg2sU.js +1 -0
  180. package/apps/web/dist/assets/mk-CkaQRpqe.js +1 -0
  181. package/apps/web/dist/assets/mk-MK-BU5BLpXM.js +1 -0
  182. package/apps/web/dist/assets/nl-BtnPzRih.js +1 -0
  183. package/apps/web/dist/assets/nl-DiEjqOA8.js +1 -0
  184. package/apps/web/dist/assets/nl-NL-2aSpdZkg.js +1 -0
  185. package/apps/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  186. package/apps/web/dist/assets/packet-JDAUHWVQ-WJpqlH1d.js +1 -0
  187. package/apps/web/dist/assets/path-fybaL0A-.js +1 -0
  188. package/apps/web/dist/assets/pegDiagram-ECIUJNBX-CUvajD93.js +1 -0
  189. package/apps/web/dist/assets/pie-XZMESJXO-DvandPby.js +1 -0
  190. package/apps/web/dist/assets/pieDiagram-GNMPY74T-CqxeyIaP.js +39 -0
  191. package/apps/web/dist/assets/pl-Beg8hEdK.js +1 -0
  192. package/apps/web/dist/assets/pl-CauoMfkf.js +1 -0
  193. package/apps/web/dist/assets/pl-PL-C5Wvnv7f.js +1 -0
  194. package/apps/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
  195. package/apps/web/dist/assets/project-lifecycle-CIAMJ-SX.js +1 -0
  196. package/apps/web/dist/assets/project-settings-C7lolDSl.js +1 -0
  197. package/apps/web/dist/assets/pt-BR-B4VZ35N_.js +1 -0
  198. package/apps/web/dist/assets/pt-BR-BpOhwoDW.js +1 -0
  199. package/apps/web/dist/assets/pt-BR-CVvI-ZB7.js +1 -0
  200. package/apps/web/dist/assets/public-project-DvvfaUd2.js +1 -0
  201. package/apps/web/dist/assets/quadrantDiagram-D57K4MC3-DFjNmaZc.js +7 -0
  202. package/apps/web/dist/assets/radar-ABXABTNO-CJhZF9j2.js +1 -0
  203. package/apps/web/dist/assets/railroad-I3PHUGI6-DZC10oBD.js +1 -0
  204. package/apps/web/dist/assets/railroad-abnf-LNEFI6M7-BUJF2n6u.js +1 -0
  205. package/apps/web/dist/assets/railroad-ebnf-DWYD2UWJ-CG7oB02K.js +1 -0
  206. package/apps/web/dist/assets/railroad-peg-JR7BVNR7-DBCZuZJY.js +1 -0
  207. package/apps/web/dist/assets/railroadDiagram-Y72NRBL7-CvN3poBq.js +1 -0
  208. package/apps/web/dist/assets/requirementDiagram-EQYSW5RA-BEItNirc.js +84 -0
  209. package/apps/web/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
  210. package/apps/web/dist/assets/rotate-ccw-GjVJPDML.js +1 -0
  211. package/apps/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  212. package/apps/web/dist/assets/ru-DOaRp00B.js +1 -0
  213. package/apps/web/dist/assets/ru-DjVJVHzv.js +1 -0
  214. package/apps/web/dist/assets/ru-RU-C7UUdGIh.js +1 -0
  215. package/apps/web/dist/assets/sankeyDiagram-SY24JHO5-DluLIjax.js +40 -0
  216. package/apps/web/dist/assets/search-ZaQmVnj1.js +85 -0
  217. package/apps/web/dist/assets/send-Dr9bXHhf.js +1 -0
  218. package/apps/web/dist/assets/sequenceDiagram-BLEHNHQU-ZcwnsNxK.js +169 -0
  219. package/apps/web/dist/assets/settings-DXvrVvyS.js +1 -0
  220. package/apps/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  221. package/apps/web/dist/assets/sliders-horizontal-C1t_zA7D.js +1 -0
  222. package/apps/web/dist/assets/src-BWF7jMDv.js +1 -0
  223. package/apps/web/dist/assets/stateDiagram-v2-A6IESKWZ-DD6vH3rM.js +291 -0
  224. package/apps/web/dist/assets/swimlanes-B3ENRKFQ-pX_qyO4X.js +1 -0
  225. package/apps/web/dist/assets/swimlanesDiagram-AYQRV4AK-CeVsLUsg.js +8 -0
  226. package/apps/web/dist/assets/switch-D-WTDajm.js +1 -0
  227. package/apps/web/dist/assets/table-BaRMFP84.js +1 -0
  228. package/apps/web/dist/assets/ticket-detail-CFJI4qoK.js +2 -0
  229. package/apps/web/dist/assets/ticket-editor-DL3nHG_q.js +2 -0
  230. package/apps/web/dist/assets/ticket-list-CDkweYGS.js +5 -0
  231. package/apps/web/dist/assets/ticket-list-controls-CmTgFQQ-.js +1 -0
  232. package/apps/web/dist/assets/timeline-definition-JXYKGLGB-DLBjAxKq.js +120 -0
  233. package/apps/web/dist/assets/toDate-B3HYxwtT.js +1 -0
  234. package/apps/web/dist/assets/tr-Ckc4TA71.js +1 -0
  235. package/apps/web/dist/assets/tr-DeYojjLx.js +1 -0
  236. package/apps/web/dist/assets/tr-TR-VVsL3uMp.js +1 -0
  237. package/apps/web/dist/assets/treeView-D4JQ5SDB-DsJ44q_q.js +1 -0
  238. package/apps/web/dist/assets/treemap-SAJKECNS-BmXoBpVL.js +1 -0
  239. package/apps/web/dist/assets/uk-B4ZvJ8xG.js +1 -0
  240. package/apps/web/dist/assets/uk-DACrfgCP.js +1 -0
  241. package/apps/web/dist/assets/uk-UA-DRpCbOps.js +1 -0
  242. package/apps/web/dist/assets/upload-BwGa8shS.js +1 -0
  243. package/apps/web/dist/assets/useQueries-sQEHjB0a.js +1 -0
  244. package/apps/web/dist/assets/usecaseDiagram-WPOLBNNX-eU2tr2si.js +328 -0
  245. package/apps/web/dist/assets/vennDiagram-Q3Y3IDCW-CoVTWGNg.js +34 -0
  246. package/apps/web/dist/assets/vi-CkqUo4w_.js +1 -0
  247. package/apps/web/dist/assets/vi-Dq2krfY8.js +1 -0
  248. package/apps/web/dist/assets/vi-VN-C0YFqq9j.js +1 -0
  249. package/apps/web/dist/assets/visual-editor-CBhwqTDu.js +208 -0
  250. package/apps/web/dist/assets/wardley-7MLQ67FV-C6mVVk98.js +1 -0
  251. package/apps/web/dist/assets/wardleyDiagram-IVAIDKWA-m3CeJc7a.js +78 -0
  252. package/apps/web/dist/assets/workspace-settings-DgLyieJN.js +1 -0
  253. package/apps/web/dist/assets/xychartDiagram-4URNHDU4-Dw6fLaip.js +7 -0
  254. package/apps/web/dist/assets/zh-CN-B7Rxc3Mp.js +1 -0
  255. package/apps/web/dist/assets/zh-CN-CC7ghLLr.js +1 -0
  256. package/apps/web/dist/assets/zh-CN-pcgTdR8c.js +1 -0
  257. package/apps/web/dist/assets/zh-TW-BlIRMi1C.js +1 -0
  258. package/apps/web/dist/assets/zh-TW-DymSXkFg.js +1 -0
  259. package/apps/web/dist/assets/zh-TW-awiwHjeM.js +1 -0
  260. package/apps/web/dist/index.html +14 -8
  261. package/bin/kanvil.mjs +3 -1
  262. package/compose.yml +12 -0
  263. package/docs/api.md +311 -10
  264. package/docs/cli.md +74 -0
  265. package/docs/infrastructure.md +2 -2
  266. package/docs/localization.md +25 -0
  267. package/docs/plan.md +34 -11
  268. package/docs/self-hosting.md +44 -5
  269. package/docs/themes.md +1 -1
  270. package/docs/verification.md +1 -1
  271. package/docs/workspaces.md +18 -2
  272. package/package.json +12 -8
  273. package/apps/server/dist/auth-I1XEr0d4.mjs +0 -2931
  274. package/apps/web/dist/assets/CompositeItem-BWPJ5Mol.js +0 -1
  275. package/apps/web/dist/assets/admin-settings-DZqkC6ju.js +0 -1
  276. package/apps/web/dist/assets/date-picker-6H_iwP6t.js +0 -1
  277. package/apps/web/dist/assets/ellipsis-BT172Uvu.js +0 -1
  278. package/apps/web/dist/assets/forms-B4Vg3CPz.js +0 -92
  279. package/apps/web/dist/assets/image-edit-trigger-DGmVtWLC.js +0 -1
  280. package/apps/web/dist/assets/index-C_a21taR.js +0 -50
  281. package/apps/web/dist/assets/index-DM-D_3Sv.css +0 -2
  282. package/apps/web/dist/assets/kbd-tTQGZxaK.js +0 -1
  283. package/apps/web/dist/assets/link-DoIP9KJ6.js +0 -2
  284. package/apps/web/dist/assets/list-order-Bzsrihyr.js +0 -1
  285. package/apps/web/dist/assets/lucide-U995NW8E.js +0 -1
  286. package/apps/web/dist/assets/member-directory-BiCE8DRq.js +0 -1
  287. package/apps/web/dist/assets/popover-BhzZ0U1d.js +0 -1
  288. package/apps/web/dist/assets/project-label-provider-9L7Uts3_.js +0 -1
  289. package/apps/web/dist/assets/project-settings-Q7Q9Reec.js +0 -1
  290. package/apps/web/dist/assets/rotate-ccw-sjyLwLLy.js +0 -1
  291. package/apps/web/dist/assets/settings-B6qD6q1J.js +0 -1
  292. package/apps/web/dist/assets/settings-layout-B-AwUIMu.js +0 -1
  293. package/apps/web/dist/assets/switch-D0nFHj6l.js +0 -1
  294. package/apps/web/dist/assets/table-CYZiwo9g.js +0 -1
  295. package/apps/web/dist/assets/ticket-detail-B-3vbHSB.js +0 -2
  296. package/apps/web/dist/assets/ticket-editor-C3d8TZIT.js +0 -1
  297. package/apps/web/dist/assets/ticket-list-LtGGOnwN.js +0 -5
  298. package/apps/web/dist/assets/ticket-tags-2rdmkcJV.js +0 -1
  299. package/apps/web/dist/assets/upload-DXPRzAaa.js +0 -1
  300. package/apps/web/dist/assets/useNavigate-DFohfUD3.js +0 -1
  301. package/apps/web/dist/assets/workspace-settings-D3tCm1vH.js +0 -1
@@ -1,2931 +0,0 @@
1
- import { a as instanceUpdateSchema, c as s3SettingsSchema, f as account, g as user, h as session, i as brokerSettingsSchema, l as searchAuthentication, m as auth_schema_exports, r as authenticationPolicySchema, u as searchSettingsSchema } from "./config-CbNqtJEj.mjs";
2
- import { and, asc, count, desc, eq, getTableColumns, gt, ilike, inArray, isNotNull, isNull, lt, ne, notInArray, or, sql } from "drizzle-orm";
3
- import { alias, bigint, boolean, customType, index, integer, jsonb, pgTable, primaryKey, text, timestamp, unique, uuid } from "drizzle-orm/pg-core";
4
- import { z } from "zod";
5
- import { decodeJwt } from "jose";
6
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from "node:crypto";
7
- import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
8
- import { icons } from "@iconify-json/lucide";
9
- import { APIError, createAuthMiddleware } from "better-auth/api";
10
- import { betterAuth } from "better-auth";
11
- import { admin, genericOAuth, jwt } from "better-auth/plugins";
12
- import { drizzleAdapter } from "@better-auth/drizzle-adapter";
13
- import { apiKey } from "@better-auth/api-key";
14
- import { expo } from "@better-auth/expo";
15
- import { mcp } from "@better-auth/mcp";
16
- import { cimd } from "@better-auth/cimd";
17
- import { fetchClientMetadataResource } from "@better-auth/cimd/node";
18
- //#region src/adapters/oidc.ts
19
- const identitySchema = z.object({
20
- sub: z.string().min(1),
21
- email: z.email(),
22
- email_verified: z.literal(true),
23
- name: z.string().nullish(),
24
- picture: z.string().nullish()
25
- });
26
- const discoverySchema = z.object({
27
- issuer: z.string(),
28
- userinfo_endpoint: z.url({ protocol: /^https?$/ })
29
- });
30
- function createOIDCUserInfo(config) {
31
- return async (tokens) => {
32
- if (!tokens.idToken) return null;
33
- const claims = decodeJwt(tokens.idToken);
34
- let profile = identitySchema.safeParse(claims);
35
- if (!profile.success) {
36
- if (!config.OIDC_DISCOVERY_URL || !tokens.accessToken) return null;
37
- const discovery = await fetch(config.OIDC_DISCOVERY_URL, { signal: AbortSignal.timeout(1e4) });
38
- if (!discovery.ok) return null;
39
- const metadata = discoverySchema.parse(await discovery.json());
40
- if (metadata.issuer !== claims.iss) return null;
41
- const response = await fetch(metadata.userinfo_endpoint, {
42
- headers: { Authorization: `Bearer ${tokens.accessToken}` },
43
- signal: AbortSignal.timeout(1e4)
44
- });
45
- if (!response.ok) return null;
46
- profile = identitySchema.safeParse(await response.json());
47
- }
48
- if (!profile.success || profile.data.sub !== claims.sub) return null;
49
- return {
50
- ...profile.data,
51
- id: profile.data.sub,
52
- emailVerified: true,
53
- name: profile.data.name ?? "",
54
- image: profile.data.picture ?? void 0
55
- };
56
- };
57
- }
58
- function matchesOIDCBinding(binding, issuer, clientId) {
59
- if (!binding.idToken || !issuer || !clientId) return false;
60
- try {
61
- const claims = decodeJwt(binding.idToken);
62
- const audience = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
63
- return claims.iss === issuer && claims.sub === binding.accountId && audience.includes(clientId);
64
- } catch {
65
- return false;
66
- }
67
- }
68
- //#endregion
69
- //#region ../../packages/contracts/src/index.ts
70
- const states = [
71
- "backlog",
72
- "todo",
73
- "in_progress",
74
- "in_review",
75
- "done",
76
- "cancelled"
77
- ];
78
- const closedStates = ["done", "cancelled"];
79
- const priorities = [
80
- "urgent",
81
- "high",
82
- "medium",
83
- "low",
84
- "none"
85
- ];
86
- const kinds = [
87
- "feature",
88
- "bug",
89
- "chore",
90
- "research",
91
- "documentation"
92
- ];
93
- const difficulties = [
94
- "low",
95
- "med",
96
- "high"
97
- ];
98
- const roles = [
99
- "reader",
100
- "writer",
101
- "admin"
102
- ];
103
- const projectIconColors = [
104
- "default",
105
- "red",
106
- "orange",
107
- "amber",
108
- "yellow",
109
- "lime",
110
- "green",
111
- "emerald",
112
- "teal",
113
- "cyan",
114
- "sky",
115
- "blue",
116
- "indigo",
117
- "violet",
118
- "purple",
119
- "fuchsia",
120
- "pink",
121
- "rose",
122
- "gray"
123
- ];
124
- const hexColor = z.string().regex(/^#[0-9a-f]{6}$/i).overwrite((value) => value.toLowerCase());
125
- const labelColor = z.union([z.enum(projectIconColors), hexColor]);
126
- const isHexColor = (value) => hexColor.safeParse(value).success;
127
- const hasAdminRole = (role) => role?.split(",").includes("admin") ?? false;
128
- const scopes = [
129
- "read",
130
- "write",
131
- "claim",
132
- "admin"
133
- ];
134
- const attachmentLimit = 1e7;
135
- const id = z.string().min(1).max(80);
136
- const timestamp$1 = z.iso.datetime();
137
- const dueDate = z.iso.datetime().refine((value) => new Date(value).getUTCFullYear() >= 1, "Due date must be in year 1 or later");
138
- const version = z.number().int().positive();
139
- const text$1 = z.string().trim().min(1);
140
- const tags = z.array(text$1.max(40)).max(30).overwrite((values) => [...new Set(values)]);
141
- const projectLabelSchema = z.object({
142
- id: z.uuid(),
143
- projectId: z.uuid(),
144
- name: text$1.max(40),
145
- color: labelColor,
146
- version
147
- });
148
- const pagination = {
149
- limit: z.coerce.number().int().min(1).max(100).default(50),
150
- offset: z.coerce.number().int().min(0).default(0)
151
- };
152
- const mutation = { requestId: z.uuid().optional() };
153
- const projectInput = z.object({ projectId: id });
154
- const ticketInput = projectInput.extend({ ticketId: id });
155
- const ticketFilter = z.object({
156
- q: z.string().trim().max(200).optional(),
157
- status: z.enum(states).optional(),
158
- priority: z.enum(priorities).optional(),
159
- kind: z.enum(kinds).optional(),
160
- difficulty: z.enum(difficulties).optional(),
161
- label: z.string().max(40).optional(),
162
- assigneeId: z.union([id, z.literal("none")]).optional(),
163
- parentId: id.optional(),
164
- archived: z.enum([
165
- "include",
166
- "only",
167
- "exclude"
168
- ]).default("exclude"),
169
- due: z.enum([
170
- "overdue",
171
- "today",
172
- "week",
173
- "scheduled",
174
- "none"
175
- ]).optional(),
176
- sort: z.enum([
177
- "priority",
178
- "updated",
179
- "created",
180
- "title",
181
- "labels",
182
- "due",
183
- "number",
184
- "status",
185
- "difficulty",
186
- "kind",
187
- "assignee"
188
- ]).default("priority"),
189
- order: z.enum(["asc", "desc"]).optional()
190
- }).strict();
191
- const defaultOrder = {
192
- priority: "asc",
193
- updated: "desc",
194
- created: "desc",
195
- title: "asc",
196
- labels: "asc",
197
- due: "asc",
198
- number: "asc",
199
- status: "asc",
200
- difficulty: "asc",
201
- kind: "asc",
202
- assignee: "asc"
203
- };
204
- const ticketFields = z.object({
205
- title: text$1.max(500).overwrite((value) => value.replace(/\s+/gu, " ")),
206
- description: z.string().max(1e5),
207
- acceptanceCriteria: z.string().max(5e4),
208
- status: z.enum(states),
209
- priority: z.enum(priorities),
210
- kind: z.enum(kinds),
211
- difficulty: z.enum(difficulties).nullable(),
212
- labels: tags,
213
- scopes: tags,
214
- assigneeId: z.string().nullable(),
215
- parentId: z.uuid().nullable(),
216
- automationReady: z.boolean(),
217
- holdReason: z.string().trim().max(2e3).nullable(),
218
- dueAt: dueDate.nullable(),
219
- archived: z.boolean()
220
- }).strict();
221
- const ticketChanges = ticketFields.partial().refine((value) => Object.keys(value).length > 0, "Provide at least one change");
222
- const workspaceSchema = z.object({
223
- id: z.uuid(),
224
- slug: text$1.max(80),
225
- name: text$1.max(120),
226
- description: z.string(),
227
- version,
228
- createdAt: timestamp$1,
229
- updatedAt: timestamp$1,
230
- role: z.enum(roles).nullable()
231
- });
232
- const memberSchema = z.object({
233
- userId: z.string(),
234
- name: z.string(),
235
- email: z.string(),
236
- role: z.enum(roles),
237
- image: z.string().nullable().optional(),
238
- source: z.enum(["project", "workspace"]).optional(),
239
- inheritedRole: z.enum(roles).nullable().optional()
240
- });
241
- const projectSchema = z.object({
242
- iconName: z.string().nullable().default(null),
243
- iconColor: z.enum(projectIconColors).default("default"),
244
- image: z.string().nullable().default(null),
245
- workspaceId: z.uuid(),
246
- visibility: z.enum(["workspace", "private"]),
247
- id: z.uuid(),
248
- key: z.string(),
249
- name: z.string(),
250
- description: z.string(),
251
- version,
252
- createdAt: timestamp$1,
253
- updatedAt: timestamp$1
254
- });
255
- const ticketSchema = ticketFields.extend({
256
- claimRequired: z.boolean(),
257
- id: z.uuid(),
258
- projectId: z.uuid(),
259
- key: z.string(),
260
- number: z.number().int().positive(),
261
- version,
262
- createdBy: z.string(),
263
- createdAt: timestamp$1,
264
- updatedAt: timestamp$1
265
- });
266
- const snapshotSchema = z.object({
267
- project: projectSchema,
268
- tickets: z.array(ticketSchema),
269
- cursor: z.string().regex(/^\d+$/)
270
- });
271
- z.object({ cursor: z.string().regex(/^\d+$/) });
272
- const readinessSchema = z.object({
273
- ready: z.boolean(),
274
- reasons: z.array(z.object({
275
- code: z.string(),
276
- message: z.string(),
277
- ticketId: z.uuid().optional()
278
- }))
279
- });
280
- const claimSchema = z.object({
281
- id: z.uuid(),
282
- projectId: z.uuid(),
283
- ticketId: z.uuid(),
284
- actorId: z.string(),
285
- workerId: z.string(),
286
- expiresAt: timestamp$1,
287
- createdAt: timestamp$1
288
- });
289
- const relationSchema = z.object({
290
- id: z.uuid(),
291
- projectId: z.uuid(),
292
- sourceId: z.uuid(),
293
- targetId: z.uuid(),
294
- type: z.enum([
295
- "blocks",
296
- "related",
297
- "duplicate",
298
- "causes",
299
- "clones",
300
- "implements",
301
- "reviews"
302
- ]),
303
- sourceKey: z.string().optional(),
304
- sourceTitle: z.string().optional(),
305
- targetKey: z.string().optional(),
306
- targetTitle: z.string().optional()
307
- });
308
- const relationDirections = z.enum(["outgoing", "incoming"]);
309
- const relationLabels = {
310
- blocks: ["Blocks", "Is blocked by"],
311
- related: ["Related to", "Related to"],
312
- duplicate: ["Duplicates", "Is duplicated by"],
313
- causes: ["Causes", "Is caused by"],
314
- clones: ["Clones", "Is cloned by"],
315
- implements: ["Implements", "Is implemented by"],
316
- reviews: ["Reviews", "Is reviewed by"]
317
- };
318
- relationSchema.shape.type.options.flatMap((type) => (type === "related" ? ["outgoing"] : relationDirections.options).map((direction) => ({
319
- value: `${type}:${direction}`,
320
- label: relationLabels[type][direction === "outgoing" ? 0 : 1],
321
- type,
322
- direction
323
- })));
324
- const commentSchema = z.object({
325
- id: z.uuid(),
326
- projectId: z.uuid(),
327
- ticketId: z.uuid(),
328
- actorId: z.string(),
329
- body: z.string(),
330
- version,
331
- createdAt: timestamp$1,
332
- updatedAt: timestamp$1
333
- });
334
- const attachmentSchema = z.object({
335
- id: z.uuid(),
336
- projectId: z.uuid(),
337
- ticketId: z.uuid(),
338
- actorId: z.string(),
339
- name: z.string(),
340
- contentType: z.string(),
341
- size: z.number().int().nonnegative(),
342
- createdAt: timestamp$1
343
- });
344
- const evidenceFields = z.object({
345
- kind: z.enum([
346
- "pull_request",
347
- "test",
348
- "review",
349
- "deployment",
350
- "device",
351
- "other"
352
- ]),
353
- title: text$1.max(200),
354
- url: z.url().regex(/^https?:\/\//i, "Use an HTTP or HTTPS URL"),
355
- commit: z.string().regex(/^[a-f0-9]{40,64}$/i).nullable().default(null),
356
- outcome: z.enum([
357
- "reported",
358
- "pending",
359
- "passed",
360
- "failed"
361
- ]).default("reported"),
362
- note: z.string().max(2e4).default("")
363
- });
364
- const evidenceSchema = evidenceFields.extend({
365
- id: z.uuid(),
366
- projectId: z.uuid(),
367
- ticketId: z.uuid(),
368
- actorId: z.string(),
369
- createdAt: timestamp$1
370
- });
371
- const eventSchema = z.object({
372
- id: z.string().regex(/^\d+$/),
373
- projectId: z.uuid(),
374
- ticketId: z.uuid().nullable(),
375
- actorId: z.string(),
376
- type: z.string(),
377
- data: z.record(z.string(), z.unknown()),
378
- createdAt: timestamp$1
379
- });
380
- z.object({
381
- authentication: authenticationPolicySchema.default(() => authenticationPolicySchema.parse({})),
382
- name: z.string(),
383
- description: z.string().default(""),
384
- version: z.string(),
385
- setupRequired: z.boolean(),
386
- oidc: z.object({
387
- id: z.string(),
388
- name: z.string()
389
- }).nullable(),
390
- push: z.boolean().default(false)
391
- });
392
- z.array(z.object({
393
- id: z.uuid(),
394
- readAt: timestamp$1.nullable(),
395
- event: eventSchema
396
- }));
397
- const notificationPreferencesSchema = z.object({
398
- inApp: z.boolean().default(true),
399
- push: z.boolean().default(false)
400
- });
401
- const notificationPreferencesInput = z.object({
402
- inApp: z.boolean(),
403
- push: z.boolean().optional()
404
- }).strict();
405
- const pushDeviceInput = z.object({ token: z.string().min(1).max(250) }).strict();
406
- z.array(z.object({
407
- id: z.string(),
408
- name: z.string().nullable(),
409
- start: z.string().nullable(),
410
- enabled: z.boolean(),
411
- expiresAt: timestamp$1.nullable(),
412
- createdAt: timestamp$1,
413
- permissions: z.string().nullable(),
414
- metadata: z.string().nullable()
415
- }));
416
- z.object({
417
- id: z.string(),
418
- key: z.string(),
419
- name: z.string().nullable(),
420
- expiresAt: timestamp$1.nullable()
421
- });
422
- const okSchema = z.object({ ok: z.literal(true) });
423
- const page = (item) => z.object({
424
- items: z.array(item),
425
- total: z.number().int().nonnegative()
426
- });
427
- const op = (input, output, method, path, scope, description) => ({
428
- input,
429
- output,
430
- method,
431
- path,
432
- scope,
433
- description
434
- });
435
- const operations = {
436
- list_workspaces: op(z.object({}), z.array(workspaceSchema), "GET", "/workspaces", "read", "List accessible workspaces and the current membership role."),
437
- create_workspace: op(z.object({
438
- ...mutation,
439
- name: text$1.max(120),
440
- slug: z.string().trim().regex(/^[a-z0-9][a-z0-9-]{1,79}$/),
441
- description: z.string().max(5e3).default("")
442
- }).strict(), workspaceSchema, "POST", "/workspaces", "write", "Create a workspace and become its administrator."),
443
- update_workspace: op(z.object({
444
- ...mutation,
445
- workspaceId: z.uuid(),
446
- expectedVersion: version,
447
- name: text$1.max(120),
448
- description: z.string().max(5e3)
449
- }).strict(), workspaceSchema, "PATCH", "/workspaces/{workspaceId}", "admin", "Update workspace details with version checks."),
450
- list_workspace_members: op(z.object({ workspaceId: z.uuid() }), z.array(memberSchema), "GET", "/workspaces/{workspaceId}/members", "read", "List members of a workspace you belong to."),
451
- set_workspace_member: op(z.object({
452
- ...mutation,
453
- workspaceId: z.uuid(),
454
- email: z.email(),
455
- role: z.enum(roles).nullable()
456
- }).strict(), okSchema, "POST", "/workspaces/{workspaceId}/members", "admin", "Manage workspace access; removal also revokes direct project access in this workspace."),
457
- list_projects: op(z.object({ workspaceId: z.uuid().optional() }), z.array(projectSchema.extend({
458
- openTickets: z.number().int().nonnegative(),
459
- overdueTickets: z.number().int().nonnegative()
460
- })), "GET", "/projects", "read", "List projects accessible to the current identity, each with its open and overdue ticket counts."),
461
- create_project: op(z.object({
462
- ...mutation,
463
- workspaceId: z.uuid().optional(),
464
- visibility: z.enum(["workspace", "private"]).default("private"),
465
- key: z.string().trim().regex(/^[A-Z][A-Z0-9]{1,9}$/),
466
- name: text$1.max(120),
467
- description: z.string().max(5e3).default("")
468
- }).strict(), projectSchema, "POST", "/projects", "write", "Create a project with a permanent ticket prefix."),
469
- get_project: op(projectInput, projectSchema, "GET", "/projects/{projectId}", "read", "Read project details, workspace identity, and appearance."),
470
- set_project_icon: op(projectInput.extend({
471
- ...mutation,
472
- expectedVersion: version,
473
- icon: z.union([
474
- z.object({
475
- kind: z.literal("library"),
476
- name: z.string().regex(/^[a-z0-9-]{1,80}$/),
477
- color: z.enum(projectIconColors).default("default")
478
- }).strict(),
479
- z.object({
480
- kind: z.literal("image"),
481
- contentType: z.enum([
482
- "image/png",
483
- "image/jpeg",
484
- "image/webp"
485
- ]),
486
- data: z.string().max(7e5)
487
- }).strict(),
488
- z.null()
489
- ])
490
- }).strict(), projectSchema, "POST", "/projects/{projectId}/icon", "admin", "Choose an installed library icon, upload an image, or reset the project icon."),
491
- update_project: op(projectInput.extend({
492
- ...mutation,
493
- expectedVersion: version,
494
- visibility: z.enum(["workspace", "private"]).optional(),
495
- name: text$1.max(120),
496
- description: z.string().max(5e3)
497
- }).strict(), projectSchema, "PATCH", "/projects/{projectId}", "admin", "Update project details without changing its prefix."),
498
- list_members: op(projectInput, z.array(memberSchema), "GET", "/projects/{projectId}/members", "read", "List project members."),
499
- set_member: op(projectInput.extend({
500
- ...mutation,
501
- email: z.email(),
502
- role: z.enum(roles).nullable()
503
- }).strict(), okSchema, "POST", "/projects/{projectId}/members", "admin", "Add or update an existing user's membership, or remove it with a null role."),
504
- list_tickets: op(projectInput.extend({
505
- ...ticketFilter.shape,
506
- ...pagination
507
- }), page(ticketSchema), "GET", "/projects/{projectId}/tickets", "read", "Search tickets with server-side filters, stable ordering, and pagination."),
508
- list_ticket_labels: op(projectInput.extend({ archived: ticketFilter.shape.archived }), z.array(z.object({
509
- name: z.string(),
510
- count: z.number().int().nonnegative()
511
- })), "GET", "/projects/{projectId}/labels", "read", "List distinct ticket labels and usage counts across the authorized project, independent of ticket pagination."),
512
- list_project_labels: op(projectInput, z.array(projectLabelSchema.extend({ count: z.number().int().nonnegative() })), "GET", "/projects/{projectId}/labels/catalog", "read", "List project label definitions, colors, and ticket counts, including unused labels and archived tickets."),
513
- create_project_label: op(projectInput.extend({
514
- ...mutation,
515
- name: text$1.max(40),
516
- color: labelColor.default("gray")
517
- }).strict(), projectLabelSchema, "POST", "/projects/{projectId}/labels", "admin", "Create a reusable project label. Label names are unique within the project."),
518
- update_project_label: op(projectInput.extend({
519
- ...mutation,
520
- labelId: z.uuid(),
521
- expectedVersion: version,
522
- name: text$1.max(40),
523
- color: labelColor
524
- }).strict(), projectLabelSchema, "PATCH", "/projects/{projectId}/labels/{labelId}", "admin", "Rename or recolor a project label. Renames update tickets and saved-view filters atomically."),
525
- delete_project_label: op(projectInput.extend({
526
- ...mutation,
527
- labelId: z.uuid(),
528
- expectedVersion: version
529
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/labels/{labelId}", "admin", "Delete a project label and remove it from tickets and saved-view filters atomically."),
530
- list_label_colors: op(z.object({}), z.array(z.object({
531
- value: hexColor,
532
- createdAt: timestamp$1
533
- })), "GET", "/label-colors", "read", "List the site-wide custom label colors, newest first. A custom color joins the list when a label is saved with it."),
534
- remove_label_color: op(z.object({
535
- ...mutation,
536
- value: hexColor
537
- }).strict(), okSchema, "POST", "/label-colors/remove", "write", "Remove a custom color from the site-wide palette; instance administrators and the person who added it may do so. Labels already using it keep their color."),
538
- get_ticket: op(ticketInput, z.object({
539
- ticket: ticketSchema,
540
- readiness: readinessSchema,
541
- claim: claimSchema.nullable(),
542
- subscribed: z.boolean(),
543
- canReleaseClaim: z.boolean()
544
- }), "GET", "/projects/{projectId}/tickets/{ticketId}", "read", "Read a ticket by ID or key, with readiness and ownership."),
545
- create_ticket: op(projectInput.extend({
546
- ...mutation,
547
- title: ticketFields.shape.title,
548
- description: z.string().max(1e5).default(""),
549
- acceptanceCriteria: z.string().max(5e4).default(""),
550
- status: z.enum(states).default("todo"),
551
- priority: z.enum(priorities).default("medium"),
552
- kind: z.enum(kinds).default("feature"),
553
- difficulty: z.enum(difficulties).nullable().default(null),
554
- dueAt: dueDate.nullable().default(null),
555
- labels: tags.default([]),
556
- scopes: tags.default([]),
557
- assigneeId: z.string().nullable().default(null),
558
- parentId: z.uuid().nullable().default(null),
559
- automationReady: z.boolean().default(false),
560
- holdReason: z.string().trim().max(2e3).nullable().default(null),
561
- archived: z.boolean().default(false)
562
- }).strict(), ticketSchema, "POST", "/projects/{projectId}/tickets", "write", "Create a ticket. Reuse requestId when retrying this write."),
563
- update_ticket: op(ticketInput.extend({
564
- ...mutation,
565
- expectedVersion: version,
566
- changes: ticketChanges,
567
- claimId: z.uuid().optional()
568
- }).strict(), ticketSchema, "PATCH", "/projects/{projectId}/tickets/{ticketId}", "write", "Update a ticket with optimistic concurrency. Worker completion requires its active claim."),
569
- bulk_update_tickets: op(projectInput.extend({
570
- ...mutation,
571
- tickets: z.array(z.object({
572
- ticketId: id,
573
- expectedVersion: version
574
- })).min(1).max(100).refine((values) => new Set(values.map((v) => v.ticketId)).size === values.length, "Tickets must be unique"),
575
- changes: ticketChanges
576
- }).strict(), z.array(ticketSchema), "POST", "/projects/{projectId}/tickets/bulk", "write", "Apply an atomic bulk edit using each ticket's current version."),
577
- delete_ticket: op(ticketInput.extend({
578
- ...mutation,
579
- expectedVersion: version
580
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/tickets/{ticketId}", "admin", "Delete a ticket and publish a deletion event."),
581
- search_tickets: op(z.object({
582
- q: z.string().trim().min(1).max(200),
583
- workspaceId: z.uuid().optional(),
584
- limit: z.coerce.number().int().min(1).max(50).default(20)
585
- }).strict(), z.object({ items: z.array(z.object({
586
- ticket: ticketSchema,
587
- project: z.object({
588
- id: z.uuid(),
589
- key: z.string(),
590
- name: z.string()
591
- })
592
- })) }), "GET", "/tickets/search", "read", "Search unarchived tickets by key or text across every accessible project, optionally within one workspace."),
593
- list_eligible_tickets: op(projectInput.extend(pagination), page(z.object({
594
- ticket: ticketSchema,
595
- readiness: readinessSchema
596
- })), "GET", "/projects/{projectId}/eligibility", "read", "Inspect eligibility and explicit blocking reasons for open tickets."),
597
- list_relations: op(ticketInput, z.array(relationSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/relations", "read", "List incoming and outgoing ticket relationships."),
598
- add_relation: op(ticketInput.extend({
599
- ...mutation,
600
- targetId: z.uuid(),
601
- type: relationSchema.shape.type,
602
- direction: relationDirections.optional()
603
- }).strict(), relationSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/relations", "write", "Relate tickets while rejecting dependency cycles."),
604
- remove_relation: op(projectInput.extend({
605
- ...mutation,
606
- relationId: z.uuid()
607
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/relations/{relationId}", "write", "Remove a ticket relationship."),
608
- list_comments: op(ticketInput.extend(pagination), page(commentSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/comments", "read", "Read ticket discussion."),
609
- add_comment: op(ticketInput.extend({
610
- ...mutation,
611
- body: text$1.max(5e4)
612
- }).strict(), commentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/comments", "write", "Add a comment with authenticated attribution."),
613
- update_comment: op(projectInput.extend({
614
- ...mutation,
615
- commentId: z.uuid(),
616
- expectedVersion: version,
617
- body: text$1.max(5e4)
618
- }).strict(), commentSchema, "PATCH", "/projects/{projectId}/comments/{commentId}", "write", "Edit your own comment or moderate as a project administrator."),
619
- delete_comment: op(projectInput.extend({
620
- ...mutation,
621
- commentId: z.uuid(),
622
- expectedVersion: version
623
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/comments/{commentId}", "write", "Delete your own comment or moderate as a project administrator."),
624
- list_activity: op(ticketInput.extend(pagination), page(eventSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/activity", "read", "Read durable ticket activity."),
625
- upload_attachment: op(ticketInput.extend({
626
- ...mutation,
627
- name: text$1.max(255).refine((value) => {
628
- for (const character of value) if (character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) return false;
629
- return true;
630
- }, "Use a filename without control characters"),
631
- contentType: z.string().max(120).regex(/^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*$/i).default("application/octet-stream"),
632
- data: z.string().max(Math.ceil(attachmentLimit / 3) * 4)
633
- }).strict(), attachmentSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/attachments", "write", "Upload up to 10 MB of base64-encoded bytes. The original filename is metadata, never a filesystem path."),
634
- list_attachments: op(ticketInput, z.array(attachmentSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/attachments", "read", "List authorized attachment metadata."),
635
- delete_attachment: op(projectInput.extend({
636
- ...mutation,
637
- attachmentId: z.uuid()
638
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/attachments/{attachmentId}", "write", "Remove an attachment."),
639
- list_evidence: op(ticketInput, z.array(evidenceSchema), "GET", "/projects/{projectId}/tickets/{ticketId}/evidence", "read", "Read attributed evidence. Outcomes are reports from the submitting identity."),
640
- add_evidence: op(ticketInput.extend({
641
- ...mutation,
642
- ...evidenceFields.shape,
643
- claimId: z.uuid().optional()
644
- }).strict(), evidenceSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/evidence", "write", "Attach evidence with its commit and provenance."),
645
- claim_ticket: op(ticketInput.extend({
646
- ...mutation,
647
- workerId: text$1.max(200),
648
- expectedVersion: version,
649
- leaseSeconds: z.number().int().min(30).max(3600).default(300)
650
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim", "claim", "Atomically claim ready work. Expired claims require explicit recovery."),
651
- renew_claim: op(ticketInput.extend({
652
- ...mutation,
653
- claimId: z.uuid(),
654
- leaseSeconds: z.number().int().min(30).max(3600).default(300)
655
- }).strict(), claimSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/renew", "claim", "Renew the caller's unexpired claim."),
656
- release_claim: op(ticketInput.extend({
657
- ...mutation,
658
- claimId: z.uuid()
659
- }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/release", "claim", "Release the caller's claim."),
660
- recover_claim: op(ticketInput.extend({
661
- ...mutation,
662
- claimId: z.uuid(),
663
- reason: text$1.min(10).max(2e3),
664
- confirmedStopped: z.literal(true)
665
- }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/claim/recover", "admin", "Recover an expired claim only after confirming the worker stopped."),
666
- set_subscription: op(ticketInput.extend({
667
- ...mutation,
668
- enabled: z.boolean()
669
- }).strict(), okSchema, "POST", "/projects/{projectId}/tickets/{ticketId}/subscription", "read", "Subscribe to ticket updates or unsubscribe."),
670
- list_views: op(projectInput, z.array(z.object({
671
- id: z.uuid(),
672
- name: z.string(),
673
- filters: ticketFilter
674
- })), "GET", "/projects/{projectId}/views", "read", "List your saved project views."),
675
- save_view: op(projectInput.extend({
676
- ...mutation,
677
- name: text$1.max(80),
678
- filters: ticketFilter
679
- }).strict(), z.object({ id: z.uuid() }), "POST", "/projects/{projectId}/views", "read", "Save a personal filtered view."),
680
- delete_view: op(projectInput.extend({
681
- ...mutation,
682
- viewId: z.uuid()
683
- }).strict(), okSchema, "DELETE", "/projects/{projectId}/views/{viewId}", "read", "Delete your own saved view.")
684
- };
685
- function isOperation(name) {
686
- return Object.hasOwn(operations, name);
687
- }
688
- const errorSchema = z.object({ error: z.object({
689
- code: z.string(),
690
- message: z.string(),
691
- details: z.unknown().optional()
692
- }) });
693
- z.object({
694
- status: z.number().int().min(400).max(599),
695
- code: z.string(),
696
- message: z.string()
697
- });
698
- var ApiError = class extends Error {
699
- status;
700
- code;
701
- details;
702
- constructor(status, code, message, details) {
703
- super(message);
704
- this.name = "ApiError";
705
- this.status = status;
706
- this.code = code;
707
- this.details = details;
708
- }
709
- };
710
- function parseOutput(name, value) {
711
- return operations[name].output.parse(value);
712
- }
713
- //#endregion
714
- //#region src/schema.ts
715
- const time = (name) => timestamp(name, { withTimezone: true }).notNull().defaultNow();
716
- const actor = () => text("actor_id").notNull().references(() => user.id);
717
- const workspaces = pgTable("workspaces", {
718
- id: uuid("id").primaryKey().defaultRandom(),
719
- slug: text("slug").notNull().unique(),
720
- name: text("name").notNull(),
721
- description: text("description").notNull().default(""),
722
- version: integer("version").notNull().default(1),
723
- createdAt: time("created_at"),
724
- updatedAt: time("updated_at")
725
- });
726
- const workspaceMembers = pgTable("workspace_members", {
727
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id, { onDelete: "cascade" }),
728
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
729
- role: text("role").notNull()
730
- }, (table) => [primaryKey({ columns: [table.workspaceId, table.userId] }), index("workspace_member_user").on(table.userId)]);
731
- const projects = pgTable("projects", {
732
- iconName: text("icon_name"),
733
- iconColor: text("icon_color").notNull().default("default"),
734
- image: text("image"),
735
- iconObjectId: uuid("icon_object_id").references(() => blobObjects.id),
736
- workspaceId: uuid("workspace_id").notNull().references(() => workspaces.id),
737
- visibility: text("visibility").notNull().default("private"),
738
- id: uuid("id").primaryKey().defaultRandom(),
739
- key: text("key").notNull().unique(),
740
- name: text("name").notNull(),
741
- description: text("description").notNull().default(""),
742
- nextNumber: integer("next_number").notNull().default(1),
743
- version: integer("version").notNull().default(1),
744
- createdAt: time("created_at"),
745
- updatedAt: time("updated_at")
746
- }, (table) => [index("project_workspace").on(table.workspaceId)]);
747
- const members = pgTable("project_members", {
748
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
749
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
750
- role: text("role").notNull()
751
- }, (table) => [primaryKey({ columns: [table.projectId, table.userId] }), index("member_user").on(table.userId)]);
752
- const projectLabels = pgTable("project_labels", {
753
- id: uuid("id").primaryKey().defaultRandom(),
754
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
755
- name: text("name").notNull(),
756
- color: text("color").notNull().default("gray"),
757
- version: integer("version").notNull().default(1)
758
- }, (table) => [unique("project_label_name").on(table.projectId, table.name)]);
759
- const labelColors = pgTable("label_colors", {
760
- value: text("value").primaryKey(),
761
- createdBy: text("created_by").references(() => user.id, { onDelete: "set null" }),
762
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
763
- });
764
- const tickets = pgTable("tickets", {
765
- id: uuid("id").primaryKey().defaultRandom(),
766
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
767
- key: text("key").notNull().unique(),
768
- number: integer("number").notNull(),
769
- title: text("title").notNull(),
770
- description: text("description").notNull().default(""),
771
- acceptanceCriteria: text("acceptance_criteria").notNull().default(""),
772
- status: text("status").notNull().default("todo"),
773
- priority: text("priority").notNull().default("medium"),
774
- kind: text("kind").notNull().default("feature"),
775
- difficulty: text("difficulty"),
776
- labels: text("labels").array().notNull().default([]),
777
- scopes: text("scopes").array().notNull().default([]),
778
- assigneeId: text("assignee_id").references(() => user.id, { onDelete: "set null" }),
779
- parentId: uuid("parent_id"),
780
- automationReady: boolean("automation_ready").notNull().default(false),
781
- claimRequired: boolean("claim_required").notNull().default(false),
782
- holdReason: text("hold_reason"),
783
- dueAt: timestamp("due_at", { withTimezone: true }),
784
- archived: boolean("archived").notNull().default(false),
785
- createdBy: text("created_by").notNull().references(() => user.id),
786
- version: integer("version").notNull().default(1),
787
- createdAt: time("created_at"),
788
- updatedAt: time("updated_at")
789
- }, (table) => [
790
- unique("ticket_project_number").on(table.projectId, table.number),
791
- index("ticket_project_status").on(table.projectId, table.status),
792
- index("ticket_parent").on(table.parentId)
793
- ]);
794
- const relations$1 = pgTable("ticket_relations", {
795
- id: uuid("id").primaryKey().defaultRandom(),
796
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
797
- sourceId: uuid("source_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
798
- targetId: uuid("target_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
799
- type: text("type").notNull()
800
- }, (table) => [unique("ticket_relation_edge").on(table.sourceId, table.targetId, table.type), index("relation_target").on(table.targetId)]);
801
- const comments = pgTable("comments", {
802
- id: uuid("id").primaryKey().defaultRandom(),
803
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
804
- ticketId: uuid("ticket_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
805
- actorId: actor(),
806
- body: text("body").notNull(),
807
- version: integer("version").notNull().default(1),
808
- createdAt: time("created_at"),
809
- updatedAt: time("updated_at")
810
- }, (table) => [index("comment_ticket").on(table.ticketId)]);
811
- const claims = pgTable("claims", {
812
- id: uuid("id").primaryKey().defaultRandom(),
813
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
814
- ticketId: uuid("ticket_id").notNull().unique().references(() => tickets.id, { onDelete: "cascade" }),
815
- actorId: actor(),
816
- ownerId: text("owner_id").notNull(),
817
- workerId: text("worker_id").notNull(),
818
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
819
- createdAt: time("created_at")
820
- });
821
- const events = pgTable("events", {
822
- id: bigint("id", { mode: "bigint" }).primaryKey(),
823
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
824
- ticketId: uuid("ticket_id"),
825
- actorId: actor(),
826
- type: text("type").notNull(),
827
- data: jsonb("data").$type().notNull(),
828
- createdAt: time("created_at")
829
- }, (table) => [index("event_project_cursor").on(table.projectId, table.id), index("event_ticket").on(table.ticketId, table.id)]);
830
- const eventClock = pgTable("event_clock", {
831
- id: integer("id").primaryKey(),
832
- value: bigint("value", { mode: "bigint" }).notNull().default(sql`0`),
833
- floor: bigint("floor", { mode: "bigint" }).notNull().default(sql`0`)
834
- });
835
- const receipts = pgTable("mutation_receipts", {
836
- actorId: actor(),
837
- requestId: uuid("request_id").notNull(),
838
- operation: text("operation").notNull(),
839
- hash: text("hash").notNull(),
840
- result: jsonb("result").notNull(),
841
- createdAt: time("created_at")
842
- }, (table) => [primaryKey({ columns: [table.actorId, table.requestId] })]);
843
- const storageBackends = pgTable("storage_backends", {
844
- id: uuid("id").primaryKey().defaultRandom(),
845
- identity: text("identity").notNull().unique(),
846
- config: text("config").notNull(),
847
- createdAt: time("created_at")
848
- });
849
- const binary = customType({ dataType: () => "bytea" });
850
- const blobObjects = pgTable("blob_objects", {
851
- id: uuid("id").primaryKey(),
852
- backendId: uuid("backend_id").references(() => storageBackends.id),
853
- objectKey: text("object_key"),
854
- data: binary("data"),
855
- contentType: text("content_type").notNull(),
856
- size: integer("size").notNull(),
857
- sha256: text("sha256").notNull(),
858
- createdAt: time("created_at")
859
- }, (table) => [index("blob_objects_created").on(table.createdAt)]);
860
- const profilePhotos = pgTable("profile_photos", {
861
- userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
862
- objectId: uuid("object_id").notNull().references(() => blobObjects.id)
863
- });
864
- const instanceSettings = pgTable("instance_settings", {
865
- id: integer("id").primaryKey().default(1),
866
- identity: uuid("identity").notNull().defaultRandom(),
867
- version: integer("version").notNull().default(1),
868
- name: text("name").notNull().default("Kanvil"),
869
- description: text("description").notNull().default("Ticketing for people and agents"),
870
- storageBackendId: uuid("storage_backend_id").references(() => storageBackends.id),
871
- searchConfig: text("search_config"),
872
- brokerConfig: text("broker_config"),
873
- emailPasswordEnabled: boolean("email_password_enabled").notNull().default(true),
874
- ssoAutoProvision: boolean("sso_auto_provision").notNull().default(false)
875
- });
876
- const deliveryCheckpoints = pgTable("delivery_checkpoints", {
877
- destination: text("destination").primaryKey(),
878
- cursor: bigint("cursor", { mode: "bigint" }).notNull().default(sql`0`)
879
- });
880
- const searchState = pgTable("search_state", {
881
- id: integer("id").primaryKey().default(1),
882
- configHash: text("config_hash").notNull().default(""),
883
- cursor: bigint("cursor", { mode: "bigint" }).notNull().default(sql`0`),
884
- error: text("error")
885
- });
886
- const attachments = pgTable("attachments", {
887
- objectId: uuid("object_id").references(() => blobObjects.id),
888
- id: uuid("id").primaryKey().defaultRandom(),
889
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
890
- ticketId: uuid("ticket_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
891
- actorId: actor(),
892
- name: text("name").notNull(),
893
- contentType: text("content_type").notNull(),
894
- size: integer("size").notNull(),
895
- createdAt: time("created_at")
896
- });
897
- const bytea = customType({ dataType: () => "bytea" });
898
- const attachmentContent = pgTable("attachment_content", {
899
- id: uuid("id").primaryKey().references(() => attachments.id, { onDelete: "cascade" }),
900
- data: bytea("data").notNull()
901
- });
902
- const evidence = pgTable("evidence", {
903
- id: uuid("id").primaryKey().defaultRandom(),
904
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
905
- ticketId: uuid("ticket_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
906
- actorId: actor(),
907
- title: text("title").notNull(),
908
- kind: text("kind").notNull(),
909
- url: text("url").notNull(),
910
- commit: text("commit"),
911
- outcome: text("outcome").notNull(),
912
- note: text("note").notNull(),
913
- createdAt: time("created_at")
914
- });
915
- const subscriptions = pgTable("subscriptions", {
916
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
917
- ticketId: uuid("ticket_id").notNull().references(() => tickets.id, { onDelete: "cascade" }),
918
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" })
919
- }, (table) => [primaryKey({ columns: [table.ticketId, table.userId] })]);
920
- const notificationPreferences = pgTable("notification_preferences", {
921
- userId: text("user_id").primaryKey().references(() => user.id, { onDelete: "cascade" }),
922
- inApp: boolean("in_app").notNull().default(true),
923
- push: boolean("push").notNull().default(false)
924
- });
925
- const notifications = pgTable("notifications", {
926
- id: uuid("id").primaryKey().defaultRandom(),
927
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
928
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
929
- eventId: bigint("event_id", { mode: "bigint" }).notNull(),
930
- readAt: timestamp("read_at", { withTimezone: true }),
931
- inbox: boolean("inbox").notNull().default(true),
932
- createdAt: time("created_at")
933
- });
934
- const pushDevices = pgTable("push_devices", {
935
- id: uuid("id").primaryKey().defaultRandom(),
936
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
937
- sessionId: text("session_id").notNull().references(() => session.id, { onDelete: "cascade" }),
938
- token: text("token").notNull().unique(),
939
- createdAt: time("created_at")
940
- });
941
- const pushDeliveries = pgTable("push_deliveries", {
942
- id: uuid("id").primaryKey().defaultRandom(),
943
- notificationId: uuid("notification_id").notNull().references(() => notifications.id, { onDelete: "cascade" }),
944
- deviceId: uuid("device_id").notNull().references(() => pushDevices.id, { onDelete: "cascade" }),
945
- attempts: integer("attempts").notNull().default(0),
946
- availableAt: time("available_at"),
947
- receiptId: text("receipt_id"),
948
- finishedAt: timestamp("finished_at", { withTimezone: true }),
949
- error: text("error")
950
- }, (table) => [index("push_pending_idx").on(table.finishedAt, table.availableAt), unique().on(table.notificationId, table.deviceId)]);
951
- const views = pgTable("saved_views", {
952
- id: uuid("id").primaryKey().defaultRandom(),
953
- projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
954
- userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
955
- name: text("name").notNull(),
956
- filters: jsonb("filters").notNull()
957
- });
958
- //#endregion
959
- //#region src/adapters/polling-jobs.ts
960
- const createPollingJob = (name, intervalMs, run) => {
961
- let pending, closed = false, again = false;
962
- function trigger() {
963
- if (closed) return Promise.reject(/* @__PURE__ */ new Error("The background job is closed"));
964
- again = true;
965
- return pending ??= (async () => {
966
- let result;
967
- do {
968
- again = false;
969
- result = await run();
970
- if (closed) break;
971
- } while (again);
972
- return result;
973
- })().finally(() => {
974
- pending = void 0;
975
- });
976
- }
977
- const timer = setInterval(() => {
978
- trigger().catch(() => console.error("Background job failed", { job: name }));
979
- }, intervalMs);
980
- timer.unref();
981
- return {
982
- trigger,
983
- async close() {
984
- closed = true;
985
- clearInterval(timer);
986
- await pending?.catch(() => {});
987
- }
988
- };
989
- };
990
- //#endregion
991
- //#region src/adapters/s3.ts
992
- function createS3ObjectStore(configuration) {
993
- const settings = s3SettingsSchema.parse(configuration);
994
- const client = new S3Client({
995
- region: settings.region,
996
- ...settings.endpoint ? { endpoint: settings.endpoint } : {},
997
- forcePathStyle: settings.forcePathStyle,
998
- maxAttempts: 2,
999
- ...settings.accessKeyId && settings.secretAccessKey ? { credentials: {
1000
- accessKeyId: settings.accessKeyId,
1001
- secretAccessKey: settings.secretAccessKey
1002
- } } : {}
1003
- });
1004
- return {
1005
- keyFor: (id) => `${settings.prefix ? settings.prefix.replace(/\/?$/, "/") : ""}${id}`,
1006
- async put(key, bytes, contentType) {
1007
- await client.send(new PutObjectCommand({
1008
- Bucket: settings.bucket,
1009
- Key: key,
1010
- Body: bytes,
1011
- ContentType: contentType,
1012
- ContentMD5: createHash("md5").update(bytes).digest("base64")
1013
- }), { abortSignal: AbortSignal.timeout(15e3) });
1014
- },
1015
- async get(key, maxBytes) {
1016
- const response = await client.send(new GetObjectCommand({
1017
- Bucket: settings.bucket,
1018
- Key: key
1019
- }), { abortSignal: AbortSignal.timeout(15e3) });
1020
- if (!response.Body) throw new Error("Object content missing");
1021
- const reader = response.Body.transformToWebStream().getReader(), chunks = [];
1022
- let size = 0;
1023
- const timeout = setTimeout(() => {
1024
- reader.cancel().catch(() => {});
1025
- }, 15e3);
1026
- try {
1027
- for (;;) {
1028
- const part = await reader.read();
1029
- if (part.done) break;
1030
- size += part.value.byteLength;
1031
- if (size > maxBytes) throw new Error("Object size exceeds metadata");
1032
- chunks.push(part.value);
1033
- }
1034
- return Buffer.concat(chunks);
1035
- } finally {
1036
- clearTimeout(timeout);
1037
- await reader.cancel().catch(() => {});
1038
- }
1039
- },
1040
- async remove(key) {
1041
- await client.send(new DeleteObjectCommand({
1042
- Bucket: settings.bucket,
1043
- Key: key
1044
- }), { abortSignal: AbortSignal.timeout(15e3) });
1045
- },
1046
- close() {
1047
- client.destroy();
1048
- }
1049
- };
1050
- }
1051
- //#endregion
1052
- //#region src/adapters/object-stores.ts
1053
- const providers$1 = { s3: createS3ObjectStore };
1054
- const createObjectStore = (configuration) => {
1055
- const create = providers$1[configuration.provider];
1056
- if (!create) throw new Error("Unsupported object storage provider");
1057
- return create(configuration.config);
1058
- };
1059
- //#endregion
1060
- //#region src/blob-storage.ts
1061
- const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
1062
- function decodeBlobData(value, limit) {
1063
- const bytes = Buffer.from(value, "base64");
1064
- if (bytes.length > limit || bytes.toString("base64") !== value) fail(400, "invalid_attachment", `Provide valid base64 data of at most ${limit} bytes`);
1065
- return bytes;
1066
- }
1067
- async function writeLocalBlob(db, id, bytes, contentType) {
1068
- await db.insert(blobObjects).values({
1069
- id,
1070
- data: bytes,
1071
- contentType,
1072
- size: bytes.length,
1073
- sha256: digest(bytes)
1074
- });
1075
- }
1076
- function createBlobStorage(database, instance, factory = createObjectStore, schedule = createPollingJob) {
1077
- async function transfer(configuration, run) {
1078
- const store = factory(configuration);
1079
- try {
1080
- return await run(store);
1081
- } catch {
1082
- return fail(503, "storage_unavailable", "Object storage is unavailable. Check the instance storage settings.");
1083
- } finally {
1084
- store.close();
1085
- }
1086
- }
1087
- async function put(db, id, bytes, contentType) {
1088
- const active = await instance.storage();
1089
- if (!active) {
1090
- await writeLocalBlob(db, id, bytes, contentType);
1091
- return;
1092
- }
1093
- const backend = await instance.backend(active.id), store = factory(backend.spec);
1094
- try {
1095
- await db.insert(blobObjects).values({
1096
- id,
1097
- data: bytes,
1098
- backendId: backend.id,
1099
- objectKey: store.keyFor(id),
1100
- contentType,
1101
- size: bytes.length,
1102
- sha256: digest(bytes)
1103
- });
1104
- } finally {
1105
- store.close();
1106
- }
1107
- }
1108
- let transferError = null;
1109
- async function flushBatch() {
1110
- try {
1111
- await database.db.transaction(async (tx) => {
1112
- const rows = await tx.select().from(blobObjects).where(and(isNotNull(blobObjects.data), isNotNull(blobObjects.backendId), sql`(EXISTS (SELECT 1 FROM attachments a WHERE a.object_id=${blobObjects.id}) OR EXISTS (SELECT 1 FROM profile_photos p WHERE p.object_id=${blobObjects.id}))`)).orderBy(blobObjects.createdAt).limit(10).for("update", {
1113
- of: blobObjects,
1114
- skipLocked: true
1115
- });
1116
- for (const row of rows) {
1117
- const backend = await instance.backend(requireRow(row.backendId ?? void 0)), key = requireRow(row.objectKey ?? void 0), bytes = requireRow(row.data ?? void 0);
1118
- await transfer(backend.spec, async (store) => {
1119
- await store.put(key, bytes, row.contentType);
1120
- const verified = await store.get(key, row.size);
1121
- if (digest(verified) !== row.sha256) throw new Error("Storage readback failed");
1122
- });
1123
- await tx.update(blobObjects).set({ data: null }).where(eq(blobObjects.id, row.id));
1124
- }
1125
- });
1126
- transferError = null;
1127
- } catch (error) {
1128
- transferError = "File transfer is unavailable. Pending files remain readable from the local database.";
1129
- throw error;
1130
- }
1131
- }
1132
- async function get(id) {
1133
- const row = one(await database.db.select().from(blobObjects).where(eq(blobObjects.id, id)));
1134
- let data = row.data;
1135
- if (!data) {
1136
- if (!row.backendId || !row.objectKey) fail(500, "blob_corrupt", "The stored file has no content location");
1137
- const backend = await instance.backend(row.backendId), key = row.objectKey;
1138
- data = Buffer.from(await transfer(backend.spec, (store) => store.get(key, row.size)));
1139
- }
1140
- if (data.length !== row.size || digest(data) !== row.sha256) fail(500, "blob_corrupt", "The stored file failed its integrity check");
1141
- return {
1142
- ...row,
1143
- data
1144
- };
1145
- }
1146
- async function test(configuration) {
1147
- return transfer(configuration, async (store) => {
1148
- const key = store.keyFor(`connection-test-${crypto.randomUUID()}`), bytes = Buffer.from("Kanvil object storage test");
1149
- try {
1150
- await store.put(key, bytes, "text/plain");
1151
- if (Buffer.from(await store.get(key, bytes.length)).compare(bytes)) throw new Error("Readback failed");
1152
- } finally {
1153
- await store.remove(key);
1154
- }
1155
- });
1156
- }
1157
- async function collectBatch() {
1158
- return database.db.transaction(async (tx) => {
1159
- const rows = await tx.select({ object: blobObjects }).from(blobObjects).leftJoin(attachments, eq(attachments.objectId, blobObjects.id)).leftJoin(profilePhotos, eq(profilePhotos.objectId, blobObjects.id)).leftJoin(projects, eq(projects.iconObjectId, blobObjects.id)).where(and(isNull(attachments.id), isNull(profilePhotos.userId), isNull(projects.id), lt(blobObjects.createdAt, /* @__PURE__ */ new Date(Date.now() - 864e5)))).limit(20).for("update", {
1160
- of: blobObjects,
1161
- skipLocked: true
1162
- });
1163
- for (const { object } of rows) {
1164
- if (object.backendId && object.objectKey) {
1165
- const backend = await instance.backend(object.backendId), key = object.objectKey;
1166
- await transfer(backend.spec, (store) => store.remove(key));
1167
- }
1168
- await tx.delete(blobObjects).where(eq(blobObjects.id, object.id));
1169
- }
1170
- return rows.length;
1171
- });
1172
- }
1173
- const job = schedule("blob-cleanup", 6e4, collectBatch);
1174
- const uploads = schedule("blob-transfer", 1e3, flushBatch);
1175
- return {
1176
- put,
1177
- get,
1178
- test,
1179
- collect: job.trigger,
1180
- sync: uploads.trigger,
1181
- async status() {
1182
- const [row] = await database.db.select({ pending: count() }).from(blobObjects).where(and(isNotNull(blobObjects.data), isNotNull(blobObjects.backendId)));
1183
- const pending = row?.pending ?? 0;
1184
- return {
1185
- pending,
1186
- status: transferError ? "unavailable" : pending ? "transferring" : "ready",
1187
- error: transferError
1188
- };
1189
- },
1190
- async close() {
1191
- await uploads.close();
1192
- await job.close();
1193
- }
1194
- };
1195
- }
1196
- function decodeImage(input) {
1197
- const bytes = decodeBlobData(input.data, 512e3);
1198
- if (!(input.contentType === "image/png" ? bytes.subarray(0, 8).equals(Buffer.from([
1199
- 137,
1200
- 80,
1201
- 78,
1202
- 71,
1203
- 13,
1204
- 10,
1205
- 26,
1206
- 10
1207
- ])) : input.contentType === "image/jpeg" ? bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255 : input.contentType === "image/webp" && bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WEBP")) fail(400, "invalid_image", "Choose a PNG, JPEG, or WebP image");
1208
- return bytes;
1209
- }
1210
- function imageResponse(blob) {
1211
- return new Response(new Uint8Array(blob.data), { headers: {
1212
- "Content-Type": blob.contentType,
1213
- "Cache-Control": "private, max-age=300",
1214
- ETag: `"${blob.sha256}"`
1215
- } });
1216
- }
1217
- //#endregion
1218
- //#region src/adapters/elasticsearch.ts
1219
- function fail$1(_status, _code, message) {
1220
- throw new Error(message);
1221
- }
1222
- function headers(settings) {
1223
- return {
1224
- "Content-Type": "application/json",
1225
- ...searchAuthentication(settings) === "apiKey" && settings.apiKey ? { Authorization: `ApiKey ${settings.apiKey}` } : searchAuthentication(settings) === "basic" && settings.username && settings.password ? { Authorization: `Basic ${Buffer.from(`${settings.username}:${settings.password}`).toString("base64")}` } : {}
1226
- };
1227
- }
1228
- async function call(settings, path, init = {}) {
1229
- const requestHeaders = new Headers(headers(settings));
1230
- new Headers(init.headers).forEach((value, name) => requestHeaders.set(name, value));
1231
- return fetch(`${settings.url.replace(/\/$/, "")}/${path}`, {
1232
- ...init,
1233
- headers: requestHeaders,
1234
- signal: AbortSignal.timeout(15e3)
1235
- });
1236
- }
1237
- async function accepted(response) {
1238
- if (!response.ok) fail$1(503, "search_unavailable", "Elasticsearch is unavailable. Check its address, index, and credentials.");
1239
- return response;
1240
- }
1241
- async function ensure(settings, identity) {
1242
- const mapping = await call(settings, `${settings.index}/_mapping`);
1243
- if (mapping.status === 404) {
1244
- await accepted(await call(settings, settings.index, {
1245
- method: "PUT",
1246
- body: JSON.stringify({ mappings: {
1247
- _meta: { kanvilInstanceId: identity },
1248
- properties: {
1249
- instanceId: { type: "keyword" },
1250
- projectId: { type: "keyword" },
1251
- key: { type: "text" },
1252
- title: { type: "text" },
1253
- description: { type: "text" },
1254
- acceptanceCriteria: { type: "text" },
1255
- labels: { type: "keyword" },
1256
- scopes: { type: "keyword" }
1257
- }
1258
- } })
1259
- }));
1260
- return;
1261
- }
1262
- if (z.record(z.string(), z.object({ mappings: z.object({ _meta: z.object({ kanvilInstanceId: z.string() }).optional() }) })).parse(await (await accepted(mapping)).json())[settings.index]?.mappings["_meta"]?.kanvilInstanceId !== identity) fail$1(409, "index_not_owned", "Choose a dedicated, unused Kanvil index name.");
1263
- }
1264
- async function bulk(settings, identity, rows, deleted = []) {
1265
- const lines = rows.flatMap((row) => [JSON.stringify({ index: { _id: row.id } }), JSON.stringify({
1266
- id: row.id,
1267
- instanceId: identity,
1268
- projectId: row.projectId,
1269
- key: row.key,
1270
- title: row.title,
1271
- description: row.description,
1272
- acceptanceCriteria: row.acceptanceCriteria,
1273
- labels: row.labels,
1274
- scopes: row.scopes
1275
- })]);
1276
- for (const id of deleted) lines.push(JSON.stringify({ delete: { _id: id } }));
1277
- if (!lines.length) return;
1278
- const response = await accepted(await call(settings, `${settings.index}/_bulk?refresh=wait_for`, {
1279
- method: "POST",
1280
- headers: { "Content-Type": "application/x-ndjson" },
1281
- body: lines.join("\n") + "\n"
1282
- }));
1283
- if (z.object({ errors: z.boolean() }).parse(await response.json()).errors) fail$1(503, "search_unavailable", "Elasticsearch rejected an indexing operation");
1284
- }
1285
- async function testSearchConnection(settings, identity) {
1286
- const id = crypto.randomUUID(), token = crypto.randomUUID();
1287
- try {
1288
- await accepted(await call(settings, `${settings.index}/_doc/${id}?refresh=wait_for`, {
1289
- method: "PUT",
1290
- body: JSON.stringify({
1291
- instanceId: identity,
1292
- projectId: crypto.randomUUID(),
1293
- title: token
1294
- })
1295
- }));
1296
- if (z.object({ _source: z.object({ title: z.string() }) }).parse(await (await accepted(await call(settings, `${settings.index}/_doc/${id}`))).json())["_source"].title !== token) throw new Error("Search readback failed");
1297
- } finally {
1298
- await accepted(await call(settings, `${settings.index}/_doc/${id}`, { method: "DELETE" }));
1299
- }
1300
- }
1301
- function createElasticsearchIndex(configuration) {
1302
- const settings = searchSettingsSchema.parse(configuration);
1303
- return {
1304
- initialize: (identity) => ensure(settings, identity),
1305
- async reset(identity) {
1306
- await accepted(await call(settings, `${settings.index}/_delete_by_query?refresh=true`, {
1307
- method: "POST",
1308
- body: JSON.stringify({ query: { term: { instanceId: identity } } })
1309
- }));
1310
- },
1311
- write: (identity, rows, deleted) => bulk(settings, identity, rows, deleted),
1312
- probe: (identity) => testSearchConnection(settings, identity),
1313
- async find(identity, projectIds, query) {
1314
- let scrollId;
1315
- try {
1316
- const found = [];
1317
- let response = await accepted(await call(settings, `${settings.index}/_search?scroll=1m`, {
1318
- method: "POST",
1319
- body: JSON.stringify({
1320
- size: 1e3,
1321
- _source: false,
1322
- sort: ["_doc"],
1323
- query: { bool: {
1324
- filter: [{ terms: { projectId: [...projectIds] } }, { term: { instanceId: identity } }],
1325
- must: [{ multi_match: {
1326
- query,
1327
- fields: [
1328
- "title^3",
1329
- "key^3",
1330
- "description",
1331
- "acceptanceCriteria",
1332
- "labels",
1333
- "scopes"
1334
- ],
1335
- operator: "and",
1336
- fuzziness: "AUTO"
1337
- } }]
1338
- } }
1339
- })
1340
- }));
1341
- for (;;) {
1342
- const result = z.object({
1343
- _scroll_id: z.string().optional(),
1344
- hits: z.object({ hits: z.array(z.object({ _id: z.uuid() })) })
1345
- }).parse(await response.json());
1346
- scrollId = result["_scroll_id"] ?? scrollId;
1347
- found.push(...result.hits.hits.map((hit) => hit["_id"]));
1348
- if (found.length > 1e4) throw new Error("External search unavailable");
1349
- if (!result.hits.hits.length || !scrollId) break;
1350
- response = await accepted(await call(settings, "_search/scroll", {
1351
- method: "POST",
1352
- body: JSON.stringify({
1353
- scroll: "1m",
1354
- scroll_id: scrollId
1355
- })
1356
- }));
1357
- }
1358
- return found;
1359
- } catch {
1360
- throw new Error("External search unavailable");
1361
- } finally {
1362
- if (scrollId) await call(settings, "_search/scroll", {
1363
- method: "DELETE",
1364
- body: JSON.stringify({ scroll_id: [scrollId] })
1365
- }).catch(() => {});
1366
- }
1367
- }
1368
- };
1369
- }
1370
- //#endregion
1371
- //#region src/adapters/ticket-indexes.ts
1372
- const providers = { elasticsearch: createElasticsearchIndex };
1373
- const createTicketIndex = (configuration) => {
1374
- const create = providers[configuration.provider];
1375
- if (!create) throw new Error("Unsupported ticket search provider");
1376
- return create(configuration.config);
1377
- };
1378
- //#endregion
1379
- //#region src/search.ts
1380
- function postgresSearch(query) {
1381
- const value = `%${query.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
1382
- return or(ilike(tickets.title, value), ilike(tickets.description, value), ilike(tickets.key, value), ilike(tickets.acceptanceCriteria, value), sql`array_to_string(${tickets.labels}, ' ') ILIKE ${value}`, sql`array_to_string(${tickets.scopes}, ' ') ILIKE ${value}`);
1383
- }
1384
- function createTicketSearch(database, instance, factory = createTicketIndex, schedule = createPollingJob) {
1385
- const fingerprint = (settings) => createHmac("sha256", instance.config.BETTER_AUTH_SECRET).update(JSON.stringify(settings)).digest("hex");
1386
- async function work() {
1387
- const settings = await instance.search();
1388
- if (!settings) return;
1389
- const index = factory({
1390
- provider: "elasticsearch",
1391
- config: settings
1392
- });
1393
- try {
1394
- await database.db.transaction(async (tx) => {
1395
- if (!(await tx.execute(sql`SELECT pg_try_advisory_xact_lock(7361084) AS acquired`)).rows[0]?.acquired) return;
1396
- const root = await instance.read(tx), hash = fingerprint(settings), clock = one(await tx.select().from(eventClock).where(eq(eventClock.id, 1)));
1397
- await tx.insert(searchState).values({ id: 1 }).onConflictDoNothing();
1398
- const state = one(await tx.select().from(searchState).where(eq(searchState.id, 1)));
1399
- if (state.configHash === hash && state.cursor === clock.value && !state.error) return;
1400
- await index.initialize(root.identity);
1401
- let cursor = clock.value;
1402
- if (state.configHash !== hash || state.cursor < clock.floor || state.cursor > clock.value) {
1403
- await index.reset(root.identity);
1404
- const all = await tx.select().from(tickets);
1405
- for (let offset = 0; offset < all.length; offset += 500) await index.write(root.identity, all.slice(offset, offset + 500));
1406
- } else {
1407
- const pending = await tx.select().from(events).where(sql`${events.id}>${state.cursor}`).orderBy(events.id).limit(500);
1408
- const ids = [...new Set(pending.flatMap((event) => event.ticketId ? [event.ticketId] : []))];
1409
- const rows = ids.length ? await tx.select().from(tickets).where(inArray(tickets.id, ids)) : [];
1410
- await index.write(root.identity, rows, ids.filter((id) => !rows.some((row) => row.id === id)));
1411
- cursor = pending.at(-1)?.id ?? clock.value;
1412
- }
1413
- await tx.update(searchState).set({
1414
- configHash: hash,
1415
- cursor,
1416
- error: null
1417
- }).where(eq(searchState.id, 1));
1418
- }, { isolationLevel: "repeatable read" });
1419
- } catch {
1420
- await database.db.insert(searchState).values({
1421
- id: 1,
1422
- error: "External search is unavailable. PostgreSQL search remains active."
1423
- }).onConflictDoUpdate({
1424
- target: searchState.id,
1425
- set: { error: "External search is unavailable. PostgreSQL search remains active." }
1426
- });
1427
- }
1428
- }
1429
- async function where(_db, projectIds, query) {
1430
- const fallback = () => postgresSearch(query), settings = await instance.search();
1431
- if (!settings) return fallback();
1432
- const state = (await database.db.select().from(searchState).where(eq(searchState.id, 1)))[0], clock = (await database.db.select().from(eventClock).where(eq(eventClock.id, 1)))[0];
1433
- if (!state || !clock || state.error || state.configHash !== fingerprint(settings) || state.cursor !== clock.value) return fallback();
1434
- const root = await instance.read();
1435
- try {
1436
- const ids = await factory({
1437
- provider: "elasticsearch",
1438
- config: settings
1439
- }).find(root.identity, projectIds, query);
1440
- return ids.length ? inArray(tickets.id, ids) : sql`false`;
1441
- } catch {
1442
- return fallback();
1443
- }
1444
- }
1445
- async function test(settings) {
1446
- try {
1447
- const index = factory({
1448
- provider: "elasticsearch",
1449
- config: settings
1450
- });
1451
- const root = await instance.read();
1452
- await index.initialize(root.identity);
1453
- await index.probe(root.identity);
1454
- } catch {
1455
- fail(503, "search_unavailable", "External search is unavailable. Check the address, index permissions, and credentials.");
1456
- }
1457
- }
1458
- async function rebuild() {
1459
- await database.db.insert(searchState).values({
1460
- id: 1,
1461
- configHash: ""
1462
- }).onConflictDoUpdate({
1463
- target: searchState.id,
1464
- set: {
1465
- configHash: "",
1466
- error: null
1467
- }
1468
- });
1469
- sync().catch(() => {});
1470
- }
1471
- const job = schedule("ticket-index", 1e3, work);
1472
- const sync = job.trigger;
1473
- return {
1474
- where,
1475
- sync,
1476
- test,
1477
- rebuild,
1478
- async status() {
1479
- const settings = await instance.search(), state = (await database.db.select().from(searchState).where(eq(searchState.id, 1)))[0], clock = (await database.db.select().from(eventClock).where(eq(eventClock.id, 1)))[0];
1480
- return {
1481
- status: !settings ? "builtin" : state?.error ? "unavailable" : state?.configHash === fingerprint(settings) && state?.cursor === clock?.value ? "ready" : "indexing",
1482
- cursor: state?.cursor.toString() ?? "0",
1483
- error: state?.error ?? null
1484
- };
1485
- },
1486
- close: job.close
1487
- };
1488
- }
1489
- //#endregion
1490
- //#region src/project-access.ts
1491
- function projectRoleSQL(userId, projectId) {
1492
- return sql`(SELECT CASE WHEN wm.role = 'admin' THEN 'admin'
1493
- WHEN pm.role IS NOT NULL THEN pm.role
1494
- WHEN p.visibility = 'workspace' THEN wm.role ELSE NULL END
1495
- FROM projects p
1496
- LEFT JOIN project_members pm ON pm.project_id = p.id AND pm.user_id = ${userId}
1497
- LEFT JOIN workspace_members wm ON wm.workspace_id = p.workspace_id AND wm.user_id = ${userId}
1498
- WHERE p.id = ${projectId})`;
1499
- }
1500
- async function projectRole(db, actor, row) {
1501
- if (actor.admin) return "admin";
1502
- const result = (await db.select({ role: projectRoleSQL(actor.id, row.id) }).from(projects).where(eq(projects.id, row.id)))[0];
1503
- return z.enum(roles).safeParse(result?.role).data;
1504
- }
1505
- async function accessibleProjectRows(db, actor, workspaceId) {
1506
- return db.select().from(projects).where(and(workspaceId ? eq(projects.workspaceId, workspaceId) : void 0, actor.admin ? void 0 : isNotNull(projectRoleSQL(actor.id, projects.id)), actor.projectIds ? actor.projectIds.length ? inArray(projects.id, [...actor.projectIds]) : sql`false` : void 0)).orderBy(projects.name);
1507
- }
1508
- //#endregion
1509
- //#region src/workspace-operations.ts
1510
- async function requireWorkspace(ctx, workspaceId, admin = false) {
1511
- if (ctx.actor.projectIds) fail(403, "workspace_scope_required", "Use an account with workspace access for this action");
1512
- const row = one(await ctx.db.select().from(workspaces).where(eq(workspaces.id, workspaceId)));
1513
- const membership = (await ctx.db.select().from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, workspaceId), eq(workspaceMembers.userId, ctx.actor.id))))[0];
1514
- if (!ctx.actor.admin && (!membership || admin && membership.role !== "admin")) fail(admin ? 403 : 404, "workspace_access_required", admin ? "Workspace administrator access is required" : "Workspace was not found");
1515
- return workspaceSchema.parse({
1516
- ...row,
1517
- createdAt: row.createdAt.toISOString(),
1518
- updatedAt: row.updatedAt.toISOString(),
1519
- role: ctx.actor.admin ? "admin" : membership.role
1520
- });
1521
- }
1522
- async function publishWorkspace(ctx, workspaceId, type, data) {
1523
- await publishMany(ctx, (await ctx.db.select({ id: projects.id }).from(projects).where(eq(projects.workspaceId, workspaceId))).map((row) => ({
1524
- type,
1525
- ticketId: null,
1526
- projectId: row.id,
1527
- data: {
1528
- workspaceId,
1529
- ...data
1530
- }
1531
- })));
1532
- }
1533
- const workspaceHandlers = {
1534
- async list_workspaces(ctx) {
1535
- const allowed = await accessibleProjectRows(ctx.db, ctx.actor);
1536
- const memberships = await ctx.db.select().from(workspaceMembers).where(eq(workspaceMembers.userId, ctx.actor.id));
1537
- const ids = [.../* @__PURE__ */ new Set([...allowed.map((row) => row.workspaceId), ...!ctx.actor.projectIds ? memberships.map((row) => row.workspaceId) : []])];
1538
- return (await ctx.db.select().from(workspaces).where(ctx.actor.admin && !ctx.actor.projectIds ? void 0 : ids.length ? inArray(workspaces.id, ids) : sql`false`).orderBy(workspaces.name)).map((row) => ({
1539
- ...row,
1540
- role: ctx.actor.projectIds ? null : ctx.actor.admin ? "admin" : memberships.find((m) => m.workspaceId === row.id)?.role ?? null
1541
- }));
1542
- },
1543
- async create_workspace(ctx, input) {
1544
- if (ctx.actor.projectIds) fail(403, "workspace_scope_required", "Project-bound credentials cannot create workspaces");
1545
- if ((await ctx.db.select().from(workspaces).where(eq(workspaces.slug, input.slug))).length) fail(409, "duplicate_workspace", "This workspace URL key is already in use");
1546
- const row = one(await ctx.db.insert(workspaces).values({
1547
- name: input.name,
1548
- slug: input.slug,
1549
- description: input.description
1550
- }).returning());
1551
- await ctx.db.insert(workspaceMembers).values({
1552
- workspaceId: row.id,
1553
- userId: ctx.actor.id,
1554
- role: "admin"
1555
- });
1556
- return {
1557
- ...row,
1558
- role: "admin"
1559
- };
1560
- },
1561
- async update_workspace(ctx, input) {
1562
- const current = workspace(ctx);
1563
- assertVersion(current, input.expectedVersion, "workspace", "saving");
1564
- const row = one(await ctx.db.update(workspaces).set({
1565
- name: input.name,
1566
- description: input.description,
1567
- version: current.version + 1,
1568
- updatedAt: /* @__PURE__ */ new Date()
1569
- }).where(eq(workspaces.id, current.id)).returning());
1570
- await publishWorkspace(ctx, row.id, "workspace.updated", { version: row.version });
1571
- return {
1572
- ...row,
1573
- role: current.role
1574
- };
1575
- },
1576
- async list_workspace_members(ctx, input) {
1577
- workspace(ctx);
1578
- return ctx.db.select({
1579
- userId: user.id,
1580
- name: user.name,
1581
- email: user.email,
1582
- image: user.image,
1583
- role: workspaceMembers.role
1584
- }).from(workspaceMembers).innerJoin(user, eq(user.id, workspaceMembers.userId)).where(eq(workspaceMembers.workspaceId, input.workspaceId)).orderBy(user.name);
1585
- },
1586
- async set_workspace_member(ctx, input) {
1587
- workspace(ctx);
1588
- const person = await accountByEmail(ctx.db, input.email);
1589
- if ((await ctx.db.select().from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, input.workspaceId), eq(workspaceMembers.userId, person.id))))[0]?.role === "admin" && input.role !== "admin") {
1590
- if ((await ctx.db.select().from(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, input.workspaceId), eq(workspaceMembers.role, "admin")))).length <= 1) fail(409, "last_administrator", "Keep at least one workspace administrator");
1591
- }
1592
- if (input.role) await ctx.db.insert(workspaceMembers).values({
1593
- workspaceId: input.workspaceId,
1594
- userId: person.id,
1595
- role: input.role
1596
- }).onConflictDoUpdate({
1597
- target: [workspaceMembers.workspaceId, workspaceMembers.userId],
1598
- set: { role: input.role }
1599
- });
1600
- else {
1601
- await ctx.db.delete(workspaceMembers).where(and(eq(workspaceMembers.workspaceId, input.workspaceId), eq(workspaceMembers.userId, person.id)));
1602
- await ctx.db.delete(members).where(and(eq(members.userId, person.id), inArray(members.projectId, ctx.db.select({ id: projects.id }).from(projects).where(eq(projects.workspaceId, input.workspaceId)))));
1603
- }
1604
- await publishWorkspace(ctx, input.workspaceId, "membership.updated", {
1605
- userId: person.id,
1606
- role: input.role
1607
- });
1608
- return { ok: true };
1609
- }
1610
- };
1611
- //#endregion
1612
- //#region src/ticket-operations.ts
1613
- const openTicket = notInArray(tickets.status, [...closedStates]);
1614
- const overdueTicket = sql`${tickets.dueAt} < now()`;
1615
- const rank = (column, values) => sql`array_position(ARRAY[${sql.join(values.map((value) => sql`${value}`), sql`, `)}]::text[], ${column}::text)`;
1616
- const labelOrder = (column) => [asc(sql`lower(${column}) COLLATE "C"`), asc(sql`${column} COLLATE "C"`)];
1617
- const dueColumn = (dueAt) => dueAt === void 0 ? {} : { dueAt: dueAt ? new Date(dueAt) : null };
1618
- async function readinessMany(ctx, selected) {
1619
- const ids = selected.map((ticket) => ticket.id);
1620
- const result = /* @__PURE__ */ new Map();
1621
- if (!ids.length) return result;
1622
- const blockers = await ctx.db.select({
1623
- targetId: relations$1.targetId,
1624
- ticket: tickets
1625
- }).from(relations$1).innerJoin(tickets, eq(tickets.id, relations$1.sourceId)).where(and(inArray(relations$1.targetId, ids), eq(relations$1.type, "blocks")));
1626
- const children = await ctx.db.select().from(tickets).where(inArray(tickets.parentId, ids));
1627
- const ownership = await ctx.db.select().from(claims).where(inArray(claims.ticketId, ids));
1628
- const duplicates = await ctx.db.select({ sourceId: relations$1.sourceId }).from(relations$1).where(and(inArray(relations$1.sourceId, ids), eq(relations$1.type, "duplicate")));
1629
- for (const ticket of selected) {
1630
- const reasons = [];
1631
- if (ticket.status !== "todo" || ticket.archived) reasons.push({
1632
- code: "not_open",
1633
- message: "Ticket must be in To do and unarchived"
1634
- });
1635
- if (!ticket.automationReady) reasons.push({
1636
- code: "not_enabled",
1637
- message: "Agent work has not been enabled"
1638
- });
1639
- if (ticket.holdReason) reasons.push({
1640
- code: "hold",
1641
- message: ticket.holdReason
1642
- });
1643
- if (!ticket.description.trim()) reasons.push({
1644
- code: "missing_description",
1645
- message: "Add a description"
1646
- });
1647
- if (!ticket.acceptanceCriteria.trim()) reasons.push({
1648
- code: "missing_acceptance_criteria",
1649
- message: "Add acceptance criteria"
1650
- });
1651
- if (!ticket.difficulty) reasons.push({
1652
- code: "missing_difficulty",
1653
- message: "Choose a difficulty"
1654
- });
1655
- const dependencies = [...blockers.filter((row) => row.targetId === ticket.id).map((row) => row.ticket), ...children.filter((row) => row.parentId === ticket.id)];
1656
- const seen = /* @__PURE__ */ new Set();
1657
- for (const dependency of dependencies) if (!complete(dependency.status) && !seen.has(dependency.id)) {
1658
- seen.add(dependency.id);
1659
- reasons.push({
1660
- code: "dependency",
1661
- message: `${dependency.key} is unfinished`,
1662
- ticketId: dependency.id
1663
- });
1664
- }
1665
- if (duplicates.some((row) => row.sourceId === ticket.id)) reasons.push({
1666
- code: "duplicate",
1667
- message: "This ticket is marked as a duplicate"
1668
- });
1669
- const claim = ownership.find((row) => row.ticketId === ticket.id);
1670
- if (claim) {
1671
- const expired = claim.expiresAt <= /* @__PURE__ */ new Date();
1672
- reasons.push({
1673
- code: expired ? "claim_recovery_required" : "claimed",
1674
- message: expired ? "Confirm the previous worker stopped before recovering ownership" : `Claimed by ${claim.workerId}`
1675
- });
1676
- }
1677
- result.set(ticket.id, {
1678
- ready: reasons.length === 0,
1679
- reasons
1680
- });
1681
- }
1682
- return result;
1683
- }
1684
- async function readiness(ctx, ticket) {
1685
- return requireRow((await readinessMany(ctx, [ticket])).get(ticket.id));
1686
- }
1687
- async function validateReferences(ctx, ticketId, changes) {
1688
- if (changes.labels?.length) await ctx.db.insert(projectLabels).values(changes.labels.map((name) => ({
1689
- projectId: project(ctx).id,
1690
- name
1691
- }))).onConflictDoNothing();
1692
- if (changes.assigneeId && !(await ctx.db.select().from(members).where(and(eq(members.projectId, project(ctx).id), eq(members.userId, changes.assigneeId))).limit(1)).length) fail(400, "invalid_assignee", "Assign a project member");
1693
- if (changes.parentId) {
1694
- await getTicket(ctx, changes.parentId);
1695
- await assertNoCycle(ctx, ticketId, changes.parentId);
1696
- }
1697
- }
1698
- async function assertNoCycle(ctx, sourceId, targetId) {
1699
- if (sourceId === targetId) fail(409, "dependency_cycle", "A ticket cannot depend on itself");
1700
- const all = await ctx.db.select({
1701
- id: tickets.id,
1702
- parentId: tickets.parentId
1703
- }).from(tickets).where(eq(tickets.projectId, project(ctx).id));
1704
- const links = await ctx.db.select().from(relations$1).where(and(eq(relations$1.projectId, project(ctx).id), eq(relations$1.type, "blocks")));
1705
- const edges = /* @__PURE__ */ new Map();
1706
- const add = (from, to) => edges.set(from, [...edges.get(from) ?? [], to]);
1707
- for (const row of all) if (row.parentId) add(row.id, row.parentId);
1708
- for (const row of links) add(row.sourceId, row.targetId);
1709
- const pending = [targetId], visited = /* @__PURE__ */ new Set();
1710
- while (pending.length) {
1711
- const node = pending.pop();
1712
- if (!node || visited.has(node)) continue;
1713
- if (node === sourceId) fail(409, "dependency_cycle", "This relationship would create a dependency cycle");
1714
- visited.add(node);
1715
- pending.push(...edges.get(node) ?? []);
1716
- }
1717
- }
1718
- async function updateTicketRow(ctx, input) {
1719
- const current = await getTicket(ctx, input.ticketId);
1720
- assertVersion(current, input.expectedVersion, "ticket");
1721
- await assertWorkerClaim(ctx, current, input.claimId);
1722
- await validateReferences(ctx, current.id, input.changes);
1723
- const { dueAt, ...changes } = input.changes;
1724
- const row = one(await ctx.db.update(tickets).set({
1725
- ...changes,
1726
- ...dueColumn(dueAt),
1727
- version: current.version + 1,
1728
- updatedAt: /* @__PURE__ */ new Date()
1729
- }).where(eq(tickets.id, current.id)).returning());
1730
- await subscribe(ctx, row.projectId, row.id, [row.assigneeId]);
1731
- return {
1732
- row,
1733
- event: {
1734
- type: "ticket.updated",
1735
- ticketId: row.id,
1736
- data: {
1737
- version: row.version,
1738
- changedFields: Object.keys(input.changes)
1739
- }
1740
- }
1741
- };
1742
- }
1743
- async function updateTicket(ctx, input) {
1744
- const { row, event } = await updateTicketRow(ctx, input);
1745
- await publish(ctx, event.type, event.ticketId, event.data);
1746
- return row;
1747
- }
1748
- function labelUsage(ctx, archived) {
1749
- const labels = ctx.db.select({ name: sql`unnest(${tickets.labels})`.as("name") }).from(tickets).where(and(eq(tickets.projectId, project(ctx).id), archived === "include" ? void 0 : eq(tickets.archived, archived === "only"))).as("ticket_labels");
1750
- return ctx.db.select({
1751
- name: labels.name,
1752
- count: count().as("count")
1753
- }).from(labels).groupBy(labels.name);
1754
- }
1755
- const dueWithin = (interval) => and(sql`${tickets.dueAt} >= now()`, sql`${tickets.dueAt} < now() + ${interval}::interval`);
1756
- function dueWhere(due) {
1757
- switch (due) {
1758
- case "overdue": return and(overdueTicket, openTicket);
1759
- case "today": return and(dueWithin("1 day"), openTicket);
1760
- case "week": return and(dueWithin("7 days"), openTicket);
1761
- case "scheduled": return isNotNull(tickets.dueAt);
1762
- case "none": return isNull(tickets.dueAt);
1763
- default: return;
1764
- }
1765
- }
1766
- async function pageOf(ctx, where, order, input) {
1767
- const rows = await ctx.db.select({
1768
- ...getTableColumns(tickets),
1769
- total: sql`count(*) over()`.mapWith(Number)
1770
- }).from(tickets).where(where).orderBy(...order).limit(input.limit).offset(input.offset);
1771
- const total = rows[0]?.total ?? (input.offset ? one(await ctx.db.select({ count: count() }).from(tickets).where(where)).count : 0);
1772
- return {
1773
- items: rows.map(({ total: _total, ...row }) => row),
1774
- total
1775
- };
1776
- }
1777
- const ticketHandlers = {
1778
- async list_ticket_labels(ctx, input) {
1779
- const labels = labelUsage(ctx, input.archived).as("usage");
1780
- return ctx.db.select().from(labels).orderBy(...labelOrder(labels.name));
1781
- },
1782
- async list_tickets(ctx, input) {
1783
- const parent = input.parentId ? await getTicket(ctx, input.parentId) : void 0;
1784
- const search = input.q ? await ctx.search.where(ctx.db, [project(ctx).id], input.q) : void 0;
1785
- const where = and(eq(tickets.projectId, project(ctx).id), parent ? eq(tickets.parentId, parent.id) : void 0, input.status ? eq(tickets.status, input.status) : void 0, input.priority ? eq(tickets.priority, input.priority) : void 0, input.kind ? eq(tickets.kind, input.kind) : void 0, input.difficulty ? eq(tickets.difficulty, input.difficulty) : void 0, input.assigneeId === "none" ? isNull(tickets.assigneeId) : input.assigneeId ? eq(tickets.assigneeId, input.assigneeId) : void 0, input.label ? sql`${input.label} = ANY(${tickets.labels})` : void 0, input.archived === "include" ? void 0 : eq(tickets.archived, input.archived === "only"), dueWhere(input.due), search);
1786
- const direction = input.order ?? defaultOrder[input.sort];
1787
- const by = (column, flip = false) => direction === "asc" !== flip ? sql`${column} ASC NULLS LAST` : sql`${column} DESC NULLS LAST`;
1788
- return pageOf(ctx, where, [{
1789
- priority: by(rank(tickets.priority, priorities)),
1790
- updated: by(tickets.updatedAt),
1791
- created: by(tickets.createdAt),
1792
- title: by(sql`lower(${tickets.title})`),
1793
- labels: by(sql`(SELECT min(lower(label) COLLATE "C") FROM unnest(${tickets.labels}) AS label)`),
1794
- due: by(tickets.dueAt),
1795
- number: by(tickets.number),
1796
- status: by(rank(tickets.status, states)),
1797
- difficulty: by(rank(tickets.difficulty, difficulties)),
1798
- kind: by(tickets.kind),
1799
- assignee: by(sql`(SELECT lower(name) FROM "user" WHERE id = ${tickets.assigneeId})`)
1800
- }[input.sort], asc(tickets.number)], input);
1801
- },
1802
- async get_ticket(ctx, input) {
1803
- const ticket = await getTicket(ctx, input.ticketId);
1804
- const claim = (await ctx.db.select().from(claims).where(eq(claims.ticketId, ticket.id)))[0] ?? null;
1805
- return {
1806
- ticket,
1807
- canReleaseClaim: claim ? ownsLiveClaim(ctx, claim) : false,
1808
- subscribed: (await ctx.db.select({ id: subscriptions.ticketId }).from(subscriptions).where(and(eq(subscriptions.ticketId, ticket.id), eq(subscriptions.userId, ctx.actor.id)))).length > 0,
1809
- readiness: await readiness(ctx, ticket),
1810
- claim
1811
- };
1812
- },
1813
- async create_ticket(ctx, input) {
1814
- const id = crypto.randomUUID();
1815
- await validateReferences(ctx, id, input);
1816
- const counter = one(await ctx.db.update(projects).set({ nextNumber: sql`${projects.nextNumber} + 1` }).where(eq(projects.id, project(ctx).id)).returning());
1817
- const { projectId: _projectId, requestId: _requestId, dueAt, ...fields } = input;
1818
- const row = one(await ctx.db.insert(tickets).values({
1819
- ...fields,
1820
- ...dueColumn(dueAt),
1821
- id,
1822
- projectId: counter.id,
1823
- number: counter.nextNumber - 1,
1824
- key: `${counter.key}-${counter.nextNumber - 1}`,
1825
- createdBy: ctx.actor.id
1826
- }).returning());
1827
- await subscribe(ctx, row.projectId, row.id, [ctx.actor.id, row.assigneeId]);
1828
- await publish(ctx, "ticket.created", row.id, {
1829
- key: row.key,
1830
- version: row.version
1831
- });
1832
- return row;
1833
- },
1834
- update_ticket: updateTicket,
1835
- async bulk_update_tickets(ctx, input) {
1836
- const result = [], pending = [];
1837
- for (const item of input.tickets) {
1838
- const { row, event } = await updateTicketRow(ctx, {
1839
- projectId: input.projectId,
1840
- ticketId: item.ticketId,
1841
- expectedVersion: item.expectedVersion,
1842
- changes: input.changes
1843
- });
1844
- result.push(row);
1845
- pending.push(event);
1846
- }
1847
- await publishMany(ctx, pending);
1848
- return result;
1849
- },
1850
- async delete_ticket(ctx, input) {
1851
- const ticket = await getTicket(ctx, input.ticketId);
1852
- assertVersion(ticket, input.expectedVersion, "ticket", "deleting");
1853
- if ((await ctx.db.select().from(claims).where(eq(claims.ticketId, ticket.id))).length) fail(409, "claim_conflict", "Release ownership before deleting this ticket");
1854
- await publishMany(ctx, [...(await ctx.db.update(tickets).set({
1855
- parentId: null,
1856
- version: sql`${tickets.version} + 1`,
1857
- updatedAt: /* @__PURE__ */ new Date()
1858
- }).where(eq(tickets.parentId, ticket.id)).returning()).map((child) => ({
1859
- type: "ticket.updated",
1860
- ticketId: child.id,
1861
- data: {
1862
- version: child.version,
1863
- changedFields: ["parentId"]
1864
- }
1865
- })), {
1866
- type: "ticket.deleted",
1867
- ticketId: ticket.id,
1868
- data: {
1869
- key: ticket.key,
1870
- version: ticket.version
1871
- }
1872
- }]);
1873
- await ctx.db.delete(tickets).where(eq(tickets.id, ticket.id));
1874
- return { ok: true };
1875
- },
1876
- async list_eligible_tickets(ctx, input) {
1877
- const page = await pageOf(ctx, and(eq(tickets.projectId, project(ctx).id), openTicket, eq(tickets.archived, false)), [asc(tickets.number)], input);
1878
- const eligibility = await readinessMany(ctx, page.items);
1879
- return {
1880
- items: page.items.map((ticket) => ({
1881
- ticket,
1882
- readiness: requireRow(eligibility.get(ticket.id))
1883
- })),
1884
- total: page.total
1885
- };
1886
- },
1887
- async search_tickets(ctx, input) {
1888
- const allowed = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId);
1889
- if (!allowed.length) return { items: [] };
1890
- const ids = allowed.map((row) => row.id);
1891
- return { items: (await ctx.db.select().from(tickets).where(and(inArray(tickets.projectId, ids), eq(tickets.archived, false), or(eq(tickets.key, input.q.toUpperCase()), await ctx.search.where(ctx.db, ids, input.q)))).orderBy(sql`CASE WHEN ${tickets.key} = ${input.q.toUpperCase()} THEN 0 ELSE 1 END`, desc(tickets.updatedAt)).limit(input.limit)).flatMap((ticket) => {
1892
- const owner = allowed.find((row) => row.id === ticket.projectId);
1893
- return owner ? [{
1894
- ticket,
1895
- project: {
1896
- id: owner.id,
1897
- key: owner.key,
1898
- name: owner.name
1899
- }
1900
- }] : [];
1901
- }) };
1902
- },
1903
- async claim_ticket(ctx, input) {
1904
- const ticket = await getTicket(ctx, input.ticketId);
1905
- assertVersion(ticket, input.expectedVersion, "ticket", "claiming");
1906
- const eligible = await readiness(ctx, ticket);
1907
- if (!eligible.ready) fail(409, "not_ready", "The ticket is not ready to claim", eligible);
1908
- const claim = one(await ctx.db.insert(claims).values({
1909
- projectId: project(ctx).id,
1910
- ticketId: ticket.id,
1911
- actorId: ctx.actor.id,
1912
- ownerId: ctx.actor.ownerId,
1913
- workerId: input.workerId,
1914
- expiresAt: new Date(Date.now() + input.leaseSeconds * 1e3)
1915
- }).returning());
1916
- const row = one(await ctx.db.update(tickets).set({
1917
- status: "in_progress",
1918
- claimRequired: true,
1919
- version: ticket.version + 1,
1920
- updatedAt: /* @__PURE__ */ new Date()
1921
- }).where(eq(tickets.id, ticket.id)).returning());
1922
- await publish(ctx, "ticket.claimed", ticket.id, {
1923
- claimId: claim.id,
1924
- workerId: claim.workerId,
1925
- version: row.version
1926
- });
1927
- return claim;
1928
- },
1929
- async renew_claim(ctx, input) {
1930
- const ticket = await getTicket(ctx, input.ticketId);
1931
- const current = one(await ctx.db.select().from(claims).where(and(eq(claims.id, input.claimId), eq(claims.ticketId, ticket.id))));
1932
- if (!ownsLiveClaim(ctx, current)) fail(409, "claim_conflict", "Only the current worker can renew an unexpired claim");
1933
- const claim = one(await ctx.db.update(claims).set({ expiresAt: new Date(Date.now() + input.leaseSeconds * 1e3) }).where(eq(claims.id, current.id)).returning());
1934
- await publish(ctx, "claim.renewed", ticket.id, {
1935
- claimId: claim.id,
1936
- expiresAt: claim.expiresAt.toISOString()
1937
- });
1938
- return claim;
1939
- },
1940
- async release_claim(ctx, input) {
1941
- const ticket = await getTicket(ctx, input.ticketId);
1942
- const current = one(await ctx.db.select().from(claims).where(and(eq(claims.id, input.claimId), eq(claims.ticketId, ticket.id))));
1943
- if (!ownsLiveClaim(ctx, current)) fail(409, "claim_conflict", "Only the owning worker can release an unexpired claim. Expired claims require administrator recovery.");
1944
- await ctx.db.delete(claims).where(eq(claims.id, current.id));
1945
- await publish(ctx, "claim.released", ticket.id, { claimId: current.id });
1946
- return { ok: true };
1947
- },
1948
- async recover_claim(ctx, input) {
1949
- const ticket = await getTicket(ctx, input.ticketId);
1950
- const current = one(await ctx.db.select().from(claims).where(and(eq(claims.id, input.claimId), eq(claims.ticketId, ticket.id))));
1951
- if (current.expiresAt > /* @__PURE__ */ new Date()) fail(409, "claim_not_expired", "The current claim has not expired");
1952
- await ctx.db.delete(claims).where(eq(claims.id, current.id));
1953
- await publish(ctx, "claim.recovered", ticket.id, {
1954
- claimId: current.id,
1955
- reason: input.reason,
1956
- confirmedStopped: true
1957
- });
1958
- return { ok: true };
1959
- }
1960
- };
1961
- //#endregion
1962
- //#region src/project-operations.ts
1963
- async function getLabel(ctx, labelId, expectedVersion) {
1964
- const label = one(await ctx.db.select().from(projectLabels).where(and(eq(projectLabels.projectId, project(ctx).id), eq(projectLabels.id, labelId))));
1965
- assertVersion(label, expectedVersion, "label", "saving");
1966
- return label;
1967
- }
1968
- async function rememberLabelColor(ctx, color) {
1969
- if (isHexColor(color)) await ctx.db.insert(labelColors).values({
1970
- value: color,
1971
- createdBy: ctx.actor.id
1972
- }).onConflictDoNothing();
1973
- }
1974
- async function availableLabelName(ctx, name, exceptId) {
1975
- if ((await ctx.db.select({ id: projectLabels.id }).from(projectLabels).where(and(eq(projectLabels.projectId, project(ctx).id), eq(projectLabels.name, name), exceptId ? ne(projectLabels.id, exceptId) : void 0))).length) fail(409, "duplicate_label", "A label with this name already exists in this project.");
1976
- }
1977
- async function changeLabelUsage(ctx, name, replacement) {
1978
- const affected = await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), sql`${name} = ANY(${tickets.labels})`));
1979
- const pending = [];
1980
- for (const ticket of affected) pending.push((await updateTicketRow(ctx, {
1981
- projectId: project(ctx).id,
1982
- ticketId: ticket.id,
1983
- expectedVersion: ticket.version,
1984
- changes: { labels: replacement ? ticket.labels.map((tag) => tag === name ? replacement : tag) : ticket.labels.filter((tag) => tag !== name) }
1985
- })).event);
1986
- await publishMany(ctx, pending);
1987
- await ctx.db.update(views).set({ filters: replacement ? sql`jsonb_set(${views.filters}, '{label}', to_jsonb(${replacement}::text))` : sql`${views.filters} - 'label'` }).where(and(eq(views.projectId, project(ctx).id), sql`${views.filters}->>'label' = ${name}`));
1988
- }
1989
- const projectHandlers = {
1990
- async list_project_labels(ctx) {
1991
- const usage = labelUsage(ctx, "include").as("usage");
1992
- return ctx.db.select({
1993
- ...getTableColumns(projectLabels),
1994
- count: sql`coalesce(${usage.count}, 0)`.mapWith(Number)
1995
- }).from(projectLabels).leftJoin(usage, eq(projectLabels.name, sql`"usage"."name"`)).where(eq(projectLabels.projectId, project(ctx).id)).orderBy(...labelOrder(projectLabels.name));
1996
- },
1997
- async create_project_label(ctx, input) {
1998
- await availableLabelName(ctx, input.name);
1999
- await rememberLabelColor(ctx, input.color);
2000
- const label = one(await ctx.db.insert(projectLabels).values({
2001
- projectId: project(ctx).id,
2002
- name: input.name,
2003
- color: input.color
2004
- }).returning());
2005
- await publish(ctx, "label.created", null, {
2006
- labelId: label.id,
2007
- name: label.name,
2008
- color: label.color,
2009
- version: label.version
2010
- });
2011
- return label;
2012
- },
2013
- async update_project_label(ctx, input) {
2014
- const current = await getLabel(ctx, input.labelId, input.expectedVersion);
2015
- await availableLabelName(ctx, input.name, current.id);
2016
- if (input.color !== current.color) await rememberLabelColor(ctx, input.color);
2017
- const label = one(await ctx.db.update(projectLabels).set({
2018
- name: input.name,
2019
- color: input.color,
2020
- version: current.version + 1
2021
- }).where(eq(projectLabels.id, current.id)).returning());
2022
- if (current.name !== label.name) await changeLabelUsage(ctx, current.name, label.name);
2023
- await publish(ctx, "label.updated", null, {
2024
- labelId: label.id,
2025
- name: label.name,
2026
- previousName: current.name,
2027
- color: label.color,
2028
- version: label.version
2029
- });
2030
- return label;
2031
- },
2032
- async delete_project_label(ctx, input) {
2033
- const label = await getLabel(ctx, input.labelId, input.expectedVersion);
2034
- await changeLabelUsage(ctx, label.name);
2035
- await ctx.db.delete(projectLabels).where(eq(projectLabels.id, label.id));
2036
- await publish(ctx, "label.deleted", null, {
2037
- labelId: label.id,
2038
- name: label.name
2039
- });
2040
- return { ok: true };
2041
- },
2042
- async list_label_colors(ctx) {
2043
- return ctx.db.select({
2044
- value: labelColors.value,
2045
- createdAt: labelColors.createdAt
2046
- }).from(labelColors).orderBy(desc(labelColors.createdAt), asc(labelColors.value));
2047
- },
2048
- async remove_label_color(ctx, input) {
2049
- const current = (await ctx.db.select().from(labelColors).where(eq(labelColors.value, input.value)))[0];
2050
- if (!current) return { ok: true };
2051
- if (!ctx.actor.admin && current.createdBy !== ctx.actor.id) fail(403, "forbidden", "Only an instance administrator or the person who added this color can remove it");
2052
- await ctx.db.delete(labelColors).where(eq(labelColors.value, input.value));
2053
- await publishMany(ctx, (await ctx.db.select({ id: projects.id }).from(projects).where(ctx.actor.admin ? void 0 : inArray(projectRoleSQL(ctx.actor.id, projects.id), ["writer", "admin"]))).map((row) => ({
2054
- type: "label.color_removed",
2055
- ticketId: null,
2056
- projectId: row.id,
2057
- data: { value: input.value }
2058
- })));
2059
- return { ok: true };
2060
- },
2061
- async get_project(ctx) {
2062
- return project(ctx);
2063
- },
2064
- async set_project_icon(ctx, input) {
2065
- const current = project(ctx);
2066
- assertVersion(current, input.expectedVersion, "project icon", "saving");
2067
- const icon = input.icon;
2068
- if (icon?.kind === "library" && !icons.icons[icon.name]) fail(400, "invalid_icon", "Choose an icon from the installed library");
2069
- let objectId = null;
2070
- if (icon?.kind === "image") {
2071
- objectId = crypto.randomUUID();
2072
- await ctx.blobs.put(ctx.db, objectId, decodeImage(icon), icon.contentType);
2073
- }
2074
- const row = one(await ctx.db.update(projects).set({
2075
- iconName: icon?.kind === "library" ? icon.name : null,
2076
- iconColor: icon?.kind === "library" ? icon.color : "default",
2077
- image: objectId ? `/api/v1/projects/${current.id}/icon?v=${objectId}` : null,
2078
- iconObjectId: objectId,
2079
- version: current.version + 1,
2080
- updatedAt: /* @__PURE__ */ new Date()
2081
- }).where(eq(projects.id, current.id)).returning());
2082
- await publish(ctx, "project.updated", null, { version: row.version });
2083
- return row;
2084
- },
2085
- async list_projects(ctx, input) {
2086
- const rows = await accessibleProjectRows(ctx.db, ctx.actor, input.workspaceId);
2087
- if (!rows.length) return [];
2088
- const counts = await ctx.db.select({
2089
- projectId: tickets.projectId,
2090
- open: count(),
2091
- overdue: count(sql`CASE WHEN ${overdueTicket} THEN 1 END`)
2092
- }).from(tickets).where(and(inArray(tickets.projectId, rows.map((row) => row.id)), eq(tickets.archived, false), openTicket)).groupBy(tickets.projectId);
2093
- return rows.map((row) => {
2094
- const stats = counts.find((entry) => entry.projectId === row.id);
2095
- return {
2096
- ...toProject(row),
2097
- openTickets: stats?.open ?? 0,
2098
- overdueTickets: stats?.overdue ?? 0
2099
- };
2100
- });
2101
- },
2102
- async create_project(ctx, input) {
2103
- if (ctx.actor.projectIds) fail(403, "insufficient_scope", "Project-bound credentials cannot create projects");
2104
- let workspaceId = input.workspaceId;
2105
- if (workspaceId) {
2106
- if (workspace(ctx).role === "reader") fail(403, "forbidden", "Project creation requires workspace write access");
2107
- } else {
2108
- workspaceId = (ctx.actor.admin ? await ctx.db.select({ workspaceId: workspaces.id }).from(workspaces).orderBy(workspaces.createdAt).limit(1) : await ctx.db.select({ workspaceId: workspaceMembers.workspaceId }).from(workspaceMembers).where(and(eq(workspaceMembers.userId, ctx.actor.id), inArray(workspaceMembers.role, ["writer", "admin"]))).orderBy(workspaceMembers.workspaceId).limit(1))[0]?.workspaceId;
2109
- if (!workspaceId) {
2110
- workspaceId = one(await ctx.db.insert(workspaces).values({
2111
- name: "Personal workspace",
2112
- slug: `personal-${crypto.randomUUID()}`
2113
- }).returning()).id;
2114
- await ctx.db.insert(workspaceMembers).values({
2115
- workspaceId,
2116
- userId: ctx.actor.id,
2117
- role: "admin"
2118
- });
2119
- }
2120
- }
2121
- if ((await ctx.db.select({ id: projects.id }).from(projects).where(eq(projects.key, input.key))).length) fail(409, "duplicate_prefix", "This project prefix is already in use");
2122
- const row = one(await ctx.db.insert(projects).values({
2123
- workspaceId,
2124
- visibility: input.visibility,
2125
- key: input.key,
2126
- name: input.name,
2127
- description: input.description
2128
- }).returning());
2129
- await ctx.db.insert(members).values({
2130
- projectId: row.id,
2131
- userId: ctx.actor.id,
2132
- role: "admin"
2133
- });
2134
- await publish({
2135
- ...ctx,
2136
- project: toProject(row)
2137
- }, "project.created", null, { projectId: row.id });
2138
- return row;
2139
- },
2140
- async update_project(ctx, input) {
2141
- const current = project(ctx);
2142
- assertVersion(current, input.expectedVersion, "project");
2143
- const row = one(await ctx.db.update(projects).set({
2144
- name: input.name,
2145
- ...input.visibility ? { visibility: input.visibility } : {},
2146
- description: input.description,
2147
- version: current.version + 1,
2148
- updatedAt: /* @__PURE__ */ new Date()
2149
- }).where(eq(projects.id, current.id)).returning());
2150
- await publish(ctx, "project.updated", null, { version: row.version });
2151
- return row;
2152
- },
2153
- async list_members(ctx) {
2154
- const current = project(ctx);
2155
- const inherited = await ctx.db.select({
2156
- userId: user.id,
2157
- role: workspaceMembers.role,
2158
- name: user.name,
2159
- email: user.email,
2160
- image: user.image
2161
- }).from(workspaceMembers).innerJoin(user, eq(user.id, workspaceMembers.userId)).where(and(eq(workspaceMembers.workspaceId, current.workspaceId), current.visibility === "workspace" ? void 0 : eq(workspaceMembers.role, "admin")));
2162
- const direct = await ctx.db.select({
2163
- userId: user.id,
2164
- role: members.role,
2165
- name: user.name,
2166
- email: user.email,
2167
- image: user.image
2168
- }).from(members).innerJoin(user, eq(user.id, members.userId)).where(eq(members.projectId, current.id));
2169
- const people = new Map(inherited.map((person) => [person.userId, {
2170
- ...person,
2171
- source: "workspace"
2172
- }]));
2173
- for (const person of direct) if (people.get(person.userId)?.role !== "admin") people.set(person.userId, {
2174
- ...person,
2175
- source: "project"
2176
- });
2177
- const inheritedRoles = new Map(inherited.map((person) => [person.userId, person.role]));
2178
- return [...people.values()].map((person) => ({
2179
- ...person,
2180
- inheritedRole: inheritedRoles.get(person.userId) ?? null
2181
- })).toSorted((a, b) => a.name.localeCompare(b.name));
2182
- },
2183
- async set_member(ctx, input) {
2184
- const member = await accountByEmail(ctx.db, input.email);
2185
- const projectId = project(ctx).id;
2186
- if ((await ctx.db.select().from(members).where(and(eq(members.projectId, projectId), eq(members.userId, member.id))))[0]?.role === "admin" && input.role !== "admin") {
2187
- if (!(await ctx.db.select({ id: user.id }).from(user).leftJoin(workspaceMembers, and(eq(workspaceMembers.workspaceId, project(ctx).workspaceId), eq(workspaceMembers.userId, user.id))).where(and(eq(projectRoleSQL(user.id, projectId), "admin"), sql`NOT coalesce(${user.banned},false)`, or(ne(user.id, member.id), eq(workspaceMembers.role, "admin")))).limit(1)).length) fail(409, "last_administrator", "Keep at least one project or workspace administrator");
2188
- }
2189
- if (input.role) await ctx.db.insert(members).values({
2190
- projectId,
2191
- userId: member.id,
2192
- role: input.role
2193
- }).onConflictDoUpdate({
2194
- target: [members.projectId, members.userId],
2195
- set: { role: input.role }
2196
- });
2197
- else await ctx.db.delete(members).where(and(eq(members.projectId, projectId), eq(members.userId, member.id)));
2198
- await publish(ctx, "membership.updated", null, {
2199
- userId: member.id,
2200
- role: input.role
2201
- });
2202
- return { ok: true };
2203
- },
2204
- async list_views(ctx) {
2205
- return ctx.db.select().from(views).where(and(eq(views.projectId, project(ctx).id), eq(views.userId, ctx.actor.id))).orderBy(views.name);
2206
- },
2207
- async save_view(ctx, input) {
2208
- return one(await ctx.db.insert(views).values({
2209
- projectId: project(ctx).id,
2210
- userId: ctx.actor.id,
2211
- name: input.name,
2212
- filters: input.filters
2213
- }).returning({ id: views.id }));
2214
- },
2215
- async delete_view(ctx, input) {
2216
- one(await ctx.db.delete(views).where(and(eq(views.projectId, project(ctx).id), eq(views.userId, ctx.actor.id), eq(views.id, input.viewId))).returning());
2217
- return { ok: true };
2218
- }
2219
- };
2220
- //#endregion
2221
- //#region src/collaboration-operations.ts
2222
- const sourceTicket = alias(tickets, "source_ticket");
2223
- const targetTicket = alias(tickets, "target_ticket");
2224
- async function editableComment(ctx, commentId, expectedVersion) {
2225
- const row = one(await ctx.db.select().from(comments).where(and(eq(comments.projectId, project(ctx).id), eq(comments.id, commentId))));
2226
- if (row.actorId !== ctx.actor.id && ctx.role !== "admin") fail(403, "forbidden", "Only the author or a project administrator can edit this comment");
2227
- assertVersion(row, expectedVersion, "comment");
2228
- return row;
2229
- }
2230
- async function publishRelation(ctx, type, relation) {
2231
- await publishMany(ctx, [relation.sourceId, relation.targetId].map((ticketId) => ({
2232
- type,
2233
- ticketId,
2234
- data: { relation }
2235
- })));
2236
- }
2237
- const collaborationHandlers = {
2238
- async list_relations(ctx, input) {
2239
- const ticket = await getTicket(ctx, input.ticketId);
2240
- return ctx.db.select({
2241
- ...getTableColumns(relations$1),
2242
- sourceKey: sourceTicket.key,
2243
- sourceTitle: sourceTicket.title,
2244
- targetKey: targetTicket.key,
2245
- targetTitle: targetTicket.title
2246
- }).from(relations$1).innerJoin(sourceTicket, eq(sourceTicket.id, relations$1.sourceId)).innerJoin(targetTicket, eq(targetTicket.id, relations$1.targetId)).where(and(eq(relations$1.projectId, project(ctx).id), or(eq(relations$1.sourceId, ticket.id), eq(relations$1.targetId, ticket.id))));
2247
- },
2248
- async add_relation(ctx, input) {
2249
- const current = await getTicket(ctx, input.ticketId), other = await getTicket(ctx, input.targetId);
2250
- const [source, target] = input.direction === "incoming" ? [other, current] : [current, other];
2251
- if (source.id === target.id) fail(409, "invalid_relation", "Choose a different ticket");
2252
- if (input.type === "blocks") await assertNoCycle(ctx, source.id, target.id);
2253
- const [sourceId, targetId] = input.type === "related" ? [source.id, target.id].toSorted() : [source.id, target.id];
2254
- const row = one(await ctx.db.insert(relations$1).values({
2255
- projectId: project(ctx).id,
2256
- sourceId: requireRow(sourceId),
2257
- targetId: requireRow(targetId),
2258
- type: input.type
2259
- }).onConflictDoUpdate({
2260
- target: [
2261
- relations$1.sourceId,
2262
- relations$1.targetId,
2263
- relations$1.type
2264
- ],
2265
- set: { type: input.type }
2266
- }).returning());
2267
- await publishRelation(ctx, "relation.added", row);
2268
- return row;
2269
- },
2270
- async remove_relation(ctx, input) {
2271
- await publishRelation(ctx, "relation.removed", one(await ctx.db.delete(relations$1).where(and(eq(relations$1.projectId, project(ctx).id), eq(relations$1.id, input.relationId))).returning()));
2272
- return { ok: true };
2273
- },
2274
- async list_comments(ctx, input) {
2275
- const ticket = await getTicket(ctx, input.ticketId), where = eq(comments.ticketId, ticket.id);
2276
- return {
2277
- items: await ctx.db.select().from(comments).where(where).orderBy(asc(comments.createdAt), asc(comments.id)).limit(input.limit).offset(input.offset),
2278
- total: one(await ctx.db.select({ count: count() }).from(comments).where(where)).count
2279
- };
2280
- },
2281
- async add_comment(ctx, input) {
2282
- const ticket = await getTicket(ctx, input.ticketId);
2283
- const row = one(await ctx.db.insert(comments).values({
2284
- projectId: project(ctx).id,
2285
- ticketId: ticket.id,
2286
- actorId: ctx.actor.id,
2287
- body: input.body
2288
- }).returning());
2289
- await subscribe(ctx, row.projectId, row.ticketId, [ctx.actor.id]);
2290
- await publish(ctx, "comment.created", ticket.id, { commentId: row.id });
2291
- return row;
2292
- },
2293
- async update_comment(ctx, input) {
2294
- const current = await editableComment(ctx, input.commentId, input.expectedVersion);
2295
- const row = one(await ctx.db.update(comments).set({
2296
- body: input.body,
2297
- version: current.version + 1,
2298
- updatedAt: /* @__PURE__ */ new Date()
2299
- }).where(eq(comments.id, current.id)).returning());
2300
- await publish(ctx, "comment.updated", row.ticketId, {
2301
- commentId: row.id,
2302
- version: row.version
2303
- });
2304
- return row;
2305
- },
2306
- async delete_comment(ctx, input) {
2307
- const current = await editableComment(ctx, input.commentId, input.expectedVersion);
2308
- await ctx.db.delete(comments).where(eq(comments.id, current.id));
2309
- await publish(ctx, "comment.deleted", current.ticketId, { commentId: current.id });
2310
- return { ok: true };
2311
- },
2312
- async list_activity(ctx, input) {
2313
- const ticket = await getTicket(ctx, input.ticketId), where = and(eq(events.projectId, project(ctx).id), eq(events.ticketId, ticket.id));
2314
- return {
2315
- items: await ctx.db.select().from(events).where(where).orderBy(desc(events.id)).limit(input.limit).offset(input.offset),
2316
- total: one(await ctx.db.select({ count: count() }).from(events).where(where)).count
2317
- };
2318
- },
2319
- async upload_attachment(ctx, input) {
2320
- const ticket = await getTicket(ctx, input.ticketId);
2321
- const bytes = decodeBlobData(input.data, attachmentLimit);
2322
- const id = crypto.randomUUID();
2323
- await ctx.blobs.put(ctx.db, id, bytes, input.contentType);
2324
- const row = one(await ctx.db.insert(attachments).values({
2325
- id,
2326
- objectId: id,
2327
- projectId: project(ctx).id,
2328
- ticketId: ticket.id,
2329
- actorId: ctx.actor.id,
2330
- name: input.name,
2331
- contentType: input.contentType,
2332
- size: bytes.length
2333
- }).returning());
2334
- await publish(ctx, "attachment.created", ticket.id, {
2335
- attachmentId: row.id,
2336
- size: row.size
2337
- });
2338
- return row;
2339
- },
2340
- async list_attachments(ctx, input) {
2341
- const ticket = await getTicket(ctx, input.ticketId);
2342
- return ctx.db.select().from(attachments).where(eq(attachments.ticketId, ticket.id)).orderBy(asc(attachments.createdAt));
2343
- },
2344
- async delete_attachment(ctx, input) {
2345
- const row = one(await ctx.db.delete(attachments).where(and(eq(attachments.projectId, project(ctx).id), eq(attachments.id, input.attachmentId))).returning());
2346
- await publish(ctx, "attachment.deleted", row.ticketId, { attachmentId: row.id });
2347
- return { ok: true };
2348
- },
2349
- async list_evidence(ctx, input) {
2350
- const ticket = await getTicket(ctx, input.ticketId);
2351
- return ctx.db.select().from(evidence).where(eq(evidence.ticketId, ticket.id)).orderBy(asc(evidence.createdAt));
2352
- },
2353
- async add_evidence(ctx, input) {
2354
- const ticket = await getTicket(ctx, input.ticketId);
2355
- await assertWorkerClaim(ctx, ticket, input.claimId);
2356
- const row = one(await ctx.db.insert(evidence).values({
2357
- projectId: project(ctx).id,
2358
- ticketId: ticket.id,
2359
- actorId: ctx.actor.id,
2360
- title: input.title,
2361
- kind: input.kind,
2362
- url: input.url,
2363
- commit: input.commit,
2364
- outcome: input.outcome,
2365
- note: input.note
2366
- }).returning());
2367
- await publish(ctx, "evidence.added", ticket.id, {
2368
- evidenceId: row.id,
2369
- kind: row.kind,
2370
- outcome: row.outcome,
2371
- commit: row.commit
2372
- });
2373
- return row;
2374
- },
2375
- async set_subscription(ctx, input) {
2376
- const ticket = await getTicket(ctx, input.ticketId);
2377
- if (input.enabled) await subscribe(ctx, project(ctx).id, ticket.id, [ctx.actor.id]);
2378
- else {
2379
- await ctx.db.delete(subscriptions).where(and(eq(subscriptions.ticketId, ticket.id), eq(subscriptions.userId, ctx.actor.id)));
2380
- await ctx.db.update(pushDeliveries).set({
2381
- finishedAt: /* @__PURE__ */ new Date(),
2382
- error: "unsubscribed"
2383
- }).where(and(isNull(pushDeliveries.finishedAt), inArray(pushDeliveries.notificationId, ctx.db.select({ id: notifications.id }).from(notifications).innerJoin(events, eq(events.id, notifications.eventId)).where(and(eq(notifications.userId, ctx.actor.id), eq(events.ticketId, ticket.id))))));
2384
- }
2385
- return { ok: true };
2386
- }
2387
- };
2388
- //#endregion
2389
- //#region src/domain.ts
2390
- function fail(status, code, message, details) {
2391
- throw new ApiError(status, code, message, details);
2392
- }
2393
- function requireRow(row) {
2394
- if (!row) fail(404, "not_found", "The requested record was not found");
2395
- return row;
2396
- }
2397
- const one = (rows) => requireRow(rows[0]);
2398
- function assertVersion(current, expected, subject, action = "editing") {
2399
- if (current.version !== expected) fail(409, "version_conflict", `The ${subject} changed; reload before ${action}`, { currentVersion: current.version });
2400
- }
2401
- function toProject(row) {
2402
- return projectSchema.parse({
2403
- ...row,
2404
- createdAt: row.createdAt.toISOString(),
2405
- updatedAt: row.updatedAt.toISOString()
2406
- });
2407
- }
2408
- async function accountByEmail(db, email) {
2409
- const found = (await db.select().from(user).where(eq(user.email, email.toLowerCase())).limit(1))[0];
2410
- if (!found) fail(404, "account_not_found", "No account uses this email. Ask an instance administrator to create one.");
2411
- return found;
2412
- }
2413
- function workspace(ctx) {
2414
- return requireRow(ctx.workspace ?? void 0);
2415
- }
2416
- function project(ctx) {
2417
- return requireRow(ctx.project ?? void 0);
2418
- }
2419
- function json(value) {
2420
- return JSON.parse(JSON.stringify(value, (_, item) => typeof item === "bigint" ? item.toString() : item));
2421
- }
2422
- const complete = (status) => closedStates.some((closed) => closed === status);
2423
- const hasScope = (actor, scope) => actor.scopes.includes("admin") || actor.scopes.includes(scope) || scope === "read" && actor.scopes.length > 0;
2424
- async function context(db, actor, scope, projectId, workspaceId) {
2425
- if (!hasScope(actor, scope)) fail(403, "insufficient_scope", `This credential requires ${scope} permission`);
2426
- const ctx = {
2427
- db,
2428
- actor,
2429
- project: null,
2430
- workspace: null,
2431
- role: "reader",
2432
- blobs: { put: writeLocalBlob },
2433
- search: { where: async (_db, _ids, query) => postgresSearch(query) }
2434
- };
2435
- if (workspaceId) {
2436
- ctx.workspace = await requireWorkspace(ctx, workspaceId, scope === "admin");
2437
- if ((scope === "write" || scope === "claim") && ctx.workspace.role === "reader") fail(403, "forbidden", "Workspace write access is required");
2438
- }
2439
- if (!projectId) return ctx;
2440
- const row = one(await db.select().from(projects).where(or(eq(projects.key, projectId.toUpperCase()), sql`${projects.id}::text = ${projectId}`)).limit(1));
2441
- if (actor.projectIds && !actor.projectIds.includes(row.id)) fail(404, "not_found", "Project was not found");
2442
- const role = await projectRole(db, actor, row);
2443
- if (!role) fail(404, "not_found", "Project was not found");
2444
- if (role !== "reader" && role !== "writer" && role !== "admin") fail(403, "forbidden", "Project access is unavailable");
2445
- if (scope === "admin" && role !== "admin") fail(403, "forbidden", "Project administration permission is required");
2446
- if ((scope === "write" || scope === "claim") && role === "reader") fail(403, "forbidden", "Project write permission is required");
2447
- return {
2448
- ...ctx,
2449
- project: toProject(row),
2450
- role
2451
- };
2452
- }
2453
- async function getTicket(ctx, identifier) {
2454
- return one(await ctx.db.select().from(tickets).where(and(eq(tickets.projectId, project(ctx).id), or(eq(tickets.key, identifier.toUpperCase()), sql`${tickets.id}::text = ${identifier}`))).limit(1));
2455
- }
2456
- const insertBatch = 1e3;
2457
- async function insertAll(rows, insert) {
2458
- const out = [];
2459
- for (let at = 0; at < rows.length; at += insertBatch) out.push(...await insert(rows.slice(at, at + insertBatch)));
2460
- return out;
2461
- }
2462
- async function subscribe(ctx, projectId, ticketId, userIds) {
2463
- const ids = [...new Set(userIds.filter((id) => !!id))];
2464
- if (ids.length) await ctx.db.insert(subscriptions).values(ids.map((userId) => ({
2465
- projectId,
2466
- ticketId,
2467
- userId
2468
- }))).onConflictDoNothing();
2469
- }
2470
- async function publishMany(ctx, pending) {
2471
- if (!pending.length) return [];
2472
- const ticketIds = [...new Set(pending.flatMap((item) => item.ticketId ? [item.ticketId] : []))];
2473
- const titles = new Map(ticketIds.length ? (await ctx.db.select({
2474
- id: tickets.id,
2475
- key: tickets.key,
2476
- title: tickets.title
2477
- }).from(tickets).where(inArray(tickets.id, ticketIds))).map((row) => [row.id, {
2478
- key: row.key,
2479
- title: row.title
2480
- }]) : []);
2481
- const clock = one(await ctx.db.update(eventClock).set({ value: sql`${eventClock.value} + ${pending.length}` }).where(eq(eventClock.id, 1)).returning());
2482
- const first = clock.value - BigInt(pending.length) + 1n;
2483
- const source = {
2484
- kind: ctx.actor.machine ? "credential" : "session",
2485
- id: ctx.actor.ownerId
2486
- };
2487
- const rows = await insertAll(pending.map((item, index) => ({
2488
- id: first + BigInt(index),
2489
- projectId: item.projectId ?? project(ctx).id,
2490
- ticketId: item.ticketId,
2491
- actorId: ctx.actor.id,
2492
- type: item.type,
2493
- data: {
2494
- ...item.data,
2495
- ...item.ticketId ? titles.get(item.ticketId) : {},
2496
- source
2497
- }
2498
- })), (batch) => ctx.db.insert(events).values(batch).returning());
2499
- if (ticketIds.length) {
2500
- const followers = await ctx.db.select({
2501
- ticketId: subscriptions.ticketId,
2502
- userId: subscriptions.userId,
2503
- inApp: notificationPreferences.inApp,
2504
- push: notificationPreferences.push
2505
- }).from(subscriptions).innerJoin(user, eq(user.id, subscriptions.userId)).leftJoin(notificationPreferences, eq(notificationPreferences.userId, subscriptions.userId)).where(and(inArray(subscriptions.ticketId, ticketIds), or(isNotNull(projectRoleSQL(user.id, subscriptions.projectId)), sql`'admin' = any(string_to_array(${user.role}, ','))`), sql`not coalesce(${user.banned}, false)`, ctx.actor.machine ? void 0 : ne(subscriptions.userId, ctx.actor.id), or(sql`coalesce(${notificationPreferences.inApp}, true)`, eq(notificationPreferences.push, true))));
2506
- const entries = rows.flatMap((event) => followers.filter((follower) => follower.ticketId === event.ticketId).map((follower) => ({
2507
- event,
2508
- follower
2509
- })));
2510
- if (entries.length) {
2511
- const created = await insertAll(entries.map(({ event, follower }) => ({
2512
- userId: follower.userId,
2513
- projectId: event.projectId,
2514
- eventId: event.id,
2515
- inbox: follower.inApp ?? true
2516
- })), (batch) => ctx.db.insert(notifications).values(batch).returning());
2517
- const pushing = new Set(entries.filter(({ follower }) => follower.push).map(({ event, follower }) => `${event.id}:${follower.userId}`));
2518
- if (pushing.size) {
2519
- const devices = await ctx.db.select({
2520
- id: pushDevices.id,
2521
- userId: pushDevices.userId
2522
- }).from(pushDevices).where(inArray(pushDevices.userId, [...new Set(entries.filter((e) => e.follower.push).map((e) => e.follower.userId))]));
2523
- await insertAll(created.flatMap((notification) => pushing.has(`${notification.eventId}:${notification.userId}`) ? devices.filter((device) => device.userId === notification.userId).map((device) => ({
2524
- notificationId: notification.id,
2525
- deviceId: device.id
2526
- })) : []), (batch) => ctx.db.insert(pushDeliveries).values(batch).returning());
2527
- }
2528
- }
2529
- }
2530
- await ctx.db.execute(sql`SELECT pg_notify('kanvil_events', ${clock.value.toString()})`);
2531
- return rows;
2532
- }
2533
- async function publish(ctx, type, ticketId, data) {
2534
- return one(await publishMany(ctx, [{
2535
- type,
2536
- ticketId,
2537
- data
2538
- }]));
2539
- }
2540
- function ownsLiveClaim(ctx, claim) {
2541
- return claim.ownerId === ctx.actor.ownerId && claim.expiresAt > /* @__PURE__ */ new Date() && hasScope(ctx.actor, "claim") && ctx.role !== "reader";
2542
- }
2543
- async function assertWorkerClaim(ctx, ticket, claimId) {
2544
- if (!ctx.actor.machine) return;
2545
- const claim = (await ctx.db.select().from(claims).where(eq(claims.ticketId, ticket.id)).limit(1))[0];
2546
- if (claim && (claim.id !== claimId || !ownsLiveClaim(ctx, claim))) fail(409, "claim_conflict", "An active matching claim is required for this worker write");
2547
- if (!claim && (claimId || ticket.claimRequired)) fail(409, "claim_conflict", "This ticket requires current ownership for worker writes");
2548
- }
2549
- function createDomain(database, services = {}) {
2550
- const handlers = {
2551
- ...workspaceHandlers,
2552
- ...projectHandlers,
2553
- ...ticketHandlers,
2554
- ...collaborationHandlers
2555
- };
2556
- const workspaceIsFilter = /* @__PURE__ */ new Set(["list_projects", "search_tickets"]);
2557
- async function execute(name, value, actor) {
2558
- const spec = operations[name];
2559
- const input = spec.input.parse(value);
2560
- const projectId = "projectId" in input ? input.projectId : void 0;
2561
- const workspaceId = "workspaceId" in input && !workspaceIsFilter.has(name) ? input.workspaceId : void 0;
2562
- if (actor.projectIds && spec.method !== "GET" && !projectId) fail(403, "insufficient_scope", "Project-bound credentials can only change their configured projects");
2563
- const invoke = async (ctx) => {
2564
- if (services.blobs) ctx.blobs = services.blobs;
2565
- if (services.search) ctx.search = services.search;
2566
- const handler = handlers[name];
2567
- return parseOutput(name, json(await handler(ctx, input)));
2568
- };
2569
- if (spec.method === "GET") return invoke(await context(database.db, actor, spec.scope, projectId, workspaceId));
2570
- return database.db.transaction(async (tx) => {
2571
- await tx.execute(sql`SELECT pg_advisory_xact_lock(7361080)`);
2572
- const ctx = await context(tx, actor, spec.scope, projectId, workspaceId);
2573
- const requestId = "requestId" in input ? input.requestId : void 0;
2574
- const hash = createHash("sha256").update(JSON.stringify({
2575
- name,
2576
- input
2577
- })).digest("hex");
2578
- if (requestId) {
2579
- const receipt = (await tx.select().from(receipts).where(and(eq(receipts.actorId, actor.id), eq(receipts.requestId, requestId))).limit(1))[0];
2580
- if (receipt) {
2581
- if (receipt.hash !== hash || receipt.operation !== name) fail(409, "idempotency_conflict", "This requestId was already used for another write");
2582
- const prior = parseOutput(name, receipt.result);
2583
- if (name === "create_project") await context(tx, actor, "read", projectSchema.parse(prior).id);
2584
- if (name === "create_workspace") await requireWorkspace(ctx, workspaceSchema.parse(prior).id);
2585
- return prior;
2586
- }
2587
- }
2588
- const result = await invoke(ctx);
2589
- if (requestId) await tx.insert(receipts).values({
2590
- actorId: actor.id,
2591
- requestId,
2592
- hash,
2593
- operation: name,
2594
- result
2595
- });
2596
- return result;
2597
- });
2598
- }
2599
- async function resolveActor(id, credential) {
2600
- const row = one(await database.db.select().from(user).where(eq(user.id, id)).limit(1));
2601
- if (row.banned) fail(403, "account_disabled", "This account is disabled");
2602
- return {
2603
- id,
2604
- admin: hasAdminRole(row.role),
2605
- machine: !!credential,
2606
- ownerId: credential?.ownerId ?? id,
2607
- scopes: credential?.scopes ?? scopes,
2608
- ...credential?.projectIds ? { projectIds: credential.projectIds } : {}
2609
- };
2610
- }
2611
- async function accessibleProjects(identity) {
2612
- return accessibleProjectRows(database.db, identity);
2613
- }
2614
- async function readEvents(identity, cursor, projectId) {
2615
- if (!hasScope(identity, "read")) fail(403, "insufficient_scope", "Read permission is required");
2616
- const ids = (await accessibleProjects(identity)).filter((row) => !projectId || row.id === projectId || row.key === projectId.toUpperCase()).map((row) => row.id);
2617
- if (projectId && !ids.length) fail(404, "not_found", "Project was not found");
2618
- const clock = one(await database.db.select().from(eventClock).where(eq(eventClock.id, 1)));
2619
- if (cursor < clock.floor || cursor > clock.value) fail(410, "snapshot_required", "Fetch a fresh snapshot before reconnecting", { cursor: clock.value.toString() });
2620
- const rows = ids.length ? await database.db.select().from(events).where(and(inArray(events.projectId, ids), gt(events.id, cursor))).orderBy(asc(events.id)).limit(200) : [];
2621
- const last = rows.at(-1)?.id ?? cursor;
2622
- return {
2623
- items: rows,
2624
- cursor: rows.length === 200 || last > clock.value ? last : clock.value
2625
- };
2626
- }
2627
- async function snapshot(identity, identifier) {
2628
- return database.db.transaction(async (tx) => {
2629
- const ctx = await context(tx, identity, "read", identifier);
2630
- const clock = one(await tx.select().from(eventClock).where(eq(eventClock.id, 1)));
2631
- const rows = await tx.select().from(tickets).where(eq(tickets.projectId, project(ctx).id)).orderBy(asc(tickets.number));
2632
- return snapshotSchema.parse(json({
2633
- project: project(ctx),
2634
- tickets: rows,
2635
- cursor: clock.value.toString()
2636
- }));
2637
- }, {
2638
- isolationLevel: "repeatable read",
2639
- accessMode: "read only"
2640
- });
2641
- }
2642
- return {
2643
- execute,
2644
- actor: resolveActor,
2645
- accessibleProjects,
2646
- readEvents,
2647
- snapshot,
2648
- database
2649
- };
2650
- }
2651
- //#endregion
2652
- //#region src/instance.ts
2653
- function storageIdentity(settings) {
2654
- return createHash("sha256").update(JSON.stringify([
2655
- settings.endpoint,
2656
- settings.region,
2657
- settings.bucket,
2658
- settings.prefix,
2659
- settings.forcePathStyle
2660
- ])).digest("hex");
2661
- }
2662
- function createInstance(database, config) {
2663
- const key = createHash("sha256").update("kanvil:instance-config:v1:").update(config.BETTER_AUTH_SECRET).digest();
2664
- function seal(value) {
2665
- const iv = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, iv);
2666
- const bytes = Buffer.concat([cipher.update(JSON.stringify(value)), cipher.final()]);
2667
- return Buffer.concat([
2668
- iv,
2669
- cipher.getAuthTag(),
2670
- bytes
2671
- ]).toString("base64");
2672
- }
2673
- function unseal(value) {
2674
- try {
2675
- const bytes = Buffer.from(value, "base64"), decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(0, 12));
2676
- decipher.setAuthTag(bytes.subarray(12, 28));
2677
- return JSON.parse(Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString());
2678
- } catch {
2679
- return fail(503, "configuration_key_changed", "Saved integration credentials cannot be decrypted. Restore the instance secret from your backup.");
2680
- }
2681
- }
2682
- async function read(db = database.db) {
2683
- await db.insert(instanceSettings).values({ id: 1 }).onConflictDoNothing();
2684
- return one(await db.select().from(instanceSettings).where(eq(instanceSettings.id, 1)));
2685
- }
2686
- async function backend(id, db = database.db) {
2687
- const row = one(await db.select().from(storageBackends).where(eq(storageBackends.id, id)));
2688
- const settings = s3SettingsSchema.parse(unseal(row.config));
2689
- return {
2690
- ...row,
2691
- settings,
2692
- spec: {
2693
- provider: "s3",
2694
- config: settings
2695
- }
2696
- };
2697
- }
2698
- async function resolveS3(settings, db = database.db) {
2699
- settings = s3SettingsSchema.strip().parse(settings);
2700
- const identity = storageIdentity(settings);
2701
- const previous = (await db.select().from(storageBackends).where(eq(storageBackends.identity, identity)))[0];
2702
- const old = previous ? s3SettingsSchema.parse(unseal(previous.config)) : void 0;
2703
- const { secretAccessKey: suppliedSecret, ...publicSettings } = settings, secret = suppliedSecret || (old?.accessKeyId === settings.accessKeyId ? old.secretAccessKey : void 0);
2704
- if (settings.accessKeyId && !secret) fail(400, "missing_secret", "Provide the S3 secret access key");
2705
- return {
2706
- ...publicSettings,
2707
- ...secret ? { secretAccessKey: secret } : {}
2708
- };
2709
- }
2710
- async function resolveSearch(settings, db = database.db) {
2711
- settings = searchSettingsSchema.strip().parse(settings);
2712
- const row = await read(db), old = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null, same = old?.url === settings.url && old.index === settings.index;
2713
- const { apiKey: suppliedKey, password: suppliedPassword, auth: requestedAuth, ...publicSettings } = settings, auth = requestedAuth ?? (suppliedKey ? "apiKey" : suppliedPassword && settings.username ? "basic" : same && old ? searchAuthentication(old) : searchAuthentication(settings)), apiKey = suppliedKey || (same ? old?.apiKey : void 0), password = suppliedPassword || (same && old?.username === settings.username ? old?.password : void 0);
2714
- if (auth === "apiKey" && !apiKey) fail(400, "missing_secret", "Provide the search API key");
2715
- if (auth === "basic" && (!settings.username || !password)) fail(400, "missing_secret", "Provide the search username and password");
2716
- return {
2717
- ...publicSettings,
2718
- auth,
2719
- ...auth === "apiKey" && apiKey ? { apiKey } : {},
2720
- ...auth === "basic" && password ? { password } : {}
2721
- };
2722
- }
2723
- async function saveBackend(settings, db = database.db) {
2724
- const complete = await resolveS3(settings, db), identity = storageIdentity(settings);
2725
- return one(await db.insert(storageBackends).values({
2726
- identity,
2727
- config: seal(complete)
2728
- }).onConflictDoUpdate({
2729
- target: storageBackends.identity,
2730
- set: { config: seal(complete) }
2731
- }).returning());
2732
- }
2733
- async function storage() {
2734
- if (config.KANVIL_STORAGE_PROVIDER === "local") return null;
2735
- if (config.KANVIL_STORAGE_PROVIDER === "s3") return saveBackend(s3SettingsSchema.parse({
2736
- endpoint: config.S3_ENDPOINT ?? "",
2737
- region: config.S3_REGION,
2738
- bucket: config.S3_BUCKET,
2739
- prefix: config.S3_PREFIX,
2740
- forcePathStyle: config.S3_FORCE_PATH_STYLE,
2741
- accessKeyId: config.S3_ACCESS_KEY_ID ?? "",
2742
- secretAccessKey: config.S3_SECRET_ACCESS_KEY
2743
- }));
2744
- const row = await read();
2745
- return row.storageBackendId ? backend(row.storageBackendId) : null;
2746
- }
2747
- async function search() {
2748
- const row = await read();
2749
- if (config.KANVIL_SEARCH_PROVIDER === "postgres") return null;
2750
- if (config.KANVIL_SEARCH_PROVIDER === "elasticsearch") {
2751
- const settings = searchSettingsSchema.parse({
2752
- url: config.ELASTICSEARCH_URL,
2753
- index: config.ELASTICSEARCH_INDEX ?? `kanvil-${row.identity}`,
2754
- auth: config.ELASTICSEARCH_AUTH,
2755
- apiKey: config.ELASTICSEARCH_API_KEY,
2756
- username: config.ELASTICSEARCH_USERNAME,
2757
- password: config.ELASTICSEARCH_PASSWORD
2758
- });
2759
- return {
2760
- ...settings,
2761
- auth: searchAuthentication(settings)
2762
- };
2763
- }
2764
- const settings = row.searchConfig ? searchSettingsSchema.parse(unseal(row.searchConfig)) : null;
2765
- return settings ? {
2766
- ...settings,
2767
- auth: searchAuthentication(settings)
2768
- } : null;
2769
- }
2770
- async function broker() {
2771
- if (config.KANVIL_BROKER_PROVIDER === "postgres") return null;
2772
- if (config.KANVIL_BROKER_PROVIDER === "rabbitmq") return brokerSettingsSchema.parse({
2773
- url: config.RABBITMQ_URL,
2774
- exchange: config.RABBITMQ_EXCHANGE
2775
- });
2776
- const row = await read();
2777
- return row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
2778
- }
2779
- async function resolveBroker(settings, db = database.db) {
2780
- settings = brokerSettingsSchema.strip().parse(settings);
2781
- const row = await read(db), old = row.brokerConfig ? brokerSettingsSchema.parse(unseal(row.brokerConfig)) : null;
2782
- const url = settings.url || old?.url;
2783
- if (!url) return fail(400, "missing_secret", "Provide the RabbitMQ connection URL");
2784
- return {
2785
- ...settings,
2786
- url
2787
- };
2788
- }
2789
- async function authentication() {
2790
- return authenticationPolicySchema.strip().parse(await read());
2791
- }
2792
- async function update(value, actorId, oidcIssuer) {
2793
- const input = instanceUpdateSchema.parse(value);
2794
- return database.db.transaction(async (tx) => {
2795
- await tx.execute(sql`SELECT pg_advisory_xact_lock(7361083)`);
2796
- const current = await read(tx);
2797
- assertVersion(current, input.expectedVersion, "instance settings", "saving");
2798
- if (input.authentication && !input.authentication.emailPasswordEnabled) {
2799
- if (!config.OIDC_DISCOVERY_URL || !config.OIDC_CLIENT_ID || !config.OIDC_CLIENT_SECRET) fail(409, "oidc_required", "Configure an OIDC provider before disabling password sign-in");
2800
- if (!(actorId ? await tx.select({
2801
- idToken: account.idToken,
2802
- accountId: account.accountId
2803
- }).from(account).where(and(eq(account.userId, actorId), eq(account.providerId, "oidc"))) : []).some((binding) => matchesOIDCBinding(binding, oidcIssuer, config.OIDC_CLIENT_ID))) fail(409, "oidc_binding_required", "Link your OIDC account in User settings using the configured provider before disabling password sign-in");
2804
- }
2805
- let storageId = current.storageBackendId, searchConfig = current.searchConfig, brokerConfig = current.brokerConfig;
2806
- if (!config.KANVIL_STORAGE_PROVIDER) storageId = input.storage.provider === "s3" ? (await saveBackend(input.storage, tx)).id : null;
2807
- if (!config.KANVIL_SEARCH_PROVIDER) {
2808
- if (input.search.provider === "postgres") searchConfig = null;
2809
- else searchConfig = seal(await resolveSearch(input.search, tx));
2810
- }
2811
- if (!config.KANVIL_BROKER_PROVIDER && input.broker) brokerConfig = input.broker.provider === "postgres" ? null : seal(await resolveBroker(input.broker, tx));
2812
- await tx.update(instanceSettings).set({
2813
- ...input.authentication,
2814
- name: input.name,
2815
- description: input.description,
2816
- storageBackendId: storageId,
2817
- searchConfig,
2818
- brokerConfig,
2819
- version: current.version + 1
2820
- }).where(eq(instanceSettings.id, 1));
2821
- });
2822
- }
2823
- return {
2824
- read,
2825
- authentication,
2826
- storage,
2827
- search,
2828
- backend,
2829
- saveBackend,
2830
- resolveS3,
2831
- resolveSearch,
2832
- broker,
2833
- resolveBroker,
2834
- update,
2835
- config
2836
- };
2837
- }
2838
- //#endregion
2839
- //#region src/auth.ts
2840
- function createAuth(database, config) {
2841
- const instance = createInstance(database, config);
2842
- return betterAuth({
2843
- appName: "Kanvil",
2844
- baseURL: config.KANVIL_URL,
2845
- secret: config.BETTER_AUTH_SECRET,
2846
- database: drizzleAdapter(database.db, {
2847
- provider: "pg",
2848
- schema: auth_schema_exports,
2849
- transaction: true
2850
- }),
2851
- trustedOrigins: config.trustedOrigins,
2852
- emailAndPassword: {
2853
- enabled: true,
2854
- disableSignUp: true,
2855
- minPasswordLength: 12
2856
- },
2857
- account: { accountLinking: {
2858
- enabled: true,
2859
- disableImplicitLinking: true,
2860
- allowDifferentEmails: true
2861
- } },
2862
- onAPIError: { errorURL: `${config.KANVIL_URL}/login` },
2863
- hooks: { before: createAuthMiddleware(async (ctx) => {
2864
- if (ctx.path === "/sign-in/email" && !(await instance.authentication()).emailPasswordEnabled) throw new APIError("FORBIDDEN", {
2865
- code: "password_sign_in_disabled",
2866
- message: "Use your identity provider to sign in"
2867
- });
2868
- }) },
2869
- user: { validateUserInfo: async ({ user, source }) => {
2870
- if (source.method !== "oauth") return void 0;
2871
- if (source.oauth?.providerId !== "oidc" || !user.email || user.emailVerified !== true) return {
2872
- error: "email_not_verified",
2873
- errorDescription: "The identity provider must verify your email address"
2874
- };
2875
- if (source.action === "create-user" && !(await instance.authentication()).ssoAutoProvision) return {
2876
- error: "signup_disabled",
2877
- errorDescription: "Ask an administrator to create and link your account"
2878
- };
2879
- } },
2880
- telemetry: { enabled: false },
2881
- plugins: [
2882
- admin(),
2883
- apiKey({
2884
- defaultPrefix: "kv_",
2885
- enableMetadata: true,
2886
- maximumNameLength: 100,
2887
- keyExpiration: {
2888
- minExpiresIn: 1 / 24,
2889
- maxExpiresIn: 365
2890
- }
2891
- }),
2892
- expo(),
2893
- jwt(),
2894
- mcp({
2895
- loginPage: "/login",
2896
- consentPage: "/authorize",
2897
- resource: `${config.KANVIL_URL}/mcp`,
2898
- scopes: [
2899
- "openid",
2900
- "profile",
2901
- "offline_access",
2902
- "kanvil:read",
2903
- "kanvil:write",
2904
- "kanvil:claim",
2905
- "kanvil:admin"
2906
- ]
2907
- }),
2908
- cimd({
2909
- fetchClientMetadataResource,
2910
- metadataProfile: "mcp-2026-07-28"
2911
- }),
2912
- genericOAuth({ config: config.OIDC_DISCOVERY_URL && config.OIDC_CLIENT_ID && config.OIDC_CLIENT_SECRET ? [{
2913
- providerId: "oidc",
2914
- discoveryUrl: config.OIDC_DISCOVERY_URL,
2915
- requireIdTokenVerification: true,
2916
- getUserInfo: createOIDCUserInfo(config),
2917
- clientId: config.OIDC_CLIENT_ID,
2918
- clientSecret: config.OIDC_CLIENT_SECRET,
2919
- scopes: [
2920
- "openid",
2921
- "profile",
2922
- "email"
2923
- ],
2924
- pkce: true,
2925
- disableSignUp: false
2926
- }] : [] })
2927
- ]
2928
- });
2929
- }
2930
- //#endregion
2931
- export { pushDevices as A, memberSchema as B, instanceSettings as C, profilePhotos as D, notifications as E, workspaces as F, pushDeviceInput as G, notificationPreferencesSchema as H, ApiError as I, matchesOIDCBinding as J, roles as K, errorSchema as L, subscriptions as M, tickets as N, projects as O, workspaceMembers as P, hasAdminRole as R, events as S, notificationPreferences as T, okSchema as U, notificationPreferencesInput as V, operations as W, attachmentContent as _, fail as a, deliveryCheckpoints as b, requireRow as c, createTicketIndex as d, createBlobStorage as f, createPollingJob as g, createObjectStore as h, createDomain as i, storageBackends as j, pushDeliveries as k, projectRoleSQL as l, imageResponse as m, createInstance as n, json as o, decodeImage as p, scopes as q, assertVersion as r, one as s, createAuth as t, createTicketSearch as u, attachments as v, members as w, eventClock as x, blobObjects as y, isOperation as z };