@dyrected/core 2.5.53 → 2.5.56

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 (409) hide show
  1. package/dist/{app-config-oOarB6Fw.d.cts → app-config-Bxg_DVxv.d.cts} +3 -1
  2. package/dist/{app-config-oOarB6Fw.d.ts → app-config-Bxg_DVxv.d.ts} +3 -1
  3. package/dist/index.d.cts +2 -2
  4. package/dist/index.d.ts +2 -2
  5. package/dist/server.d.cts +1 -1
  6. package/dist/server.d.ts +1 -1
  7. package/package.json +1 -1
  8. package/dist/__tests__/admin-auth.test.d.ts +0 -2
  9. package/dist/__tests__/admin-auth.test.d.ts.map +0 -1
  10. package/dist/__tests__/admin-auth.test.js +0 -440
  11. package/dist/__tests__/admin-auth.test.js.map +0 -1
  12. package/dist/__tests__/app.test.d.ts +0 -2
  13. package/dist/__tests__/app.test.d.ts.map +0 -1
  14. package/dist/__tests__/app.test.js +0 -27
  15. package/dist/__tests__/app.test.js.map +0 -1
  16. package/dist/__tests__/config.test.d.ts +0 -2
  17. package/dist/__tests__/config.test.d.ts.map +0 -1
  18. package/dist/__tests__/config.test.js +0 -91
  19. package/dist/__tests__/config.test.js.map +0 -1
  20. package/dist/__tests__/deleteMany.test.d.ts +0 -2
  21. package/dist/__tests__/deleteMany.test.d.ts.map +0 -1
  22. package/dist/__tests__/deleteMany.test.js +0 -75
  23. package/dist/__tests__/deleteMany.test.js.map +0 -1
  24. package/dist/__tests__/depth.test.d.ts +0 -2
  25. package/dist/__tests__/depth.test.d.ts.map +0 -1
  26. package/dist/__tests__/depth.test.js +0 -81
  27. package/dist/__tests__/depth.test.js.map +0 -1
  28. package/dist/__tests__/dynamic-options.test.d.ts +0 -2
  29. package/dist/__tests__/dynamic-options.test.d.ts.map +0 -1
  30. package/dist/__tests__/dynamic-options.test.js +0 -132
  31. package/dist/__tests__/dynamic-options.test.js.map +0 -1
  32. package/dist/__tests__/email.service.test.d.ts +0 -2
  33. package/dist/__tests__/email.service.test.d.ts.map +0 -1
  34. package/dist/__tests__/email.service.test.js +0 -46
  35. package/dist/__tests__/email.service.test.js.map +0 -1
  36. package/dist/__tests__/field-inference.test-types.d.ts +0 -24
  37. package/dist/__tests__/field-inference.test-types.d.ts.map +0 -1
  38. package/dist/__tests__/field-inference.test-types.js +0 -87
  39. package/dist/__tests__/field-inference.test-types.js.map +0 -1
  40. package/dist/__tests__/fixtures/editorial-workflow.d.ts +0 -9
  41. package/dist/__tests__/fixtures/editorial-workflow.d.ts.map +0 -1
  42. package/dist/__tests__/fixtures/editorial-workflow.js +0 -12
  43. package/dist/__tests__/fixtures/editorial-workflow.js.map +0 -1
  44. package/dist/__tests__/hooks.test.d.ts +0 -2
  45. package/dist/__tests__/hooks.test.d.ts.map +0 -1
  46. package/dist/__tests__/hooks.test.js +0 -320
  47. package/dist/__tests__/hooks.test.js.map +0 -1
  48. package/dist/__tests__/mocks.d.ts +0 -74
  49. package/dist/__tests__/mocks.d.ts.map +0 -1
  50. package/dist/__tests__/mocks.js +0 -159
  51. package/dist/__tests__/mocks.js.map +0 -1
  52. package/dist/__tests__/openapi.test.d.ts +0 -2
  53. package/dist/__tests__/openapi.test.d.ts.map +0 -1
  54. package/dist/__tests__/openapi.test.js +0 -95
  55. package/dist/__tests__/openapi.test.js.map +0 -1
  56. package/dist/__tests__/parse-sort.test.d.ts +0 -2
  57. package/dist/__tests__/parse-sort.test.d.ts.map +0 -1
  58. package/dist/__tests__/parse-sort.test.js +0 -24
  59. package/dist/__tests__/parse-sort.test.js.map +0 -1
  60. package/dist/__tests__/preferences.test.d.ts +0 -2
  61. package/dist/__tests__/preferences.test.d.ts.map +0 -1
  62. package/dist/__tests__/preferences.test.js +0 -168
  63. package/dist/__tests__/preferences.test.js.map +0 -1
  64. package/dist/__tests__/router.test.d.ts +0 -2
  65. package/dist/__tests__/router.test.d.ts.map +0 -1
  66. package/dist/__tests__/router.test.js +0 -52
  67. package/dist/__tests__/router.test.js.map +0 -1
  68. package/dist/__tests__/where.test.d.ts +0 -2
  69. package/dist/__tests__/where.test.d.ts.map +0 -1
  70. package/dist/__tests__/where.test.js +0 -105
  71. package/dist/__tests__/where.test.js.map +0 -1
  72. package/dist/__tests__/workflow.test.d.ts +0 -2
  73. package/dist/__tests__/workflow.test.d.ts.map +0 -1
  74. package/dist/__tests__/workflow.test.js +0 -497
  75. package/dist/__tests__/workflow.test.js.map +0 -1
  76. package/dist/app-B2tg7Djj.d.cts +0 -1575
  77. package/dist/app-B2tg7Djj.d.ts +0 -1575
  78. package/dist/app-BElen1tP.d.cts +0 -1690
  79. package/dist/app-BElen1tP.d.ts +0 -1690
  80. package/dist/app-BOrsS7Tz.d.cts +0 -1771
  81. package/dist/app-BOrsS7Tz.d.ts +0 -1771
  82. package/dist/app-Bh4_Opv0.d.cts +0 -1522
  83. package/dist/app-Bh4_Opv0.d.ts +0 -1522
  84. package/dist/app-BibuoHQG.d.cts +0 -1764
  85. package/dist/app-BibuoHQG.d.ts +0 -1764
  86. package/dist/app-Bv9gaDAN.d.cts +0 -561
  87. package/dist/app-Bv9gaDAN.d.ts +0 -561
  88. package/dist/app-BvG3bRc8.d.cts +0 -419
  89. package/dist/app-BvG3bRc8.d.ts +0 -419
  90. package/dist/app-C-HgyliB.d.cts +0 -1694
  91. package/dist/app-C-HgyliB.d.ts +0 -1694
  92. package/dist/app-C3B9N1KR.d.cts +0 -1522
  93. package/dist/app-C3B9N1KR.d.ts +0 -1522
  94. package/dist/app-Cc2mG3zi.d.cts +0 -373
  95. package/dist/app-Cc2mG3zi.d.ts +0 -373
  96. package/dist/app-Ctfmo2Re.d.cts +0 -1692
  97. package/dist/app-Ctfmo2Re.d.ts +0 -1692
  98. package/dist/app-D0ffogDd.d.cts +0 -1690
  99. package/dist/app-D0ffogDd.d.ts +0 -1690
  100. package/dist/app-D2uR47gK.d.cts +0 -1690
  101. package/dist/app-D2uR47gK.d.ts +0 -1690
  102. package/dist/app-DDJJa0ep.d.cts +0 -1621
  103. package/dist/app-DDJJa0ep.d.ts +0 -1621
  104. package/dist/app-DO1s9YW1.d.cts +0 -1621
  105. package/dist/app-DO1s9YW1.d.ts +0 -1621
  106. package/dist/app-DTP3-9PJ.d.cts +0 -561
  107. package/dist/app-DTP3-9PJ.d.ts +0 -561
  108. package/dist/app-DZt5nslu.d.cts +0 -374
  109. package/dist/app-DZt5nslu.d.ts +0 -377
  110. package/dist/app-DbKDGYTI.d.cts +0 -566
  111. package/dist/app-DbKDGYTI.d.ts +0 -566
  112. package/dist/app-Dk3SzV1a.d.cts +0 -374
  113. package/dist/app-Dk3SzV1a.d.ts +0 -374
  114. package/dist/app-DnX9mNsh.d.cts +0 -374
  115. package/dist/app-DnX9mNsh.d.ts +0 -374
  116. package/dist/app-DqRO-CMi.d.cts +0 -1457
  117. package/dist/app-DqRO-CMi.d.ts +0 -1457
  118. package/dist/app-DvaFpOtj.d.cts +0 -398
  119. package/dist/app-DvaFpOtj.d.ts +0 -398
  120. package/dist/app-FGzip4XM.d.cts +0 -1563
  121. package/dist/app-FGzip4XM.d.ts +0 -1563
  122. package/dist/app-KApbf4XL.d.cts +0 -362
  123. package/dist/app-KApbf4XL.d.ts +0 -362
  124. package/dist/app-T0alZAE0.d.cts +0 -383
  125. package/dist/app-T0alZAE0.d.ts +0 -383
  126. package/dist/app-aW2FMuYM.d.cts +0 -1759
  127. package/dist/app-aW2FMuYM.d.ts +0 -1759
  128. package/dist/app-config-2vF6W56z.d.cts +0 -1335
  129. package/dist/app-config-2vF6W56z.d.ts +0 -1335
  130. package/dist/app-config-3XBnu9Xz.d.cts +0 -1379
  131. package/dist/app-config-3XBnu9Xz.d.ts +0 -1379
  132. package/dist/app-config-BLMX-9MN.d.cts +0 -1045
  133. package/dist/app-config-BLMX-9MN.d.ts +0 -1045
  134. package/dist/app-config-CaEQH0Kb.d.cts +0 -1383
  135. package/dist/app-config-CaEQH0Kb.d.ts +0 -1383
  136. package/dist/app-config-DLK6bCaP.d.cts +0 -1390
  137. package/dist/app-config-DLK6bCaP.d.ts +0 -1390
  138. package/dist/app-oQt5-9MU.d.cts +0 -1560
  139. package/dist/app-oQt5-9MU.d.ts +0 -1560
  140. package/dist/app-rZj1VFer.d.cts +0 -1621
  141. package/dist/app-rZj1VFer.d.ts +0 -1621
  142. package/dist/app-wo82JRHl.d.cts +0 -445
  143. package/dist/app-wo82JRHl.d.ts +0 -445
  144. package/dist/app.d.ts +0 -21
  145. package/dist/app.d.ts.map +0 -1
  146. package/dist/app.js +0 -56
  147. package/dist/app.js.map +0 -1
  148. package/dist/auth/jexl.d.ts +0 -10
  149. package/dist/auth/jexl.d.ts.map +0 -1
  150. package/dist/auth/jexl.js +0 -22
  151. package/dist/auth/jexl.js.map +0 -1
  152. package/dist/auth/password.d.ts +0 -10
  153. package/dist/auth/password.d.ts.map +0 -1
  154. package/dist/auth/password.js +0 -28
  155. package/dist/auth/password.js.map +0 -1
  156. package/dist/auth/token.d.ts +0 -22
  157. package/dist/auth/token.d.ts.map +0 -1
  158. package/dist/auth/token.js +0 -40
  159. package/dist/auth/token.js.map +0 -1
  160. package/dist/chunk-22JTWD74.js +0 -1784
  161. package/dist/chunk-23URSKPI.js +0 -2371
  162. package/dist/chunk-2JMA3M5S.js +0 -2475
  163. package/dist/chunk-3FZEUK36.js +0 -2470
  164. package/dist/chunk-3VUH2MNW.js +0 -1903
  165. package/dist/chunk-44O2LDPT.js +0 -1513
  166. package/dist/chunk-45JDQDA2.js +0 -1899
  167. package/dist/chunk-4C4L6SD2.js +0 -1524
  168. package/dist/chunk-4EDMZAM5.js +0 -2692
  169. package/dist/chunk-52T4RCFU.js +0 -1972
  170. package/dist/chunk-6ZXEGKT6.js +0 -619
  171. package/dist/chunk-7GGHK75W.js +0 -1835
  172. package/dist/chunk-ALNQINX7.js +0 -2496
  173. package/dist/chunk-B6KDKYXB.js +0 -2502
  174. package/dist/chunk-BEONZV6M.js +0 -1951
  175. package/dist/chunk-C4GEQ5RP.js +0 -1993
  176. package/dist/chunk-CQDVPMEU.js +0 -613
  177. package/dist/chunk-DOJHZ7XN.js +0 -2394
  178. package/dist/chunk-EH7G3LLL.js +0 -2065
  179. package/dist/chunk-EXXOPW3I.js +0 -2483
  180. package/dist/chunk-F4ZC6TXR.js +0 -2065
  181. package/dist/chunk-FCXE5CVC.js +0 -1528
  182. package/dist/chunk-FDQYPPG3.js +0 -2698
  183. package/dist/chunk-FURYJ254.mjs +0 -33
  184. package/dist/chunk-G2QQLBHW.js +0 -24
  185. package/dist/chunk-GM4WW6IE.js +0 -66
  186. package/dist/chunk-GZODLJ3C.js +0 -1861
  187. package/dist/chunk-IIY3E7Q7.js +0 -1996
  188. package/dist/chunk-JJN4J5NS.js +0 -2069
  189. package/dist/chunk-MRUHCNDM.js +0 -1940
  190. package/dist/chunk-NKDX67AW.js +0 -2698
  191. package/dist/chunk-NYUB4AHZ.js +0 -1951
  192. package/dist/chunk-OD4SLW7H.js +0 -1930
  193. package/dist/chunk-OE5MH3NB.js +0 -2070
  194. package/dist/chunk-PH36WQYS.js +0 -1875
  195. package/dist/chunk-PKNFV7KE.js +0 -2469
  196. package/dist/chunk-SUGK7UYL.js +0 -311
  197. package/dist/chunk-TIJHR54M.js +0 -1879
  198. package/dist/chunk-TUUHGLB5.js +0 -2609
  199. package/dist/chunk-UBTRANFX.js +0 -2476
  200. package/dist/chunk-VJWUG2XK.js +0 -1951
  201. package/dist/chunk-W6KURRMW.js +0 -2471
  202. package/dist/chunk-Y4AYI52W.js +0 -631
  203. package/dist/chunk-YDOBB7MY.js +0 -1457
  204. package/dist/chunk-YNJ3YC4N.js +0 -2483
  205. package/dist/chunk-ZFAOBRHT.js +0 -2709
  206. package/dist/controllers/admin-auth.controller.d.ts +0 -68
  207. package/dist/controllers/admin-auth.controller.d.ts.map +0 -1
  208. package/dist/controllers/admin-auth.controller.js +0 -414
  209. package/dist/controllers/admin-auth.controller.js.map +0 -1
  210. package/dist/controllers/auth.controller.d.ts +0 -125
  211. package/dist/controllers/auth.controller.d.ts.map +0 -1
  212. package/dist/controllers/auth.controller.js +0 -323
  213. package/dist/controllers/auth.controller.js.map +0 -1
  214. package/dist/controllers/collection.controller.d.ts +0 -157
  215. package/dist/controllers/collection.controller.d.ts.map +0 -1
  216. package/dist/controllers/collection.controller.js +0 -779
  217. package/dist/controllers/collection.controller.js.map +0 -1
  218. package/dist/controllers/global.controller.d.ts +0 -17
  219. package/dist/controllers/global.controller.d.ts.map +0 -1
  220. package/dist/controllers/global.controller.js +0 -132
  221. package/dist/controllers/global.controller.js.map +0 -1
  222. package/dist/controllers/media.controller.d.ts +0 -38
  223. package/dist/controllers/media.controller.d.ts.map +0 -1
  224. package/dist/controllers/media.controller.js +0 -162
  225. package/dist/controllers/media.controller.js.map +0 -1
  226. package/dist/controllers/preview.controller.d.ts +0 -37
  227. package/dist/controllers/preview.controller.d.ts.map +0 -1
  228. package/dist/controllers/preview.controller.js +0 -48
  229. package/dist/controllers/preview.controller.js.map +0 -1
  230. package/dist/index-BKYugiYp.d.cts +0 -1988
  231. package/dist/index-BKYugiYp.d.ts +0 -1988
  232. package/dist/index-BSWCiAen.d.cts +0 -1983
  233. package/dist/index-BSWCiAen.d.ts +0 -1983
  234. package/dist/index-BfLDb3b0.d.cts +0 -1467
  235. package/dist/index-BfLDb3b0.d.ts +0 -1467
  236. package/dist/index-Bi4sX_eC.d.cts +0 -1491
  237. package/dist/index-Bi4sX_eC.d.ts +0 -1491
  238. package/dist/index-Bp7PDOYG.d.cts +0 -1750
  239. package/dist/index-Bp7PDOYG.d.ts +0 -1750
  240. package/dist/index-ByXooLy-.d.cts +0 -1762
  241. package/dist/index-ByXooLy-.d.ts +0 -1762
  242. package/dist/index-C9Vp1DCT.d.cts +0 -1563
  243. package/dist/index-C9Vp1DCT.d.ts +0 -1563
  244. package/dist/index-CFezd8p1.d.cts +0 -1986
  245. package/dist/index-CFezd8p1.d.ts +0 -1986
  246. package/dist/index-CdQwnbfh.d.cts +0 -1889
  247. package/dist/index-CdQwnbfh.d.ts +0 -1889
  248. package/dist/index-D38_E0gf.d.cts +0 -336
  249. package/dist/index-D38_E0gf.d.ts +0 -336
  250. package/dist/index-DR2z1rrf.d.cts +0 -1503
  251. package/dist/index-DR2z1rrf.d.ts +0 -1503
  252. package/dist/index-DTrpTru7.d.cts +0 -1881
  253. package/dist/index-DTrpTru7.d.ts +0 -1881
  254. package/dist/index-DfAmTZXk.d.cts +0 -1749
  255. package/dist/index-DfAmTZXk.d.ts +0 -1749
  256. package/dist/index-DfkPT4NR.d.cts +0 -1584
  257. package/dist/index-DfkPT4NR.d.ts +0 -1584
  258. package/dist/index-DzUlE5Hc.d.cts +0 -1751
  259. package/dist/index-DzUlE5Hc.d.ts +0 -1751
  260. package/dist/index-M_m6dXZV.d.cts +0 -1889
  261. package/dist/index-M_m6dXZV.d.ts +0 -1889
  262. package/dist/index-RylhgOwj.d.cts +0 -340
  263. package/dist/index-RylhgOwj.d.ts +0 -340
  264. package/dist/index-qhLus8ZO.d.cts +0 -1881
  265. package/dist/index-qhLus8ZO.d.ts +0 -1881
  266. package/dist/index-xB1lMg79.d.cts +0 -1456
  267. package/dist/index-xB1lMg79.d.ts +0 -1456
  268. package/dist/index.d.mts +0 -417
  269. package/dist/index.d.ts.map +0 -1
  270. package/dist/index.js.map +0 -1
  271. package/dist/index.mjs +0 -1974
  272. package/dist/middleware/auth.d.ts +0 -18
  273. package/dist/middleware/auth.d.ts.map +0 -1
  274. package/dist/middleware/auth.js +0 -45
  275. package/dist/middleware/auth.js.map +0 -1
  276. package/dist/password-ACI2YUGZ.js +0 -8
  277. package/dist/router.d.ts +0 -8
  278. package/dist/router.d.ts.map +0 -1
  279. package/dist/router.js +0 -625
  280. package/dist/router.js.map +0 -1
  281. package/dist/server.d.ts.map +0 -1
  282. package/dist/server.js.map +0 -1
  283. package/dist/services/audit.service.d.ts +0 -23
  284. package/dist/services/audit.service.d.ts.map +0 -1
  285. package/dist/services/audit.service.js +0 -28
  286. package/dist/services/audit.service.js.map +0 -1
  287. package/dist/services/defaults.service.d.ts +0 -8
  288. package/dist/services/defaults.service.d.ts.map +0 -1
  289. package/dist/services/defaults.service.js +0 -55
  290. package/dist/services/defaults.service.js.map +0 -1
  291. package/dist/services/email-template.d.ts +0 -46
  292. package/dist/services/email-template.d.ts.map +0 -1
  293. package/dist/services/email-template.js +0 -88
  294. package/dist/services/email-template.js.map +0 -1
  295. package/dist/services/email.service.d.ts +0 -33
  296. package/dist/services/email.service.d.ts.map +0 -1
  297. package/dist/services/email.service.js +0 -96
  298. package/dist/services/email.service.js.map +0 -1
  299. package/dist/services/media.service.d.ts +0 -20
  300. package/dist/services/media.service.d.ts.map +0 -1
  301. package/dist/services/media.service.js +0 -49
  302. package/dist/services/media.service.js.map +0 -1
  303. package/dist/services/population.service.d.ts +0 -20
  304. package/dist/services/population.service.d.ts.map +0 -1
  305. package/dist/services/population.service.js +0 -194
  306. package/dist/services/population.service.js.map +0 -1
  307. package/dist/token-7QG5DBME.mjs +0 -10
  308. package/dist/types/access.d.ts +0 -38
  309. package/dist/types/access.d.ts.map +0 -1
  310. package/dist/types/access.js +0 -2
  311. package/dist/types/access.js.map +0 -1
  312. package/dist/types/adapters.d.ts +0 -146
  313. package/dist/types/adapters.d.ts.map +0 -1
  314. package/dist/types/adapters.js +0 -2
  315. package/dist/types/adapters.js.map +0 -1
  316. package/dist/types/admin-auth.d.ts +0 -132
  317. package/dist/types/admin-auth.d.ts.map +0 -1
  318. package/dist/types/admin-auth.js +0 -2
  319. package/dist/types/admin-auth.js.map +0 -1
  320. package/dist/types/admin.d.ts +0 -65
  321. package/dist/types/admin.d.ts.map +0 -1
  322. package/dist/types/admin.js +0 -2
  323. package/dist/types/admin.js.map +0 -1
  324. package/dist/types/app-config.d.ts +0 -143
  325. package/dist/types/app-config.d.ts.map +0 -1
  326. package/dist/types/app-config.js +0 -2
  327. package/dist/types/app-config.js.map +0 -1
  328. package/dist/types/documents.d.ts +0 -65
  329. package/dist/types/documents.d.ts.map +0 -1
  330. package/dist/types/documents.js +0 -2
  331. package/dist/types/documents.js.map +0 -1
  332. package/dist/types/hooks.d.ts +0 -122
  333. package/dist/types/hooks.d.ts.map +0 -1
  334. package/dist/types/hooks.js +0 -2
  335. package/dist/types/hooks.js.map +0 -1
  336. package/dist/types/index.d.ts +0 -13
  337. package/dist/types/index.d.ts.map +0 -1
  338. package/dist/types/index.js +0 -13
  339. package/dist/types/index.js.map +0 -1
  340. package/dist/types/request.d.ts +0 -42
  341. package/dist/types/request.d.ts.map +0 -1
  342. package/dist/types/request.js +0 -2
  343. package/dist/types/request.js.map +0 -1
  344. package/dist/types/schema-config.d.ts +0 -303
  345. package/dist/types/schema-config.d.ts.map +0 -1
  346. package/dist/types/schema-config.js +0 -2
  347. package/dist/types/schema-config.js.map +0 -1
  348. package/dist/types/schema-core.d.ts +0 -294
  349. package/dist/types/schema-core.d.ts.map +0 -1
  350. package/dist/types/schema-core.js +0 -2
  351. package/dist/types/schema-core.js.map +0 -1
  352. package/dist/types/schema-inference.d.ts +0 -73
  353. package/dist/types/schema-inference.d.ts.map +0 -1
  354. package/dist/types/schema-inference.js +0 -2
  355. package/dist/types/schema-inference.js.map +0 -1
  356. package/dist/types/workflows.d.ts +0 -84
  357. package/dist/types/workflows.d.ts.map +0 -1
  358. package/dist/types/workflows.js +0 -7
  359. package/dist/types/workflows.js.map +0 -1
  360. package/dist/utils/admin-auth.d.ts +0 -5
  361. package/dist/utils/admin-auth.d.ts.map +0 -1
  362. package/dist/utils/admin-auth.js +0 -39
  363. package/dist/utils/admin-auth.js.map +0 -1
  364. package/dist/utils/config.d.ts +0 -8
  365. package/dist/utils/config.d.ts.map +0 -1
  366. package/dist/utils/config.js +0 -238
  367. package/dist/utils/config.js.map +0 -1
  368. package/dist/utils/hooks.d.ts +0 -41
  369. package/dist/utils/hooks.d.ts.map +0 -1
  370. package/dist/utils/hooks.js +0 -173
  371. package/dist/utils/hooks.js.map +0 -1
  372. package/dist/utils/openapi.d.ts +0 -6
  373. package/dist/utils/openapi.d.ts.map +0 -1
  374. package/dist/utils/openapi.js +0 -842
  375. package/dist/utils/openapi.js.map +0 -1
  376. package/dist/utils/parse-sort.d.ts +0 -7
  377. package/dist/utils/parse-sort.d.ts.map +0 -1
  378. package/dist/utils/parse-sort.js +0 -23
  379. package/dist/utils/parse-sort.js.map +0 -1
  380. package/dist/utils/parse-where.d.ts +0 -63
  381. package/dist/utils/parse-where.d.ts.map +0 -1
  382. package/dist/utils/parse-where.js +0 -196
  383. package/dist/utils/parse-where.js.map +0 -1
  384. package/dist/utils/readonly-db.d.ts +0 -9
  385. package/dist/utils/readonly-db.d.ts.map +0 -1
  386. package/dist/utils/readonly-db.js +0 -21
  387. package/dist/utils/readonly-db.js.map +0 -1
  388. package/dist/utils/setup-prompt.d.ts +0 -11
  389. package/dist/utils/setup-prompt.d.ts.map +0 -1
  390. package/dist/utils/setup-prompt.js +0 -863
  391. package/dist/utils/setup-prompt.js.map +0 -1
  392. package/dist/utils/swagger.d.ts +0 -11
  393. package/dist/utils/swagger.d.ts.map +0 -1
  394. package/dist/utils/swagger.js +0 -62
  395. package/dist/utils/swagger.js.map +0 -1
  396. package/dist/utils/upload-validation.d.ts +0 -23
  397. package/dist/utils/upload-validation.d.ts.map +0 -1
  398. package/dist/utils/upload-validation.js +0 -44
  399. package/dist/utils/upload-validation.js.map +0 -1
  400. package/dist/utils/where-sanitizer.d.ts +0 -10
  401. package/dist/utils/where-sanitizer.d.ts.map +0 -1
  402. package/dist/utils/where-sanitizer.js +0 -67
  403. package/dist/utils/where-sanitizer.js.map +0 -1
  404. package/dist/where-sanitizer-7Q4JXMX6.js +0 -57
  405. package/dist/where-sanitizer-DQIWTQZW.js +0 -50
  406. package/dist/workflows.d.ts +0 -54
  407. package/dist/workflows.d.ts.map +0 -1
  408. package/dist/workflows.js +0 -282
  409. package/dist/workflows.js.map +0 -1
@@ -1,2698 +0,0 @@
1
- // src/utils/config.ts
2
- var AUDIT_COLLECTION_SLUG = "__audit";
3
- var SYSTEM_FIELDS = [
4
- {
5
- name: "createdAt",
6
- type: "date",
7
- label: "Created At",
8
- admin: { readOnly: true, hidden: true }
9
- },
10
- {
11
- name: "updatedAt",
12
- type: "date",
13
- label: "Updated At",
14
- admin: { readOnly: true, hidden: true }
15
- },
16
- {
17
- name: "createdBy",
18
- type: "text",
19
- label: "Created By",
20
- admin: { readOnly: true, hidden: true }
21
- },
22
- {
23
- name: "updatedBy",
24
- type: "text",
25
- label: "Updated By",
26
- admin: { readOnly: true, hidden: true }
27
- }
28
- ];
29
- var AUDIT_COLLECTION = {
30
- slug: AUDIT_COLLECTION_SLUG,
31
- labels: { singular: "Audit Log", plural: "Audit Logs" },
32
- fields: [
33
- { name: "collection", type: "text", label: "Collection", required: true },
34
- { name: "documentId", type: "text", label: "Document ID" },
35
- { name: "operation", type: "select", label: "Operation", options: ["create", "update", "delete"], required: true },
36
- { name: "user", type: "text", label: "User ID" },
37
- { name: "timestamp", type: "date", label: "Timestamp", required: true },
38
- { name: "changes", type: "json", label: "Changes" }
39
- ],
40
- admin: { hidden: true }
41
- };
42
- function normalizeConfig(config) {
43
- const collections = config?.collections || [];
44
- const globals = config?.globals || [];
45
- const needsAudit = collections.some((col) => col.audit);
46
- const normalizedCollections = collections.map((col) => {
47
- let fields = col.fields || [];
48
- const existingFieldNames = new Set(fields.map((f) => f.name));
49
- if (col.auth) {
50
- if (!existingFieldNames.has("email")) {
51
- fields = [
52
- ...fields,
53
- {
54
- name: "email",
55
- type: "email",
56
- label: "Email",
57
- required: true,
58
- unique: true,
59
- promoted: true,
60
- access: {
61
- update: "!id"
62
- }
63
- }
64
- ];
65
- }
66
- if (!existingFieldNames.has("password")) {
67
- fields = [
68
- ...fields,
69
- {
70
- name: "password",
71
- type: "text",
72
- label: "Password",
73
- required: true,
74
- access: {
75
- update: "!id || user.id == id"
76
- }
77
- }
78
- ];
79
- }
80
- if (!existingFieldNames.has("roles")) {
81
- fields = [
82
- ...fields,
83
- {
84
- name: "roles",
85
- type: "select",
86
- label: "Roles",
87
- defaultValue: [],
88
- options: [
89
- { value: "admin", label: "Admin" },
90
- { value: "editor", label: "Editor" },
91
- { value: "viewer", label: "Viewer" }
92
- ],
93
- access: {
94
- update: "user.roles && 'admin' in user.roles"
95
- }
96
- }
97
- ];
98
- }
99
- fields = fields.map((field) => {
100
- if (field.name === "email") {
101
- return {
102
- ...field,
103
- access: {
104
- ...field.access || {},
105
- update: "!id"
106
- }
107
- };
108
- }
109
- if (field.name === "password") {
110
- return {
111
- ...field,
112
- admin: { ...field.admin || {} },
113
- access: {
114
- ...field.access || {},
115
- update: "!id || user.id == id"
116
- }
117
- };
118
- }
119
- if (field.name === "roles") {
120
- return {
121
- ...field,
122
- access: {
123
- ...field.access || {},
124
- // Must be an admin; cannot edit own roles (no self-elevation).
125
- update: "user.roles && 'admin' in user.roles && user.id != id"
126
- }
127
- };
128
- }
129
- return field;
130
- });
131
- }
132
- const updatedFieldNames = new Set(fields.map((f) => f.name));
133
- const fieldsToInject = SYSTEM_FIELDS.filter((f) => !updatedFieldNames.has(f.name));
134
- return {
135
- ...col,
136
- fields: [...fields, ...fieldsToInject]
137
- };
138
- });
139
- const hasAuditCollection = normalizedCollections.some((col) => col.slug === AUDIT_COLLECTION_SLUG);
140
- return {
141
- ...config,
142
- collections: [...normalizedCollections, ...needsAudit && !hasAuditCollection ? [AUDIT_COLLECTION] : []],
143
- globals
144
- };
145
- }
146
-
147
- // src/utils/hooks.ts
148
- async function runCollectionHooks(hooks, args, options = {}) {
149
- if (!hooks || hooks.length === 0) {
150
- return args.data ?? args.doc ?? void 0;
151
- }
152
- let currentPayload = args.data ?? args.doc ?? void 0;
153
- for (const hook of hooks) {
154
- try {
155
- const result = await hook({
156
- ...args,
157
- data: args.data !== void 0 ? currentPayload : void 0,
158
- doc: args.doc !== void 0 ? currentPayload : void 0
159
- });
160
- if (result !== void 0) {
161
- currentPayload = result;
162
- }
163
- } catch (err) {
164
- if (options.isolated) {
165
- console.error("[dyrected/core] Side-effect hook failed (error isolated \u2014 DB write was successful):", err);
166
- } else {
167
- throw err;
168
- }
169
- }
170
- }
171
- return currentPayload;
172
- }
173
- async function executeFieldBeforeChange(fields, data, originalDoc, user, db) {
174
- if (!data || typeof data !== "object") return data;
175
- const result = { ...data };
176
- for (const field of fields) {
177
- if (!field.name) continue;
178
- const value = result[field.name];
179
- const origValue = originalDoc?.[field.name];
180
- let updatedValue = value;
181
- if (field.hooks?.beforeChange) {
182
- for (const hook of field.hooks.beforeChange) {
183
- updatedValue = await hook({
184
- value: updatedValue,
185
- originalDoc: originalDoc ?? void 0,
186
- data: result,
187
- user,
188
- db
189
- });
190
- }
191
- result[field.name] = updatedValue;
192
- }
193
- if (updatedValue !== void 0 && updatedValue !== null) {
194
- if (field.type === "object" && field.fields) {
195
- result[field.name] = await executeFieldBeforeChange(
196
- field.fields,
197
- updatedValue,
198
- origValue,
199
- user,
200
- db
201
- );
202
- } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
203
- const arrayResult = [];
204
- for (let i = 0; i < updatedValue.length; i++) {
205
- const item = updatedValue[i];
206
- const origItem = Array.isArray(origValue) ? origValue[i] : null;
207
- arrayResult.push(
208
- await executeFieldBeforeChange(field.fields, item, origItem, user, db)
209
- );
210
- }
211
- result[field.name] = arrayResult;
212
- } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
213
- const blocksResult = [];
214
- for (let i = 0; i < updatedValue.length; i++) {
215
- const blockData = updatedValue[i];
216
- const origBlock = Array.isArray(origValue) ? origValue[i] : null;
217
- const blockConfig = field.blocks.find(
218
- (b) => b.slug === blockData.blockType
219
- );
220
- if (blockConfig) {
221
- blocksResult.push(
222
- await executeFieldBeforeChange(
223
- blockConfig.fields,
224
- blockData,
225
- origBlock,
226
- user,
227
- db
228
- )
229
- );
230
- } else {
231
- blocksResult.push(blockData);
232
- }
233
- }
234
- result[field.name] = blocksResult;
235
- }
236
- }
237
- }
238
- return result;
239
- }
240
- async function executeFieldAfterRead(fields, doc, user, db) {
241
- if (!doc || typeof doc !== "object") return doc;
242
- const result = { ...doc };
243
- for (const field of fields) {
244
- if (!field.name) continue;
245
- const value = result[field.name];
246
- let updatedValue = value;
247
- if (field.hooks?.afterRead) {
248
- for (const hook of field.hooks.afterRead) {
249
- updatedValue = await hook({
250
- value: updatedValue,
251
- doc: result,
252
- user,
253
- db
254
- });
255
- }
256
- result[field.name] = updatedValue;
257
- }
258
- if (updatedValue !== void 0 && updatedValue !== null) {
259
- if (field.type === "object" && field.fields) {
260
- result[field.name] = await executeFieldAfterRead(
261
- field.fields,
262
- updatedValue,
263
- user,
264
- db
265
- );
266
- } else if (field.type === "array" && field.fields && Array.isArray(updatedValue)) {
267
- const arrayResult = [];
268
- for (const item of updatedValue) {
269
- arrayResult.push(
270
- await executeFieldAfterRead(
271
- field.fields,
272
- item,
273
- user,
274
- db
275
- )
276
- );
277
- }
278
- result[field.name] = arrayResult;
279
- } else if (field.type === "blocks" && field.blocks && Array.isArray(updatedValue)) {
280
- const blocksResult = [];
281
- for (const blockData of updatedValue) {
282
- const typedBlock = blockData;
283
- const blockConfig = field.blocks.find(
284
- (b) => b.slug === typedBlock.blockType
285
- );
286
- if (blockConfig) {
287
- blocksResult.push(
288
- await executeFieldAfterRead(
289
- blockConfig.fields,
290
- typedBlock,
291
- user,
292
- db
293
- )
294
- );
295
- } else {
296
- blocksResult.push(blockData);
297
- }
298
- }
299
- result[field.name] = blocksResult;
300
- }
301
- }
302
- }
303
- return result;
304
- }
305
-
306
- // src/services/defaults.service.ts
307
- var DefaultsService = class {
308
- /**
309
- * Recursively apply default values to a data object based on field definitions.
310
- */
311
- static apply(fields, data = {}) {
312
- const result = { ...data || {} };
313
- fields.forEach((field) => {
314
- if (field.type === "join") return;
315
- if (field.type === "row" && field.fields) {
316
- Object.assign(result, this.apply(field.fields, data));
317
- return;
318
- }
319
- if (!field.name) return;
320
- let value = result[field.name];
321
- if ((value === void 0 || value === null) && field.renameTo) {
322
- const legacyValue = result[field.renameTo];
323
- if (legacyValue !== void 0 && legacyValue !== null) {
324
- value = legacyValue;
325
- result[field.name] = legacyValue;
326
- }
327
- }
328
- if (value === void 0 || value === null) {
329
- if (field.defaultValue !== void 0) {
330
- result[field.name] = field.defaultValue;
331
- } else {
332
- if (field.type === "boolean") result[field.name] = false;
333
- else if (field.type === "array") result[field.name] = [];
334
- else if (field.type === "multiSelect") result[field.name] = [];
335
- else if (field.type === "object") {
336
- result[field.name] = this.apply(field.fields || [], {});
337
- }
338
- }
339
- } else if (field.type === "object" && field.fields) {
340
- result[field.name] = this.apply(field.fields, value);
341
- } else if (field.type === "array" && field.fields && Array.isArray(value)) {
342
- result[field.name] = value.map((item) => this.apply(field.fields, item));
343
- }
344
- });
345
- return result;
346
- }
347
- };
348
-
349
- // src/services/population.service.ts
350
- var PopulationService = class {
351
- db;
352
- collections;
353
- constructor(db, collections) {
354
- this.db = db;
355
- this.collections = collections;
356
- }
357
- /**
358
- * Recursively populate relationship fields in a document or array of documents.
359
- */
360
- async populate(args) {
361
- const { data, fields, currentDepth = 0, maxDepth = 10 } = args;
362
- if (currentDepth >= maxDepth || !data) {
363
- return data;
364
- }
365
- if (Array.isArray(data)) {
366
- return Promise.all(data.map((item) => this.populate({ ...args, data: item })));
367
- }
368
- const populatedDoc = { ...data };
369
- for (const field of fields) {
370
- if (field.type === "join" && field.collection && field.on) {
371
- const targetCollection = this.collections.find((c) => c.slug === field.collection);
372
- if (targetCollection) {
373
- const docId = populatedDoc.id;
374
- if (docId) {
375
- const where = { [field.on]: { equals: docId } };
376
- const joinLimit = field.limit ?? 10;
377
- const result = await this.db.find({
378
- collection: field.collection,
379
- where,
380
- limit: joinLimit
381
- });
382
- const populatedDocs = await this.populate({
383
- data: result.docs.map((doc) => DefaultsService.apply(targetCollection.fields, doc)),
384
- fields: targetCollection.fields,
385
- currentDepth: currentDepth + 1,
386
- maxDepth
387
- });
388
- populatedDoc[field.name] = {
389
- docs: populatedDocs,
390
- hasNextPage: result.page * result.limit < result.total,
391
- totalDocs: result.total
392
- };
393
- }
394
- }
395
- continue;
396
- }
397
- if (field.type === "row" && field.fields) {
398
- const rowPopulated = await this.populate({ data, fields: field.fields, currentDepth, maxDepth });
399
- Object.assign(populatedDoc, rowPopulated);
400
- continue;
401
- }
402
- if (!field.name) continue;
403
- const value = populatedDoc[field.name];
404
- if (field.type === "relationship" && field.relationTo && value) {
405
- const relatedCollection = this.collections.find((c) => c.slug === field.relationTo);
406
- if (!relatedCollection) continue;
407
- if (Array.isArray(value)) {
408
- populatedDoc[field.name] = await Promise.all(
409
- value.map(async (id) => {
410
- if (!id) return id;
411
- let doc = id;
412
- if (typeof id === "string") {
413
- doc = await this.db.findOne({ collection: field.relationTo, id });
414
- }
415
- if (!doc || typeof doc !== "object") return id;
416
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
417
- return this.populate({
418
- data: docWithDefaults,
419
- fields: relatedCollection.fields,
420
- currentDepth: currentDepth + 1,
421
- maxDepth
422
- });
423
- })
424
- );
425
- } else if (value) {
426
- let doc = value;
427
- if (typeof value === "string") {
428
- doc = await this.db.findOne({ collection: field.relationTo, id: value });
429
- }
430
- if (doc && typeof doc === "object") {
431
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
432
- populatedDoc[field.name] = await this.populate({
433
- data: docWithDefaults,
434
- fields: relatedCollection.fields,
435
- currentDepth: currentDepth + 1,
436
- maxDepth
437
- });
438
- }
439
- }
440
- }
441
- if (field.type === "url" && value && typeof value === "object" && value.type === "internal" && value.relationTo && value.value) {
442
- const relatedCollection = this.collections.find((c) => c.slug === value.relationTo);
443
- if (relatedCollection) {
444
- const doc = await this.db.findOne({ collection: value.relationTo, id: value.value });
445
- if (doc && typeof doc === "object") {
446
- const docWithDefaults = DefaultsService.apply(relatedCollection.fields, doc);
447
- const populatedDocValue = await this.populate({
448
- data: docWithDefaults,
449
- fields: relatedCollection.fields,
450
- currentDepth: currentDepth + 1,
451
- maxDepth
452
- });
453
- const identifier = docWithDefaults.slug || docWithDefaults.id;
454
- const resolvedUrl = `/collections/${value.relationTo}/${identifier}`;
455
- populatedDoc[field.name] = {
456
- ...value,
457
- url: resolvedUrl,
458
- doc: populatedDocValue
459
- };
460
- }
461
- }
462
- }
463
- if ((field.type === "array" || field.type === "object") && field.fields && value) {
464
- populatedDoc[field.name] = await this.populate({
465
- data: value,
466
- fields: field.fields,
467
- currentDepth,
468
- // Nested fields don't consume depth, only relationships do
469
- maxDepth
470
- });
471
- }
472
- if (field.type === "blocks" && field.blocks && Array.isArray(value)) {
473
- populatedDoc[field.name] = await Promise.all(
474
- value.map(async (blockData) => {
475
- const blockConfig = field.blocks.find((b) => b.slug === blockData.blockType);
476
- if (!blockConfig) return blockData;
477
- return this.populate({
478
- data: blockData,
479
- fields: blockConfig.fields,
480
- currentDepth,
481
- maxDepth
482
- });
483
- })
484
- );
485
- }
486
- }
487
- return populatedDoc;
488
- }
489
- /**
490
- * Helper to populate a PaginatedResult
491
- */
492
- async populateResult(result, fields, maxDepth) {
493
- if (maxDepth <= 0) return result;
494
- const populatedDocs = await this.populate({
495
- data: result.docs,
496
- fields,
497
- currentDepth: 0,
498
- maxDepth
499
- });
500
- return {
501
- ...result,
502
- docs: populatedDocs
503
- };
504
- }
505
- };
506
-
507
- // src/services/audit.service.ts
508
- var AuditService = class {
509
- /**
510
- * Writes a single entry to the __audit collection.
511
- * Called without await — runs asynchronously and never blocks the primary operation.
512
- */
513
- static async log(db, args) {
514
- try {
515
- await db.create({
516
- collection: "__audit",
517
- data: {
518
- collection: args.collection,
519
- documentId: args.documentId ?? null,
520
- operation: args.operation,
521
- user: args.user?.id ?? null,
522
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
523
- changes: JSON.stringify({
524
- before: args.before ?? null,
525
- after: args.after ?? null
526
- })
527
- }
528
- });
529
- } catch (err) {
530
- console.error("[dyrected/audit] Failed to write audit log:", err);
531
- }
532
- }
533
- };
534
-
535
- // src/auth/password.ts
536
- import { promisify } from "util";
537
- import { scrypt, randomBytes, timingSafeEqual } from "crypto";
538
- var scryptAsync = promisify(scrypt);
539
- var SALT_LEN = 16;
540
- var KEY_LEN = 64;
541
- async function hashPassword(plain) {
542
- const salt = randomBytes(SALT_LEN).toString("hex");
543
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
544
- return `${salt}:${derivedKey.toString("hex")}`;
545
- }
546
- async function verifyPassword(plain, stored) {
547
- const [salt, storedHash] = stored.split(":");
548
- if (!salt || !storedHash) return false;
549
- const derivedKey = await scryptAsync(plain, salt, KEY_LEN);
550
- const storedBuffer = Buffer.from(storedHash, "hex");
551
- if (derivedKey.length !== storedBuffer.length) return false;
552
- return timingSafeEqual(derivedKey, storedBuffer);
553
- }
554
-
555
- // src/utils/readonly-db.ts
556
- var WRITE_METHODS = ["create", "update", "delete", "updateGlobal", "execute"];
557
- function createReadonlyDb(db) {
558
- return new Proxy(db, {
559
- get(target, prop) {
560
- if (WRITE_METHODS.includes(prop)) {
561
- return () => {
562
- throw new Error(
563
- `[dyrected] Write operation "${String(prop)}" is not allowed in this hook phase. Use afterChange/afterDelete hooks for write operations.`
564
- );
565
- };
566
- }
567
- return Reflect.get(target, prop);
568
- }
569
- });
570
- }
571
-
572
- // src/controllers/collection.controller.ts
573
- var CollectionController = class {
574
- collection;
575
- constructor(collection) {
576
- this.collection = collection;
577
- }
578
- async find(c) {
579
- const config = c.get("config");
580
- const db = config.db;
581
- if (!db) return c.json({ message: "Database not configured" }, 500);
582
- const readonlyDb = createReadonlyDb(db);
583
- const limit = Number(c.req.query("limit")) || 10;
584
- const page = Number(c.req.query("page")) || 1;
585
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 2;
586
- const sort = c.req.query("sort") || void 0;
587
- const user = c.get("user");
588
- let where = void 0;
589
- const whereRaw = c.req.query("where");
590
- if (whereRaw) {
591
- try {
592
- where = JSON.parse(decodeURIComponent(whereRaw));
593
- } catch {
594
- }
595
- }
596
- const beforeReadResult = await runCollectionHooks(this.collection.hooks?.beforeRead, {
597
- req: c.req,
598
- query: where,
599
- user,
600
- db: readonlyDb
601
- });
602
- if (beforeReadResult !== void 0) {
603
- where = beforeReadResult;
604
- }
605
- let result = await db.find({
606
- collection: this.collection.slug,
607
- limit,
608
- page,
609
- sort,
610
- where
611
- });
612
- if (result.total === 0 && this.collection.initialData && !where && page === 1) {
613
- console.log(`[dyrected/core] Auto-seeding collection "${this.collection.slug}" from config.initialData`);
614
- for (const data of this.collection.initialData) {
615
- await db.create({ collection: this.collection.slug, data });
616
- }
617
- result = await db.find({
618
- collection: this.collection.slug,
619
- limit,
620
- page,
621
- sort,
622
- where
623
- });
624
- }
625
- result.docs = result.docs.map((doc) => DefaultsService.apply(this.collection.fields, doc));
626
- const processedDocs = [];
627
- for (const doc of result.docs) {
628
- const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
629
- doc,
630
- req: c.req,
631
- user,
632
- db: readonlyDb
633
- });
634
- const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
635
- processedDocs.push(docWithFieldHooks);
636
- }
637
- result.docs = processedDocs;
638
- if (depth > 0) {
639
- const populationService = new PopulationService(db, config.collections);
640
- result = await populationService.populateResult(result, this.collection.fields, depth);
641
- }
642
- return c.json(result);
643
- }
644
- async findOne(c) {
645
- const config = c.get("config");
646
- const db = config.db;
647
- if (!db) return c.json({ message: "Database not configured" }, 500);
648
- const readonlyDb = createReadonlyDb(db);
649
- const id = c.req.param("id");
650
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
651
- const user = c.get("user");
652
- if (!id) return c.json({ message: "Missing ID" }, 400);
653
- const doc = await db.findOne({ collection: this.collection.slug, id });
654
- if (!doc) return c.json({ message: "Not Found" }, 404);
655
- const docWithDefaults = DefaultsService.apply(this.collection.fields, doc);
656
- const docWithCollectionHooks = await runCollectionHooks(this.collection.hooks?.afterRead, {
657
- doc: docWithDefaults,
658
- req: c.req,
659
- user,
660
- db: readonlyDb
661
- });
662
- const docWithFieldHooks = await executeFieldAfterRead(this.collection.fields, docWithCollectionHooks, user, readonlyDb);
663
- if (depth > 0 && docWithFieldHooks) {
664
- const populationService = new PopulationService(db, config.collections);
665
- const populatedDoc = await populationService.populate({
666
- data: docWithFieldHooks,
667
- fields: this.collection.fields,
668
- currentDepth: 0,
669
- maxDepth: depth
670
- });
671
- return c.json(populatedDoc);
672
- }
673
- return c.json(docWithFieldHooks);
674
- }
675
- async create(c) {
676
- const config = c.get("config");
677
- const db = config.db;
678
- if (!db) return c.json({ message: "Database not configured" }, 500);
679
- const readonlyDb = createReadonlyDb(db);
680
- const contentType = c.req.header("Content-Type") || "";
681
- if (contentType.toLowerCase().includes("multipart/form-data")) {
682
- return this.upload(c);
683
- }
684
- const body = await c.req.json();
685
- const user = c.get("user");
686
- const now = (/* @__PURE__ */ new Date()).toISOString();
687
- let data = {
688
- ...body,
689
- createdAt: now,
690
- updatedAt: now,
691
- createdBy: user?.sub ?? null,
692
- updatedBy: user?.sub ?? null
693
- };
694
- if (this.collection.auth && data.password) {
695
- data.password = await hashPassword(data.password);
696
- }
697
- data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
698
- data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
699
- data,
700
- req: c.req,
701
- user,
702
- operation: "create",
703
- db: readonlyDb
704
- });
705
- const doc = await db.create({ collection: this.collection.slug, data });
706
- if (this.collection.audit && db) {
707
- AuditService.log(db, {
708
- operation: "create",
709
- collection: this.collection.slug,
710
- documentId: doc.id,
711
- user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
712
- before: null,
713
- after: doc
714
- });
715
- }
716
- await runCollectionHooks(this.collection.hooks?.afterChange, {
717
- doc,
718
- user,
719
- req: c.req,
720
- operation: "create",
721
- db
722
- }, { isolated: true });
723
- const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
724
- doc,
725
- req: c.req,
726
- user,
727
- db: readonlyDb
728
- });
729
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
730
- return c.json(finalDoc, 201);
731
- }
732
- async upload(c) {
733
- const config = c.get("config");
734
- const storage = config.storage;
735
- if (!storage) return c.json({ message: "Storage not configured" }, 500);
736
- const db = config.db;
737
- if (!db) return c.json({ message: "Database not configured" }, 500);
738
- const readonlyDb = createReadonlyDb(db);
739
- const formData = await c.req.formData();
740
- const file = formData.get("file");
741
- if (!file) return c.json({ message: "No file uploaded" }, 400);
742
- const buffer = new Uint8Array(await file.arrayBuffer());
743
- const siteId = c.get("siteId");
744
- const workspaceId = c.get("workspaceId");
745
- const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId;
746
- const fileData = await storage.upload({
747
- filename: file.name,
748
- buffer,
749
- mimeType: file.type,
750
- prefix
751
- });
752
- const otherData = {};
753
- formData.forEach((value, key) => {
754
- if (key !== "file" && typeof value === "string") {
755
- otherData[key] = value;
756
- }
757
- });
758
- const user = c.get("user");
759
- const now = (/* @__PURE__ */ new Date()).toISOString();
760
- let data = {
761
- ...otherData,
762
- ...fileData,
763
- createdAt: now,
764
- updatedAt: now,
765
- createdBy: user?.sub ?? null,
766
- updatedBy: user?.sub ?? null
767
- };
768
- data = await executeFieldBeforeChange(this.collection.fields, data, null, user, readonlyDb);
769
- data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
770
- data,
771
- req: c.req,
772
- user,
773
- operation: "create",
774
- db: readonlyDb
775
- });
776
- const doc = await db.create({
777
- collection: this.collection.slug,
778
- data
779
- });
780
- await runCollectionHooks(this.collection.hooks?.afterChange, {
781
- doc,
782
- user,
783
- req: c.req,
784
- operation: "create",
785
- db
786
- }, { isolated: true });
787
- const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
788
- doc,
789
- req: c.req,
790
- user,
791
- db: readonlyDb
792
- });
793
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
794
- return c.json(finalDoc, 201);
795
- }
796
- async update(c) {
797
- const config = c.get("config");
798
- const db = config.db;
799
- if (!db) return c.json({ message: "Database not configured" }, 500);
800
- const readonlyDb = createReadonlyDb(db);
801
- const id = c.req.param("id");
802
- if (!id) return c.json({ message: "Missing ID" }, 400);
803
- const body = await c.req.json();
804
- const user = c.get("user");
805
- let data = { ...body };
806
- if (this.collection.auth) {
807
- delete data.password;
808
- delete data.oldPassword;
809
- delete data.confirmPassword;
810
- }
811
- Object.assign(data, {
812
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
813
- updatedBy: user?.sub ?? null
814
- });
815
- const originalDoc = await db.findOne({ collection: this.collection.slug, id });
816
- if (!originalDoc) return c.json({ message: "Not Found" }, 404);
817
- let before = null;
818
- if (this.collection.audit) {
819
- before = originalDoc;
820
- }
821
- data = await executeFieldBeforeChange(this.collection.fields, data, originalDoc, user, readonlyDb);
822
- data = await runCollectionHooks(this.collection.hooks?.beforeChange, {
823
- data,
824
- doc: originalDoc,
825
- req: c.req,
826
- user,
827
- operation: "update",
828
- db: readonlyDb
829
- });
830
- const doc = await db.update({ collection: this.collection.slug, id, data });
831
- if (this.collection.audit && db) {
832
- AuditService.log(db, {
833
- operation: "update",
834
- collection: this.collection.slug,
835
- documentId: id,
836
- user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
837
- before,
838
- after: doc
839
- });
840
- }
841
- await runCollectionHooks(this.collection.hooks?.afterChange, {
842
- doc,
843
- previousDoc: originalDoc,
844
- user,
845
- req: c.req,
846
- operation: "update",
847
- db
848
- }, { isolated: true });
849
- const readDoc = await runCollectionHooks(this.collection.hooks?.afterRead, {
850
- doc,
851
- req: c.req,
852
- user,
853
- db: readonlyDb
854
- });
855
- const finalDoc = await executeFieldAfterRead(this.collection.fields, readDoc, user, readonlyDb);
856
- return c.json(finalDoc);
857
- }
858
- /**
859
- * POST /api/collections/:slug/:id/change-password
860
- *
861
- * Dedicated endpoint for password changes. Requires the caller to supply:
862
- * { oldPassword, newPassword, confirmPassword }
863
- *
864
- * Rules:
865
- * - Only the account owner or an admin may change the password.
866
- * - Non-admin callers MUST provide a valid oldPassword.
867
- * - newPassword and confirmPassword must match.
868
- */
869
- async changePassword(c) {
870
- const config = c.get("config");
871
- const db = config.db;
872
- if (!db) return c.json({ message: "Database not configured" }, 500);
873
- if (!this.collection.auth) {
874
- return c.json({ message: "This collection does not support authentication" }, 400);
875
- }
876
- const id = c.req.param("id");
877
- if (!id) return c.json({ message: "Missing ID" }, 400);
878
- const user = c.get("user");
879
- if (!user) return c.json({ message: "Authentication required" }, 401);
880
- const body = await c.req.json().catch(() => null);
881
- const { oldPassword, newPassword, confirmPassword } = body ?? {};
882
- if (!newPassword) {
883
- return c.json({ message: "newPassword is required" }, 400);
884
- }
885
- if (newPassword !== confirmPassword) {
886
- return c.json({ message: "Passwords do not match" }, 400);
887
- }
888
- if (newPassword.length < 8) {
889
- return c.json({ message: "Password must be at least 8 characters" }, 400);
890
- }
891
- const isAdmin = Array.isArray(user.roles) && user.roles.includes("admin");
892
- const isSelf = user.sub === id;
893
- if (!isAdmin && !isSelf) {
894
- return c.json({ message: "You are not authorised to change this password" }, 403);
895
- }
896
- if (!isAdmin) {
897
- if (!oldPassword) {
898
- return c.json({ message: "Current password is required" }, 400);
899
- }
900
- const existing = await db.findOne({ collection: this.collection.slug, id });
901
- if (!existing) return c.json({ message: "User not found" }, 404);
902
- const valid = await verifyPassword(oldPassword, existing.password);
903
- if (!valid) {
904
- return c.json({ message: "Invalid current password" }, 400);
905
- }
906
- }
907
- const hashed = await hashPassword(newPassword);
908
- const doc = await db.update({
909
- collection: this.collection.slug,
910
- id,
911
- data: {
912
- password: hashed,
913
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
914
- updatedBy: user.sub
915
- }
916
- });
917
- if (this.collection.audit) {
918
- AuditService.log(db, {
919
- operation: "update",
920
- collection: this.collection.slug,
921
- documentId: id,
922
- user: { id: user.sub, collection: user.collection, email: user.email },
923
- before: null,
924
- after: { id }
925
- });
926
- }
927
- return c.json({ success: true, message: "Password updated successfully" });
928
- }
929
- async delete(c) {
930
- const config = c.get("config");
931
- const db = config.db;
932
- if (!db) return c.json({ message: "Database not configured" }, 500);
933
- const readonlyDb = createReadonlyDb(db);
934
- const id = c.req.param("id");
935
- if (!id) return c.json({ message: "Missing ID" }, 400);
936
- const user = c.get("user");
937
- const doc = await db.findOne({ collection: this.collection.slug, id });
938
- if (!doc) return c.json({ message: "Not Found" }, 404);
939
- let before = null;
940
- if (this.collection.audit) {
941
- before = doc;
942
- }
943
- await runCollectionHooks(this.collection.hooks?.beforeDelete, {
944
- id,
945
- doc,
946
- user,
947
- req: c.req,
948
- db: readonlyDb
949
- });
950
- await db.delete({ collection: this.collection.slug, id });
951
- if (this.collection.audit && db) {
952
- AuditService.log(db, {
953
- operation: "delete",
954
- collection: this.collection.slug,
955
- documentId: id,
956
- user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
957
- before,
958
- after: null
959
- });
960
- }
961
- await runCollectionHooks(this.collection.hooks?.afterDelete, {
962
- id,
963
- doc,
964
- user,
965
- req: c.req,
966
- db
967
- }, { isolated: true });
968
- return c.json({ message: "Deleted" });
969
- }
970
- async deleteMany(c) {
971
- const config = c.get("config");
972
- const db = config.db;
973
- if (!db) return c.json({ message: "Database not configured" }, 500);
974
- const readonlyDb = createReadonlyDb(db);
975
- const user = c.get("user");
976
- let ids = [];
977
- try {
978
- const body = await c.req.json().catch(() => null);
979
- if (body?.ids && Array.isArray(body.ids)) {
980
- ids = body.ids;
981
- }
982
- } catch {
983
- }
984
- if (!ids.length) {
985
- const raw = c.req.queries("ids") ?? c.req.queries("ids[]") ?? [];
986
- ids = raw.filter(Boolean);
987
- }
988
- if (!ids.length) return c.json({ message: "No IDs provided" }, 400);
989
- const deleted = [];
990
- const failed = [];
991
- for (const id of ids) {
992
- try {
993
- const doc = await db.findOne({ collection: this.collection.slug, id });
994
- if (!doc) {
995
- failed.push({ id, error: "Not Found" });
996
- continue;
997
- }
998
- let before = null;
999
- if (this.collection.audit) {
1000
- before = doc;
1001
- }
1002
- await runCollectionHooks(this.collection.hooks?.beforeDelete, {
1003
- id,
1004
- doc,
1005
- user,
1006
- req: c.req,
1007
- db: readonlyDb
1008
- });
1009
- await db.delete({ collection: this.collection.slug, id });
1010
- deleted.push(id);
1011
- if (this.collection.audit) {
1012
- AuditService.log(db, {
1013
- operation: "delete",
1014
- collection: this.collection.slug,
1015
- documentId: id,
1016
- user: user ? { id: user.sub, collection: user.collection, email: user.email } : void 0,
1017
- before,
1018
- after: null
1019
- });
1020
- }
1021
- await runCollectionHooks(this.collection.hooks?.afterDelete, {
1022
- id,
1023
- doc,
1024
- user,
1025
- req: c.req,
1026
- db
1027
- }, { isolated: true });
1028
- } catch (err) {
1029
- failed.push({ id, error: err?.message ?? "Unknown error" });
1030
- }
1031
- }
1032
- return c.json({
1033
- message: `Deleted ${deleted.length} document(s)`,
1034
- deleted,
1035
- ...failed.length ? { failed } : {}
1036
- });
1037
- }
1038
- async seed(c) {
1039
- const config = c.get("config");
1040
- const db = config.db;
1041
- if (!db) return c.json({ message: "Database not configured" }, 500);
1042
- const body = await c.req.json();
1043
- const initialData = body.data;
1044
- if (!initialData || !Array.isArray(initialData)) {
1045
- return c.json({ message: "Invalid initial data" }, 400);
1046
- }
1047
- const result = await db.find({ collection: this.collection.slug, limit: 1 });
1048
- if (result.total > 0) {
1049
- return c.json({ message: "Collection is not empty, skipping seed" });
1050
- }
1051
- console.log(`[dyrected/core] Auto-seeding collection: ${this.collection.slug}`);
1052
- const createdDocs = [];
1053
- for (const data of initialData) {
1054
- const doc = await db.create({ collection: this.collection.slug, data });
1055
- createdDocs.push(doc);
1056
- }
1057
- return c.json({ message: "Seed successful", count: createdDocs.length }, 201);
1058
- }
1059
- };
1060
-
1061
- // src/controllers/global.controller.ts
1062
- var GlobalController = class {
1063
- global;
1064
- constructor(global) {
1065
- this.global = global;
1066
- }
1067
- async get(c) {
1068
- const config = c.get("config");
1069
- const db = config.db;
1070
- if (!db) return c.json({ message: "Database not configured" }, 500);
1071
- const readonlyDb = createReadonlyDb(db);
1072
- const depth = c.req.query("depth") !== void 0 ? Number(c.req.query("depth")) : 10;
1073
- const user = c.get("user");
1074
- let query = void 0;
1075
- const beforeReadResult = await runCollectionHooks(this.global.hooks?.beforeRead, {
1076
- req: c.req,
1077
- query,
1078
- user,
1079
- db: readonlyDb
1080
- });
1081
- if (beforeReadResult !== void 0) {
1082
- query = beforeReadResult;
1083
- }
1084
- let data = await db.getGlobal({ slug: this.global.slug });
1085
- const isEmpty = !data || Object.keys(data).length === 0;
1086
- if (isEmpty && this.global.initialData) {
1087
- console.log(`[dyrected/core] Auto-seeding global "${this.global.slug}" from config.initialData`);
1088
- await db.updateGlobal({ slug: this.global.slug, data: this.global.initialData });
1089
- data = this.global.initialData;
1090
- }
1091
- const dataWithDefaults = DefaultsService.apply(this.global.fields, data);
1092
- const docWithCollectionHooks = await runCollectionHooks(this.global.hooks?.afterRead, {
1093
- doc: dataWithDefaults,
1094
- req: c.req,
1095
- user,
1096
- db: readonlyDb
1097
- });
1098
- const docWithFieldHooks = await executeFieldAfterRead(this.global.fields, docWithCollectionHooks, user, readonlyDb);
1099
- if (depth > 0 && docWithFieldHooks) {
1100
- const populationService = new PopulationService(db, config.collections);
1101
- const populatedData = await populationService.populate({
1102
- data: docWithFieldHooks,
1103
- fields: this.global.fields,
1104
- currentDepth: 0,
1105
- maxDepth: depth
1106
- });
1107
- return c.json(populatedData);
1108
- }
1109
- return c.json(docWithFieldHooks);
1110
- }
1111
- async update(c) {
1112
- const config = c.get("config");
1113
- const db = config.db;
1114
- if (!db) return c.json({ message: "Database not configured" }, 500);
1115
- const readonlyDb = createReadonlyDb(db);
1116
- const body = await c.req.json();
1117
- const user = c.get("user");
1118
- const originalDoc = await db.getGlobal({ slug: this.global.slug }) || {};
1119
- let data = await executeFieldBeforeChange(this.global.fields, body, originalDoc, user, readonlyDb);
1120
- data = await runCollectionHooks(this.global.hooks?.beforeChange, {
1121
- data,
1122
- doc: originalDoc,
1123
- req: c.req,
1124
- user,
1125
- operation: "update",
1126
- db: readonlyDb
1127
- });
1128
- const updated = await db.updateGlobal({ slug: this.global.slug, data });
1129
- await runCollectionHooks(this.global.hooks?.afterChange, {
1130
- doc: updated,
1131
- previousDoc: originalDoc,
1132
- user,
1133
- req: c.req,
1134
- operation: "update",
1135
- db
1136
- }, { isolated: true });
1137
- const readDoc = await runCollectionHooks(this.global.hooks?.afterRead, {
1138
- doc: updated,
1139
- req: c.req,
1140
- user,
1141
- db: readonlyDb
1142
- });
1143
- const finalDoc = await executeFieldAfterRead(this.global.fields, readDoc, user, readonlyDb);
1144
- return c.json(finalDoc);
1145
- }
1146
- async seed(c) {
1147
- const config = c.get("config");
1148
- const db = config.db;
1149
- if (!db) return c.json({ message: "Database not configured" }, 500);
1150
- const body = await c.req.json();
1151
- const initialData = body.data;
1152
- if (!initialData) {
1153
- return c.json({ message: "Invalid initial data" }, 400);
1154
- }
1155
- const existing = await db.getGlobal({ slug: this.global.slug });
1156
- if (existing && Object.keys(existing).length > 0) {
1157
- return c.json({ message: "Global is not empty, skipping seed" });
1158
- }
1159
- console.log(`[dyrected/core] Auto-seeding global: ${this.global.slug}`);
1160
- await db.updateGlobal({ slug: this.global.slug, data: initialData });
1161
- return c.json({ message: "Seed successful", data: initialData }, 201);
1162
- }
1163
- };
1164
-
1165
- // src/controllers/media.controller.ts
1166
- var MediaController = class {
1167
- collection;
1168
- constructor(collection = "media") {
1169
- this.collection = collection;
1170
- }
1171
- async upload(c) {
1172
- const config = c.get("config");
1173
- const storage = config.storage;
1174
- const imageService = config.image;
1175
- if (!storage) {
1176
- return c.json({ message: "Storage not configured" }, 500);
1177
- }
1178
- const body = await c.req.parseBody();
1179
- const file = body["file"];
1180
- const focalPointStr = body["focalPoint"];
1181
- const focalPoint = focalPointStr ? JSON.parse(focalPointStr) : void 0;
1182
- if (!file) {
1183
- return c.json({ message: "No file uploaded" }, 400);
1184
- }
1185
- const buffer = new Uint8Array(await file.arrayBuffer());
1186
- const siteId = c.get("siteId");
1187
- const workspaceId = c.get("workspaceId");
1188
- const prefix = workspaceId ? `${workspaceId}/${siteId}` : siteId || "default";
1189
- let imageMetadata = {};
1190
- let imageSizes = {};
1191
- if (imageService && file.type.startsWith("image/")) {
1192
- let colConfig = config.collections.find((col) => col.slug === this.collection);
1193
- if (!colConfig && config.onSchemaFetch && siteId) {
1194
- const dynamic = await config.onSchemaFetch(siteId);
1195
- colConfig = dynamic.collections?.find((col) => col.slug === this.collection);
1196
- }
1197
- try {
1198
- const processed = await imageService.process({
1199
- buffer,
1200
- mimeType: file.type,
1201
- config: colConfig?.upload,
1202
- focalPoint
1203
- });
1204
- imageMetadata = processed.metadata;
1205
- imageSizes = processed.sizes;
1206
- } catch (err) {
1207
- console.error("[MediaController] Image processing failed:", err);
1208
- }
1209
- }
1210
- const fileData = await storage.upload({
1211
- filename: file.name,
1212
- buffer,
1213
- mimeType: file.type,
1214
- prefix
1215
- });
1216
- const finalFileData = {
1217
- ...fileData,
1218
- ...imageMetadata,
1219
- focalPoint,
1220
- sizes: {}
1221
- };
1222
- if (imageSizes) {
1223
- for (const [sizeName, sizeData] of Object.entries(imageSizes)) {
1224
- const ext = file.name.split(".").pop();
1225
- const baseName = file.name.substring(0, file.name.lastIndexOf("."));
1226
- const sizeFilename = `${baseName}-${sizeName}.${ext}`;
1227
- try {
1228
- const sizeFileData = await storage.upload({
1229
- filename: sizeFilename,
1230
- buffer: sizeData.buffer,
1231
- mimeType: file.type,
1232
- prefix
1233
- });
1234
- finalFileData.sizes[sizeName] = {
1235
- ...sizeFileData,
1236
- width: sizeData.width,
1237
- height: sizeData.height
1238
- };
1239
- } catch (err) {
1240
- console.error(`[MediaController] Failed to upload size ${sizeName}:`, err);
1241
- }
1242
- }
1243
- }
1244
- const db = config.db;
1245
- if (!db) return c.json({ message: "Database not configured" }, 500);
1246
- const doc = await db.create({
1247
- collection: this.collection,
1248
- data: finalFileData
1249
- });
1250
- return c.json(doc, 201);
1251
- }
1252
- async find(c) {
1253
- const db = c.get("config").db;
1254
- if (!db) return c.json({ message: "Database not configured" }, 500);
1255
- const limit = Number(c.req.query("limit")) || 10;
1256
- const page = Number(c.req.query("page")) || 1;
1257
- const result = await db.find({
1258
- collection: this.collection,
1259
- limit,
1260
- page
1261
- });
1262
- return c.json(result);
1263
- }
1264
- async delete(c) {
1265
- const config = c.get("config");
1266
- const storage = config.storage;
1267
- const db = config.db;
1268
- if (!db) return c.json({ message: "Database not configured" }, 500);
1269
- const id = c.req.param("id");
1270
- if (!id) return c.json({ message: "Missing ID" }, 400);
1271
- const doc = await db.findOne({ collection: this.collection, id });
1272
- if (!doc) return c.json({ message: "Not Found" }, 404);
1273
- if (storage) {
1274
- await storage.delete({ filename: doc.filename });
1275
- if (doc.sizes) {
1276
- for (const size of Object.values(doc.sizes)) {
1277
- if (size.filename) {
1278
- await storage.delete({ filename: size.filename });
1279
- }
1280
- }
1281
- }
1282
- }
1283
- await db.delete({ collection: this.collection, id });
1284
- return c.json({ message: "Deleted" });
1285
- }
1286
- async serve(c) {
1287
- const config = c.get("config");
1288
- const storage = config.storage;
1289
- if (!storage || !storage.resolve) {
1290
- return c.json({ message: "Storage not configured for serving" }, 404);
1291
- }
1292
- const filename = c.req.param("filename");
1293
- if (!filename) return c.json({ message: "Missing filename" }, 400);
1294
- let res = await storage.resolve({ filename });
1295
- if (!res && !filename.includes("/")) {
1296
- res = await storage.resolve({ filename: `default/${filename}` });
1297
- }
1298
- if (!res) return c.json({ message: "Not Found" }, 404);
1299
- c.header("Content-Type", res.mimeType);
1300
- return c.body(res.buffer);
1301
- }
1302
- };
1303
-
1304
- // src/auth/token.ts
1305
- import { SignJWT, jwtVerify, decodeJwt } from "jose";
1306
- import { TextEncoder } from "util";
1307
- function getSecret() {
1308
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET;
1309
- if (!secret) {
1310
- throw new Error(
1311
- "[dyrected/core] DYRECTED_JWT_SECRET is not set. Add it to your environment variables to enable auth collections."
1312
- );
1313
- }
1314
- return new TextEncoder().encode(secret);
1315
- }
1316
- var DEFAULT_EXPIRY = "7d";
1317
- async function signCollectionToken(payload, expiresIn = DEFAULT_EXPIRY) {
1318
- return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(getSecret());
1319
- }
1320
- async function verifyCollectionToken(token) {
1321
- const { payload } = await jwtVerify(token, getSecret());
1322
- return payload;
1323
- }
1324
-
1325
- // src/services/email.service.ts
1326
- var _devSend = null;
1327
- var _devSendPromise = null;
1328
- async function getDevSend() {
1329
- if (_devSend) return _devSend;
1330
- if (_devSendPromise) return _devSendPromise;
1331
- _devSendPromise = (async () => {
1332
- try {
1333
- const nodemailer = await import("nodemailer");
1334
- const account = await nodemailer.default.createTestAccount();
1335
- const transport = nodemailer.default.createTransport({
1336
- host: "smtp.ethereal.email",
1337
- port: 587,
1338
- auth: { user: account.user, pass: account.pass }
1339
- });
1340
- console.log("[dyrected/core] No email config \u2014 using Ethereal for dev email preview.");
1341
- console.log(`[dyrected/core] Ethereal login: https://ethereal.email user: ${account.user} pass: ${account.pass}`);
1342
- _devSend = async ({ to, subject, html }) => {
1343
- const info = await transport.sendMail({ from: '"Dyrected Dev" <dev@dyrected.local>', to, subject, html });
1344
- console.log(`[dyrected/core] Email preview URL: ${nodemailer.default.getTestMessageUrl(info)}`);
1345
- };
1346
- return _devSend;
1347
- } catch {
1348
- console.warn("[dyrected/core] nodemailer not available \u2014 emails will not be sent in dev.");
1349
- return null;
1350
- }
1351
- })();
1352
- return _devSendPromise;
1353
- }
1354
- async function sendEmail(config, payload) {
1355
- if (config.email) {
1356
- await config.email.send(payload);
1357
- return;
1358
- }
1359
- if (process.env.NODE_ENV !== "production") {
1360
- const devSend = await getDevSend();
1361
- await devSend?.(payload);
1362
- }
1363
- }
1364
- function buildWelcomeEmail(config, args) {
1365
- const custom = config.email?.templates?.welcome?.(args);
1366
- return {
1367
- subject: custom?.subject ?? "Welcome \u2014 your account is ready",
1368
- html: custom?.html ?? `
1369
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1370
- <tr>
1371
- <td align="center" style="padding:40px 16px">
1372
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1373
- <tr>
1374
- <td style="padding:32px 32px 0">
1375
- <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
1376
- <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Welcome!</h1>
1377
- </td>
1378
- </tr>
1379
- <tr>
1380
- <td style="padding:0 32px">
1381
- <p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Your account has been created. You can now log in with:</p>
1382
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
1383
- <tr>
1384
- <td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
1385
- ${args.email}
1386
- </td>
1387
- </tr>
1388
- </table>
1389
- </td>
1390
- </tr>
1391
- <tr>
1392
- <td style="padding:32px">
1393
- <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't create this account, you can safely ignore this email.</p>
1394
- </td>
1395
- </tr>
1396
- </table>
1397
- </td>
1398
- </tr>
1399
- </table>`
1400
- };
1401
- }
1402
- function buildInviteEmail(config, args) {
1403
- const custom = config.email?.templates?.invite?.(args);
1404
- return {
1405
- subject: custom?.subject ?? "You've been invited",
1406
- html: custom?.html ?? `
1407
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1408
- <tr>
1409
- <td align="center" style="padding:40px 16px">
1410
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1411
- <tr>
1412
- <td style="padding:32px 32px 0">
1413
- <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
1414
- <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">You've been invited</h1>
1415
- </td>
1416
- </tr>
1417
- <tr>
1418
- <td style="padding:0 32px">
1419
- ${args.invitedByEmail ? `<p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">You were invited by <strong style="color:#111827">${args.invitedByEmail}</strong>.</p>` : ""}
1420
- <p style="margin:0 0 16px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">Use the token below to accept your invitation. It expires in 7 days.</p>
1421
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
1422
- <tr>
1423
- <td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
1424
- ${args.token}
1425
- </td>
1426
- </tr>
1427
- </table>
1428
- </td>
1429
- </tr>
1430
- <tr>
1431
- <td style="padding:32px">
1432
- <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you weren't expecting this invitation, you can safely ignore this email.</p>
1433
- </td>
1434
- </tr>
1435
- </table>
1436
- </td>
1437
- </tr>
1438
- </table>`
1439
- };
1440
- }
1441
- function buildResetPasswordEmail(config, args) {
1442
- const custom = config.email?.templates?.resetPassword?.(args);
1443
- const resetLink = args.url;
1444
- return {
1445
- subject: custom?.subject ?? "Reset your password",
1446
- html: custom?.html ?? `
1447
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1448
- <tr>
1449
- <td align="center" style="padding:40px 16px">
1450
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1451
- <tr>
1452
- <td style="padding:32px 32px 0">
1453
- <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
1454
- <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Reset your password</h1>
1455
- </td>
1456
- </tr>
1457
- <tr>
1458
- <td style="padding:0 32px">
1459
- <p style="margin:0 0 24px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">We received a request to reset your password. Use the button below to set a new password. It will expire in 1 hour.</p>
1460
- ${resetLink ? `
1461
- <table cellpadding="0" cellspacing="0" border="0" style="margin-bottom:24px">
1462
- <tr>
1463
- <td style="border-radius:6px;background-color:#111827">
1464
- <a href="${resetLink}" style="display:inline-block;padding:12px 28px;font-size:14px;font-weight:600;color:#ffffff;text-decoration:none;font-family:sans-serif;border-radius:6px">
1465
- Reset Password
1466
- </a>
1467
- </td>
1468
- </tr>
1469
- </table>
1470
- ` : ""}
1471
- <p style="margin:0 0 8px;font-size:12px;color:#9ca3af;font-family:sans-serif">Or copy and paste this token manually in the admin dashboard:</p>
1472
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
1473
- <tr>
1474
- <td style="padding:12px 16px;font-family:monospace;font-size:12px;color:#374151;word-break:break-all;white-space:normal;line-height:1.4">
1475
- ${args.token}
1476
- </td>
1477
- </tr>
1478
- </table>
1479
- </td>
1480
- </tr>
1481
- <tr>
1482
- <td style="padding:32px">
1483
- <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">If you didn't request a password reset, you can safely ignore this email.</p>
1484
- </td>
1485
- </tr>
1486
- </table>
1487
- </td>
1488
- </tr>
1489
- </table>`
1490
- };
1491
- }
1492
- function buildPasswordChangedEmail(config, args) {
1493
- const custom = config.email?.templates?.passwordChanged?.(args);
1494
- return {
1495
- subject: custom?.subject ?? "Your password has been changed",
1496
- html: custom?.html ?? `
1497
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f9fafb;table-layout:fixed">
1498
- <tr>
1499
- <td align="center" style="padding:40px 16px">
1500
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:600px;background-color:#ffffff;border-radius:12px;border:1px solid #e5e7eb;table-layout:fixed">
1501
- <tr>
1502
- <td style="padding:32px 32px 0">
1503
- <p style="margin:0 0 4px;font-size:12px;font-weight:600;color:#6b7280;font-family:sans-serif;text-transform:uppercase;letter-spacing:0.05em">Dyrected</p>
1504
- <h1 style="margin:0 0 24px;font-size:22px;font-weight:700;color:#111827;font-family:sans-serif">Password changed</h1>
1505
- </td>
1506
- </tr>
1507
- <tr>
1508
- <td style="padding:0 32px">
1509
- <p style="margin:0 0 12px;font-size:14px;color:#4b5563;line-height:1.6;font-family:sans-serif">The password for your account was just changed:</p>
1510
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;background-color:#f3f4f6;border-radius:6px;table-layout:fixed">
1511
- <tr>
1512
- <td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827;font-family:sans-serif;word-break:break-all">
1513
- ${args.email}
1514
- </td>
1515
- </tr>
1516
- </table>
1517
- <table cellpadding="0" cellspacing="0" border="0" style="width:100%;margin-top:16px;background-color:#fef2f2;border-radius:6px;border:1px solid #fecaca;table-layout:fixed">
1518
- <tr>
1519
- <td style="padding:12px 16px;font-size:13px;color:#b91c1c;line-height:1.5;font-family:sans-serif">
1520
- If you did not make this change, please contact support immediately.
1521
- </td>
1522
- </tr>
1523
- </table>
1524
- </td>
1525
- </tr>
1526
- <tr>
1527
- <td style="padding:32px">
1528
- <p style="margin:0;font-size:12px;color:#9ca3af;font-family:sans-serif">This is an automated security notification.</p>
1529
- </td>
1530
- </tr>
1531
- </table>
1532
- </td>
1533
- </tr>
1534
- </table>`
1535
- };
1536
- }
1537
-
1538
- // src/controllers/auth.controller.ts
1539
- var AuthController = class {
1540
- collection;
1541
- constructor(collection) {
1542
- this.collection = collection;
1543
- }
1544
- // ---------------------------------------------------------------------------
1545
- // GET /init
1546
- // Checks if the first user needs to be created.
1547
- // ---------------------------------------------------------------------------
1548
- async init(c) {
1549
- const db = c.get("config").db;
1550
- if (!db) return c.json({ message: "Database not configured" }, 500);
1551
- const result = await db.find({
1552
- collection: this.collection.slug,
1553
- limit: 1
1554
- });
1555
- return c.json({
1556
- initialized: result.total > 0
1557
- });
1558
- }
1559
- // ---------------------------------------------------------------------------
1560
- // POST /first-user
1561
- // Creates the first user if none exist.
1562
- // ---------------------------------------------------------------------------
1563
- async registerFirstUser(c) {
1564
- const config = c.get("config");
1565
- const db = config.db;
1566
- if (!db) return c.json({ message: "Database not configured" }, 500);
1567
- const check = await db.find({
1568
- collection: this.collection.slug,
1569
- limit: 1
1570
- });
1571
- if (check.total > 0) {
1572
- return c.json({ error: true, message: "Initial user already exists." }, 403);
1573
- }
1574
- const body = await c.req.json().catch(() => null);
1575
- if (!body?.email || !body?.password) {
1576
- return c.json({ error: true, message: "email and password are required." }, 400);
1577
- }
1578
- const hashedPassword = await hashPassword(body.password);
1579
- const user = await db.create({
1580
- collection: this.collection.slug,
1581
- data: {
1582
- ...body,
1583
- password: hashedPassword,
1584
- roles: ["admin"]
1585
- // Default first user to admin
1586
- }
1587
- });
1588
- const token = await signCollectionToken({
1589
- sub: user.id,
1590
- email: user.email,
1591
- collection: this.collection.slug
1592
- });
1593
- const { subject, html } = buildWelcomeEmail(config, { email: body.email });
1594
- sendEmail(config, { to: body.email, subject, html }).catch(
1595
- (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
1596
- );
1597
- const { password: _, ...safeUser } = user;
1598
- return c.json({ token, user: safeUser });
1599
- }
1600
- // ---------------------------------------------------------------------------
1601
- // POST /login
1602
- // ---------------------------------------------------------------------------
1603
- async login(c) {
1604
- const db = c.get("config").db;
1605
- if (!db) return c.json({ message: "Database not configured" }, 500);
1606
- const body = await c.req.json().catch(() => null);
1607
- if (!body?.email || !body?.password) {
1608
- return c.json({ error: true, message: "email and password are required." }, 400);
1609
- }
1610
- const result = await db.find({
1611
- collection: this.collection.slug,
1612
- where: { email: body.email },
1613
- limit: 1
1614
- });
1615
- const user = result.docs[0];
1616
- if (!user) {
1617
- return c.json({ error: true, message: "Invalid email or password." }, 401);
1618
- }
1619
- const valid = await verifyPassword(body.password, user.password);
1620
- if (!valid) {
1621
- return c.json({ error: true, message: "Invalid email or password." }, 401);
1622
- }
1623
- const token = await signCollectionToken({
1624
- sub: user.id,
1625
- email: user.email,
1626
- collection: this.collection.slug
1627
- });
1628
- const { password: _, ...safeUser } = user;
1629
- return c.json({ token, user: safeUser });
1630
- }
1631
- // ---------------------------------------------------------------------------
1632
- // POST /logout
1633
- // Auth collections use stateless JWTs — logout is handled client-side.
1634
- // This endpoint exists so clients have a consistent API surface.
1635
- // ---------------------------------------------------------------------------
1636
- async logout(c) {
1637
- return c.json({ success: true, message: "Logged out. Discard your token." });
1638
- }
1639
- // ---------------------------------------------------------------------------
1640
- // GET /me
1641
- // ---------------------------------------------------------------------------
1642
- async me(c) {
1643
- const db = c.get("config").db;
1644
- if (!db) return c.json({ message: "Database not configured" }, 500);
1645
- const requestUser = c.get("user");
1646
- if (!requestUser) {
1647
- return c.json({ error: true, message: "Authentication required." }, 401);
1648
- }
1649
- const user = await db.findOne({ collection: this.collection.slug, id: requestUser.sub });
1650
- if (!user) {
1651
- return c.json({ error: true, message: "User not found." }, 404);
1652
- }
1653
- const { password: _, ...safeUser } = user;
1654
- return c.json(safeUser);
1655
- }
1656
- // ---------------------------------------------------------------------------
1657
- // POST /refresh-token
1658
- // ---------------------------------------------------------------------------
1659
- async refreshToken(c) {
1660
- const requestUser = c.get("user");
1661
- if (!requestUser) {
1662
- return c.json({ error: true, message: "Authentication required." }, 401);
1663
- }
1664
- const token = await signCollectionToken({
1665
- sub: requestUser.sub,
1666
- email: requestUser.email,
1667
- collection: this.collection.slug
1668
- });
1669
- return c.json({ token });
1670
- }
1671
- // ---------------------------------------------------------------------------
1672
- // POST /forgot-password
1673
- // Requires config.email to be set. Silently succeeds if email not found
1674
- // to prevent email enumeration.
1675
- // ---------------------------------------------------------------------------
1676
- async forgotPassword(c) {
1677
- const config = c.get("config");
1678
- const db = config.db;
1679
- if (!db) return c.json({ message: "Database not configured" }, 500);
1680
- const body = await c.req.json().catch(() => null);
1681
- if (!body?.email) {
1682
- return c.json({ error: true, message: "email is required." }, 400);
1683
- }
1684
- const result = await db.find({
1685
- collection: this.collection.slug,
1686
- where: { email: body.email },
1687
- limit: 1
1688
- });
1689
- const user = result.docs[0];
1690
- if (user) {
1691
- const resetToken = await signCollectionToken(
1692
- { sub: user.id, email: user.email, collection: this.collection.slug, purpose: "reset" },
1693
- "1h"
1694
- );
1695
- const resetUrl = body?.resetUrl;
1696
- const url = resetUrl ? `${resetUrl}${resetUrl.includes("?") ? "&" : "?"}token=${encodeURIComponent(resetToken)}` : void 0;
1697
- try {
1698
- const { subject, html } = buildResetPasswordEmail(config, { token: resetToken, url });
1699
- await sendEmail(config, { to: user.email, subject, html });
1700
- } catch (err) {
1701
- console.error("[dyrected/core] Failed to send password reset email:", err);
1702
- }
1703
- }
1704
- return c.json({
1705
- success: true,
1706
- message: "If an account with that email exists, a reset link has been sent."
1707
- });
1708
- }
1709
- // ---------------------------------------------------------------------------
1710
- // POST /reset-password
1711
- // Expects { token: string, password: string } in body.
1712
- // The token is the reset JWT issued by /forgot-password.
1713
- // ---------------------------------------------------------------------------
1714
- async resetPassword(c) {
1715
- const config = c.get("config");
1716
- const db = config.db;
1717
- if (!db) return c.json({ message: "Database not configured" }, 500);
1718
- const body = await c.req.json().catch(() => null);
1719
- if (!body?.token || !body?.password) {
1720
- return c.json({ error: true, message: "token and password are required." }, 400);
1721
- }
1722
- let payload;
1723
- try {
1724
- payload = await verifyCollectionToken(body.token);
1725
- } catch {
1726
- return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
1727
- }
1728
- if (payload.collection !== this.collection.slug || payload.purpose !== "reset") {
1729
- return c.json({ error: true, message: "Reset token is invalid or has expired." }, 400);
1730
- }
1731
- const hashedPassword = await hashPassword(body.password);
1732
- await db.update({
1733
- collection: this.collection.slug,
1734
- id: payload.sub,
1735
- data: { password: hashedPassword }
1736
- });
1737
- const { subject, html } = buildPasswordChangedEmail(config, { email: payload.email });
1738
- sendEmail(config, { to: payload.email, subject, html }).catch(
1739
- (err) => console.error("[dyrected/core] Failed to send password-changed email:", err)
1740
- );
1741
- return c.json({ success: true, message: "Password has been reset. You can now log in." });
1742
- }
1743
- // ---------------------------------------------------------------------------
1744
- // POST /invite
1745
- // Requires auth. Issues a signed invite token and emails it to the invitee.
1746
- // ---------------------------------------------------------------------------
1747
- async invite(c) {
1748
- const config = c.get("config");
1749
- const db = config.db;
1750
- if (!db) return c.json({ message: "Database not configured" }, 500);
1751
- const requestUser = c.get("user");
1752
- if (!requestUser) {
1753
- return c.json({ error: true, message: "Authentication required." }, 401);
1754
- }
1755
- const body = await c.req.json().catch(() => null);
1756
- if (!body?.email) {
1757
- return c.json({ error: true, message: "email is required." }, 400);
1758
- }
1759
- const existing = await db.find({
1760
- collection: this.collection.slug,
1761
- where: { email: body.email },
1762
- limit: 1
1763
- });
1764
- if (existing.total > 0) {
1765
- return c.json({ error: true, message: "An account with that email already exists." }, 409);
1766
- }
1767
- const inviteToken = await signCollectionToken(
1768
- { sub: body.email, email: body.email, collection: this.collection.slug, purpose: "invite" },
1769
- "7d"
1770
- );
1771
- try {
1772
- const { subject, html } = buildInviteEmail(config, {
1773
- token: inviteToken,
1774
- invitedByEmail: requestUser.email
1775
- });
1776
- await sendEmail(config, { to: body.email, subject, html });
1777
- } catch (err) {
1778
- console.error("[dyrected/core] Failed to send invite email:", err);
1779
- }
1780
- return c.json({ success: true, message: `Invite sent to ${body.email}.` });
1781
- }
1782
- // ---------------------------------------------------------------------------
1783
- // POST /accept-invite
1784
- // Public. Validates the invite token and creates the user account.
1785
- // Body: { token, password, ...extraFields }
1786
- // ---------------------------------------------------------------------------
1787
- async acceptInvite(c) {
1788
- const config = c.get("config");
1789
- const db = config.db;
1790
- if (!db) return c.json({ message: "Database not configured" }, 500);
1791
- const body = await c.req.json().catch(() => null);
1792
- if (!body?.token || !body?.password) {
1793
- return c.json({ error: true, message: "token and password are required." }, 400);
1794
- }
1795
- let payload;
1796
- try {
1797
- payload = await verifyCollectionToken(body.token);
1798
- } catch {
1799
- return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
1800
- }
1801
- if (payload.collection !== this.collection.slug || payload.purpose !== "invite") {
1802
- return c.json({ error: true, message: "Invite token is invalid or has expired." }, 400);
1803
- }
1804
- const inviteeEmail = payload.sub;
1805
- const existing = await db.find({
1806
- collection: this.collection.slug,
1807
- where: { email: inviteeEmail },
1808
- limit: 1
1809
- });
1810
- if (existing.total > 0) {
1811
- return c.json({ error: true, message: "An account with that email already exists." }, 409);
1812
- }
1813
- const { token: _t, password: _p, ...extraFields } = body;
1814
- const hashedPassword = await hashPassword(body.password);
1815
- const user = await db.create({
1816
- collection: this.collection.slug,
1817
- data: { ...extraFields, email: inviteeEmail, password: hashedPassword }
1818
- });
1819
- const sessionToken = await signCollectionToken({
1820
- sub: user.id,
1821
- email: inviteeEmail,
1822
- collection: this.collection.slug
1823
- });
1824
- const { subject, html } = buildWelcomeEmail(config, { email: inviteeEmail });
1825
- sendEmail(config, { to: inviteeEmail, subject, html }).catch(
1826
- (err) => console.error("[dyrected/core] Failed to send welcome email:", err)
1827
- );
1828
- const { password: _, ...safeUser } = user;
1829
- return c.json({ token: sessionToken, user: safeUser }, 201);
1830
- }
1831
- };
1832
-
1833
- // src/controllers/preview.controller.ts
1834
- import { SignJWT as SignJWT2, jwtVerify as jwtVerify2 } from "jose";
1835
- import { TextEncoder as TextEncoder2 } from "util";
1836
- var PreviewController = class {
1837
- getSecret() {
1838
- const secret = process.env.DYRECTED_JWT_SECRET || process.env.JWT_SECRET || "dyrected-preview-secret-change-me";
1839
- return new TextEncoder2().encode(secret);
1840
- }
1841
- /**
1842
- * POST /api/preview-token
1843
- * Generates a short-lived token for previewing unsaved data.
1844
- */
1845
- async createToken(c) {
1846
- const body = await c.req.json().catch(() => null);
1847
- if (!body?.collectionSlug || !body?.data) {
1848
- return c.json({ error: true, message: "collectionSlug and data are required." }, 400);
1849
- }
1850
- const token = await new SignJWT2({
1851
- collectionSlug: body.collectionSlug,
1852
- documentId: body.documentId,
1853
- data: body.data
1854
- }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime("15m").sign(this.getSecret());
1855
- const expiresAt = new Date(Date.now() + 15 * 60 * 1e3).toISOString();
1856
- return c.json({ token, expiresAt });
1857
- }
1858
- /**
1859
- * GET /api/preview-data?token=<jwt>
1860
- * Returns the data stored in the preview token.
1861
- */
1862
- async getData(c) {
1863
- const token = c.req.query("token");
1864
- if (!token) {
1865
- return c.json({ error: true, message: "token query parameter is required." }, 400);
1866
- }
1867
- try {
1868
- const { payload } = await jwtVerify2(token, this.getSecret());
1869
- return c.json(payload);
1870
- } catch (err) {
1871
- return c.json({ error: true, message: "Invalid or expired preview token." }, 401);
1872
- }
1873
- }
1874
- };
1875
-
1876
- // src/middleware/auth.ts
1877
- function requireAuth() {
1878
- return async (c, next) => {
1879
- const authHeader = c.req.header("Authorization");
1880
- const token = authHeader?.replace(/^Bearer\s+/i, "");
1881
- if (!token) {
1882
- return c.json({ error: true, message: "Authentication required." }, 401);
1883
- }
1884
- try {
1885
- const user = await verifyCollectionToken(token);
1886
- c.set("user", user);
1887
- await next();
1888
- } catch {
1889
- return c.json({ error: true, message: "Invalid or expired token." }, 401);
1890
- }
1891
- };
1892
- }
1893
- function optionalAuth() {
1894
- return async (c, next) => {
1895
- const authHeader = c.req.header("Authorization");
1896
- const token = authHeader?.replace(/^Bearer\s+/i, "");
1897
- if (token) {
1898
- try {
1899
- const user = await verifyCollectionToken(token);
1900
- c.set("user", user);
1901
- } catch {
1902
- }
1903
- }
1904
- await next();
1905
- };
1906
- }
1907
-
1908
- // src/utils/openapi.ts
1909
- function generateOpenApi(config) {
1910
- const spec = {
1911
- openapi: "3.0.0",
1912
- info: {
1913
- title: "Dyrected API",
1914
- version: "1.0.0",
1915
- description: "Automatically generated OpenAPI specification for the Dyrected project."
1916
- },
1917
- components: {
1918
- schemas: {},
1919
- securitySchemes: {
1920
- ApiKeyAuth: {
1921
- type: "apiKey",
1922
- in: "header",
1923
- name: "x-api-key"
1924
- }
1925
- }
1926
- },
1927
- paths: {},
1928
- security: [{ ApiKeyAuth: [] }]
1929
- };
1930
- for (const collection of config.collections) {
1931
- spec.components.schemas[collection.slug] = collectionToSchema(collection);
1932
- }
1933
- for (const global of config.globals) {
1934
- spec.components.schemas[global.slug] = globalToSchema(global);
1935
- }
1936
- for (const collection of config.collections) {
1937
- const slug = collection.slug;
1938
- const path = `/api/collections/${slug}`;
1939
- const labels = collection.labels || { singular: slug, plural: `${slug}s` };
1940
- spec.paths[path] = {
1941
- get: {
1942
- tags: ["Collections"],
1943
- summary: `Find ${labels.plural}`,
1944
- parameters: [
1945
- { name: "limit", in: "query", schema: { type: "integer", default: 10 } },
1946
- { name: "page", in: "query", schema: { type: "integer", default: 1 } },
1947
- { name: "where", in: "query", schema: { type: "string" }, description: "JSON filter" },
1948
- { name: "sort", in: "query", schema: { type: "string" }, description: "Sort field (e.g. -createdAt)" }
1949
- ],
1950
- responses: {
1951
- 200: {
1952
- description: "Success",
1953
- content: {
1954
- "application/json": {
1955
- schema: {
1956
- type: "object",
1957
- properties: {
1958
- docs: { type: "array", items: { $ref: `#/components/schemas/${slug}` } },
1959
- total: { type: "integer" },
1960
- limit: { type: "integer" },
1961
- page: { type: "integer" }
1962
- }
1963
- }
1964
- }
1965
- }
1966
- }
1967
- }
1968
- },
1969
- post: {
1970
- tags: ["Collections"],
1971
- summary: `Create ${labels.singular}`,
1972
- requestBody: {
1973
- required: true,
1974
- content: {
1975
- "application/json": {
1976
- schema: { $ref: `#/components/schemas/${slug}` }
1977
- }
1978
- }
1979
- },
1980
- responses: {
1981
- 201: {
1982
- description: "Created",
1983
- content: {
1984
- "application/json": {
1985
- schema: { $ref: `#/components/schemas/${slug}` }
1986
- }
1987
- }
1988
- }
1989
- }
1990
- }
1991
- };
1992
- spec.paths[`${path}/{id}`] = {
1993
- get: {
1994
- tags: ["Collections"],
1995
- summary: `Get a single ${labels.singular}`,
1996
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
1997
- responses: {
1998
- 200: {
1999
- description: "Success",
2000
- content: {
2001
- "application/json": {
2002
- schema: { $ref: `#/components/schemas/${slug}` }
2003
- }
2004
- }
2005
- }
2006
- }
2007
- },
2008
- patch: {
2009
- tags: ["Collections"],
2010
- summary: `Update ${labels.singular}`,
2011
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2012
- requestBody: {
2013
- required: true,
2014
- content: {
2015
- "application/json": {
2016
- schema: { $ref: `#/components/schemas/${slug}` }
2017
- }
2018
- }
2019
- },
2020
- responses: {
2021
- 200: {
2022
- description: "Updated",
2023
- content: {
2024
- "application/json": {
2025
- schema: { $ref: `#/components/schemas/${slug}` }
2026
- }
2027
- }
2028
- }
2029
- }
2030
- },
2031
- delete: {
2032
- tags: ["Collections"],
2033
- summary: `Delete ${labels.singular}`,
2034
- parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
2035
- responses: {
2036
- 204: { description: "Deleted" }
2037
- }
2038
- }
2039
- };
2040
- }
2041
- for (const global of config.globals) {
2042
- const slug = global.slug;
2043
- const path = `/api/globals/${slug}`;
2044
- spec.paths[path] = {
2045
- get: {
2046
- tags: ["Globals"],
2047
- summary: `Get ${global.label || slug}`,
2048
- responses: {
2049
- 200: {
2050
- description: "Success",
2051
- content: {
2052
- "application/json": {
2053
- schema: { $ref: `#/components/schemas/${slug}` }
2054
- }
2055
- }
2056
- }
2057
- }
2058
- },
2059
- patch: {
2060
- tags: ["Globals"],
2061
- summary: `Update ${global.label || slug}`,
2062
- requestBody: {
2063
- required: true,
2064
- content: {
2065
- "application/json": {
2066
- schema: { $ref: `#/components/schemas/${slug}` }
2067
- }
2068
- }
2069
- },
2070
- responses: {
2071
- 200: {
2072
- description: "Updated",
2073
- content: {
2074
- "application/json": {
2075
- schema: { $ref: `#/components/schemas/${slug}` }
2076
- }
2077
- }
2078
- }
2079
- }
2080
- }
2081
- };
2082
- }
2083
- if (config.storage) {
2084
- spec.paths["/api/media"] = {
2085
- get: {
2086
- tags: ["Media"],
2087
- summary: "List Media",
2088
- responses: {
2089
- 200: {
2090
- description: "Success",
2091
- content: {
2092
- "application/json": {
2093
- schema: {
2094
- type: "object",
2095
- properties: {
2096
- docs: { type: "array", items: { type: "object", additionalProperties: true } }
2097
- }
2098
- }
2099
- }
2100
- }
2101
- }
2102
- }
2103
- },
2104
- post: {
2105
- tags: ["Media"],
2106
- summary: "Upload Media",
2107
- requestBody: {
2108
- content: {
2109
- "multipart/form-data": {
2110
- schema: {
2111
- type: "object",
2112
- properties: {
2113
- file: { type: "string", format: "binary" }
2114
- }
2115
- }
2116
- }
2117
- }
2118
- },
2119
- responses: {
2120
- 201: { description: "Uploaded" }
2121
- }
2122
- }
2123
- };
2124
- }
2125
- return spec;
2126
- }
2127
- function collectionToSchema(collection) {
2128
- const { properties, required } = fieldsToProperties(collection.fields);
2129
- return {
2130
- type: "object",
2131
- properties: {
2132
- id: { type: "string" },
2133
- createdAt: { type: "string", format: "date-time" },
2134
- updatedAt: { type: "string", format: "date-time" },
2135
- ...properties
2136
- },
2137
- required: ["id", ...required]
2138
- };
2139
- }
2140
- function globalToSchema(global) {
2141
- const { properties, required } = fieldsToProperties(global.fields);
2142
- return {
2143
- type: "object",
2144
- properties,
2145
- required
2146
- };
2147
- }
2148
- function fieldsToProperties(fields) {
2149
- const props = {};
2150
- const required = [];
2151
- for (const field of fields) {
2152
- if (!field.name || field.type === "join" || field.type === "row") continue;
2153
- props[field.name] = fieldToSchema(field);
2154
- if (field.required) {
2155
- required.push(field.name);
2156
- }
2157
- }
2158
- return { properties: props, required };
2159
- }
2160
- function fieldToSchema(field) {
2161
- let schema = {};
2162
- switch (field.type) {
2163
- case "text":
2164
- case "textarea":
2165
- case "email":
2166
- case "url":
2167
- schema = { type: "string" };
2168
- break;
2169
- case "number":
2170
- schema = { type: "number" };
2171
- break;
2172
- case "boolean":
2173
- schema = { type: "boolean" };
2174
- break;
2175
- case "date":
2176
- schema = { type: "string", format: "date-time" };
2177
- break;
2178
- case "select":
2179
- case "radio":
2180
- schema = { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 };
2181
- break;
2182
- case "multiSelect":
2183
- schema = {
2184
- type: "array",
2185
- items: { type: "string", enum: Array.isArray(field.options) ? field.options.map((o) => typeof o === "string" ? o : o.value) : void 0 }
2186
- };
2187
- break;
2188
- case "relationship":
2189
- schema = { type: "string", description: `ID of a ${field.relationTo} record` };
2190
- break;
2191
- case "object": {
2192
- const { properties, required } = fieldsToProperties(field.fields || []);
2193
- schema = { type: "object", properties, required };
2194
- break;
2195
- }
2196
- case "array": {
2197
- const { properties, required } = fieldsToProperties(field.fields || []);
2198
- schema = { type: "array", items: { type: "object", properties, required } };
2199
- break;
2200
- }
2201
- case "json":
2202
- case "richText":
2203
- schema = { type: "object", additionalProperties: true };
2204
- break;
2205
- case "blocks":
2206
- schema = {
2207
- type: "array",
2208
- items: {
2209
- oneOf: field.blocks?.map((block) => {
2210
- const { properties, required } = fieldsToProperties(block.fields);
2211
- return {
2212
- type: "object",
2213
- properties: {
2214
- blockType: { type: "string", enum: [block.slug] },
2215
- ...properties
2216
- },
2217
- required: ["blockType", ...required]
2218
- };
2219
- })
2220
- }
2221
- };
2222
- break;
2223
- default:
2224
- schema = { type: "string" };
2225
- }
2226
- if (field.label) schema.description = field.label;
2227
- return schema;
2228
- }
2229
-
2230
- // src/utils/swagger.ts
2231
- function getSwaggerHtml(specUrl = "/api/openapi.json") {
2232
- return `
2233
- <!DOCTYPE html>
2234
- <html lang="en">
2235
- <head>
2236
- <meta charset="utf-8" />
2237
- <meta name="viewport" content="width=device-width, initial-scale=1" />
2238
- <meta name="description" content="SwaggerUI" />
2239
- <title>Dyrected API Documentation</title>
2240
- <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
2241
- </head>
2242
- <body>
2243
- <div id="swagger-ui"></div>
2244
- <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" charset="UTF-8"></script>
2245
- <script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-standalone-preset.js" charset="UTF-8"></script>
2246
- <script>
2247
- window.onload = () => {
2248
- // Forward the apikey query param when loading the spec and making API calls
2249
- const params = new URLSearchParams(window.location.search);
2250
- const apiKey = params.get('apikey');
2251
- const specUrlWithKey = apiKey ? '${specUrl}?apikey=' + encodeURIComponent(apiKey) : '${specUrl}';
2252
-
2253
- window.ui = SwaggerUIBundle({
2254
- url: specUrlWithKey,
2255
- dom_id: '#swagger-ui',
2256
- presets: [
2257
- SwaggerUIBundle.presets.apis,
2258
- SwaggerUIStandalonePreset
2259
- ],
2260
- layout: "BaseLayout",
2261
- deepLinking: true,
2262
- showExtensions: true,
2263
- showCommonExtensions: true,
2264
- // Inject x-api-key header on every request made from the Swagger UI
2265
- requestInterceptor: (request) => {
2266
- if (apiKey) {
2267
- request.headers['x-api-key'] = apiKey;
2268
- }
2269
- return request;
2270
- }
2271
- });
2272
- };
2273
- </script>
2274
- </body>
2275
- </html>
2276
- `;
2277
- }
2278
-
2279
- // src/auth/jexl.ts
2280
- import jexl from "jexl";
2281
- async function evaluateAccess(expression, context) {
2282
- if (expression === void 0 || expression === null) return false;
2283
- if (typeof expression === "boolean") return expression;
2284
- try {
2285
- const result = await jexl.eval(expression, context);
2286
- return !!result;
2287
- } catch (err) {
2288
- console.error("[dyrected/core] Jexl evaluation failed:", err);
2289
- return false;
2290
- }
2291
- }
2292
-
2293
- // src/router.ts
2294
- function accessGate(target, action) {
2295
- return async (c, next) => {
2296
- const user = c.get("user");
2297
- const accessExpr = target.access?.[action];
2298
- if (accessExpr === void 0 || accessExpr === null) {
2299
- return await next();
2300
- }
2301
- const accessArgs = { user, req: c.req, doc: null };
2302
- const allowed = await evaluateAccess(accessExpr, accessArgs);
2303
- if (!allowed) {
2304
- return c.json({ error: true, message: `Access denied: ${action} on ${target.slug}` }, 403);
2305
- }
2306
- await next();
2307
- };
2308
- }
2309
- async function checkAccess(access, accessArgs) {
2310
- if (access === void 0 || access === null) return true;
2311
- if (typeof access === "function") {
2312
- try {
2313
- const result = await access(accessArgs);
2314
- return typeof result === "boolean" ? result : !!result;
2315
- } catch (err) {
2316
- console.error("[dyrected/core] Functional access check failed:", err);
2317
- return false;
2318
- }
2319
- }
2320
- if (typeof access === "string" || typeof access === "boolean") {
2321
- return evaluateAccess(access, accessArgs);
2322
- }
2323
- return true;
2324
- }
2325
- function serializeFieldForApi(f) {
2326
- if (!f) return f;
2327
- const serialized = { ...f };
2328
- if (serialized.admin?.hooks) {
2329
- const hooks = { ...serialized.admin.hooks };
2330
- if (typeof hooks.onChange === "function") {
2331
- hooks.onChange = hooks.onChange.toString();
2332
- }
2333
- if (typeof hooks.options === "function") {
2334
- hooks.options = hooks.options.toString();
2335
- }
2336
- serialized.admin = { ...serialized.admin, hooks };
2337
- }
2338
- if (typeof serialized.options === "function" || serialized.options && typeof serialized.options === "object" && "resolve" in serialized.options) {
2339
- serialized.options = { _dynamic: true };
2340
- }
2341
- if (serialized.fields) {
2342
- serialized.fields = serialized.fields.map(serializeFieldForApi);
2343
- }
2344
- if (serialized.blocks) {
2345
- serialized.blocks = serialized.blocks.map((b) => ({
2346
- ...b,
2347
- fields: b.fields?.map(serializeFieldForApi)
2348
- }));
2349
- }
2350
- return serialized;
2351
- }
2352
- function registerRoutes(app, config) {
2353
- app.get("/api/schemas", optionalAuth(), async (c) => {
2354
- const siteId = c.req.header("X-Site-Id");
2355
- let collections = [...config.collections];
2356
- let globals = [...config.globals];
2357
- if (siteId && config.onSchemaFetch) {
2358
- const dynamic = await config.onSchemaFetch(siteId);
2359
- if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2360
- if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2361
- if (dynamic.admin) {
2362
- config.admin = { ...config.admin, ...dynamic.admin };
2363
- }
2364
- }
2365
- const user = c.get("user");
2366
- const accessArgs = { user, req: c.req, doc: null };
2367
- const serializeAccess = async (access) => {
2368
- if (typeof access === "string") return access;
2369
- if (typeof access === "boolean") return access;
2370
- return checkAccess(access, accessArgs);
2371
- };
2372
- const filteredCollections = await Promise.all(collections.filter((col) => !siteId || col.shared || !col.siteId || col.siteId === siteId).map(async (col) => ({
2373
- slug: col.slug,
2374
- labels: col.labels,
2375
- access: {
2376
- read: await serializeAccess(col.access?.read),
2377
- create: await serializeAccess(col.access?.create),
2378
- update: await serializeAccess(col.access?.update),
2379
- delete: await serializeAccess(col.access?.delete)
2380
- },
2381
- fields: await Promise.all(col.fields.map(serializeFieldForApi).map(async (f) => ({
2382
- name: f.name,
2383
- type: f.type,
2384
- label: f.label,
2385
- required: f.required,
2386
- defaultValue: f.defaultValue,
2387
- options: f.options,
2388
- relationTo: f.relationTo,
2389
- hasMany: f.hasMany,
2390
- fields: f.fields,
2391
- blocks: f.blocks,
2392
- collection: f.collection,
2393
- on: f.on,
2394
- limit: f.limit,
2395
- admin: f.admin,
2396
- access: {
2397
- read: await serializeAccess(f.access?.read),
2398
- update: await serializeAccess(f.access?.update)
2399
- }
2400
- }))),
2401
- upload: !!col.upload,
2402
- auth: !!col.auth,
2403
- admin: col.admin
2404
- })));
2405
- const filteredGlobals = await Promise.all(globals.filter((glb) => !siteId || glb.shared || !glb.siteId || glb.siteId === siteId).map(async (glb) => ({
2406
- slug: glb.slug,
2407
- label: glb.label,
2408
- access: {
2409
- read: await serializeAccess(glb.access?.read),
2410
- update: await serializeAccess(glb.access?.update)
2411
- },
2412
- fields: await Promise.all(glb.fields.map(serializeFieldForApi).map(async (f) => ({
2413
- name: f.name,
2414
- type: f.type,
2415
- label: f.label,
2416
- required: f.required,
2417
- defaultValue: f.defaultValue,
2418
- options: f.options,
2419
- relationTo: f.relationTo,
2420
- hasMany: f.hasMany,
2421
- fields: f.fields,
2422
- blocks: f.blocks,
2423
- collection: f.collection,
2424
- on: f.on,
2425
- limit: f.limit,
2426
- admin: f.admin,
2427
- access: {
2428
- read: await serializeAccess(f.access?.read),
2429
- update: await serializeAccess(f.access?.update)
2430
- }
2431
- }))),
2432
- admin: glb.admin
2433
- })));
2434
- return c.json({
2435
- collections: filteredCollections,
2436
- globals: filteredGlobals,
2437
- admin: config.admin || {}
2438
- });
2439
- });
2440
- app.get("/api/dyrected/options/:collection/:field", optionalAuth(), async (c) => {
2441
- const { collection: colSlug, field: fieldName } = c.req.param();
2442
- const siteId = c.req.header("X-Site-Id");
2443
- let collections = [...config.collections];
2444
- if (siteId && config.onSchemaFetch) {
2445
- const dynamic = await config.onSchemaFetch(siteId);
2446
- if (dynamic.collections) collections = [...collections, ...dynamic.collections];
2447
- }
2448
- const user = c.get("user");
2449
- let collection = collections.find((col) => col.slug === colSlug);
2450
- let field;
2451
- if (collection) {
2452
- const accessExpr = collection.access?.read;
2453
- if (accessExpr !== void 0 && accessExpr !== null) {
2454
- const accessArgs = { user, req: c.req, doc: null };
2455
- const allowed = await checkAccess(accessExpr, accessArgs);
2456
- if (!allowed) {
2457
- return c.json({ error: true, message: `Access denied: read on ${colSlug}` }, 403);
2458
- }
2459
- }
2460
- field = collection.fields.find((f) => f.name === fieldName);
2461
- } else {
2462
- let globals = [...config.globals];
2463
- if (siteId && config.onSchemaFetch) {
2464
- const dynamic = await config.onSchemaFetch(siteId);
2465
- if (dynamic.globals) globals = [...globals, ...dynamic.globals];
2466
- }
2467
- const glb = globals.find((g) => g.slug === colSlug);
2468
- if (!glb) {
2469
- return c.json({ error: true, message: `${colSlug} not found as collection or global` }, 404);
2470
- }
2471
- const accessExpr = glb.access?.read;
2472
- if (accessExpr !== void 0 && accessExpr !== null) {
2473
- const accessArgs = { user, req: c.req, doc: null };
2474
- const allowed = await checkAccess(accessExpr, accessArgs);
2475
- if (!allowed) {
2476
- return c.json({ error: true, message: `Access denied: read on global ${colSlug}` }, 403);
2477
- }
2478
- }
2479
- field = glb.fields.find((f) => f.name === fieldName);
2480
- }
2481
- if (!field) {
2482
- return c.json({ error: true, message: `Field ${fieldName} not found in ${colSlug}` }, 404);
2483
- }
2484
- let resolver;
2485
- if (typeof field.options === "function") {
2486
- resolver = field.options;
2487
- } else if (field.options && typeof field.options === "object" && "resolve" in field.options) {
2488
- resolver = field.options.resolve;
2489
- }
2490
- if (!resolver) {
2491
- return c.json({ error: true, message: `Field ${fieldName} in ${colSlug} is not dynamic` }, 400);
2492
- }
2493
- try {
2494
- const db = c.get("db") || config.db;
2495
- const queryParams = c.req.query();
2496
- const reqContext = {
2497
- query: queryParams,
2498
- headers: c.req.header(),
2499
- raw: c.req.raw
2500
- };
2501
- const result = await resolver({
2502
- db,
2503
- user,
2504
- req: reqContext
2505
- });
2506
- return c.json(result);
2507
- } catch (err) {
2508
- console.error(`[dyrected/core] Failed to resolve dynamic options for field ${fieldName}:`, err);
2509
- return c.json({ error: true, message: err.message || "Failed to resolve dynamic options" }, 500);
2510
- }
2511
- });
2512
- app.get("/api/openapi.json", (c) => {
2513
- return c.json(generateOpenApi(config));
2514
- });
2515
- app.get("/api/docs", (c) => {
2516
- return c.html(getSwaggerHtml());
2517
- });
2518
- app.get("/api/media/:filename{.+$}", async (c) => {
2519
- const mediaController = new MediaController("media");
2520
- return mediaController.serve(c);
2521
- });
2522
- app.get("/media/:filename{.+$}", async (c) => {
2523
- const mediaController = new MediaController("media");
2524
- return mediaController.serve(c);
2525
- });
2526
- if (config.storage) {
2527
- const uploadCollections = config.collections.filter((c) => c.upload);
2528
- for (const col of uploadCollections) {
2529
- const mediaController = new MediaController(col.slug);
2530
- const prefix = `/api/collections/${col.slug}`;
2531
- app.get(`${prefix}/media`, accessGate(col, "read"), (c) => mediaController.find(c));
2532
- app.get(`${prefix}/media/:filename{.+$}`, (c) => mediaController.serve(c));
2533
- app.post(`${prefix}/media`, accessGate(col, "create"), (c) => mediaController.upload(c));
2534
- app.delete(`${prefix}/media/:id`, accessGate(col, "delete"), (c) => mediaController.delete(c));
2535
- }
2536
- }
2537
- for (const collection of config.collections) {
2538
- if (!collection.auth) continue;
2539
- const path = `/api/collections/${collection.slug}`;
2540
- const authController = new AuthController(collection);
2541
- app.post(`${path}/login`, (c) => authController.login(c));
2542
- app.post(`${path}/logout`, (c) => authController.logout(c));
2543
- app.get(`${path}/init`, (c) => authController.init(c));
2544
- app.post(`${path}/first-user`, (c) => authController.registerFirstUser(c));
2545
- app.get(`${path}/me`, requireAuth(), (c) => authController.me(c));
2546
- app.post(`${path}/refresh-token`, requireAuth(), (c) => authController.refreshToken(c));
2547
- app.post(`${path}/forgot-password`, (c) => authController.forgotPassword(c));
2548
- app.post(`${path}/reset-password`, (c) => authController.resetPassword(c));
2549
- app.post(`${path}/invite`, requireAuth(), (c) => authController.invite(c));
2550
- app.post(`${path}/accept-invite`, (c) => authController.acceptInvite(c));
2551
- }
2552
- for (const collection of config.collections) {
2553
- const path = `/api/collections/${collection.slug}`;
2554
- const controller = new CollectionController(collection);
2555
- app.get(path, accessGate(collection, "read"), (c) => controller.find(c));
2556
- app.post(path, accessGate(collection, "create"), (c) => controller.create(c));
2557
- app.post(`${path}/media`, accessGate(collection, "create"), (c) => controller.create(c));
2558
- app.delete(`${path}/delete-many`, accessGate(collection, "delete"), (c) => controller.deleteMany(c));
2559
- app.get(`${path}/:id`, accessGate(collection, "read"), (c) => controller.findOne(c));
2560
- app.patch(`${path}/:id`, accessGate(collection, "update"), (c) => controller.update(c));
2561
- app.delete(`${path}/:id`, accessGate(collection, "delete"), (c) => controller.delete(c));
2562
- app.post(`${path}/seed`, (c) => controller.seed(c));
2563
- if (collection.auth) {
2564
- app.post(`${path}/:id/change-password`, requireAuth(), (c) => controller.changePassword(c));
2565
- }
2566
- }
2567
- for (const global of config.globals) {
2568
- const path = `/api/globals/${global.slug}`;
2569
- const controller = new GlobalController(global);
2570
- app.get(path, accessGate(global, "read"), (c) => controller.get(c));
2571
- app.patch(path, accessGate(global, "update"), (c) => controller.update(c));
2572
- app.post(`${path}/seed`, (c) => controller.seed(c));
2573
- }
2574
- const previewController = new PreviewController();
2575
- app.post("/api/preview-token", requireAuth(), (c) => previewController.createToken(c));
2576
- app.get("/api/preview-data", (c) => previewController.getData(c));
2577
- app.all("/api/collections/:slug/:id?", async (c) => {
2578
- const slug = c.req.param("slug");
2579
- const id = c.req.param("id");
2580
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2581
- const config2 = c.get("config");
2582
- if (config2.collections.some((col) => col.slug === slug)) {
2583
- return c.json({ message: "Method Not Allowed" }, 405);
2584
- }
2585
- if (config2.onSchemaFetch && siteId) {
2586
- const dynamic = await config2.onSchemaFetch(siteId);
2587
- let collection = dynamic.collections?.find((col) => col.slug === slug);
2588
- if (!collection && slug === "media") {
2589
- collection = {
2590
- slug: "media",
2591
- labels: { singular: "Media", plural: "Media" },
2592
- upload: true,
2593
- fields: []
2594
- };
2595
- }
2596
- if (collection) {
2597
- if (collection.auth && id) {
2598
- const authController = new AuthController(collection);
2599
- const method2 = c.req.method;
2600
- if (method2 === "POST" && id === "login") return authController.login(c);
2601
- if (method2 === "POST" && id === "logout") return authController.logout(c);
2602
- if (method2 === "GET" && id === "me") return authController.me(c);
2603
- if (method2 === "POST" && id === "refresh-token") return authController.refreshToken(c);
2604
- if (method2 === "POST" && id === "forgot-password") return authController.forgotPassword(c);
2605
- if (method2 === "POST" && id === "reset-password") return authController.resetPassword(c);
2606
- }
2607
- const controller = new CollectionController(collection);
2608
- const method = c.req.method;
2609
- if (id) {
2610
- if (method === "GET") return controller.findOne(c);
2611
- if (method === "PATCH") return controller.update(c);
2612
- if (method === "DELETE" && id === "delete-many") return controller.deleteMany(c);
2613
- if (method === "DELETE") return controller.delete(c);
2614
- if (method === "POST" && id === "media") return controller.create(c);
2615
- if (method === "POST" && id === "seed") return controller.seed(c);
2616
- } else {
2617
- if (method === "GET") return controller.find(c);
2618
- if (method === "POST") return controller.create(c);
2619
- }
2620
- }
2621
- }
2622
- return c.json({ message: `Collection "${slug}" not found` }, 404);
2623
- });
2624
- app.all("/api/globals/:slug", async (c) => {
2625
- const slug = c.req.param("slug");
2626
- const siteId = c.req.header("X-Site-Id") || c.get("siteId");
2627
- const config2 = c.get("config");
2628
- if (config2.globals.some((glb) => glb.slug === slug)) {
2629
- return c.json({ message: "Method Not Allowed" }, 405);
2630
- }
2631
- if (config2.onSchemaFetch && siteId) {
2632
- const dynamic = await config2.onSchemaFetch(siteId);
2633
- const global = dynamic.globals?.find((glb) => glb.slug === slug);
2634
- if (global) {
2635
- const controller = new GlobalController(global);
2636
- if (c.req.method === "GET") return controller.get(c);
2637
- if (c.req.method === "PATCH") return controller.update(c);
2638
- }
2639
- }
2640
- return c.json({ message: `Global "${slug}" not found` }, 404);
2641
- });
2642
- }
2643
-
2644
- // src/app.ts
2645
- import { Hono } from "hono";
2646
- import { cors } from "hono/cors";
2647
- import { requestId } from "hono/request-id";
2648
- async function createDyrectedApp(rawConfig) {
2649
- const config = normalizeConfig(rawConfig);
2650
- const app = new Hono();
2651
- if (config.db?.sync) {
2652
- await config.db.sync(config.collections, config.globals);
2653
- }
2654
- app.use("*", requestId());
2655
- app.use("*", optionalAuth());
2656
- app.use("*", async (c, next) => {
2657
- const start = Date.now();
2658
- await next();
2659
- const ms = Date.now() - start;
2660
- console.log(`[dyrected/api] ${c.req.method} ${c.req.path} ${c.res.status} - ${ms}ms`);
2661
- });
2662
- app.use("*", cors());
2663
- app.use("*", async (c, next) => {
2664
- c.set("config", config);
2665
- if (!c.get("siteId")) {
2666
- c.set("siteId", "default");
2667
- }
2668
- await next();
2669
- });
2670
- app.get("/health", (c) => c.json({ status: "ok", version: "0.0.1" }));
2671
- app.get("/routes", (c) => {
2672
- const routes = app.routes.map((r) => ({ method: r.method, path: r.path }));
2673
- return c.json({ routes });
2674
- });
2675
- app.onError((err, c) => {
2676
- console.error(`[dyrected/core] Uncaught Error:`, err);
2677
- return c.json({
2678
- message: err.message || "Internal Server Error",
2679
- stack: process.env.NODE_ENV === "development" ? err.stack : void 0
2680
- }, 500);
2681
- });
2682
- registerRoutes(app, config);
2683
- return app;
2684
- }
2685
-
2686
- export {
2687
- normalizeConfig,
2688
- runCollectionHooks,
2689
- executeFieldBeforeChange,
2690
- executeFieldAfterRead,
2691
- CollectionController,
2692
- GlobalController,
2693
- MediaController,
2694
- AuthController,
2695
- PreviewController,
2696
- registerRoutes,
2697
- createDyrectedApp
2698
- };