@kyro-cms/core 0.9.0 → 0.9.2

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 (265) hide show
  1. package/README.md +55 -593
  2. package/dist/{WebhookService-AefJfqX0.d.cts → WebhookService-BKszZlG0.d.cts} +1 -1
  3. package/dist/{WebhookService-118ZTFis.d.ts → WebhookService-Ccf1j-IN.d.ts} +1 -1
  4. package/dist/api-handler-graphql.cjs +44 -0
  5. package/dist/api-handler-graphql.cjs.map +1 -0
  6. package/dist/api-handler-graphql.d.cts +6 -0
  7. package/dist/api-handler-graphql.d.ts +6 -0
  8. package/dist/api-handler-graphql.js +41 -0
  9. package/dist/api-handler-graphql.js.map +1 -0
  10. package/dist/api-handler-trpc.cjs +38 -0
  11. package/dist/api-handler-trpc.cjs.map +1 -0
  12. package/dist/api-handler-trpc.d.cts +5 -0
  13. package/dist/api-handler-trpc.d.ts +5 -0
  14. package/dist/api-handler-trpc.js +36 -0
  15. package/dist/api-handler-trpc.js.map +1 -0
  16. package/dist/api-handler.cjs +33 -99
  17. package/dist/api-handler.cjs.map +1 -1
  18. package/dist/api-handler.d.cts +2 -1
  19. package/dist/api-handler.d.ts +2 -1
  20. package/dist/api-handler.js +21 -97
  21. package/dist/api-handler.js.map +1 -1
  22. package/dist/{tenant-B1YB0Jy8.d.ts → base-CIuXkrH4.d.cts} +7 -15
  23. package/dist/{tenant-Cpeveji6.d.cts → base-fFo4lqER.d.ts} +7 -15
  24. package/dist/bootstrap-3PV3GJ3S.js +7 -0
  25. package/dist/{bootstrap-JCML6NFO.js.map → bootstrap-3PV3GJ3S.js.map} +1 -1
  26. package/dist/bootstrap-4CELFLJO.cjs +32 -0
  27. package/dist/{bootstrap-AKAUP6F6.cjs.map → bootstrap-4CELFLJO.cjs.map} +1 -1
  28. package/dist/{chunk-VJT6P4N6.cjs → chunk-3HR772HI.cjs} +199 -32
  29. package/dist/chunk-3HR772HI.cjs.map +1 -0
  30. package/dist/chunk-3KTWGODI.cjs +178 -0
  31. package/dist/chunk-3KTWGODI.cjs.map +1 -0
  32. package/dist/{chunk-QXIQWPAP.js → chunk-3UK5XBVJ.js} +4 -134
  33. package/dist/chunk-3UK5XBVJ.js.map +1 -0
  34. package/dist/{chunk-FXYP2HA6.js → chunk-4AO3A3JM.js} +48 -4
  35. package/dist/chunk-4AO3A3JM.js.map +1 -0
  36. package/dist/{chunk-Z6ZWNWWR.js → chunk-4CV4JOE5.js} +3 -9
  37. package/dist/{chunk-Z6ZWNWWR.js.map → chunk-4CV4JOE5.js.map} +1 -1
  38. package/dist/chunk-4M7X5HAB.cjs +173 -0
  39. package/dist/chunk-4M7X5HAB.cjs.map +1 -0
  40. package/dist/chunk-53NYVYVX.js +3243 -0
  41. package/dist/chunk-53NYVYVX.js.map +1 -0
  42. package/dist/{chunk-35U3FROB.js → chunk-5H3MWQJS.js} +714 -184
  43. package/dist/chunk-5H3MWQJS.js.map +1 -0
  44. package/dist/{chunk-YVUJBEXE.cjs → chunk-5PMQQFRE.cjs} +16 -7
  45. package/dist/chunk-5PMQQFRE.cjs.map +1 -0
  46. package/dist/{chunk-57P6MJKC.js → chunk-6UNONDW7.js} +94 -10
  47. package/dist/chunk-6UNONDW7.js.map +1 -0
  48. package/dist/{chunk-Y3N7UUDO.js → chunk-7OGPN7MP.js} +5 -2
  49. package/dist/chunk-7OGPN7MP.js.map +1 -0
  50. package/dist/{chunk-2OL4O2TH.cjs → chunk-7OS7TX2Q.cjs} +68 -62
  51. package/dist/chunk-7OS7TX2Q.cjs.map +1 -0
  52. package/dist/{chunk-3TPQ2BU6.js → chunk-BYBMTIMT.js} +2 -6
  53. package/dist/chunk-BYBMTIMT.js.map +1 -0
  54. package/dist/{chunk-ES5HNFFT.js → chunk-CF7OL6HR.js} +4 -2
  55. package/dist/chunk-CF7OL6HR.js.map +1 -0
  56. package/dist/chunk-CJONKRHJ.js +162 -0
  57. package/dist/chunk-CJONKRHJ.js.map +1 -0
  58. package/dist/{chunk-OHVB4AJ7.js → chunk-CJX74IYK.js} +24 -18
  59. package/dist/chunk-CJX74IYK.js.map +1 -0
  60. package/dist/{chunk-5KVM3WEY.cjs → chunk-CNKT4PME.cjs} +1592 -868
  61. package/dist/chunk-CNKT4PME.cjs.map +1 -0
  62. package/dist/{chunk-G7VZBCD6.cjs → chunk-CZLDE2OZ.cjs} +2 -9
  63. package/dist/{chunk-G7VZBCD6.cjs.map → chunk-CZLDE2OZ.cjs.map} +1 -1
  64. package/dist/{chunk-WQBRWOQT.cjs → chunk-DPA3KWPY.cjs} +4 -3
  65. package/dist/chunk-DPA3KWPY.cjs.map +1 -0
  66. package/dist/{chunk-LINKCEG4.cjs → chunk-E2763JUP.cjs} +726 -196
  67. package/dist/chunk-E2763JUP.cjs.map +1 -0
  68. package/dist/chunk-E5UJBLQ7.js +220 -0
  69. package/dist/chunk-E5UJBLQ7.js.map +1 -0
  70. package/dist/{chunk-DVD5P72E.cjs → chunk-EEJUFDMF.cjs} +2 -6
  71. package/dist/chunk-EEJUFDMF.cjs.map +1 -0
  72. package/dist/chunk-FSKONGCX.cjs +253 -0
  73. package/dist/chunk-FSKONGCX.cjs.map +1 -0
  74. package/dist/{chunk-Y3QQN7PN.js → chunk-GAAHG2Z4.js} +13 -4
  75. package/dist/chunk-GAAHG2Z4.js.map +1 -0
  76. package/dist/chunk-GAOXD3XT.js +175 -0
  77. package/dist/chunk-GAOXD3XT.js.map +1 -0
  78. package/dist/{chunk-SA7NSSIQ.cjs → chunk-GUUB5EAG.cjs} +13 -187
  79. package/dist/chunk-GUUB5EAG.cjs.map +1 -0
  80. package/dist/{chunk-4DA7QPLA.cjs → chunk-GXFOGU7N.cjs} +5 -2
  81. package/dist/chunk-GXFOGU7N.cjs.map +1 -0
  82. package/dist/{chunk-I7HHI6QV.cjs → chunk-IDVRRRAK.cjs} +17 -9
  83. package/dist/chunk-IDVRRRAK.cjs.map +1 -0
  84. package/dist/{chunk-HXRD4B37.js → chunk-IPTZM3VE.js} +1423 -704
  85. package/dist/chunk-IPTZM3VE.js.map +1 -0
  86. package/dist/chunk-KC2GDBLS.cjs +84 -0
  87. package/dist/chunk-KC2GDBLS.cjs.map +1 -0
  88. package/dist/{chunk-QUW2RZTM.cjs → chunk-L46ROHUS.cjs} +51 -7
  89. package/dist/chunk-L46ROHUS.cjs.map +1 -0
  90. package/dist/chunk-L4EZKIEX.js +185 -0
  91. package/dist/chunk-L4EZKIEX.js.map +1 -0
  92. package/dist/{chunk-REK7AYOC.js → chunk-L5UKKZQN.js} +199 -32
  93. package/dist/chunk-L5UKKZQN.js.map +1 -0
  94. package/dist/chunk-NKPKR5BW.cjs +188 -0
  95. package/dist/chunk-NKPKR5BW.cjs.map +1 -0
  96. package/dist/chunk-NWUEVLQT.cjs +99 -0
  97. package/dist/chunk-NWUEVLQT.cjs.map +1 -0
  98. package/dist/{chunk-3AJE4SEG.js → chunk-OHC6UHFY.js} +208 -76
  99. package/dist/chunk-OHC6UHFY.js.map +1 -0
  100. package/dist/chunk-PHJRNPHY.cjs +3291 -0
  101. package/dist/chunk-PHJRNPHY.cjs.map +1 -0
  102. package/dist/{chunk-DXHRBMGB.js → chunk-PQ72Z6WC.js} +67 -112
  103. package/dist/chunk-PQ72Z6WC.js.map +1 -0
  104. package/dist/{chunk-K7JPTH3G.cjs → chunk-PV2I2KMI.cjs} +214 -82
  105. package/dist/chunk-PV2I2KMI.cjs.map +1 -0
  106. package/dist/{chunk-PDYFVNUX.cjs → chunk-Q23GAMLE.cjs} +71 -116
  107. package/dist/chunk-Q23GAMLE.cjs.map +1 -0
  108. package/dist/{chunk-H727JIG7.js → chunk-Q72BOAPK.js} +16 -8
  109. package/dist/chunk-Q72BOAPK.js.map +1 -0
  110. package/dist/{chunk-IBG6V56E.cjs → chunk-QFLB4EIJ.cjs} +2 -139
  111. package/dist/chunk-QFLB4EIJ.cjs.map +1 -0
  112. package/dist/{chunk-2KVHZE6O.cjs → chunk-RFFSZSCL.cjs} +282 -190
  113. package/dist/chunk-RFFSZSCL.cjs.map +1 -0
  114. package/dist/{chunk-V3LKPM3O.cjs → chunk-SHTTJMLT.cjs} +4 -2
  115. package/dist/chunk-SHTTJMLT.cjs.map +1 -0
  116. package/dist/{chunk-WOWUL7ZY.js → chunk-UUDTPZX6.js} +5 -4
  117. package/dist/chunk-UUDTPZX6.js.map +1 -0
  118. package/dist/{chunk-QPPDLRNR.js → chunk-V7KZQIZ6.js} +277 -185
  119. package/dist/chunk-V7KZQIZ6.js.map +1 -0
  120. package/dist/{chunk-3ZFYL34R.js → chunk-WXVB364T.js} +12 -185
  121. package/dist/chunk-WXVB364T.js.map +1 -0
  122. package/dist/chunk-XEB7PH2E.js +81 -0
  123. package/dist/chunk-XEB7PH2E.js.map +1 -0
  124. package/dist/{chunk-IA6AU5PI.cjs → chunk-Y7AQK4R4.cjs} +94 -10
  125. package/dist/chunk-Y7AQK4R4.cjs.map +1 -0
  126. package/dist/chunk-YFAVQQTU.js +92 -0
  127. package/dist/chunk-YFAVQQTU.js.map +1 -0
  128. package/dist/cli/index.cjs +6 -6
  129. package/dist/cli/index.cjs.map +1 -1
  130. package/dist/cli/index.js +6 -6
  131. package/dist/cli/index.js.map +1 -1
  132. package/dist/client.cjs +4 -4
  133. package/dist/client.d.cts +3 -3
  134. package/dist/client.d.ts +3 -3
  135. package/dist/client.js +2 -2
  136. package/dist/drizzle/index.cjs +15 -14
  137. package/dist/drizzle/index.d.cts +10 -14
  138. package/dist/drizzle/index.d.ts +10 -14
  139. package/dist/drizzle/index.js +6 -5
  140. package/dist/fields/index.cjs +22 -38
  141. package/dist/fields/index.d.cts +2 -22
  142. package/dist/fields/index.d.ts +2 -22
  143. package/dist/fields/index.js +2 -2
  144. package/dist/graphql/index.cjs +6 -5
  145. package/dist/graphql/index.d.cts +5 -3
  146. package/dist/graphql/index.d.ts +5 -3
  147. package/dist/graphql/index.js +4 -3
  148. package/dist/index-BKta3cBH.d.cts +277 -0
  149. package/dist/index-ClOqnkTO.d.ts +277 -0
  150. package/dist/index.cjs +310 -168
  151. package/dist/index.cjs.map +1 -1
  152. package/dist/index.d.cts +130 -211
  153. package/dist/index.d.ts +130 -211
  154. package/dist/index.js +174 -35
  155. package/dist/index.js.map +1 -1
  156. package/dist/integration.cjs +3 -3
  157. package/dist/integration.js +2 -2
  158. package/dist/media-7WDX4BDJ.js +4 -0
  159. package/dist/{media-GPPTZ43E.js.map → media-7WDX4BDJ.js.map} +1 -1
  160. package/dist/{media-XNTUFJZR.cjs → media-TUSLVRQ6.cjs} +3 -3
  161. package/dist/{media-XNTUFJZR.cjs.map → media-TUSLVRQ6.cjs.map} +1 -1
  162. package/dist/mongo-auth-adapter-GT4S7SCU.cjs +17 -0
  163. package/dist/{mongo-auth-adapter-NHHUJHVH.cjs.map → mongo-auth-adapter-GT4S7SCU.cjs.map} +1 -1
  164. package/dist/mongo-auth-adapter-M7VV4LNB.js +4 -0
  165. package/dist/{mongo-auth-adapter-NJQUUCTP.js.map → mongo-auth-adapter-M7VV4LNB.js.map} +1 -1
  166. package/dist/mongodb/index.cjs +9 -8
  167. package/dist/mongodb/index.d.cts +6 -13
  168. package/dist/mongodb/index.d.ts +6 -13
  169. package/dist/mongodb/index.js +5 -4
  170. package/dist/postgres-auth-adapter-AFAPISH7.js +5 -0
  171. package/dist/{postgres-auth-adapter-3T2NKTSE.js.map → postgres-auth-adapter-AFAPISH7.js.map} +1 -1
  172. package/dist/postgres-auth-adapter-SFDTLONT.cjs +14 -0
  173. package/dist/{postgres-auth-adapter-7IEENCKQ.cjs.map → postgres-auth-adapter-SFDTLONT.cjs.map} +1 -1
  174. package/dist/redis-adapter-UQX4EE3B.cjs +13 -0
  175. package/dist/{redis-adapter-D2E2S3GB.cjs.map → redis-adapter-UQX4EE3B.cjs.map} +1 -1
  176. package/dist/redis-adapter-XALOGWY3.js +4 -0
  177. package/dist/{redis-adapter-VQXD7ESY.js.map → redis-adapter-XALOGWY3.js.map} +1 -1
  178. package/dist/rest/index.cjs +16 -15
  179. package/dist/rest/index.d.cts +4 -4
  180. package/dist/rest/index.d.ts +4 -4
  181. package/dist/rest/index.js +14 -13
  182. package/dist/{schema-37SE2F4B.cjs → schema-6QL3USNB.cjs} +15 -15
  183. package/dist/{schema-37SE2F4B.cjs.map → schema-6QL3USNB.cjs.map} +1 -1
  184. package/dist/{schema-5PHL5IVB.js → schema-FNNWEAAW.js} +4 -4
  185. package/dist/{schema-5PHL5IVB.js.map → schema-FNNWEAAW.js.map} +1 -1
  186. package/dist/sqlite-adapter-AQB5TCGV.cjs +13 -0
  187. package/dist/{sqlite-adapter-LVK5PS4T.cjs.map → sqlite-adapter-AQB5TCGV.cjs.map} +1 -1
  188. package/dist/sqlite-adapter-N5H6IM2X.js +4 -0
  189. package/dist/{sqlite-adapter-TR3U3W6Q.js.map → sqlite-adapter-N5H6IM2X.js.map} +1 -1
  190. package/dist/templates/index.cjs +134 -32
  191. package/dist/templates/index.d.cts +52 -9
  192. package/dist/templates/index.d.ts +52 -9
  193. package/dist/templates/index.js +4 -2
  194. package/dist/trpc/index.cjs +14 -13
  195. package/dist/trpc/index.d.cts +55 -49
  196. package/dist/trpc/index.d.ts +55 -49
  197. package/dist/trpc/index.js +5 -4
  198. package/dist/{types-D6ZLRGbH.d.cts → types-CpjuXbe7.d.cts} +2 -0
  199. package/dist/{types-D6ZLRGbH.d.ts → types-CpjuXbe7.d.ts} +2 -0
  200. package/dist/{types-VtjUxIMp.d.cts → types-DeSApf9T.d.cts} +36 -14
  201. package/dist/{types-VtjUxIMp.d.ts → types-DeSApf9T.d.ts} +36 -14
  202. package/dist/{types-J3R9nVsZ.d.cts → types-Dgzlftb7.d.ts} +32 -28
  203. package/dist/{types-Bs1up4yP.d.ts → types-Ds0tCA3L.d.cts} +32 -28
  204. package/dist/ws/index.cjs +6 -6
  205. package/dist/ws/index.js +2 -2
  206. package/package.json +22 -4
  207. package/dist/bootstrap-AKAUP6F6.cjs +0 -32
  208. package/dist/bootstrap-JCML6NFO.js +0 -7
  209. package/dist/chunk-2KVHZE6O.cjs.map +0 -1
  210. package/dist/chunk-2OL4O2TH.cjs.map +0 -1
  211. package/dist/chunk-35U3FROB.js.map +0 -1
  212. package/dist/chunk-3AJE4SEG.js.map +0 -1
  213. package/dist/chunk-3J4MFTI3.js +0 -3872
  214. package/dist/chunk-3J4MFTI3.js.map +0 -1
  215. package/dist/chunk-3TPQ2BU6.js.map +0 -1
  216. package/dist/chunk-3ZFYL34R.js.map +0 -1
  217. package/dist/chunk-4DA7QPLA.cjs.map +0 -1
  218. package/dist/chunk-57P6MJKC.js.map +0 -1
  219. package/dist/chunk-5KVM3WEY.cjs.map +0 -1
  220. package/dist/chunk-6IMPH6WV.cjs +0 -3897
  221. package/dist/chunk-6IMPH6WV.cjs.map +0 -1
  222. package/dist/chunk-ATBOUGQP.cjs +0 -513
  223. package/dist/chunk-ATBOUGQP.cjs.map +0 -1
  224. package/dist/chunk-DVD5P72E.cjs.map +0 -1
  225. package/dist/chunk-DXHRBMGB.js.map +0 -1
  226. package/dist/chunk-ES5HNFFT.js.map +0 -1
  227. package/dist/chunk-FXYP2HA6.js.map +0 -1
  228. package/dist/chunk-H727JIG7.js.map +0 -1
  229. package/dist/chunk-HXRD4B37.js.map +0 -1
  230. package/dist/chunk-I7HHI6QV.cjs.map +0 -1
  231. package/dist/chunk-IA6AU5PI.cjs.map +0 -1
  232. package/dist/chunk-IBG6V56E.cjs.map +0 -1
  233. package/dist/chunk-K7JPTH3G.cjs.map +0 -1
  234. package/dist/chunk-LINKCEG4.cjs.map +0 -1
  235. package/dist/chunk-OHVB4AJ7.js.map +0 -1
  236. package/dist/chunk-PDYFVNUX.cjs.map +0 -1
  237. package/dist/chunk-Q23JB3KL.js +0 -488
  238. package/dist/chunk-Q23JB3KL.js.map +0 -1
  239. package/dist/chunk-QPPDLRNR.js.map +0 -1
  240. package/dist/chunk-QUW2RZTM.cjs.map +0 -1
  241. package/dist/chunk-QXIQWPAP.js.map +0 -1
  242. package/dist/chunk-R3XIBBAW.cjs +0 -34
  243. package/dist/chunk-R3XIBBAW.cjs.map +0 -1
  244. package/dist/chunk-REK7AYOC.js.map +0 -1
  245. package/dist/chunk-SA7NSSIQ.cjs.map +0 -1
  246. package/dist/chunk-SDMNUYVU.js +0 -30
  247. package/dist/chunk-SDMNUYVU.js.map +0 -1
  248. package/dist/chunk-V3LKPM3O.cjs.map +0 -1
  249. package/dist/chunk-VJT6P4N6.cjs.map +0 -1
  250. package/dist/chunk-WOWUL7ZY.js.map +0 -1
  251. package/dist/chunk-WQBRWOQT.cjs.map +0 -1
  252. package/dist/chunk-Y3N7UUDO.js.map +0 -1
  253. package/dist/chunk-Y3QQN7PN.js.map +0 -1
  254. package/dist/chunk-YVUJBEXE.cjs.map +0 -1
  255. package/dist/index-CLp-DRKA.d.ts +0 -64
  256. package/dist/index-DfO7G4kN.d.cts +0 -64
  257. package/dist/media-GPPTZ43E.js +0 -4
  258. package/dist/mongo-auth-adapter-NHHUJHVH.cjs +0 -17
  259. package/dist/mongo-auth-adapter-NJQUUCTP.js +0 -4
  260. package/dist/postgres-auth-adapter-3T2NKTSE.js +0 -5
  261. package/dist/postgres-auth-adapter-7IEENCKQ.cjs +0 -14
  262. package/dist/redis-adapter-D2E2S3GB.cjs +0 -13
  263. package/dist/redis-adapter-VQXD7ESY.js +0 -4
  264. package/dist/sqlite-adapter-LVK5PS4T.cjs +0 -13
  265. package/dist/sqlite-adapter-TR3U3W6Q.js +0 -4
@@ -1,15 +1,15 @@
1
- import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-57P6MJKC.js';
2
- import { SQLiteAuthAdapter } from './chunk-H727JIG7.js';
1
+ import { PasswordPolicy, ConfigService, EmailTransport } from './chunk-6UNONDW7.js';
2
+ import { usersCollection } from './chunk-XEB7PH2E.js';
3
+ import { SQLiteAuthAdapter } from './chunk-Q72BOAPK.js';
3
4
  import { createAuditContext } from './chunk-P2YW545G.js';
4
- import { WEBHOOK_EVENTS, extractApiKeyFromRequest, validateApiKey, createApiKeyContext, API_KEY_COLLECTION, generateApiKey, generateApiKeyPrefix, hasApiKeyPermission } from './chunk-QXIQWPAP.js';
5
- import { buildGraphQLSchema } from './chunk-REK7AYOC.js';
6
- import { evaluateAccess } from './chunk-SDMNUYVU.js';
7
- import { genId, DrizzleAdapter } from './chunk-QPPDLRNR.js';
8
- import { PostgresAuthAdapter } from './chunk-OHVB4AJ7.js';
9
- import { MongoDBAdapter } from './chunk-DXHRBMGB.js';
10
- import { MongoDBAuthAdapter } from './chunk-Y3N7UUDO.js';
11
- import { hasPermission } from './chunk-3ZFYL34R.js';
12
- import { __esm, __export, __toCommonJS, __require } from './chunk-Z6ZWNWWR.js';
5
+ import { WEBHOOK_EVENTS } from './chunk-3UK5XBVJ.js';
6
+ import { API_KEY_COLLECTION, generateApiKey, generateApiKeyPrefix, evaluateAccess, hasApiKeyPermission, extractApiKeyFromRequest, validateApiKey, createApiKeyContext } from './chunk-CJONKRHJ.js';
7
+ import { genId, DrizzleAdapter } from './chunk-V7KZQIZ6.js';
8
+ import { PostgresAuthAdapter } from './chunk-CJX74IYK.js';
9
+ import { MongoDBAdapter } from './chunk-PQ72Z6WC.js';
10
+ import { MongoDBAuthAdapter } from './chunk-7OGPN7MP.js';
11
+ import { hasPermission } from './chunk-L4EZKIEX.js';
12
+ import { __esm, __export, __toCommonJS } from './chunk-4CV4JOE5.js';
13
13
  import crypto, { randomBytes, createHash } from 'crypto';
14
14
  import { createStorage } from 'unstorage';
15
15
  import fsDriver from 'unstorage/drivers/fs';
@@ -17,11 +17,13 @@ import { Hono } from 'hono';
17
17
  import path, { join, basename, extname, resolve } from 'path';
18
18
  import { existsSync, readFileSync } from 'fs';
19
19
  import sharp from 'sharp';
20
- import { parse, execute } from 'graphql';
21
20
  import { mkdir, readdir, stat, rename, unlink, writeFile } from 'fs/promises';
21
+ import process2 from 'process';
22
22
  import { S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, PutObjectCommand, HeadObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
23
+ import { NodeHttpHandler } from '@smithy/node-http-handler';
23
24
  import { Readable } from 'stream';
24
25
  import { Client } from 'basic-ftp';
26
+ import { zodToJsonSchema } from 'zod-to-json-schema';
25
27
 
26
28
  function setDbAdapter(adapter) {
27
29
  dbAdapter = adapter;
@@ -30,8 +32,8 @@ async function loadSecrets() {
30
32
  if (dbAdapter) {
31
33
  try {
32
34
  const result = await dbAdapter.findOne({
33
- collection: "_globals",
34
- where: { slug: "system" }
35
+ collection: "_globals_system",
36
+ where: {}
35
37
  });
36
38
  if (result) {
37
39
  cachedSecrets = {
@@ -441,6 +443,9 @@ function createTenantContextFromUser(user) {
441
443
  };
442
444
  }
443
445
 
446
+ // src/api/rest/hono-app.ts
447
+ init_secret();
448
+
444
449
  // src/auth/security/in-memory-rate-limit.ts
445
450
  var InMemoryRateLimiter = class {
446
451
  storage = /* @__PURE__ */ new Map();
@@ -773,7 +778,7 @@ var AuditLogger = class {
773
778
  serializeLog(log) {
774
779
  const result = {
775
780
  id: log.id,
776
- timestamp: log.timestamp.toISOString(),
781
+ timestamp: new Date(log.timestamp).toISOString(),
777
782
  action: log.action,
778
783
  resource: log.resource,
779
784
  success: log.success ? "1" : "0"
@@ -1168,23 +1173,19 @@ var AuthRoutes = class {
1168
1173
  }
1169
1174
  }
1170
1175
  async me(req) {
1171
- try {
1172
- const session = await getCurrentUser(req);
1173
- if (!session) {
1174
- return this.errorResponse("Not authenticated", 401);
1175
- }
1176
- const user = await this.authAdapter.findUserById(session.userId);
1177
- if (!user) {
1178
- return this.errorResponse("User not found", 404);
1179
- }
1180
- return this.jsonResponse({
1181
- success: true,
1182
- user: this.sanitizeUser(user)
1183
- });
1184
- } catch (error) {
1185
- console.error("[AuthRoutes.me] Authentication failed:", error.message);
1186
- return this.errorResponse("Authentication failed", 401);
1176
+ const session = await getCurrentUser(req);
1177
+ if (!session) {
1178
+ return this.errorResponse("Not authenticated", 401);
1187
1179
  }
1180
+ return this.jsonResponse({
1181
+ success: true,
1182
+ user: {
1183
+ id: session.userId,
1184
+ email: session.email,
1185
+ role: session.role,
1186
+ tenantId: session.tenantId
1187
+ }
1188
+ });
1188
1189
  }
1189
1190
  async changePassword(req) {
1190
1191
  const session = await getCurrentUser(req);
@@ -1294,7 +1295,7 @@ var AuthRoutes = class {
1294
1295
  }
1295
1296
  if (this.auditLogger) {
1296
1297
  await this.auditLogger.log({
1297
- action: "password_reset_request",
1298
+ action: "password_reset",
1298
1299
  userId: user.id,
1299
1300
  userEmail: user.email,
1300
1301
  resource: "auth",
@@ -1482,7 +1483,7 @@ var AuthRoutes = class {
1482
1483
  }
1483
1484
  };
1484
1485
  function createLocalStorage(config) {
1485
- const { uploadDir, baseUrl = "/uploads" } = config;
1486
+ const { uploadDir = join(process2.cwd(), "public", "uploads"), baseUrl = "/uploads" } = config;
1486
1487
  async function ensureDir(dir) {
1487
1488
  if (!existsSync(dir)) {
1488
1489
  await mkdir(dir, { recursive: true });
@@ -1670,6 +1671,467 @@ function createLocalStorage(config) {
1670
1671
  }
1671
1672
  };
1672
1673
  }
1674
+
1675
+ // src/storage/imgix.ts
1676
+ function createImgixStorage(config) {
1677
+ const signUrl = (path3, params) => {
1678
+ if (!config.signKey) return path3;
1679
+ const signer = new TextEncoder();
1680
+ const key = signer.encode(config.signKey);
1681
+ const data = signer.encode(path3 + params.toString());
1682
+ let hash = 0;
1683
+ const combined = new Uint8Array(key.length + data.length);
1684
+ combined.set(key);
1685
+ combined.set(data, key.length);
1686
+ for (let i = 0; i < combined.length; i++) {
1687
+ hash = (hash << 5) - hash + combined[i] | 0;
1688
+ }
1689
+ params.set("s", Math.abs(hash).toString(16));
1690
+ return path3;
1691
+ };
1692
+ return {
1693
+ name: "imgix",
1694
+ displayName: "Imgix",
1695
+ supportsDynamicResize: true,
1696
+ async upload(_file, _options) {
1697
+ throw new Error(
1698
+ "Imgix is a transformation service. Use another provider for uploads."
1699
+ );
1700
+ },
1701
+ async uploadFromUrl(url, options) {
1702
+ const filename = options?.filename || url.split("/").pop() || "file";
1703
+ const response = await fetch(url);
1704
+ if (!response.ok) {
1705
+ throw new Error(`Failed to fetch: ${response.statusText}`);
1706
+ }
1707
+ const blob = await response.blob();
1708
+ new File([blob], filename, { type: blob.type });
1709
+ return {
1710
+ id: Buffer.from(url).toString("base64").slice(0, 20),
1711
+ filename,
1712
+ originalName: filename,
1713
+ mimeType: blob.type,
1714
+ size: blob.size,
1715
+ url: this.getImageUrl(url),
1716
+ thumbnailUrl: this.getImageUrl(url, {
1717
+ width: 200,
1718
+ height: 200,
1719
+ fit: "crop"
1720
+ }),
1721
+ provider: "imgix",
1722
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1723
+ };
1724
+ },
1725
+ async delete(_url) {
1726
+ },
1727
+ async rename(_oldUrl, newKey) {
1728
+ return `https://${config.domain}/${newKey}`;
1729
+ },
1730
+ getImageUrl(url, transforms) {
1731
+ const parsed = new URL(url);
1732
+ const params = new URLSearchParams(parsed.search);
1733
+ if (config.defaultParameters) {
1734
+ Object.entries(config.defaultParameters).forEach(([key, value]) => {
1735
+ if (!params.has(key)) {
1736
+ params.set(key, value);
1737
+ }
1738
+ });
1739
+ }
1740
+ if (transforms) {
1741
+ if (transforms.width) params.set("w", String(transforms.width));
1742
+ if (transforms.height) params.set("h", String(transforms.height));
1743
+ if (transforms.quality) params.set("q", String(transforms.quality));
1744
+ if (transforms.format) params.set("fm", transforms.format);
1745
+ if (transforms.fit) params.set("fit", transforms.fit);
1746
+ if (transforms.blur) params.set("blur", String(transforms.blur));
1747
+ if (transforms.sharpen) params.set("sharp", String(transforms.sharpen));
1748
+ }
1749
+ params.set("auto", "compress,format");
1750
+ parsed.pathname + "?" + params.toString();
1751
+ if (config.signKey) {
1752
+ signUrl(parsed.pathname + params.toString(), params);
1753
+ }
1754
+ return `https://${config.domain}${parsed.pathname}${params.toString() ? "?" + params.toString() : ""}`;
1755
+ },
1756
+ async generateThumbnail(file) {
1757
+ return this.getImageUrl(file.url, {
1758
+ width: 200,
1759
+ height: 200,
1760
+ fit: "crop"
1761
+ });
1762
+ },
1763
+ async list() {
1764
+ return [];
1765
+ },
1766
+ async exists(url) {
1767
+ try {
1768
+ const response = await fetch(url, { method: "HEAD" });
1769
+ return response.ok;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ },
1774
+ async createFolder() {
1775
+ },
1776
+ async deleteFolder() {
1777
+ }
1778
+ };
1779
+ }
1780
+
1781
+ // src/storage/bunny.ts
1782
+ function getUrl(key, config) {
1783
+ if (config.cdnUrl) {
1784
+ return `${config.cdnUrl.replace(/\/$/, "")}/${key}`;
1785
+ }
1786
+ return `https://${config.storageZone}.b-cdn.net/${key}`;
1787
+ }
1788
+ function getUrlPrefix(config) {
1789
+ if (config.cdnUrl) {
1790
+ return config.cdnUrl.replace(/\/$/, "") + "/";
1791
+ }
1792
+ return `https://${config.storageZone}.b-cdn.net/`;
1793
+ }
1794
+ function createBunnyStorage(config) {
1795
+ const baseUrl = `https://storage.bunnycdn.com/${config.storageZone}`;
1796
+ const getKey = (path3) => {
1797
+ const prefix = config.prefix ? `${config.prefix}/` : "";
1798
+ return `${prefix}${path3}`.replace(/\/+/g, "/");
1799
+ };
1800
+ return {
1801
+ name: "bunny",
1802
+ displayName: "Bunny.net Storage",
1803
+ supportsDynamicResize: true,
1804
+ async upload(file, options) {
1805
+ const key = getKey(
1806
+ `${options?.folder ? `${options.folder}/` : ""}${options?.filename || file.name}`
1807
+ );
1808
+ const buffer = Buffer.from(await file.arrayBuffer());
1809
+ const response = await fetch(`${baseUrl}/${key}`, {
1810
+ method: "PUT",
1811
+ headers: {
1812
+ AccessKey: config.apiKey,
1813
+ "Content-Type": file.type
1814
+ },
1815
+ body: buffer
1816
+ });
1817
+ if (!response.ok) {
1818
+ const errorText = await response.text();
1819
+ throw new Error(
1820
+ `Bunny.net upload failed: ${response.status} ${errorText}`
1821
+ );
1822
+ }
1823
+ return {
1824
+ id: Buffer.from(key).toString("base64url"),
1825
+ filename: options?.filename || file.name,
1826
+ originalName: file.name,
1827
+ mimeType: file.type,
1828
+ size: buffer.length,
1829
+ url: getUrl(key, config),
1830
+ thumbnailUrl: file.type.startsWith("image/") ? getUrl(key, config) : void 0,
1831
+ folder: options?.folder,
1832
+ provider: "bunny",
1833
+ metadata: options?.metadata,
1834
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1835
+ };
1836
+ },
1837
+ async uploadFromUrl(url) {
1838
+ const response = await fetch(url);
1839
+ if (!response.ok) {
1840
+ throw new Error(`Failed to fetch: ${response.statusText}`);
1841
+ }
1842
+ const blob = await response.blob();
1843
+ const filename = url.split("/").pop() || "file";
1844
+ const file = new File([blob], filename, { type: blob.type });
1845
+ return this.upload(file);
1846
+ },
1847
+ async delete(url) {
1848
+ const key = url.replace(getUrlPrefix(config), "");
1849
+ const response = await fetch(`${baseUrl}/${key}`, {
1850
+ method: "DELETE",
1851
+ headers: {
1852
+ AccessKey: config.apiKey
1853
+ }
1854
+ });
1855
+ if (!response.ok && response.status !== 404) {
1856
+ const errorText = await response.text();
1857
+ throw new Error(
1858
+ `Bunny.net delete failed: ${response.status} ${errorText}`
1859
+ );
1860
+ }
1861
+ },
1862
+ async rename(oldUrl, newKey) {
1863
+ const oldKey = oldUrl.replace(getUrlPrefix(config), "");
1864
+ const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
1865
+ const response = await fetch(`${baseUrl}/${oldKey}`, {
1866
+ method: "GET",
1867
+ headers: {
1868
+ AccessKey: config.apiKey
1869
+ }
1870
+ });
1871
+ if (!response.ok) {
1872
+ throw new Error(`Bunny.net rename failed: could not read old file`);
1873
+ }
1874
+ const content = await response.arrayBuffer();
1875
+ await fetch(`${baseUrl}/${fullPath}`, {
1876
+ method: "PUT",
1877
+ headers: {
1878
+ AccessKey: config.apiKey,
1879
+ "Content-Type": response.headers.get("Content-Type") || "application/octet-stream"
1880
+ },
1881
+ body: content
1882
+ });
1883
+ await this.delete(oldUrl);
1884
+ return getUrl(fullPath, config);
1885
+ },
1886
+ getImageUrl(url, transforms) {
1887
+ if (!transforms || Object.keys(transforms).length === 0) return url;
1888
+ const params = new URLSearchParams({ url });
1889
+ if (transforms.width) params.set("w", String(transforms.width));
1890
+ if (transforms.height) params.set("h", String(transforms.height));
1891
+ if (transforms.quality) params.set("q", String(transforms.quality));
1892
+ if (transforms.format) params.set("f", transforms.format);
1893
+ return `/api/media/resize?${params.toString()}`;
1894
+ },
1895
+ async generateThumbnail(file) {
1896
+ return this.getImageUrl(file.url, { width: 400, height: 400 });
1897
+ },
1898
+ async list(prefix) {
1899
+ const key = getKey(prefix || "");
1900
+ const response = await fetch(`${baseUrl}/${key}`, {
1901
+ method: "GET",
1902
+ headers: {
1903
+ AccessKey: config.apiKey
1904
+ }
1905
+ });
1906
+ if (!response.ok) {
1907
+ const errorText = await response.text();
1908
+ throw new Error(
1909
+ `Bunny.net list failed: ${response.status} ${errorText}`
1910
+ );
1911
+ }
1912
+ const items = await response.json();
1913
+ return items.map((item) => ({
1914
+ id: Buffer.from(item.ObjectName || "").toString("base64url"),
1915
+ filename: item.ObjectName?.split("/").pop() || "",
1916
+ originalName: item.ObjectName?.split("/").pop() || "",
1917
+ mimeType: "application/octet-stream",
1918
+ size: item.Length || 0,
1919
+ url: getUrl(item.ObjectName || "", config),
1920
+ provider: "bunny",
1921
+ createdAt: item.LastChanged || (/* @__PURE__ */ new Date()).toISOString()
1922
+ }));
1923
+ },
1924
+ async exists(url) {
1925
+ const key = url.replace(getUrlPrefix(config), "");
1926
+ const response = await fetch(`${baseUrl}/${key}`, {
1927
+ method: "HEAD",
1928
+ headers: {
1929
+ AccessKey: config.apiKey
1930
+ }
1931
+ });
1932
+ return response.ok;
1933
+ },
1934
+ async createFolder(folder) {
1935
+ const key = getKey(`${folder}/`);
1936
+ await fetch(`${baseUrl}/${key}`, {
1937
+ method: "PUT",
1938
+ headers: {
1939
+ AccessKey: config.apiKey,
1940
+ "Content-Length": "0"
1941
+ },
1942
+ body: ""
1943
+ });
1944
+ },
1945
+ async deleteFolder(folder) {
1946
+ const key = getKey(`${folder}/`);
1947
+ await fetch(`${baseUrl}/${key}`, {
1948
+ method: "DELETE",
1949
+ headers: {
1950
+ AccessKey: config.apiKey
1951
+ }
1952
+ });
1953
+ }
1954
+ };
1955
+ }
1956
+ var StorageProviderRegistry = class {
1957
+ providers = /* @__PURE__ */ new Map();
1958
+ providerToPlugin = /* @__PURE__ */ new Map();
1959
+ disabledPlugins = /* @__PURE__ */ new Set();
1960
+ constructor() {
1961
+ this.registerLocal();
1962
+ this.registerImgix();
1963
+ this.registerBunny();
1964
+ }
1965
+ registerLocal() {
1966
+ this.register({
1967
+ type: "local",
1968
+ displayName: "Local Server",
1969
+ configKey: "local",
1970
+ configFields: [
1971
+ {
1972
+ name: "uploadDir",
1973
+ type: "text",
1974
+ label: "Upload Directory",
1975
+ defaultValue: "./public/uploads"
1976
+ },
1977
+ {
1978
+ name: "baseUrl",
1979
+ type: "text",
1980
+ label: "Base URL",
1981
+ defaultValue: "/uploads"
1982
+ }
1983
+ ],
1984
+ extractConfig: (sc, key) => sc[key] || {},
1985
+ extractRawConfig: (c) => {
1986
+ const localConfig = c?.local || c;
1987
+ const savedUploadDir = (localConfig?.uploadDir || "").trim();
1988
+ let uploadDir;
1989
+ if (savedUploadDir) {
1990
+ if (path.isAbsolute(savedUploadDir)) {
1991
+ uploadDir = savedUploadDir;
1992
+ } else if (savedUploadDir.includes("/") || savedUploadDir.includes("\\")) {
1993
+ uploadDir = path.resolve(process.cwd(), savedUploadDir);
1994
+ } else {
1995
+ uploadDir = path.join(process.cwd(), "public", savedUploadDir);
1996
+ }
1997
+ } else {
1998
+ uploadDir = path.join(process.cwd(), "public", "uploads");
1999
+ }
2000
+ const savedBaseUrl = (localConfig?.baseUrl || "").trim();
2001
+ let baseUrl;
2002
+ if (savedBaseUrl) {
2003
+ baseUrl = savedBaseUrl.startsWith("/") ? savedBaseUrl : `/${savedBaseUrl}`;
2004
+ } else {
2005
+ baseUrl = "/uploads";
2006
+ }
2007
+ return { uploadDir, baseUrl };
2008
+ },
2009
+ factory: (c) => createLocalStorage(c)
2010
+ });
2011
+ }
2012
+ registerImgix() {
2013
+ this.register({
2014
+ type: "imgix",
2015
+ displayName: "Imgix",
2016
+ configKey: "imgix",
2017
+ configFields: [
2018
+ { name: "domain", type: "text", label: "Domain", required: true },
2019
+ { name: "signKey", type: "password", label: "Sign Key" }
2020
+ ],
2021
+ extractConfig: (sc, key) => sc[key] || {},
2022
+ extractRawConfig: (c) => c?.imgix || c,
2023
+ factory: (c) => createImgixStorage(c)
2024
+ });
2025
+ }
2026
+ registerBunny() {
2027
+ this.register({
2028
+ type: "bunny",
2029
+ displayName: "Bunny.net",
2030
+ configKey: "bunny",
2031
+ configFields: [
2032
+ {
2033
+ name: "storageZone",
2034
+ type: "text",
2035
+ label: "Storage Zone",
2036
+ required: true
2037
+ },
2038
+ { name: "apiKey", type: "password", label: "API Key", required: true },
2039
+ { name: "cdnUrl", type: "text", label: "CDN URL" },
2040
+ { name: "prefix", type: "text", label: "Path Prefix" }
2041
+ ],
2042
+ extractConfig: (sc, key) => sc[key] || {},
2043
+ extractRawConfig: (c) => c?.bunny || c,
2044
+ factory: (c) => createBunnyStorage(c)
2045
+ });
2046
+ }
2047
+ register(registration) {
2048
+ if (this.providers.has(registration.type)) {
2049
+ console.warn(
2050
+ `[StorageRegistry] Provider "${registration.type}" already registered, skipping`
2051
+ );
2052
+ return;
2053
+ }
2054
+ if (registration.pluginName) {
2055
+ this.providerToPlugin.set(registration.type, registration.pluginName);
2056
+ }
2057
+ this.providers.set(registration.type, registration);
2058
+ }
2059
+ unregister(type) {
2060
+ this.providers.delete(type);
2061
+ this.providerToPlugin.delete(type);
2062
+ }
2063
+ get(type) {
2064
+ return this.providers.get(type);
2065
+ }
2066
+ getAll() {
2067
+ return Array.from(this.providers.values());
2068
+ }
2069
+ getAllAvailable(isPluginEnabled) {
2070
+ const all = this.getAll();
2071
+ if (!isPluginEnabled) return all;
2072
+ return all.filter((p) => {
2073
+ if (!p.pluginName) return true;
2074
+ return isPluginEnabled(p.pluginName);
2075
+ });
2076
+ }
2077
+ has(type) {
2078
+ return this.providers.has(type);
2079
+ }
2080
+ async resolve(type, storageConfig) {
2081
+ const reg = this.providers.get(type);
2082
+ if (!reg) {
2083
+ throw new Error(
2084
+ `Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
2085
+ );
2086
+ }
2087
+ if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
2088
+ throw new Error(
2089
+ `Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
2090
+ );
2091
+ }
2092
+ const configKey = reg.configKey || type;
2093
+ const config = reg.extractConfig(storageConfig, configKey);
2094
+ return reg.factory(config);
2095
+ }
2096
+ async resolveWithConfig(type, config) {
2097
+ const reg = this.providers.get(type);
2098
+ if (!reg) {
2099
+ throw new Error(
2100
+ `Unknown storage provider type: "${type}". Is the plugin that provides it enabled?`
2101
+ );
2102
+ }
2103
+ if (reg.pluginName && this.disabledPlugins.has(reg.pluginName)) {
2104
+ throw new Error(
2105
+ `Storage provider "${type}" is not available (plugin "${reg.pluginName}" is disabled)`
2106
+ );
2107
+ }
2108
+ const providerConfig = reg.extractRawConfig(config);
2109
+ return reg.factory(providerConfig);
2110
+ }
2111
+ getProviderPluginName(type) {
2112
+ return this.providerToPlugin.get(type);
2113
+ }
2114
+ getAllPluginNames() {
2115
+ return Array.from(new Set(this.providerToPlugin.values()));
2116
+ }
2117
+ setPluginEnabled(name, enabled) {
2118
+ if (enabled) {
2119
+ this.disabledPlugins.delete(name);
2120
+ } else {
2121
+ this.disabledPlugins.add(name);
2122
+ }
2123
+ }
2124
+ isPluginEnabled(name) {
2125
+ return !this.disabledPlugins.has(name);
2126
+ }
2127
+ };
2128
+ var instance = null;
2129
+ function getDefaultRegistry() {
2130
+ if (!instance) {
2131
+ instance = new StorageProviderRegistry();
2132
+ }
2133
+ return instance;
2134
+ }
1673
2135
  function extractPublicDevUrlId(url) {
1674
2136
  if (!url) return "";
1675
2137
  if (url.startsWith("pub-")) return url;
@@ -1703,7 +2165,7 @@ function getPublicUrl(key, config) {
1703
2165
  return `https://${config.bucket}.s3.${config.region}.amazonaws.com/${normalizedKey}`;
1704
2166
  }
1705
2167
  }
1706
- function getUrlPrefix(config) {
2168
+ function getUrlPrefix2(config) {
1707
2169
  if (config.cdnUrl) {
1708
2170
  return config.cdnUrl.replace(/\/$/, "") + "/";
1709
2171
  }
@@ -1765,17 +2227,17 @@ function createS3Storage(config) {
1765
2227
  tls: true,
1766
2228
  // R2 requires specific SSL configuration
1767
2229
  ...config.provider === "r2" && {
1768
- requestHandler: new (__require("@smithy/node-http-handler")).NodeHttpHandler({
2230
+ requestHandler: new NodeHttpHandler({
1769
2231
  connectionTimeout: 1e4,
1770
2232
  socketTimeout: 1e4
1771
2233
  })
1772
2234
  }
1773
2235
  });
1774
- const getKey = (path2) => {
2236
+ const getKey = (path3) => {
1775
2237
  const prefix = config.prefix ? `${config.prefix}/` : "";
1776
- return `${prefix}${path2}`.replace(/\/+/g, "/");
2238
+ return `${prefix}${path3}`.replace(/\/+/g, "/");
1777
2239
  };
1778
- const getUrl = (key) => getPublicUrl(key, config);
2240
+ const getUrl2 = (key) => getPublicUrl(key, config);
1779
2241
  return {
1780
2242
  name: config.provider,
1781
2243
  displayName: getDisplayName(config.provider),
@@ -1806,8 +2268,8 @@ function createS3Storage(config) {
1806
2268
  originalName: file.name,
1807
2269
  mimeType: file.type,
1808
2270
  size: buffer.length,
1809
- url: getUrl(key),
1810
- thumbnailUrl: file.type.startsWith("image/") ? getUrl(key) : void 0,
2271
+ url: getUrl2(key),
2272
+ thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
1811
2273
  folder: options?.folder,
1812
2274
  provider: config.provider,
1813
2275
  metadata: {
@@ -1828,7 +2290,7 @@ function createS3Storage(config) {
1828
2290
  return this.upload(file);
1829
2291
  },
1830
2292
  async delete(url) {
1831
- const key = url.replace(getUrlPrefix(config), "");
2293
+ const key = url.replace(getUrlPrefix2(config), "");
1832
2294
  await client.send(
1833
2295
  new DeleteObjectCommand({
1834
2296
  Bucket: config.bucket,
@@ -1837,7 +2299,7 @@ function createS3Storage(config) {
1837
2299
  );
1838
2300
  },
1839
2301
  async rename(oldUrl, newKey) {
1840
- const oldKey = oldUrl.replace(getUrlPrefix(config), "");
2302
+ const oldKey = oldUrl.replace(getUrlPrefix2(config), "");
1841
2303
  const newKeyWithPrefix = config.prefix ? `${config.prefix}/${newKey}` : newKey;
1842
2304
  await client.send(
1843
2305
  new CopyObjectCommand({
@@ -1852,7 +2314,7 @@ function createS3Storage(config) {
1852
2314
  Key: oldKey
1853
2315
  })
1854
2316
  );
1855
- return getUrl(newKeyWithPrefix);
2317
+ return getUrl2(newKeyWithPrefix);
1856
2318
  },
1857
2319
  getImageUrl(url, transforms) {
1858
2320
  if (!transforms || Object.keys(transforms).length === 0) return url;
@@ -1880,14 +2342,14 @@ function createS3Storage(config) {
1880
2342
  originalName: item.Key?.split("/").pop() || "",
1881
2343
  mimeType: "application/octet-stream",
1882
2344
  size: item.Size || 0,
1883
- url: getUrl(item.Key || ""),
2345
+ url: getUrl2(item.Key || ""),
1884
2346
  provider: config.provider,
1885
2347
  createdAt: item.LastModified?.toISOString() || (/* @__PURE__ */ new Date()).toISOString()
1886
2348
  }));
1887
2349
  },
1888
2350
  async exists(url) {
1889
2351
  try {
1890
- const key = url.replace(getUrlPrefix(config), "");
2352
+ const key = url.replace(getUrlPrefix2(config), "");
1891
2353
  await client.send(
1892
2354
  new HeadObjectCommand({
1893
2355
  Bucket: config.bucket,
@@ -1947,9 +2409,9 @@ function createS3Storage(config) {
1947
2409
  function createCloudinaryStorage(config) {
1948
2410
  const getBaseUrl = () => `https://api.cloudinary.com/v1_1/${config.cloudName}/upload`;
1949
2411
  const generateSignature = async (params) => {
1950
- const crypto3 = await import('crypto');
2412
+ const crypto4 = await import('crypto');
1951
2413
  const sortedParams = Object.keys(params).sort().map((key) => `${key}=${params[key]}`).join("&");
1952
- return crypto3.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
2414
+ return crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
1953
2415
  };
1954
2416
  return {
1955
2417
  name: "cloudinary",
@@ -2051,8 +2513,8 @@ function createCloudinaryStorage(config) {
2051
2513
  public_id: publicId
2052
2514
  };
2053
2515
  const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
2054
- const crypto3 = await import('crypto');
2055
- const signature = crypto3.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
2516
+ const crypto4 = await import('crypto');
2517
+ const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
2056
2518
  const deleteUrl = `https://api.cloudinary.com/v1_1/${config.cloudName}/image/destroy`;
2057
2519
  const formData = new FormData();
2058
2520
  formData.append("public_id", publicId);
@@ -2103,8 +2565,8 @@ function createCloudinaryStorage(config) {
2103
2565
  timestamp: String(timestamp)
2104
2566
  };
2105
2567
  const sortedParams = Object.keys(signatureParams).sort().map((key) => `${key}=${signatureParams[key]}`).join("&");
2106
- const crypto3 = await import('crypto');
2107
- const signature = crypto3.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
2568
+ const crypto4 = await import('crypto');
2569
+ const signature = crypto4.createHash("sha256").update(sortedParams + config.apiSecret).digest("hex");
2108
2570
  const formData = new FormData();
2109
2571
  formData.append("from_public_id", publicIdWithoutVersion);
2110
2572
  formData.append("to_public_id", newPublicId);
@@ -2122,140 +2584,38 @@ function createCloudinaryStorage(config) {
2122
2584
  const data = await response.json();
2123
2585
  console.log("[Cloudinary rename] Success:", data.secure_url);
2124
2586
  return data.secure_url;
2125
- } else {
2126
- const error = await response.json();
2127
- console.warn("[Cloudinary rename] Failed:", error);
2128
- const versionStr = version ? `/${version}/` : "/";
2129
- return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
2130
- }
2131
- } catch (e) {
2132
- console.warn("[Cloudinary rename] Error:", e.message);
2133
- const versionStr = version ? `/${version}/` : "/";
2134
- return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
2135
- }
2136
- },
2137
- getImageUrl(url, transforms) {
2138
- if (!transforms) return url;
2139
- const parts = url.split("/upload/");
2140
- if (parts.length !== 2) return url;
2141
- const transformationArr = [];
2142
- if (transforms.width) transformationArr.push(`w_${transforms.width}`);
2143
- if (transforms.height) transformationArr.push(`h_${transforms.height}`);
2144
- if (transforms.fit) {
2145
- const fitMap = {
2146
- crop: "c_fill",
2147
- clip: "c_fit",
2148
- scale: "c_scale",
2149
- fill: "c_fill"
2150
- };
2151
- transformationArr.push(fitMap[transforms.fit] || "c_limit");
2152
- }
2153
- if (transforms.quality) transformationArr.push(`q_${transforms.quality}`);
2154
- if (transforms.format) transformationArr.push(`f_${transforms.format}`);
2155
- const transformationStr = transformationArr.join(",");
2156
- return `${parts[0]}/upload/${transformationStr}/${parts[1]}`;
2157
- },
2158
- async generateThumbnail(file) {
2159
- return this.getImageUrl(file.url, {
2160
- width: 200,
2161
- height: 200,
2162
- fit: "crop"
2163
- });
2164
- },
2165
- async list() {
2166
- return [];
2167
- },
2168
- async exists(url) {
2169
- const response = await fetch(url, { method: "HEAD" });
2170
- return response.ok;
2171
- },
2172
- async createFolder() {
2173
- },
2174
- async deleteFolder() {
2175
- }
2176
- };
2177
- }
2178
-
2179
- // src/storage/imgix.ts
2180
- function createImgixStorage(config) {
2181
- const signUrl = (path2, params) => {
2182
- if (!config.signKey) return path2;
2183
- const signer = new TextEncoder();
2184
- const key = signer.encode(config.signKey);
2185
- const data = signer.encode(path2 + params.toString());
2186
- let hash = 0;
2187
- const combined = new Uint8Array(key.length + data.length);
2188
- combined.set(key);
2189
- combined.set(data, key.length);
2190
- for (let i = 0; i < combined.length; i++) {
2191
- hash = (hash << 5) - hash + combined[i] | 0;
2192
- }
2193
- params.set("s", Math.abs(hash).toString(16));
2194
- return path2;
2195
- };
2196
- return {
2197
- name: "imgix",
2198
- displayName: "Imgix",
2199
- supportsDynamicResize: true,
2200
- async upload(_file, _options) {
2201
- throw new Error(
2202
- "Imgix is a transformation service. Use another provider for uploads."
2203
- );
2204
- },
2205
- async uploadFromUrl(url, options) {
2206
- const filename = options?.filename || url.split("/").pop() || "file";
2207
- const response = await fetch(url);
2208
- if (!response.ok) {
2209
- throw new Error(`Failed to fetch: ${response.statusText}`);
2587
+ } else {
2588
+ const error = await response.json();
2589
+ console.warn("[Cloudinary rename] Failed:", error);
2590
+ const versionStr = version ? `/${version}/` : "/";
2591
+ return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
2592
+ }
2593
+ } catch (e) {
2594
+ console.warn("[Cloudinary rename] Error:", e.message);
2595
+ const versionStr = version ? `/${version}/` : "/";
2596
+ return `https://res.cloudinary.com/${config.cloudName}/image/upload${versionStr}${newPublicId}.${newKey.split(".").pop()}`;
2210
2597
  }
2211
- const blob = await response.blob();
2212
- new File([blob], filename, { type: blob.type });
2213
- return {
2214
- id: Buffer.from(url).toString("base64").slice(0, 20),
2215
- filename,
2216
- originalName: filename,
2217
- mimeType: blob.type,
2218
- size: blob.size,
2219
- url: this.getImageUrl(url),
2220
- thumbnailUrl: this.getImageUrl(url, {
2221
- width: 200,
2222
- height: 200,
2223
- fit: "crop"
2224
- }),
2225
- provider: "imgix",
2226
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
2227
- };
2228
- },
2229
- async delete(_url) {
2230
- },
2231
- async rename(_oldUrl, newKey) {
2232
- return `https://${config.domain}/${newKey}`;
2233
2598
  },
2234
2599
  getImageUrl(url, transforms) {
2235
- const parsed = new URL(url);
2236
- const params = new URLSearchParams(parsed.search);
2237
- if (config.defaultParameters) {
2238
- Object.entries(config.defaultParameters).forEach(([key, value]) => {
2239
- if (!params.has(key)) {
2240
- params.set(key, value);
2241
- }
2242
- });
2243
- }
2244
- if (transforms) {
2245
- if (transforms.width) params.set("w", String(transforms.width));
2246
- if (transforms.height) params.set("h", String(transforms.height));
2247
- if (transforms.quality) params.set("q", String(transforms.quality));
2248
- if (transforms.format) params.set("fm", transforms.format);
2249
- if (transforms.fit) params.set("fit", transforms.fit);
2250
- if (transforms.blur) params.set("blur", String(transforms.blur));
2251
- if (transforms.sharpen) params.set("sharp", String(transforms.sharpen));
2252
- }
2253
- params.set("auto", "compress,format");
2254
- parsed.pathname + "?" + params.toString();
2255
- if (config.signKey) {
2256
- signUrl(parsed.pathname + params.toString(), params);
2600
+ if (!transforms) return url;
2601
+ const parts = url.split("/upload/");
2602
+ if (parts.length !== 2) return url;
2603
+ const transformationArr = [];
2604
+ if (transforms.width) transformationArr.push(`w_${transforms.width}`);
2605
+ if (transforms.height) transformationArr.push(`h_${transforms.height}`);
2606
+ if (transforms.fit) {
2607
+ const fitMap = {
2608
+ crop: "c_fill",
2609
+ clip: "c_fit",
2610
+ scale: "c_scale",
2611
+ fill: "c_fill"
2612
+ };
2613
+ transformationArr.push(fitMap[transforms.fit] || "c_limit");
2257
2614
  }
2258
- return `https://${config.domain}${parsed.pathname}${params.toString() ? "?" + params.toString() : ""}`;
2615
+ if (transforms.quality) transformationArr.push(`q_${transforms.quality}`);
2616
+ if (transforms.format) transformationArr.push(`f_${transforms.format}`);
2617
+ const transformationStr = transformationArr.join(",");
2618
+ return `${parts[0]}/upload/${transformationStr}/${parts[1]}`;
2259
2619
  },
2260
2620
  async generateThumbnail(file) {
2261
2621
  return this.getImageUrl(file.url, {
@@ -2268,12 +2628,8 @@ function createImgixStorage(config) {
2268
2628
  return [];
2269
2629
  },
2270
2630
  async exists(url) {
2271
- try {
2272
- const response = await fetch(url, { method: "HEAD" });
2273
- return response.ok;
2274
- } catch {
2275
- return false;
2276
- }
2631
+ const response = await fetch(url, { method: "HEAD" });
2632
+ return response.ok;
2277
2633
  },
2278
2634
  async createFolder() {
2279
2635
  },
@@ -2298,15 +2654,15 @@ function createFtpStorage(config) {
2298
2654
  }
2299
2655
  return client;
2300
2656
  }
2301
- const getKey = (path2) => {
2657
+ const getKey = (path3) => {
2302
2658
  const prefix = config.prefix ? `${config.prefix}/` : "";
2303
- return `${prefix}${path2}`.replace(/\/+/g, "/");
2659
+ return `${prefix}${path3}`.replace(/\/+/g, "/");
2304
2660
  };
2305
- const getUrl = (key) => {
2661
+ const getUrl2 = (key) => {
2306
2662
  const base = config.baseUrl.replace(/\/$/, "");
2307
2663
  return `${base}/${key}`;
2308
2664
  };
2309
- const getUrlPrefix2 = () => {
2665
+ const getUrlPrefix3 = () => {
2310
2666
  const base = config.baseUrl.replace(/\/$/, "");
2311
2667
  return base + "/";
2312
2668
  };
@@ -2337,8 +2693,8 @@ function createFtpStorage(config) {
2337
2693
  originalName: file.name,
2338
2694
  mimeType: file.type,
2339
2695
  size: buffer.length,
2340
- url: getUrl(key),
2341
- thumbnailUrl: file.type.startsWith("image/") ? getUrl(key) : void 0,
2696
+ url: getUrl2(key),
2697
+ thumbnailUrl: file.type.startsWith("image/") ? getUrl2(key) : void 0,
2342
2698
  folder: options?.folder,
2343
2699
  provider: config.type,
2344
2700
  metadata: options?.metadata,
@@ -2357,15 +2713,15 @@ function createFtpStorage(config) {
2357
2713
  },
2358
2714
  async delete(url) {
2359
2715
  const ftp = await getClient();
2360
- const key = url.replace(getUrlPrefix2(), "");
2716
+ const key = url.replace(getUrlPrefix3(), "");
2361
2717
  await ftp.remove(key);
2362
2718
  },
2363
2719
  async rename(oldUrl, newKey) {
2364
2720
  const ftp = await getClient();
2365
- const oldKey = oldUrl.replace(getUrlPrefix2(), "");
2721
+ const oldKey = oldUrl.replace(getUrlPrefix3(), "");
2366
2722
  const fullPath = config.prefix ? `${config.prefix}/${newKey}` : newKey;
2367
2723
  await ftp.rename(oldKey, fullPath);
2368
- return getUrl(fullPath);
2724
+ return getUrl2(fullPath);
2369
2725
  },
2370
2726
  getImageUrl(url, transforms) {
2371
2727
  if (!transforms || Object.keys(transforms).length === 0) return url;
@@ -2394,14 +2750,14 @@ function createFtpStorage(config) {
2394
2750
  originalName: item.name,
2395
2751
  mimeType: "application/octet-stream",
2396
2752
  size: Number(item.size) || 0,
2397
- url: getUrl(`${key}/${item.name}`.replace(/\/+/g, "/")),
2753
+ url: getUrl2(`${key}/${item.name}`.replace(/\/+/g, "/")),
2398
2754
  provider: config.type,
2399
2755
  createdAt: item.modifiedAt ? item.modifiedAt.toISOString() : (/* @__PURE__ */ new Date()).toISOString()
2400
2756
  }));
2401
2757
  },
2402
2758
  async exists(url) {
2403
2759
  const ftp = await getClient();
2404
- const key = url.replace(getUrlPrefix2(), "");
2760
+ const key = url.replace(getUrlPrefix3(), "");
2405
2761
  try {
2406
2762
  await ftp.size(key);
2407
2763
  return true;
@@ -2425,105 +2781,17 @@ function createFtpStorage(config) {
2425
2781
  // src/storage/index.ts
2426
2782
  async function resolveProvider(configService) {
2427
2783
  const config = configService.getStorageConfig();
2428
- switch (config.type) {
2429
- case "aws":
2430
- return createS3Storage({
2431
- provider: "aws",
2432
- bucket: config.s3.bucket || "",
2433
- region: config.s3.region || "us-east-1",
2434
- accessKeyId: config.s3.accessKeyId || "",
2435
- secretAccessKey: config.s3.secretAccessKey || "",
2436
- endpoint: config.s3.endpoint,
2437
- cdnUrl: config.s3.cdnUrl,
2438
- prefix: config.s3.prefix
2439
- });
2440
- case "r2":
2441
- return createS3Storage({
2442
- provider: "r2",
2443
- bucket: config.r2.bucket || "",
2444
- region: "auto",
2445
- accessKeyId: config.r2.accessKeyId || "",
2446
- secretAccessKey: config.r2.secretAccessKey || "",
2447
- accountId: config.r2.accountId || "",
2448
- publicDevUrl: config.r2.publicDevUrl,
2449
- endpoint: `https://${config.r2.accountId || ""}.r2.cloudflarestorage.com`,
2450
- cdnUrl: config.r2.cdnUrl,
2451
- prefix: config.r2.prefix
2452
- });
2453
- case "gcs":
2454
- return createS3Storage({
2455
- provider: "gcs",
2456
- bucket: config.gcs.bucket || "",
2457
- region: config.gcs.projectId || "auto",
2458
- accessKeyId: config.gcs.clientEmail || "",
2459
- secretAccessKey: config.gcs.privateKey || "",
2460
- cdnUrl: config.gcs.cdnUrl,
2461
- prefix: config.gcs.prefix
2462
- });
2463
- case "digitalocean":
2464
- return createS3Storage({
2465
- provider: "digitalocean",
2466
- bucket: config.digitalocean.bucket || "",
2467
- region: config.digitalocean.region || "nyc3",
2468
- accessKeyId: config.digitalocean.accessKeyId || "",
2469
- secretAccessKey: config.digitalocean.secretAccessKey || "",
2470
- endpoint: `https://${config.digitalocean.region || "nyc3"}.digitaloceanspaces.com`,
2471
- cdnUrl: config.digitalocean.cdnUrl,
2472
- prefix: config.digitalocean.prefix
2473
- });
2474
- case "backblaze":
2475
- return createS3Storage({
2476
- provider: "backblaze",
2477
- bucket: config.backblaze.bucket || "",
2478
- region: "auto",
2479
- accessKeyId: config.backblaze.applicationKeyId || "",
2480
- secretAccessKey: config.backblaze.applicationKey || "",
2481
- accountId: config.backblaze.accountId || "",
2482
- endpoint: `https://s3.backblazeb2.com`,
2483
- cdnUrl: config.backblaze.cdnUrl,
2484
- prefix: config.backblaze.prefix
2485
- });
2486
- case "wasabi":
2487
- return createS3Storage({
2488
- provider: "wasabi",
2489
- bucket: config.wasabi.bucket || "",
2490
- region: config.wasabi.region || "us-east-1",
2491
- accessKeyId: config.wasabi.accessKeyId || "",
2492
- secretAccessKey: config.wasabi.secretAccessKey || "",
2493
- endpoint: `https://s3.${config.wasabi.region || "us-east-1"}.wasabisys.com`,
2494
- cdnUrl: config.wasabi.cdnUrl,
2495
- prefix: config.wasabi.prefix
2496
- });
2497
- case "ftp":
2498
- case "sftp":
2499
- return createFtpStorage({
2500
- host: config.ftp?.host || "",
2501
- port: config.ftp?.port || 21,
2502
- user: config.ftp?.user || "",
2503
- password: config.ftp?.password || "",
2504
- secure: config.ftp?.secure || false,
2505
- baseUrl: config.ftp?.baseUrl || "",
2506
- prefix: config.ftp?.prefix,
2507
- type: "ftp"
2508
- });
2509
- case "cloudinary":
2510
- return createCloudinaryStorage({
2511
- cloudName: config.cloudinary.cloudName || "",
2512
- apiKey: config.cloudinary.apiKey || "",
2513
- apiSecret: config.cloudinary.apiSecret || "",
2514
- folder: config.cloudinary.folder
2515
- });
2516
- case "imgix":
2517
- return createImgixStorage({
2518
- domain: config.imgix.domain || "",
2519
- signKey: config.imgix.signKey
2520
- });
2521
- case "local":
2522
- default:
2523
- return createLocalStorage({
2524
- uploadDir: config.local.uploadDir || path.join(process.cwd(), "public", "uploads"),
2525
- baseUrl: config.local.baseUrl || "/uploads"
2526
- });
2784
+ const registry = getDefaultRegistry();
2785
+ try {
2786
+ return await registry.resolve(config.type, config);
2787
+ } catch (err) {
2788
+ console.warn(
2789
+ `[resolveProvider] ${err.message} \u2014 falling back to local storage`
2790
+ );
2791
+ return createLocalStorage({
2792
+ uploadDir: path.join(process.cwd(), "public", "uploads"),
2793
+ baseUrl: "/uploads"
2794
+ });
2527
2795
  }
2528
2796
  }
2529
2797
  async function resolveProviderWithConfig(config) {
@@ -2534,121 +2802,18 @@ async function resolveProviderWithConfig(config) {
2534
2802
  baseUrl: "/uploads"
2535
2803
  });
2536
2804
  }
2537
- console.log("[resolveProviderWithConfig] Creating provider:", config.type);
2538
- switch (config.type) {
2539
- case "aws":
2540
- return createS3Storage({
2541
- provider: "aws",
2542
- bucket: config.s3?.bucket || "",
2543
- region: config.s3?.region || "us-east-1",
2544
- accessKeyId: config.s3?.accessKeyId || "",
2545
- secretAccessKey: config.s3?.secretAccessKey || "",
2546
- endpoint: config.s3?.endpoint,
2547
- cdnUrl: config.s3?.cdnUrl,
2548
- prefix: config.s3?.prefix
2549
- });
2550
- case "r2":
2551
- return createS3Storage({
2552
- provider: "r2",
2553
- bucket: config.r2?.bucket || "",
2554
- region: "auto",
2555
- accessKeyId: config.r2?.accessKeyId || "",
2556
- secretAccessKey: config.r2?.secretAccessKey || "",
2557
- accountId: config.r2?.accountId || "",
2558
- publicDevUrl: config.r2?.publicDevUrl,
2559
- endpoint: `https://${config.r2?.accountId || ""}.r2.cloudflarestorage.com`,
2560
- cdnUrl: config.r2?.cdnUrl,
2561
- prefix: config.r2?.prefix
2562
- });
2563
- case "gcs":
2564
- return createS3Storage({
2565
- provider: "gcs",
2566
- bucket: config.gcs?.bucket || "",
2567
- region: config.gcs?.projectId || "auto",
2568
- accessKeyId: config.gcs?.clientEmail || "",
2569
- secretAccessKey: config.gcs?.privateKey || "",
2570
- cdnUrl: config.gcs?.cdnUrl,
2571
- prefix: config.gcs?.prefix
2572
- });
2573
- case "digitalocean":
2574
- return createS3Storage({
2575
- provider: "digitalocean",
2576
- bucket: config.digitalocean?.bucket || "",
2577
- region: config.digitalocean?.region || "nyc3",
2578
- accessKeyId: config.digitalocean?.accessKeyId || "",
2579
- secretAccessKey: config.digitalocean?.secretAccessKey || "",
2580
- cdnUrl: config.digitalocean?.cdnUrl,
2581
- prefix: config.digitalocean?.prefix
2582
- });
2583
- case "backblaze":
2584
- return createS3Storage({
2585
- provider: "backblaze",
2586
- bucket: config.backblaze?.bucket || "",
2587
- region: "auto",
2588
- accessKeyId: config.backblaze?.applicationKeyId || "",
2589
- secretAccessKey: config.backblaze?.applicationKey || "",
2590
- cdnUrl: config.backblaze?.cdnUrl,
2591
- prefix: config.backblaze?.prefix
2592
- });
2593
- case "wasabi":
2594
- return createS3Storage({
2595
- provider: "wasabi",
2596
- bucket: config.wasabi?.bucket || "",
2597
- region: config.wasabi?.region || "us-east-1",
2598
- accessKeyId: config.wasabi?.accessKeyId || "",
2599
- secretAccessKey: config.wasabi?.secretAccessKey || "",
2600
- cdnUrl: config.wasabi?.cdnUrl,
2601
- prefix: config.wasabi?.prefix
2602
- });
2603
- case "cloudinary":
2604
- return createCloudinaryStorage({
2605
- cloudName: config.cloudinary?.cloudName || "",
2606
- apiKey: config.cloudinary?.apiKey || "",
2607
- apiSecret: config.cloudinary?.apiSecret || "",
2608
- folder: config.cloudinary?.folder
2609
- });
2610
- case "ftp":
2611
- case "sftp": {
2612
- const ftpConf = config.ftp || config;
2613
- return createFtpStorage({
2614
- type: "ftp",
2615
- host: ftpConf.host || "",
2616
- port: ftpConf.port || 21,
2617
- user: ftpConf.user || "",
2618
- password: ftpConf.password || "",
2619
- secure: ftpConf.secure || false,
2620
- baseUrl: ftpConf.baseUrl || "",
2621
- prefix: ftpConf.prefix
2622
- });
2623
- }
2624
- case "local":
2625
- default: {
2626
- const localConfig = config.local || {
2627
- uploadDir: config["local.uploadDir"],
2628
- baseUrl: config["local.baseUrl"]
2629
- };
2630
- const savedUploadDir = (localConfig?.uploadDir || "").trim();
2631
- let uploadDir;
2632
- if (savedUploadDir) {
2633
- if (path.isAbsolute(savedUploadDir)) {
2634
- uploadDir = savedUploadDir;
2635
- } else if (savedUploadDir.includes("/") || savedUploadDir.includes("\\")) {
2636
- uploadDir = path.resolve(process.cwd(), savedUploadDir);
2637
- } else {
2638
- uploadDir = path.join(process.cwd(), "public", savedUploadDir);
2639
- }
2640
- } else {
2641
- uploadDir = path.join(process.cwd(), "public", "uploads");
2642
- }
2643
- const savedBaseUrl = (localConfig?.baseUrl || "").trim();
2644
- let baseUrl;
2645
- if (savedBaseUrl) {
2646
- baseUrl = savedBaseUrl.startsWith("/") ? savedBaseUrl : `/${savedBaseUrl}`;
2647
- } else {
2648
- baseUrl = "/uploads";
2649
- }
2650
- return createLocalStorage({ uploadDir, baseUrl });
2651
- }
2805
+ const type = config.type || "local";
2806
+ const registry = getDefaultRegistry();
2807
+ try {
2808
+ return await registry.resolveWithConfig(type, config);
2809
+ } catch (err) {
2810
+ console.warn(
2811
+ `[resolveProviderWithConfig] ${err.message} \u2014 falling back to local storage`
2812
+ );
2813
+ return createLocalStorage({
2814
+ uploadDir: path.join(process.cwd(), "public", "uploads"),
2815
+ baseUrl: "/uploads"
2816
+ });
2652
2817
  }
2653
2818
  }
2654
2819
  async function processImage(buffer) {
@@ -2685,9 +2850,7 @@ var MediaService = class _MediaService {
2685
2850
  storage2 = await resolveProviderWithConfig(options.storageConfig);
2686
2851
  } else {
2687
2852
  const configService = new ConfigService(db);
2688
- if (typeof db?.select === "function") {
2689
- await configService.load();
2690
- }
2853
+ await configService.load();
2691
2854
  storage2 = await resolveProvider(configService);
2692
2855
  }
2693
2856
  const service = new _MediaService(db, storage2, options);
@@ -2889,7 +3052,7 @@ var MediaService = class _MediaService {
2889
3052
  ]
2890
3053
  );
2891
3054
  } else {
2892
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3055
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
2893
3056
  const mime = storageResult.mimeType;
2894
3057
  const mediaType = mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : mime.startsWith("audio/") ? "audio" : mime.startsWith("application/pdf") ? "document" : ["application/zip", "application/x-zip", "application/x-tar", "application/gzip", "application/x-7z"].includes(mime) ? "archive" : "other";
2895
3058
  await this.db.insert(mediaSchema).values({
@@ -2951,7 +3114,7 @@ var MediaService = class _MediaService {
2951
3114
  id
2952
3115
  ]);
2953
3116
  } else {
2954
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3117
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
2955
3118
  const { eq } = await import('drizzle-orm');
2956
3119
  const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
2957
3120
  if (row) item = this.rowToMedia(row);
@@ -2967,7 +3130,7 @@ var MediaService = class _MediaService {
2967
3130
  if (this.dialect === "sqlite") {
2968
3131
  await this.sqliteRun(`DELETE FROM ${this.mediaTable} WHERE id = ?`, [id]);
2969
3132
  } else {
2970
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3133
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
2971
3134
  const { eq } = await import('drizzle-orm');
2972
3135
  await this.db.delete(mediaSchema).where(eq(mediaSchema.id, id));
2973
3136
  }
@@ -2982,7 +3145,7 @@ var MediaService = class _MediaService {
2982
3145
  id
2983
3146
  ]);
2984
3147
  } else {
2985
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3148
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
2986
3149
  const { eq } = await import('drizzle-orm');
2987
3150
  const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
2988
3151
  if (row) item = this.rowToMedia(row);
@@ -3017,7 +3180,7 @@ var MediaService = class _MediaService {
3017
3180
  [...vals, this.now(), id]
3018
3181
  );
3019
3182
  } else {
3020
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3183
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
3021
3184
  const { eq } = await import('drizzle-orm');
3022
3185
  await this.db.update(mediaSchema).set({ ...updateData, updatedAt: this.now() }).where(eq(mediaSchema.id, id));
3023
3186
  }
@@ -3036,7 +3199,7 @@ var MediaService = class _MediaService {
3036
3199
  );
3037
3200
  return row2 ? this.rowToMedia(row2) : null;
3038
3201
  }
3039
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3202
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
3040
3203
  const { eq } = await import('drizzle-orm');
3041
3204
  const [row] = await this.db.select().from(mediaSchema).where(eq(mediaSchema.id, id));
3042
3205
  return row ? this.rowToMedia(row) : null;
@@ -3084,7 +3247,7 @@ var MediaService = class _MediaService {
3084
3247
  totalPages: Math.ceil(totalDocs2 / limit)
3085
3248
  };
3086
3249
  }
3087
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3250
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
3088
3251
  const { like, or, and, asc, desc, eq, sql } = await import('drizzle-orm');
3089
3252
  const conditions = [];
3090
3253
  if (search) {
@@ -3163,7 +3326,7 @@ var MediaService = class _MediaService {
3163
3326
  );
3164
3327
  return row ? this.rowToMedia(row) : null;
3165
3328
  }
3166
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3329
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
3167
3330
  const { eq } = await import('drizzle-orm');
3168
3331
  const [updated] = await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(mediaSchema.id, id)).returning();
3169
3332
  return updated ? this.rowToMedia(updated) : null;
@@ -3185,7 +3348,7 @@ var MediaService = class _MediaService {
3185
3348
  );
3186
3349
  }
3187
3350
  } else {
3188
- const { media: mediaSchema } = await import('./media-GPPTZ43E.js');
3351
+ const { media: mediaSchema } = await import('./media-7WDX4BDJ.js');
3189
3352
  const { eq } = await import('drizzle-orm');
3190
3353
  for (const id of ids) {
3191
3354
  await this.db.update(mediaSchema).set({ ...data, updatedAt: /* @__PURE__ */ new Date() }).where(eq(mediaSchema.id, id));
@@ -3200,7 +3363,7 @@ var MediaService = class _MediaService {
3200
3363
  );
3201
3364
  return rows.map((r) => r.path).filter((f) => f && f !== "").sort();
3202
3365
  }
3203
- const { media: mediaSchema, mediaFolders: folderSchema } = await import('./media-GPPTZ43E.js');
3366
+ const { media: mediaSchema, mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
3204
3367
  const { eq, sql } = await import('drizzle-orm');
3205
3368
  const fromMedia = await this.db.select({ folder: mediaSchema.folder }).from(mediaSchema).groupBy(mediaSchema.folder);
3206
3369
  const fromFolders = await this.db.select({ path: folderSchema.path }).from(folderSchema);
@@ -3220,7 +3383,7 @@ var MediaService = class _MediaService {
3220
3383
  [fullPath, name, parentPath || null, now]
3221
3384
  );
3222
3385
  } else {
3223
- const { mediaFolders: folderSchema } = await import('./media-GPPTZ43E.js');
3386
+ const { mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
3224
3387
  await this.db.insert(folderSchema).values({
3225
3388
  path: fullPath,
3226
3389
  name,
@@ -3241,7 +3404,7 @@ var MediaService = class _MediaService {
3241
3404
  [folder, `${folder}/%`]
3242
3405
  );
3243
3406
  } else {
3244
- const { mediaFolders: folderSchema } = await import('./media-GPPTZ43E.js');
3407
+ const { mediaFolders: folderSchema } = await import('./media-7WDX4BDJ.js');
3245
3408
  const { like, or, eq } = await import('drizzle-orm');
3246
3409
  await this.db.delete(folderSchema).where(
3247
3410
  or(
@@ -3252,8 +3415,75 @@ var MediaService = class _MediaService {
3252
3415
  }
3253
3416
  }
3254
3417
  };
3255
-
3256
- // src/api/rest/hono-app.ts
3418
+ function formatZodErrors(errors) {
3419
+ return errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
3420
+ }
3421
+ function normalizeEmptyStrings(data, fields) {
3422
+ if (!data || typeof data !== "object") return;
3423
+ for (const field of fields) {
3424
+ if (!field.name || !(field.name in data)) continue;
3425
+ const val = data[field.name];
3426
+ if (val === "") {
3427
+ const isTextual = field.type === "text" || field.type === "textarea" || field.type === "code" || field.type === "markdown" || field.type === "email" || field.type === "password" || field.type === "color";
3428
+ if (!isTextual) data[field.name] = null;
3429
+ }
3430
+ if (field.type === "tabs" && field.name && Array.isArray(field.tabs) && data[field.name] && typeof data[field.name] === "object") {
3431
+ for (const tab of field.tabs) {
3432
+ if (Array.isArray(tab.fields)) normalizeEmptyStrings(data[field.name], tab.fields);
3433
+ }
3434
+ } else if ((field.type === "group" || field.type === "collapsible") && field.name && Array.isArray(field.fields) && data[field.name] && typeof data[field.name] === "object") {
3435
+ normalizeEmptyStrings(data[field.name], field.fields);
3436
+ } else if (field.type === "array" && field.name && Array.isArray(field.fields) && Array.isArray(data[field.name])) {
3437
+ for (const item of data[field.name]) {
3438
+ if (item && typeof item === "object") normalizeEmptyStrings(item, field.fields);
3439
+ }
3440
+ } else if (field.type === "blocks" && field.name && Array.isArray(field.blocks) && Array.isArray(data[field.name])) {
3441
+ for (const item of data[field.name]) {
3442
+ if (!item || typeof item !== "object") continue;
3443
+ const blockTypeStr = item.type || item.blockType;
3444
+ if (!blockTypeStr) continue;
3445
+ const blockDef = field.blocks.find((b) => b.slug === blockTypeStr);
3446
+ if (!blockDef || !Array.isArray(blockDef.fields)) continue;
3447
+ const target = item.data && typeof item.data === "object" ? item.data : item;
3448
+ normalizeEmptyStrings(target, blockDef.fields);
3449
+ }
3450
+ }
3451
+ }
3452
+ }
3453
+ function convertRichtextFields(fields, data) {
3454
+ if (!data || typeof data !== "object") return;
3455
+ for (const field of fields) {
3456
+ if (field.type === "richtext" && field.name) {
3457
+ const val = data[field.name];
3458
+ if (typeof val === "string") {
3459
+ data[field.name] = [{ type: "paragraph", children: [{ text: val }] }];
3460
+ } else if (val && typeof val === "object" && !Array.isArray(val) && val.type === "doc" && Array.isArray(val.content)) {
3461
+ data[field.name] = val.content;
3462
+ }
3463
+ }
3464
+ if (field.type === "tabs" && field.name && Array.isArray(field.tabs) && data[field.name] && typeof data[field.name] === "object") {
3465
+ for (const tab of field.tabs) {
3466
+ if (Array.isArray(tab.fields)) convertRichtextFields(tab.fields, data[field.name]);
3467
+ }
3468
+ } else if ((field.type === "group" || field.type === "collapsible") && field.name && Array.isArray(field.fields) && data[field.name] && typeof data[field.name] === "object") {
3469
+ convertRichtextFields(field.fields, data[field.name]);
3470
+ } else if (field.type === "array" && field.name && Array.isArray(field.fields) && Array.isArray(data[field.name])) {
3471
+ for (const item of data[field.name]) {
3472
+ if (item && typeof item === "object") convertRichtextFields(field.fields, item);
3473
+ }
3474
+ } else if (field.type === "blocks" && field.name && Array.isArray(field.blocks) && Array.isArray(data[field.name])) {
3475
+ for (const item of data[field.name]) {
3476
+ if (!item || typeof item !== "object") continue;
3477
+ const blockTypeStr = item.type || item.blockType;
3478
+ if (!blockTypeStr) continue;
3479
+ const blockDef = field.blocks.find((b) => b.slug === blockTypeStr);
3480
+ if (!blockDef || !Array.isArray(blockDef.fields)) continue;
3481
+ const target = item.data && typeof item.data === "object" ? item.data : item;
3482
+ convertRichtextFields(blockDef.fields, target);
3483
+ }
3484
+ }
3485
+ }
3486
+ }
3257
3487
  var COLLECTION_EVENT_MAP = {
3258
3488
  _media: {
3259
3489
  create: WEBHOOK_EVENTS.MEDIA_UPLOAD,
@@ -3347,7 +3577,7 @@ async function checkCollectionAccess(collection, operation, req, ctxUser, ctxTen
3347
3577
  };
3348
3578
  }
3349
3579
  }
3350
- if (ctxUser) {
3580
+ if (ctxUser && !(apiKeyContext?.permissions?.length > 0)) {
3351
3581
  const resource = collection.slug;
3352
3582
  const action = operation === "read" ? "read" : operation === "create" ? "create" : operation === "update" ? "update" : "delete";
3353
3583
  const permission = `${resource}:${action}`;
@@ -3406,31 +3636,30 @@ async function checkGlobalAccess(global, operation, req, ctxUser, ctxTenantID, e
3406
3636
  return { allowed: true };
3407
3637
  }
3408
3638
  async function resolveAuthContext(req, authMw, staticUser, staticTenantID) {
3409
- if (!authMw) {
3410
- return {
3411
- user: staticUser,
3412
- tenantID: staticTenantID,
3413
- apiKeyContext: void 0
3414
- };
3415
- }
3416
- let result;
3417
- try {
3418
- result = await authMw(req);
3419
- } catch (err) {
3639
+ if (staticUser) {
3420
3640
  return {
3421
3641
  user: staticUser,
3422
3642
  tenantID: staticTenantID,
3423
- apiKeyContext: void 0
3643
+ apiKeyContext: void 0,
3644
+ authType: "static"
3424
3645
  };
3425
3646
  }
3426
- if (result.status === 401) {
3427
- return { user: void 0, tenantID: void 0, apiKeyContext: void 0 };
3647
+ if (authMw) {
3648
+ const res = await authMw(req);
3649
+ if (res.status === 200 && res.user) {
3650
+ return {
3651
+ user: res.user,
3652
+ tenantID: res.tenantContext?.tenantId,
3653
+ apiKeyContext: res.apiKeyContext,
3654
+ authType: res.authType
3655
+ };
3656
+ }
3428
3657
  }
3429
3658
  return {
3430
- user: result.user || staticUser,
3431
- tenantID: result.tenantContext?.tenantId || staticTenantID,
3432
- apiKeyContext: result.apiKeyContext,
3433
- authType: result.authType
3659
+ user: void 0,
3660
+ tenantID: void 0,
3661
+ apiKeyContext: void 0,
3662
+ authType: void 0
3434
3663
  };
3435
3664
  }
3436
3665
  function createDefaultAuthAdapter(db, rootDir) {
@@ -3544,6 +3773,13 @@ function createHonoApp(options) {
3544
3773
  baseUrl: process.env.KYRO_BASE_URL || "http://localhost:4321",
3545
3774
  rateLimiter
3546
3775
  });
3776
+ EmailTransport.fromConfig(db).then((transport) => {
3777
+ if (transport) {
3778
+ authRoutes.email = transport;
3779
+ }
3780
+ }).catch((err) => {
3781
+ console.error("[Email] Failed to initialize transport from config:", err);
3782
+ });
3547
3783
  app.post("/api/auth/login", async (c) => authRoutes.login(c.req.raw));
3548
3784
  app.post("/api/auth/register", async (c) => authRoutes.register(c.req.raw));
3549
3785
  app.post("/api/auth/logout", async (c) => authRoutes.logout(c.req.raw));
@@ -3554,69 +3790,6 @@ function createHonoApp(options) {
3554
3790
  app.delete("/api/auth/sessions", async (c) => authRoutes.revokeOtherSessions(c.req.raw));
3555
3791
  app.delete("/api/auth/sessions/:id", async (c) => authRoutes.revokeSession(c.req.raw, c.req.param("id")));
3556
3792
  app.put("/api/auth/sessions/:id/name", async (c) => authRoutes.renameSession(c.req.raw, c.req.param("id")));
3557
- app.post("/api/graphql", async (c) => {
3558
- try {
3559
- const req = c.req.raw;
3560
- const apiKeyRaw = extractApiKeyFromRequest(req);
3561
- if (apiKeyRaw && db) {
3562
- const apiKeyResult = await validateApiKey(apiKeyRaw, db);
3563
- if (!apiKeyResult.valid) {
3564
- return c.json({ errors: [{ message: apiKeyResult.error || "Invalid API key" }] }, 401);
3565
- }
3566
- const apiKeyId = apiKeyResult.apiKeyId || "";
3567
- await sessionAuthAdapter?.createAuditLog({
3568
- action: "api_key_request",
3569
- userId: apiKeyResult.userId || "",
3570
- resource: "api_key",
3571
- resourceId: apiKeyId,
3572
- success: true,
3573
- metadata: {
3574
- endpoint: "/api/graphql",
3575
- method: "POST",
3576
- ip: extractIp(req)
3577
- }
3578
- });
3579
- }
3580
- const body = await req.json().catch(() => ({}));
3581
- const { query, variables } = body;
3582
- if (!query) {
3583
- return c.json({ errors: [{ message: "No query provided" }] }, 400);
3584
- }
3585
- let gqlUser;
3586
- let apiKeyCtx;
3587
- if (apiKeyRaw && db) {
3588
- const apiKeyResult = await validateApiKey(apiKeyRaw, db);
3589
- if (apiKeyResult.valid && apiKeyResult.user) {
3590
- gqlUser = apiKeyResult.user;
3591
- apiKeyCtx = createApiKeyContext(apiKeyResult);
3592
- }
3593
- }
3594
- const schema = buildGraphQLSchema({
3595
- registry,
3596
- db,
3597
- user: gqlUser,
3598
- req,
3599
- settings
3600
- });
3601
- const document = parse(query);
3602
- const result = await execute({
3603
- schema,
3604
- document,
3605
- variableValues: variables,
3606
- contextValue: { user: gqlUser, apiKeyContext: apiKeyCtx, req, db }
3607
- });
3608
- return c.json(result);
3609
- } catch (error) {
3610
- if (error.message?.includes("GraphQL is disabled")) {
3611
- return c.json({ errors: [{ message: "GraphQL API is disabled" }] }, 403);
3612
- }
3613
- if (error instanceof SyntaxError) {
3614
- return c.json({ errors: [{ message: "Invalid request body" }] }, 400);
3615
- }
3616
- console.error("[GraphQL] execution error:", error);
3617
- return c.json({ errors: [{ message: error.message || "GraphQL execution failed" }] }, 500);
3618
- }
3619
- });
3620
3793
  app.get("/api/auth/access", async (c) => {
3621
3794
  try {
3622
3795
  const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
@@ -3701,7 +3874,13 @@ function createHonoApp(options) {
3701
3874
  return c.json({ error: error.message }, 500);
3702
3875
  }
3703
3876
  });
3704
- const usersCollection = registry.getCollection("users");
3877
+ const usersCollection2 = typeof registry.hasCollection === "function" && registry.hasCollection("users") ? registry.getCollection("users") : (() => {
3878
+ try {
3879
+ return registry.getCollection("users");
3880
+ } catch {
3881
+ return usersCollection;
3882
+ }
3883
+ })();
3705
3884
  app.get("/api/users", async (c) => {
3706
3885
  try {
3707
3886
  const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(
@@ -3711,7 +3890,7 @@ function createHonoApp(options) {
3711
3890
  tenantID
3712
3891
  );
3713
3892
  const access = await checkCollectionAccess(
3714
- usersCollection,
3893
+ usersCollection2,
3715
3894
  "read",
3716
3895
  c.req.raw,
3717
3896
  ctxUser,
@@ -3756,19 +3935,6 @@ function createHonoApp(options) {
3756
3935
  user,
3757
3936
  tenantID
3758
3937
  );
3759
- const access = await checkCollectionAccess(
3760
- usersCollection,
3761
- "read",
3762
- c.req.raw,
3763
- ctxUser,
3764
- ctxTenantID,
3765
- void 0,
3766
- enablePublicAccess,
3767
- defaultCollectionAccess
3768
- );
3769
- if (!access.allowed) {
3770
- return c.json({ error: access.error }, access.status || 403);
3771
- }
3772
3938
  const id = c.req.param("id");
3773
3939
  const found = await sessionAuthAdapter.findUserById(id);
3774
3940
  if (!found) {
@@ -3788,7 +3954,7 @@ function createHonoApp(options) {
3788
3954
  tenantID
3789
3955
  );
3790
3956
  const access = await checkCollectionAccess(
3791
- usersCollection,
3957
+ usersCollection2,
3792
3958
  "create",
3793
3959
  c.req.raw,
3794
3960
  ctxUser,
@@ -3813,8 +3979,18 @@ function createHonoApp(options) {
3813
3979
  password: body.password,
3814
3980
  name: body.name,
3815
3981
  role: body.role || "customer",
3982
+ avatar: body.avatar,
3816
3983
  tenantId: body.tenantId
3817
3984
  });
3985
+ if (ctxUser) {
3986
+ sessionAuthAdapter?.createAuditLog({
3987
+ action: "user_create",
3988
+ userId: ctxUser.id,
3989
+ resource: "users",
3990
+ resourceId: created.id,
3991
+ success: true
3992
+ });
3993
+ }
3818
3994
  return c.json(
3819
3995
  { data: created, message: "User created successfully" },
3820
3996
  201
@@ -3832,7 +4008,7 @@ function createHonoApp(options) {
3832
4008
  tenantID
3833
4009
  );
3834
4010
  const access = await checkCollectionAccess(
3835
- usersCollection,
4011
+ usersCollection2,
3836
4012
  "update",
3837
4013
  c.req.raw,
3838
4014
  ctxUser,
@@ -3854,6 +4030,9 @@ function createHonoApp(options) {
3854
4030
  if (body.name !== void 0) updateData.name = body.name;
3855
4031
  if (body.email !== void 0) updateData.email = body.email;
3856
4032
  if (body.role !== void 0) updateData.role = body.role;
4033
+ if (body.avatar !== void 0) {
4034
+ updateData.avatar = typeof body.avatar === "object" && body.avatar !== null ? body.avatar.id || String(body.avatar) : body.avatar;
4035
+ }
3857
4036
  if (body.tenantId !== void 0) updateData.tenantId = body.tenantId;
3858
4037
  if (body.emailVerified !== void 0)
3859
4038
  updateData.emailVerified = body.emailVerified;
@@ -3865,6 +4044,25 @@ function createHonoApp(options) {
3865
4044
  if (!updated) {
3866
4045
  return c.json({ error: "User update failed" }, 500);
3867
4046
  }
4047
+ if (ctxUser) {
4048
+ sessionAuthAdapter?.createAuditLog({
4049
+ action: "user_update",
4050
+ userId: ctxUser.id,
4051
+ resource: "users",
4052
+ resourceId: id,
4053
+ success: true
4054
+ });
4055
+ if (body.role && existing.role !== body.role) {
4056
+ sessionAuthAdapter?.createAuditLog({
4057
+ action: "role_change",
4058
+ userId: ctxUser.id,
4059
+ resource: "users",
4060
+ resourceId: id,
4061
+ success: true,
4062
+ metadata: { oldRole: existing.role, newRole: body.role }
4063
+ });
4064
+ }
4065
+ }
3868
4066
  return c.json({ data: updated, message: "User updated successfully" });
3869
4067
  } catch (error) {
3870
4068
  return c.json({ error: error.message }, 500);
@@ -3879,7 +4077,7 @@ function createHonoApp(options) {
3879
4077
  tenantID
3880
4078
  );
3881
4079
  const access = await checkCollectionAccess(
3882
- usersCollection,
4080
+ usersCollection2,
3883
4081
  "delete",
3884
4082
  c.req.raw,
3885
4083
  ctxUser,
@@ -3903,6 +4101,15 @@ function createHonoApp(options) {
3903
4101
  if (!deleted) {
3904
4102
  return c.json({ error: "User deletion failed" }, 500);
3905
4103
  }
4104
+ if (ctxUser) {
4105
+ sessionAuthAdapter?.createAuditLog({
4106
+ action: "user_delete",
4107
+ userId: ctxUser.id,
4108
+ resource: "users",
4109
+ resourceId: id,
4110
+ success: true
4111
+ });
4112
+ }
3906
4113
  return c.json({ data: existing, message: "User deleted successfully" });
3907
4114
  } catch (error) {
3908
4115
  return c.json({ error: error.message }, 500);
@@ -4059,11 +4266,11 @@ function createHonoApp(options) {
4059
4266
  const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4060
4267
  if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
4061
4268
  const service = await getMedia();
4062
- const path2 = c.req.query("path");
4063
- if (!path2) {
4269
+ const path3 = c.req.query("path");
4270
+ if (!path3) {
4064
4271
  return c.json({ error: "Path is required" }, 400);
4065
4272
  }
4066
- await service.deleteFolder(path2);
4273
+ await service.deleteFolder(path3);
4067
4274
  return c.json({ message: "Folder deleted" });
4068
4275
  } catch (error) {
4069
4276
  console.error("[Media] delete folder error:", error);
@@ -4216,6 +4423,119 @@ function createHonoApp(options) {
4216
4423
  return c.json({ error: error.message }, 500);
4217
4424
  }
4218
4425
  });
4426
+ app.get("/api/plugins", async (c) => {
4427
+ try {
4428
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4429
+ if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
4430
+ const plugins = registry.getPlugins();
4431
+ const storageRegistry = registry.storageProviders;
4432
+ const pluginList = await Promise.all(
4433
+ plugins.map(async (p) => {
4434
+ let enabled = true;
4435
+ const pluginName = p.name;
4436
+ let states = {};
4437
+ try {
4438
+ const doc = await db.findOne({
4439
+ collection: "_globals_plugin-settings",
4440
+ where: {}
4441
+ });
4442
+ if (doc && doc.states) states = doc.states;
4443
+ } catch {
4444
+ }
4445
+ if (states[pluginName] !== void 0) {
4446
+ enabled = states[pluginName];
4447
+ }
4448
+ storageRegistry.setPluginEnabled(pluginName, enabled);
4449
+ return {
4450
+ id: pluginName,
4451
+ name: pluginName,
4452
+ version: p.version || "1.0.0",
4453
+ description: p.description || "",
4454
+ enabled,
4455
+ status: enabled ? "active" : "disabled"
4456
+ };
4457
+ })
4458
+ );
4459
+ return c.json(pluginList);
4460
+ } catch (error) {
4461
+ console.error("[Plugins] list error:", error);
4462
+ return c.json({ error: error.message }, 500);
4463
+ }
4464
+ });
4465
+ app.put("/api/plugins/:name/toggle", async (c) => {
4466
+ try {
4467
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4468
+ if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
4469
+ const pluginName = c.req.param("name");
4470
+ const storageRegistry = registry.storageProviders;
4471
+ const currentEnabled = storageRegistry.isPluginEnabled(pluginName);
4472
+ const newEnabled = !currentEnabled;
4473
+ const affectedProviders = [];
4474
+ if (!newEnabled) {
4475
+ for (const p of storageRegistry.getAll()) {
4476
+ if (p.pluginName === pluginName) {
4477
+ affectedProviders.push(p.type);
4478
+ }
4479
+ }
4480
+ }
4481
+ const force = c.req.query("force") === "1";
4482
+ if (!force && !newEnabled && affectedProviders.length > 0) {
4483
+ let activeProvider = "local";
4484
+ try {
4485
+ const row = db?.prepare?.(`SELECT provider FROM "_globals_storage-settings" LIMIT 1`)?.get();
4486
+ if (row?.provider) activeProvider = row.provider;
4487
+ } catch {
4488
+ try {
4489
+ const result = await db?.findOne?.({
4490
+ collection: "_globals_storage-settings",
4491
+ where: {}
4492
+ });
4493
+ if (result?.provider) activeProvider = result.provider;
4494
+ } catch {
4495
+ }
4496
+ }
4497
+ if (affectedProviders.includes(activeProvider)) {
4498
+ return c.json({
4499
+ error: `Cannot disable "${pluginName}" \u2014 storage provider "${activeProvider}" is currently active. Switch to Local storage first.`,
4500
+ requiresAction: true,
4501
+ activeProvider
4502
+ }, 409);
4503
+ }
4504
+ }
4505
+ try {
4506
+ let states = {};
4507
+ let docId = "global";
4508
+ const doc = await db.findOne({
4509
+ collection: "_globals_plugin-settings",
4510
+ where: {}
4511
+ });
4512
+ if (doc) {
4513
+ states = doc.states || {};
4514
+ docId = doc.id;
4515
+ }
4516
+ states[pluginName] = newEnabled;
4517
+ if (doc) {
4518
+ await db.update({
4519
+ collection: "_globals_plugin-settings",
4520
+ id: docId,
4521
+ data: { states }
4522
+ });
4523
+ } else {
4524
+ await db.create({
4525
+ collection: "_globals_plugin-settings",
4526
+ data: { states, id: "global" }
4527
+ });
4528
+ }
4529
+ } catch (e) {
4530
+ console.warn(`[Plugins] Could not persist state for "${pluginName}":`, e);
4531
+ }
4532
+ storageRegistry.setPluginEnabled(pluginName, newEnabled);
4533
+ return c.json({ name: pluginName, enabled: newEnabled });
4534
+ } catch (error) {
4535
+ console.error("[Plugins] toggle error:", error);
4536
+ return c.json({ error: error.message }, 500);
4537
+ }
4538
+ });
4219
4539
  app.get("/api/health", (c) => {
4220
4540
  return c.json({
4221
4541
  status: "ok",
@@ -4224,6 +4544,102 @@ function createHonoApp(options) {
4224
4544
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
4225
4545
  });
4226
4546
  });
4547
+ app.get("/api/kyro/schema", async (c) => {
4548
+ const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4549
+ if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
4550
+ const extractFields = (fields) => fields.map((f) => {
4551
+ const meta = {
4552
+ name: f.name,
4553
+ type: f.type,
4554
+ label: f.label,
4555
+ required: f.required ?? false,
4556
+ unique: f.unique ?? false,
4557
+ indexed: f.indexed ?? false,
4558
+ defaultValue: f.defaultValue,
4559
+ admin: f.admin ? { ...f.admin } : void 0
4560
+ };
4561
+ if (f.minLength !== void 0) meta.minLength = f.minLength;
4562
+ if (f.maxLength !== void 0) meta.maxLength = f.maxLength;
4563
+ if (f.pattern !== void 0) meta.pattern = f.pattern;
4564
+ if (f.variant !== void 0) meta.variant = f.variant;
4565
+ if (f.hasMany !== void 0) meta.hasMany = f.hasMany;
4566
+ if (f.min !== void 0) meta.min = f.min;
4567
+ if (f.max !== void 0) meta.max = f.max;
4568
+ if (f.step !== void 0) meta.step = f.step;
4569
+ if (f.integer !== void 0) meta.integer = f.integer;
4570
+ if (f.options) meta.options = f.options;
4571
+ if (f.relationTo) meta.relationTo = f.relationTo;
4572
+ if (f.maxDepth !== void 0) meta.maxDepth = f.maxDepth;
4573
+ if (f.minRows !== void 0) meta.minRows = f.minRows;
4574
+ if (f.maxRows !== void 0) meta.maxRows = f.maxRows;
4575
+ if (f.localized !== void 0) meta.localized = f.localized;
4576
+ if (f.language) meta.language = f.language;
4577
+ if (f.format) meta.format = f.format;
4578
+ if (f.allowedTypes) meta.allowedTypes = f.allowedTypes;
4579
+ if (f.maxSize) meta.maxSize = f.maxSize;
4580
+ if (f.type === "group" || f.type === "row" || f.type === "collapsible" && f.fields) {
4581
+ meta.fields = extractFields(f.fields);
4582
+ }
4583
+ if (f.type === "array" && f.fields) {
4584
+ meta.fields = extractFields(f.fields);
4585
+ }
4586
+ if (f.type === "blocks" && f.blocks) {
4587
+ meta.blocks = f.blocks.map((b) => ({
4588
+ slug: b.slug,
4589
+ label: b.label,
4590
+ fields: extractFields(b.fields)
4591
+ }));
4592
+ }
4593
+ if (f.type === "tabs" && f.tabs) {
4594
+ meta.tabs = f.tabs.map((t) => ({
4595
+ label: t.label,
4596
+ name: t.name,
4597
+ fields: extractFields(t.fields)
4598
+ }));
4599
+ }
4600
+ return meta;
4601
+ });
4602
+ const data = { collections: {}, globals: {} };
4603
+ for (const col of registry.getCollections()) {
4604
+ const slug = col.slug;
4605
+ try {
4606
+ data.collections[slug] = {
4607
+ slug,
4608
+ label: col.label || slug,
4609
+ fields: extractFields(col.fields),
4610
+ jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
4611
+ createSchema: zodToJsonSchema(registry.getCreateZodSchema(slug), { target: "openApi3" }),
4612
+ updateSchema: zodToJsonSchema(registry.getUpdateZodSchema(slug), { target: "openApi3" }),
4613
+ procedures: {
4614
+ find: { collection: slug, where: "Record<string,any>", sort: "string", limit: "number", page: "number", depth: "number", select: "string[]", draft: "boolean" },
4615
+ findByID: { collection: slug, id: "string", depth: "number", select: "string[]", draft: "boolean" },
4616
+ create: { collection: slug, data: "Record<string,any>", depth: "number", select: "string[]" },
4617
+ update: { collection: slug, id: "string", data: "Record<string,any>", depth: "number", select: "string[]", baseUpdatedAt: "string" },
4618
+ delete: { collection: slug, id: "string" },
4619
+ count: { collection: slug, where: "Record<string,any>" }
4620
+ }
4621
+ };
4622
+ } catch {
4623
+ }
4624
+ }
4625
+ for (const global of registry.getGlobals()) {
4626
+ const slug = global.slug;
4627
+ try {
4628
+ data.globals[slug] = {
4629
+ slug,
4630
+ label: global.label || slug,
4631
+ fields: extractFields(global.fields),
4632
+ jsonSchema: zodToJsonSchema(registry.getZodSchema(slug), { target: "openApi3" }),
4633
+ procedures: {
4634
+ get: {},
4635
+ update: { data: "Record<string,any>" }
4636
+ }
4637
+ };
4638
+ } catch {
4639
+ }
4640
+ }
4641
+ return c.json(data);
4642
+ });
4227
4643
  app.get("/api/collections", async (c) => {
4228
4644
  const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4229
4645
  if (!ctxUser) return c.json({ error: "Authentication required" }, 401);
@@ -4239,6 +4655,129 @@ function createHonoApp(options) {
4239
4655
  }));
4240
4656
  return c.json(collections2);
4241
4657
  });
4658
+ function resolveDocField(fields, doc, fieldName) {
4659
+ if (fieldName in doc) return doc[fieldName];
4660
+ for (const field of fields) {
4661
+ if (!field.name) continue;
4662
+ if (field.type === "tabs" && field.tabs) {
4663
+ const data = doc[field.name];
4664
+ if (data && typeof data === "object" && fieldName in data) return data[fieldName];
4665
+ }
4666
+ if ((field.type === "group" || field.type === "collapsible") && field.fields) {
4667
+ const data = doc[field.name];
4668
+ if (data && typeof data === "object") {
4669
+ if (fieldName in data) return data[fieldName];
4670
+ const nested = resolveDocField(field.fields, data, fieldName);
4671
+ if (nested !== void 0) return nested;
4672
+ }
4673
+ }
4674
+ }
4675
+ return void 0;
4676
+ }
4677
+ function flattenRelationshipFields(fields) {
4678
+ const relFields = [];
4679
+ for (const field of fields) {
4680
+ if (field.type === "relationship") {
4681
+ relFields.push(field);
4682
+ } else if (field.type === "tabs" && field.tabs) {
4683
+ for (const tab of field.tabs) {
4684
+ relFields.push(...flattenRelationshipFields(tab.fields || []));
4685
+ }
4686
+ } else if ((field.type === "group" || field.type === "collapsible") && field.fields) {
4687
+ relFields.push(...flattenRelationshipFields(field.fields || []));
4688
+ }
4689
+ }
4690
+ return relFields;
4691
+ }
4692
+ function extractRelValue(value) {
4693
+ if (!value) return [];
4694
+ if (typeof value === "string") return [value];
4695
+ if (Array.isArray(value)) return value.map((v) => typeof v === "object" ? v.value ?? v : v);
4696
+ if (typeof value === "object") {
4697
+ const v = value.value ?? value;
4698
+ return typeof v === "string" ? [v] : Array.isArray(v) ? v : [];
4699
+ }
4700
+ return [];
4701
+ }
4702
+ async function populateRelationships(docs, collection, db2, registry2) {
4703
+ if (docs.length === 0) return;
4704
+ const relFields = flattenRelationshipFields(collection.fields);
4705
+ if (relFields.length === 0) return;
4706
+ for (const relField of relFields) {
4707
+ const targetSlugs = Array.isArray(relField.relationTo) ? relField.relationTo : [relField.relationTo];
4708
+ const idsBySlug = {};
4709
+ for (const slug of targetSlugs) {
4710
+ idsBySlug[slug] = /* @__PURE__ */ new Set();
4711
+ }
4712
+ for (const doc of docs) {
4713
+ const raw = resolveDocField(collection.fields, doc, relField.name);
4714
+ const ids = extractRelValue(raw);
4715
+ for (const id of ids) {
4716
+ if (!id) continue;
4717
+ if (targetSlugs.length === 1) {
4718
+ idsBySlug[targetSlugs[0]].add(id);
4719
+ } else {
4720
+ for (const slug of targetSlugs) {
4721
+ idsBySlug[slug].add(id);
4722
+ }
4723
+ }
4724
+ }
4725
+ }
4726
+ for (const [targetSlug, idSet] of Object.entries(idsBySlug)) {
4727
+ if (idSet.size === 0) continue;
4728
+ const targetCollection = registry2.getCollection(targetSlug);
4729
+ if (!targetCollection) continue;
4730
+ const titleField = targetCollection.admin?.useAsTitle || "title";
4731
+ const idArr = Array.from(idSet);
4732
+ const relatedDocs = [];
4733
+ for (const id of idArr) {
4734
+ try {
4735
+ const relDoc = await db2.findByID({ collection: targetSlug, id, draft: true });
4736
+ if (relDoc) {
4737
+ const title = resolveDocField(targetCollection.fields, relDoc, titleField) ?? id;
4738
+ relatedDocs.push({ id, title: String(title) });
4739
+ }
4740
+ } catch {
4741
+ relatedDocs.push({ id, title: id });
4742
+ }
4743
+ }
4744
+ const titleMap = new Map(relatedDocs.map((d) => [d.id, d.title]));
4745
+ for (const doc of docs) {
4746
+ const raw = resolveDocField(collection.fields, doc, relField.name);
4747
+ if (!raw) continue;
4748
+ const setValue = (val) => {
4749
+ if (relField.name in doc) {
4750
+ doc[relField.name] = val;
4751
+ } else {
4752
+ for (const f of collection.fields) {
4753
+ if (f.type === "tabs" && f.tabs) {
4754
+ for (const tab of f.tabs) {
4755
+ if (tab.fields?.some((tf) => tf.name === relField.name)) {
4756
+ const tabData = doc[f.name];
4757
+ if (tabData && typeof tabData === "object") {
4758
+ tabData[relField.name] = val;
4759
+ }
4760
+ }
4761
+ }
4762
+ }
4763
+ }
4764
+ }
4765
+ };
4766
+ if (typeof raw === "string") {
4767
+ setValue({ id: raw, title: titleMap.get(raw) || raw });
4768
+ } else if (Array.isArray(raw)) {
4769
+ setValue(raw.map((v) => {
4770
+ const id = typeof v === "object" ? v.value ?? v.id : v;
4771
+ return { id, title: titleMap.get(id) || id };
4772
+ }));
4773
+ } else if (typeof raw === "object") {
4774
+ const id = raw.value ?? raw.id;
4775
+ setValue({ id, title: titleMap.get(id) || id });
4776
+ }
4777
+ }
4778
+ }
4779
+ }
4780
+ }
4242
4781
  app.get("/api/search", async (c) => {
4243
4782
  try {
4244
4783
  const query = c.req.query("q") || "";
@@ -4290,11 +4829,12 @@ function createHonoApp(options) {
4290
4829
  limit,
4291
4830
  tenantID: ctxTenantID
4292
4831
  });
4832
+ await populateRelationships(searchResult.docs, collection, db, registry);
4293
4833
  for (const doc of searchResult.docs) {
4294
4834
  const titleField = collection.admin?.useAsTitle || searchableFields.find(
4295
4835
  (f) => f === "title" || f === "name" || f === "heading" || f === "slug"
4296
4836
  );
4297
- const title = titleField ? doc[titleField] : doc.id;
4837
+ const title = titleField ? resolveDocField(collection.fields, doc, titleField) ?? doc.id : doc.id;
4298
4838
  results.push({
4299
4839
  collection: collection.slug,
4300
4840
  label: collection.label || collection.slug,
@@ -4315,15 +4855,13 @@ function createHonoApp(options) {
4315
4855
  });
4316
4856
  app.get("/api/keys", async (c) => {
4317
4857
  try {
4318
- const { user: ctxUser } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4858
+ const { user: ctxUser, tenantID: ctxTenantID } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4319
4859
  if (!ctxUser || !hasPermission(ctxUser, "users:read")) {
4320
4860
  return c.json({ error: "Forbidden" }, 403);
4321
4861
  }
4322
4862
  const page = parseInt(c.req.query("page") || "1");
4323
4863
  const limit = Math.min(parseInt(c.req.query("limit") || "50"), 100);
4324
- console.log("[ApiKeys] Querying", API_KEY_COLLECTION, { page, limit });
4325
- const result = await db.find({ collection: API_KEY_COLLECTION, where: {}, page, limit });
4326
- console.log("[ApiKeys] Result:", result);
4864
+ const result = await db.find({ collection: API_KEY_COLLECTION, where: {}, page, limit, tenantID: ctxTenantID });
4327
4865
  const docs = (result.docs || []).map((doc) => ({
4328
4866
  id: doc.id,
4329
4867
  name: doc.name,
@@ -4634,37 +5172,31 @@ function createHonoApp(options) {
4634
5172
  if (!access.allowed) {
4635
5173
  return c.json({ error: access.error }, access.status || 403);
4636
5174
  }
4637
- auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "GET", c.req.raw);
5175
+ if (ctxTenantID) {
5176
+ db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
5177
+ }
4638
5178
  const url = new URL(c.req.url);
4639
5179
  const page = parseInt(url.searchParams.get("page") || "1");
4640
- const limit = Math.min(
4641
- parseInt(url.searchParams.get("limit") || "10"),
4642
- 100
4643
- );
5180
+ const limit = Math.min(parseInt(url.searchParams.get("limit") || "10"), 100);
4644
5181
  const sort = url.searchParams.get("sort") || void 0;
4645
5182
  const depth = parseInt(url.searchParams.get("depth") || "0");
4646
5183
  const select = url.searchParams.get("select")?.split(",") || void 0;
4647
- let where = {};
4648
- const whereParam = url.searchParams.get("where");
4649
- if (whereParam) {
4650
- try {
4651
- where = JSON.parse(whereParam);
4652
- } catch {
4653
- }
4654
- }
5184
+ const isDraftRequest = !!ctxUser;
4655
5185
  const result = await db.find({
4656
5186
  collection: slug,
4657
- where,
5187
+ where: {},
4658
5188
  sort,
4659
5189
  limit,
4660
5190
  page,
4661
5191
  depth,
4662
5192
  tenantID: ctxTenantID,
4663
- select
5193
+ select,
5194
+ draft: isDraftRequest
4664
5195
  });
5196
+ await populateRelationships(result.docs, collection, db, registry);
4665
5197
  return c.json(result);
4666
5198
  } catch (error) {
4667
- console.error(`[API] Error listing ${slug}:`, error);
5199
+ console.error("[API] list error:", error);
4668
5200
  return c.json({ error: error.message }, 500);
4669
5201
  }
4670
5202
  });
@@ -4689,6 +5221,9 @@ function createHonoApp(options) {
4689
5221
  return c.json({ error: access.error }, access.status || 403);
4690
5222
  }
4691
5223
  const id = c.req.param("id");
5224
+ if (!id) {
5225
+ return c.json({ error: "Missing document ID" }, 400);
5226
+ }
4692
5227
  const url = new URL(c.req.url);
4693
5228
  const compareA = url.searchParams.get("compareA");
4694
5229
  const compareB = url.searchParams.get("compareB");
@@ -4719,36 +5254,6 @@ function createHonoApp(options) {
4719
5254
  return c.json({ error: error.message }, 500);
4720
5255
  }
4721
5256
  });
4722
- app.get(`${basePath}/:id/draft`, async (c) => {
4723
- try {
4724
- const {
4725
- user: ctxUser,
4726
- tenantID: ctxTenantID,
4727
- apiKeyContext
4728
- } = await resolveAuthContext(c.req.raw, authMw, user, tenantID);
4729
- const access = await checkCollectionAccess(
4730
- collection,
4731
- "read",
4732
- c.req.raw,
4733
- ctxUser,
4734
- ctxTenantID,
4735
- apiKeyContext,
4736
- enablePublicAccess,
4737
- defaultCollectionAccess
4738
- );
4739
- if (!access.allowed) {
4740
- return c.json({ error: access.error }, access.status || 403);
4741
- }
4742
- const draft = await db.findDraft({
4743
- collection: slug,
4744
- documentId: c.req.param("id"),
4745
- tenantID: ctxTenantID
4746
- });
4747
- return c.json({ data: draft });
4748
- } catch (error) {
4749
- return c.json({ error: error.message }, 500);
4750
- }
4751
- });
4752
5257
  app.put(`${basePath}/:id/draft`, async (c) => {
4753
5258
  try {
4754
5259
  const {
@@ -4772,24 +5277,39 @@ function createHonoApp(options) {
4772
5277
  const id = c.req.param("id");
4773
5278
  const body = await c.req.json();
4774
5279
  const baseUpdatedAt = readBaseUpdatedAt(body);
4775
- const data = body.data ?? omitRevisionFields(body);
4776
5280
  const originalDoc = await db.findByID({
4777
5281
  collection: slug,
4778
5282
  id,
4779
- tenantID: ctxTenantID
5283
+ tenantID: ctxTenantID,
5284
+ draft: true
4780
5285
  });
4781
5286
  if (!originalDoc) {
4782
5287
  return c.json({ error: "Document not found" }, 404);
4783
5288
  }
4784
- const draft = await db.upsertDraft({
5289
+ let finalData;
5290
+ if (body.delta) {
5291
+ finalData = { ...originalDoc, ...body.delta };
5292
+ } else {
5293
+ finalData = body.data ?? omitRevisionFields(body);
5294
+ }
5295
+ const version = await db.updateLatestVersion({
4785
5296
  collection: slug,
4786
5297
  documentId: id,
4787
- tenantID: ctxTenantID,
4788
- data,
4789
- baseUpdatedAt,
4790
- draftUpdatedAt: body.draftUpdatedAt
5298
+ data: finalData,
5299
+ status: "draft",
5300
+ tenantID: ctxTenantID
4791
5301
  });
4792
- return c.json({ data: draft, message: "Draft saved successfully" });
5302
+ if (ctxUser) {
5303
+ sessionAuthAdapter?.createAuditLog({
5304
+ action: "document_update",
5305
+ userId: ctxUser.id,
5306
+ resource: slug,
5307
+ resourceId: id,
5308
+ success: true,
5309
+ metadata: { type: "draft_save" }
5310
+ });
5311
+ }
5312
+ return c.json({ data: version, message: "Draft saved successfully" });
4793
5313
  } catch (error) {
4794
5314
  return c.json({ error: error.message }, 500);
4795
5315
  }
@@ -4814,11 +5334,16 @@ function createHonoApp(options) {
4814
5334
  if (!access.allowed) {
4815
5335
  return c.json({ error: access.error }, access.status || 403);
4816
5336
  }
4817
- await db.deleteDraft({
4818
- collection: slug,
4819
- documentId: c.req.param("id"),
4820
- tenantID: ctxTenantID
4821
- });
5337
+ if (ctxUser) {
5338
+ sessionAuthAdapter?.createAuditLog({
5339
+ action: "document_update",
5340
+ userId: ctxUser.id,
5341
+ resource: slug,
5342
+ resourceId: c.req.param("id"),
5343
+ success: true,
5344
+ metadata: { type: "draft_discard" }
5345
+ });
5346
+ }
4822
5347
  return c.json({ message: "Draft discarded successfully" });
4823
5348
  } catch (error) {
4824
5349
  return c.json({ error: error.message }, 500);
@@ -4861,12 +5386,14 @@ function createHonoApp(options) {
4861
5386
  doc = await db.findOne({
4862
5387
  collection: slug,
4863
5388
  where: { slug: id },
4864
- tenantID: ctxTenantID
5389
+ tenantID: ctxTenantID,
5390
+ draft: isDraftRequest
4865
5391
  });
4866
5392
  }
4867
5393
  if (!doc) {
4868
5394
  return c.json({ error: "Document not found" }, 404);
4869
5395
  }
5396
+ await populateRelationships([doc], collection, db, registry);
4870
5397
  return c.json({ data: doc });
4871
5398
  } catch (error) {
4872
5399
  return c.json({ error: error.message }, 500);
@@ -4892,23 +5419,81 @@ function createHonoApp(options) {
4892
5419
  if (!access.allowed) {
4893
5420
  return c.json({ error: access.error }, access.status || 403);
4894
5421
  }
5422
+ if (ctxTenantID) {
5423
+ db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
5424
+ }
4895
5425
  auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, basePath, "POST", c.req.raw);
4896
5426
  const body = await c.req.json();
5427
+ let validated = body;
5428
+ normalizeEmptyStrings(validated, collection.fields);
5429
+ convertRichtextFields(collection.fields, validated);
5430
+ const hookReq = c.req.raw;
5431
+ if (collection.hooks?.beforeValidate) {
5432
+ for (const hook of collection.hooks.beforeValidate) {
5433
+ const hookResult = await hook({
5434
+ collection: slug,
5435
+ data: validated,
5436
+ req: hookReq,
5437
+ user: ctxUser,
5438
+ tenantID: ctxTenantID,
5439
+ operation: "create"
5440
+ });
5441
+ if (hookResult) Object.assign(validated, hookResult);
5442
+ }
5443
+ }
4897
5444
  const schema = registry.getCreateZodSchema(slug);
4898
- let validated;
4899
5445
  try {
4900
- validated = schema.parse(body);
5446
+ validated = schema.parse(validated);
4901
5447
  } catch (zodErr) {
4902
- return c.json({ error: "Validation failed", details: zodErr.errors }, 400);
5448
+ return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
4903
5449
  }
4904
5450
  if (collection.tenantScoped && ctxTenantID) {
4905
5451
  validated.tenantID = ctxTenantID;
4906
5452
  }
5453
+ const isDraftEnabled = collection.versions?.drafts === true;
5454
+ validated.status = isDraftEnabled ? "draft" : "published";
5455
+ if (collection.hooks?.beforeChange) {
5456
+ for (const hook of collection.hooks.beforeChange) {
5457
+ const hookResult = await hook({
5458
+ collection: slug,
5459
+ data: validated,
5460
+ req: hookReq,
5461
+ user: ctxUser,
5462
+ tenantID: ctxTenantID,
5463
+ operation: "create"
5464
+ });
5465
+ if (hookResult) Object.assign(validated, hookResult);
5466
+ }
5467
+ }
4907
5468
  const doc = await db.create({
4908
5469
  collection: slug,
4909
5470
  data: validated,
4910
5471
  tenantID: ctxTenantID
4911
5472
  });
5473
+ if (isDraftEnabled) {
5474
+ await db.createVersion({
5475
+ collection: slug,
5476
+ documentId: doc.id,
5477
+ data: validated,
5478
+ status: "draft",
5479
+ createdBy: ctxUser?.id,
5480
+ changeDescription: "Created",
5481
+ tenantID: ctxTenantID
5482
+ });
5483
+ }
5484
+ if (collection.hooks?.afterChange) {
5485
+ for (const hook of collection.hooks.afterChange) {
5486
+ await hook({
5487
+ collection: slug,
5488
+ doc,
5489
+ data: validated,
5490
+ req: hookReq,
5491
+ user: ctxUser,
5492
+ tenantID: ctxTenantID,
5493
+ operation: "create"
5494
+ });
5495
+ }
5496
+ }
4912
5497
  if (webhookService) {
4913
5498
  webhookService.trigger(getWebhookEvent(slug, "create"), {
4914
5499
  collection: slug,
@@ -4918,11 +5503,20 @@ function createHonoApp(options) {
4918
5503
  tenantId: ctxTenantID
4919
5504
  }).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
4920
5505
  }
5506
+ if (ctxUser) {
5507
+ sessionAuthAdapter?.createAuditLog({
5508
+ action: "document_create",
5509
+ userId: ctxUser.id,
5510
+ resource: slug,
5511
+ resourceId: doc.id,
5512
+ success: true
5513
+ });
5514
+ }
4921
5515
  return c.json({ data: doc, message: "Created successfully" }, 201);
4922
5516
  } catch (error) {
4923
5517
  if (error.name === "ZodError") {
4924
5518
  return c.json(
4925
- { error: "Validation failed", details: error.errors },
5519
+ { error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
4926
5520
  400
4927
5521
  );
4928
5522
  }
@@ -4952,61 +5546,92 @@ function createHonoApp(options) {
4952
5546
  const id = c.req.param("id");
4953
5547
  const body = await c.req.json();
4954
5548
  const baseUpdatedAt = readBaseUpdatedAt(body);
4955
- console.log(`[PATCH] ${slug}/${id}`, {
4956
- baseUpdatedAt,
4957
- bodyKeys: Object.keys(body),
4958
- tenantID: ctxTenantID
4959
- });
4960
- const cleaned = Object.fromEntries(
4961
- Object.entries(omitRevisionFields(body)).filter(
4962
- ([_, v]) => v !== "null" && v !== void 0
4963
- )
4964
- );
4965
- const schema = registry.getUpdateZodSchema(slug);
4966
- const validated = schema.parse(cleaned);
4967
- console.log(`[PATCH] Validated data:`, Object.keys(validated));
4968
5549
  const originalDoc = await db.findByID({
4969
5550
  collection: slug,
4970
5551
  id,
4971
5552
  tenantID: ctxTenantID,
4972
5553
  draft: true
4973
- // Always fetch current doc regardless of status
4974
5554
  });
4975
- if (originalDoc) {
4976
- console.log(`[PATCH] Original doc updatedAt:`, originalDoc.updatedAt);
4977
- }
4978
5555
  if (!originalDoc) {
4979
5556
  return c.json({ error: "Document not found" }, 404);
4980
5557
  }
4981
5558
  if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
4982
5559
  return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
4983
5560
  }
5561
+ let validated = Object.fromEntries(
5562
+ Object.entries(omitRevisionFields(body)).filter(
5563
+ ([_, v]) => v !== "null" && v !== void 0
5564
+ )
5565
+ );
5566
+ normalizeEmptyStrings(validated, collection.fields);
5567
+ convertRichtextFields(collection.fields, validated);
5568
+ const hookReq = c.req.raw;
5569
+ if (collection.hooks?.beforeValidate) {
5570
+ for (const hook of collection.hooks.beforeValidate) {
5571
+ const hookResult = await hook({
5572
+ collection: slug,
5573
+ data: validated,
5574
+ originalDoc,
5575
+ req: hookReq,
5576
+ user: ctxUser,
5577
+ tenantID: ctxTenantID,
5578
+ operation: "update"
5579
+ });
5580
+ if (hookResult) Object.assign(validated, hookResult);
5581
+ }
5582
+ }
5583
+ const schema = registry.getUpdateZodSchema(slug);
5584
+ validated = schema.parse(validated);
5585
+ if (collection.hooks?.beforeChange) {
5586
+ for (const hook of collection.hooks.beforeChange) {
5587
+ const hookResult = await hook({
5588
+ collection: slug,
5589
+ data: validated,
5590
+ originalDoc,
5591
+ req: hookReq,
5592
+ user: ctxUser,
5593
+ tenantID: ctxTenantID,
5594
+ operation: "update"
5595
+ });
5596
+ if (hookResult) Object.assign(validated, hookResult);
5597
+ }
5598
+ }
5599
+ const isDraft = c.req.header("X-Draft") === "true";
4984
5600
  const isDraftEnabled = collection.versions?.drafts === true;
4985
- const isAlreadyPublished = originalDoc._status === "published";
5601
+ const isAutosave = c.req.query("autosave") === "true";
4986
5602
  let doc;
4987
- if (isDraftEnabled && isAlreadyPublished) {
5603
+ if (isDraftEnabled && isDraft) {
4988
5604
  await db.createVersion({
4989
5605
  collection: slug,
4990
5606
  documentId: id,
4991
5607
  data: validated,
4992
5608
  status: "draft",
5609
+ autosave: isAutosave,
4993
5610
  createdBy: ctxUser?.id,
4994
- changeDescription: "Manual save (Draft)",
5611
+ changeDescription: isAutosave ? "Autosave" : "Draft saved",
4995
5612
  tenantID: ctxTenantID
4996
5613
  });
5614
+ } else if (isDraftEnabled) {
4997
5615
  await db.update({
4998
5616
  collection: slug,
4999
5617
  id,
5000
- data: { _has_draft: true },
5001
- // Keep old data, just set flag
5618
+ data: { ...validated, status: "published" },
5619
+ tenantID: ctxTenantID
5620
+ });
5621
+ await db.createVersion({
5622
+ collection: slug,
5623
+ documentId: id,
5624
+ data: validated,
5625
+ status: "published",
5626
+ createdBy: ctxUser?.id,
5627
+ changeDescription: "Published",
5002
5628
  tenantID: ctxTenantID
5003
5629
  });
5004
5630
  } else {
5005
- const saveData = isDraftEnabled ? { ...validated, _status: "draft", _has_draft: false } : validated;
5006
5631
  await db.update({
5007
5632
  collection: slug,
5008
5633
  id,
5009
- data: saveData,
5634
+ data: validated,
5010
5635
  tenantID: ctxTenantID
5011
5636
  });
5012
5637
  }
@@ -5016,24 +5641,19 @@ function createHonoApp(options) {
5016
5641
  tenantID: ctxTenantID,
5017
5642
  draft: true
5018
5643
  });
5019
- if (isDraftEnabled) {
5020
- if (!isAlreadyPublished) {
5021
- await db.createVersion({
5644
+ if (collection.hooks?.afterChange) {
5645
+ for (const hook of collection.hooks.afterChange) {
5646
+ await hook({
5022
5647
  collection: slug,
5023
- documentId: id,
5648
+ doc,
5024
5649
  data: validated,
5025
- status: "draft",
5026
- createdBy: ctxUser?.id,
5027
- changeDescription: "Manual save",
5028
- tenantID: ctxTenantID
5650
+ originalDoc,
5651
+ req: hookReq,
5652
+ user: ctxUser,
5653
+ tenantID: ctxTenantID,
5654
+ operation: "update"
5029
5655
  });
5030
5656
  }
5031
- } else {
5032
- await db.deleteDraft({
5033
- collection: slug,
5034
- documentId: id,
5035
- tenantID: ctxTenantID
5036
- });
5037
5657
  }
5038
5658
  if (webhookService) {
5039
5659
  webhookService.trigger(getWebhookEvent(slug, "update"), {
@@ -5045,16 +5665,26 @@ function createHonoApp(options) {
5045
5665
  tenantId: ctxTenantID
5046
5666
  }).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
5047
5667
  }
5048
- console.log(`[PATCH] Result doc updatedAt:`, doc?.updatedAt);
5668
+ auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "PATCH", c.req.raw);
5669
+ if (ctxUser) {
5670
+ sessionAuthAdapter?.createAuditLog({
5671
+ action: "document_update",
5672
+ userId: ctxUser.id,
5673
+ resource: slug,
5674
+ resourceId: id,
5675
+ success: true
5676
+ });
5677
+ }
5049
5678
  return c.json({ data: doc, message: isDraftEnabled ? "Draft saved" : "Updated successfully" });
5050
5679
  } catch (error) {
5051
5680
  if (error.name === "ZodError") {
5052
5681
  console.error(`[PATCH ${basePath}/:id] Validation failed:`, error.errors);
5053
5682
  return c.json(
5054
- { error: "Validation failed", details: error.errors },
5683
+ { error: `Validation failed: ${formatZodErrors(error.errors)}`, details: error.errors },
5055
5684
  400
5056
5685
  );
5057
5686
  }
5687
+ console.error(`[PATCH ${basePath}/:id] ERROR:`, error.message, `CAUSE:`, error.cause?.message || error.cause, `QUERY:`, error.query);
5058
5688
  return c.json({ error: error.message }, 500);
5059
5689
  }
5060
5690
  });
@@ -5078,23 +5708,50 @@ function createHonoApp(options) {
5078
5708
  if (!access.allowed) {
5079
5709
  return c.json({ error: access.error }, access.status || 403);
5080
5710
  }
5711
+ if (ctxTenantID) {
5712
+ db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
5713
+ }
5081
5714
  const id = c.req.param("id");
5082
- console.log(`[DELETE] Deleting ${slug}/${id}`);
5715
+ const hookReq = c.req.raw;
5083
5716
  const originalDoc = await db.findByID({
5084
5717
  collection: slug,
5085
5718
  id,
5086
- tenantID: ctxTenantID
5719
+ tenantID: ctxTenantID,
5720
+ draft: true
5087
5721
  });
5722
+ if (!originalDoc) {
5723
+ return c.json({ error: "Document not found" }, 404);
5724
+ }
5725
+ if (collection.hooks?.beforeDelete) {
5726
+ for (const hook of collection.hooks.beforeDelete) {
5727
+ await hook({
5728
+ collection: slug,
5729
+ doc: originalDoc,
5730
+ req: hookReq,
5731
+ user: ctxUser,
5732
+ tenantID: ctxTenantID,
5733
+ operation: "delete"
5734
+ });
5735
+ }
5736
+ }
5088
5737
  const doc = await db.delete({
5089
5738
  collection: slug,
5090
5739
  id,
5091
5740
  tenantID: ctxTenantID
5092
5741
  });
5093
- await db.deleteDraft({
5094
- collection: slug,
5095
- documentId: id,
5096
- tenantID: ctxTenantID
5097
- });
5742
+ if (collection.hooks?.afterDelete) {
5743
+ for (const hook of collection.hooks.afterDelete) {
5744
+ await hook({
5745
+ collection: slug,
5746
+ doc,
5747
+ originalDoc,
5748
+ req: hookReq,
5749
+ user: ctxUser,
5750
+ tenantID: ctxTenantID,
5751
+ operation: "delete"
5752
+ });
5753
+ }
5754
+ }
5098
5755
  if (webhookService) {
5099
5756
  webhookService.trigger(getWebhookEvent(slug, "delete"), {
5100
5757
  collection: slug,
@@ -5105,6 +5762,16 @@ function createHonoApp(options) {
5105
5762
  tenantId: ctxTenantID
5106
5763
  }).catch((err) => console.error(`[Webhook] Failed to trigger:`, err));
5107
5764
  }
5765
+ auditApiKeyUsage(sessionAuthAdapter, apiKeyContext, `${basePath}/${id}`, "DELETE", c.req.raw);
5766
+ if (ctxUser) {
5767
+ sessionAuthAdapter?.createAuditLog({
5768
+ action: "document_delete",
5769
+ userId: ctxUser.id,
5770
+ resource: slug,
5771
+ resourceId: id,
5772
+ success: true
5773
+ });
5774
+ }
5108
5775
  return c.json({ data: doc, message: "Deleted successfully" });
5109
5776
  } catch (error) {
5110
5777
  console.error(`[DELETE] Error deleting ${slug}:`, error);
@@ -5113,7 +5780,6 @@ function createHonoApp(options) {
5113
5780
  });
5114
5781
  app.post(`${basePath}/:id/duplicate`, async (c) => {
5115
5782
  try {
5116
- console.log(`[Duplicate] Request for ${slug}`);
5117
5783
  const {
5118
5784
  user: ctxUser,
5119
5785
  tenantID: ctxTenantID,
@@ -5130,25 +5796,21 @@ function createHonoApp(options) {
5130
5796
  defaultCollectionAccess
5131
5797
  );
5132
5798
  if (!access.allowed) {
5133
- console.log("[Duplicate] Access denied:", access.error);
5134
5799
  return c.json({ error: access.error }, access.status || 403);
5135
5800
  }
5136
5801
  const id = c.req.param("id");
5137
- console.log(`[Duplicate] ID: ${id}`);
5138
5802
  const originalDoc = await db.findByID({
5139
5803
  collection: slug,
5140
5804
  id,
5141
- tenantID: ctxTenantID
5805
+ tenantID: ctxTenantID,
5806
+ draft: true
5142
5807
  });
5143
5808
  if (!originalDoc) {
5144
- console.log("[Duplicate] Document not found");
5145
5809
  return c.json({ error: "Document not found" }, 404);
5146
5810
  }
5147
- console.log(`[Duplicate] Original doc:`, originalDoc);
5148
5811
  const { id: _oldId, createdAt: _oldCreated, updatedAt: _oldUpdated, ...docData } = originalDoc;
5149
5812
  const timestamp = Date.now().toString(36);
5150
5813
  const newSlug = `${originalDoc.slug || "document"}-copy-${timestamp}`;
5151
- console.log(`[Duplicate] New slug: ${newSlug}`);
5152
5814
  const newDoc = await db.create({
5153
5815
  collection: slug,
5154
5816
  data: {
@@ -5158,7 +5820,6 @@ function createHonoApp(options) {
5158
5820
  },
5159
5821
  tenantID: ctxTenantID
5160
5822
  });
5161
- console.log(`[Duplicate] Created successfully:`, newDoc.id);
5162
5823
  return c.json({ data: newDoc, message: "Document duplicated successfully" });
5163
5824
  } catch (error) {
5164
5825
  console.error("[Duplicate] Error:", error);
@@ -5198,7 +5859,7 @@ function createHonoApp(options) {
5198
5859
  const doc = await db.update({
5199
5860
  collection: slug,
5200
5861
  id,
5201
- data: { ...version.data, _status: "draft", _has_draft: false },
5862
+ data: { ...version.data, status: "draft" },
5202
5863
  tenantID: ctxTenantID
5203
5864
  });
5204
5865
  return c.json({
@@ -5243,7 +5904,7 @@ function createHonoApp(options) {
5243
5904
  const doc = await db.update({
5244
5905
  collection: slug,
5245
5906
  id: c.req.param("id"),
5246
- data: { ...version.data, _status: "draft", _has_draft: false },
5907
+ data: { ...version.data, status: "draft" },
5247
5908
  tenantID: ctxTenantID
5248
5909
  });
5249
5910
  return c.json({
@@ -5276,6 +5937,9 @@ function createHonoApp(options) {
5276
5937
  if (!access.allowed) {
5277
5938
  return c.json({ error: access.error }, access.status || 403);
5278
5939
  }
5940
+ if (ctxTenantID) {
5941
+ db.setTenantContext({ tenantId: ctxTenantID, userId: ctxUser?.id ?? "", role: ctxUser?.role, isSuperAdmin: ctxUser?.role === "super_admin" });
5942
+ }
5279
5943
  const id = c.req.param("id");
5280
5944
  const body = await c.req.json().catch(() => ({}));
5281
5945
  const baseUpdatedAt = readBaseUpdatedAt(body);
@@ -5291,9 +5955,9 @@ function createHonoApp(options) {
5291
5955
  if (baseUpdatedAt && originalDoc.updatedAt && baseUpdatedAt !== originalDoc.updatedAt) {
5292
5956
  return c.json(buildConflictResponse(baseUpdatedAt, originalDoc), 409);
5293
5957
  }
5294
- let publishData = { _status: "published", _has_draft: false };
5958
+ let publishData = { status: "published" };
5295
5959
  let finalContent = originalDoc;
5296
- if (originalDoc._has_draft) {
5960
+ if (collection.versions?.drafts) {
5297
5961
  const versions = await db.findVersions({
5298
5962
  collection: slug,
5299
5963
  documentId: id,
@@ -5301,9 +5965,10 @@ function createHonoApp(options) {
5301
5965
  sort: "-createdAt",
5302
5966
  tenantID: ctxTenantID
5303
5967
  });
5304
- if (versions.docs.length > 0 && versions.docs[0].status === "draft") {
5305
- finalContent = { ...originalDoc, ...versions.docs[0].data };
5306
- publishData = { ...versions.docs[0].data, ...publishData };
5968
+ if (versions.docs.length > 0) {
5969
+ const latestVersion = versions.docs[0];
5970
+ finalContent = { ...originalDoc, ...latestVersion.data };
5971
+ publishData = { ...latestVersion.data, ...publishData };
5307
5972
  }
5308
5973
  }
5309
5974
  const doc = await db.update({
@@ -5316,18 +5981,13 @@ function createHonoApp(options) {
5316
5981
  await db.createVersion({
5317
5982
  collection: slug,
5318
5983
  documentId: id,
5319
- data: { ...finalContent, _status: "published" },
5984
+ data: { ...finalContent, status: "published" },
5320
5985
  status: "published",
5321
5986
  createdBy: ctxUser?.id,
5322
5987
  changeDescription: "Published",
5323
5988
  tenantID: ctxTenantID
5324
5989
  });
5325
5990
  }
5326
- await db.deleteDraft({
5327
- collection: slug,
5328
- documentId: id,
5329
- tenantID: ctxTenantID
5330
- });
5331
5991
  if (webhookService) {
5332
5992
  webhookService.trigger(getWebhookEvent(slug, "update"), {
5333
5993
  collection: slug,
@@ -5375,7 +6035,7 @@ function createHonoApp(options) {
5375
6035
  const doc = await db.update({
5376
6036
  collection: slug,
5377
6037
  id,
5378
- data: { _status: "draft" },
6038
+ data: { status: "draft" },
5379
6039
  tenantID: ctxTenantID
5380
6040
  });
5381
6041
  if (webhookService) {
@@ -5410,13 +6070,31 @@ function createHonoApp(options) {
5410
6070
  if (!access.allowed) {
5411
6071
  return c.json({ error: access.error }, access.status || 403);
5412
6072
  }
5413
- const isDraftRequest = c.req.query("draft") === "true" && !!ctxUser;
5414
- const doc = await db.findOne({
6073
+ const isDraftRequest = !!ctxUser;
6074
+ let doc = await db.findOne({
5415
6075
  collection: `_globals_${slug}`,
5416
6076
  where: {},
5417
6077
  tenantID: ctxTenantID,
5418
6078
  draft: isDraftRequest
5419
6079
  });
6080
+ if (slug === "system") {
6081
+ const newSecret = crypto.randomBytes(32).toString("hex");
6082
+ if (!doc) {
6083
+ doc = await db.create({
6084
+ collection: `_globals_${slug}`,
6085
+ data: { id: slug, appSecret: newSecret },
6086
+ tenantID: ctxTenantID
6087
+ });
6088
+ } else if (!doc.appSecret) {
6089
+ await db.update({
6090
+ collection: `_globals_${slug}`,
6091
+ id: slug,
6092
+ data: { appSecret: newSecret },
6093
+ tenantID: ctxTenantID
6094
+ });
6095
+ doc.appSecret = newSecret;
6096
+ }
6097
+ }
5420
6098
  return c.json({ data: doc || {} });
5421
6099
  } catch (error) {
5422
6100
  return c.json({ error: error.message }, 500);
@@ -5436,18 +6114,20 @@ function createHonoApp(options) {
5436
6114
  if (!access.allowed) {
5437
6115
  return c.json({ error: access.error }, access.status || 403);
5438
6116
  }
5439
- const body = await c.req.json();
6117
+ const body = omitRevisionFields(await c.req.json());
5440
6118
  const cleaned = Object.fromEntries(
5441
6119
  Object.entries(body).filter(([_, v]) => v !== null && v !== "null" && v !== void 0)
5442
6120
  );
5443
- const schema = registry.getZodSchema(slug);
6121
+ normalizeEmptyStrings(cleaned, globalConfig.fields);
6122
+ convertRichtextFields(globalConfig.fields, cleaned);
6123
+ const schema = registry.getUpdateZodSchema(slug);
5444
6124
  let validated;
5445
6125
  try {
5446
6126
  validated = schema.parse(cleaned);
5447
6127
  } catch (zodErr) {
5448
- return c.json({ error: "Validation failed", details: zodErr.errors }, 400);
6128
+ return c.json({ error: `Validation failed: ${formatZodErrors(zodErr.errors)}`, details: zodErr.errors }, 400);
5449
6129
  }
5450
- const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "_status", "_has_draft"]);
6130
+ const SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "status", "baseUpdatedAt", "_baseUpdatedAt"]);
5451
6131
  const userData = Object.fromEntries(
5452
6132
  Object.entries(validated).filter(([k]) => !SYSTEM_FIELDS.has(k))
5453
6133
  );
@@ -5458,49 +6138,68 @@ function createHonoApp(options) {
5458
6138
  tenantID: ctxTenantID,
5459
6139
  draft: true
5460
6140
  });
6141
+ const isDraft = c.req.header("X-Draft") === "true";
5461
6142
  const isDraftEnabled = globalConfig.versions?.drafts === true;
5462
- const isAlreadyPublished = originalDoc?._status === "published";
6143
+ const isAutosave = c.req.query("autosave") === "true";
5463
6144
  let doc;
5464
- if (isDraftEnabled && isAlreadyPublished) {
6145
+ if (isDraftEnabled && isDraft) {
5465
6146
  await db.createVersion({
5466
6147
  collection: collectionSlug,
5467
6148
  documentId: slug,
5468
6149
  data: userData,
5469
6150
  status: "draft",
6151
+ autosave: isAutosave,
5470
6152
  createdBy: ctxUser?.id,
5471
- changeDescription: "Manual save (Draft)",
5472
- tenantID: ctxTenantID
5473
- });
5474
- doc = await db.update({
5475
- collection: collectionSlug,
5476
- id: slug,
5477
- data: { _has_draft: true },
6153
+ changeDescription: isAutosave ? "Autosave" : "Manual save",
5478
6154
  tenantID: ctxTenantID
5479
6155
  });
5480
- } else {
5481
- const saveData = isDraftEnabled ? { ...userData, _status: "draft", _has_draft: false } : { ...userData, _status: "published", _has_draft: false };
6156
+ if (!originalDoc) {
6157
+ doc = await db.create({
6158
+ collection: collectionSlug,
6159
+ data: { ...userData, id: slug, status: "draft" },
6160
+ tenantID: ctxTenantID
6161
+ });
6162
+ } else {
6163
+ doc = originalDoc;
6164
+ }
6165
+ } else if (isDraftEnabled && !isDraft) {
6166
+ const publishStatus = "published";
5482
6167
  if (originalDoc) {
5483
6168
  doc = await db.update({
5484
6169
  collection: collectionSlug,
5485
6170
  id: slug,
5486
- data: saveData,
6171
+ data: { ...userData, status: publishStatus },
5487
6172
  tenantID: ctxTenantID
5488
6173
  });
5489
6174
  } else {
5490
6175
  doc = await db.create({
5491
6176
  collection: collectionSlug,
5492
- data: { ...saveData, id: slug },
6177
+ data: { ...userData, id: slug, status: publishStatus },
5493
6178
  tenantID: ctxTenantID
5494
6179
  });
5495
6180
  }
5496
- if (isDraftEnabled) {
5497
- await db.createVersion({
6181
+ await db.createVersion({
6182
+ collection: collectionSlug,
6183
+ documentId: slug,
6184
+ data: userData,
6185
+ status: publishStatus,
6186
+ autosave: false,
6187
+ createdBy: ctxUser?.id,
6188
+ changeDescription: "Published",
6189
+ tenantID: ctxTenantID
6190
+ });
6191
+ } else {
6192
+ if (originalDoc) {
6193
+ doc = await db.update({
5498
6194
  collection: collectionSlug,
5499
- documentId: slug,
6195
+ id: slug,
5500
6196
  data: userData,
5501
- status: "draft",
5502
- createdBy: ctxUser?.id,
5503
- changeDescription: "Manual save",
6197
+ tenantID: ctxTenantID
6198
+ });
6199
+ } else {
6200
+ doc = await db.create({
6201
+ collection: collectionSlug,
6202
+ data: { ...userData, id: slug },
5504
6203
  tenantID: ctxTenantID
5505
6204
  });
5506
6205
  }
@@ -5509,6 +6208,22 @@ function createHonoApp(options) {
5509
6208
  mediaService = null;
5510
6209
  mediaServiceInitError = null;
5511
6210
  }
6211
+ if (slug === "email-settings") {
6212
+ const newEmailTransport = await EmailTransport.fromConfig(db);
6213
+ authRoutes.email = newEmailTransport || void 0;
6214
+ }
6215
+ if (slug === "system") {
6216
+ await loadSecrets();
6217
+ }
6218
+ if (ctxUser) {
6219
+ sessionAuthAdapter?.createAuditLog({
6220
+ action: "settings_change",
6221
+ userId: ctxUser.id,
6222
+ resource: `global:${slug}`,
6223
+ resourceId: slug,
6224
+ success: true
6225
+ });
6226
+ }
5512
6227
  return c.json({ data: doc, message: "Updated successfully" });
5513
6228
  } catch (error) {
5514
6229
  console.error(`[API] Save global "${slug}" failed:`, error);
@@ -5533,9 +6248,9 @@ function createHonoApp(options) {
5533
6248
  draft: true
5534
6249
  });
5535
6250
  if (!originalDoc) return c.json({ error: "Global not found" }, 404);
5536
- let publishData = { _status: "published", _has_draft: false };
6251
+ let publishData = { status: "published" };
5537
6252
  let finalContent = originalDoc;
5538
- if (originalDoc._has_draft) {
6253
+ if (globalConfig.versions?.drafts) {
5539
6254
  const versions = await db.findVersions({
5540
6255
  collection: collectionSlug,
5541
6256
  documentId: slug,
@@ -5543,7 +6258,7 @@ function createHonoApp(options) {
5543
6258
  sort: "-createdAt",
5544
6259
  tenantID: ctxTenantID
5545
6260
  });
5546
- if (versions.docs.length > 0 && versions.docs[0].status === "draft") {
6261
+ if (versions.docs.length > 0) {
5547
6262
  finalContent = { ...originalDoc, ...versions.docs[0].data };
5548
6263
  publishData = { ...versions.docs[0].data, ...publishData };
5549
6264
  }
@@ -5558,7 +6273,7 @@ function createHonoApp(options) {
5558
6273
  await db.createVersion({
5559
6274
  collection: collectionSlug,
5560
6275
  documentId: slug,
5561
- data: { ...finalContent, _status: "published" },
6276
+ data: { ...finalContent, status: "published" },
5562
6277
  status: "published",
5563
6278
  createdBy: ctxUser?.id,
5564
6279
  changeDescription: "Published",
@@ -5578,7 +6293,7 @@ function createHonoApp(options) {
5578
6293
  const doc = await db.update({
5579
6294
  collection: `_globals_${slug}`,
5580
6295
  id: slug,
5581
- data: { _status: "draft", _has_draft: false },
6296
+ data: { status: "draft" },
5582
6297
  tenantID: ctxTenantID
5583
6298
  });
5584
6299
  return c.json({ data: doc, message: "Unpublished successfully" });
@@ -5638,9 +6353,13 @@ function createHonoApp(options) {
5638
6353
  const doc = await db.update({
5639
6354
  collection: collectionSlug,
5640
6355
  id: slug,
5641
- data: { ...version.data, _status: "draft", _has_draft: false },
6356
+ data: { ...version.data, status: "draft" },
5642
6357
  tenantID: ctxTenantID
5643
6358
  });
6359
+ return c.json({
6360
+ data: doc,
6361
+ message: "Version restored successfully"
6362
+ });
5644
6363
  return c.json({ data: doc, message: "Restored successfully" });
5645
6364
  } catch (error) {
5646
6365
  return c.json({ error: error.message }, 500);
@@ -5742,6 +6461,6 @@ function createRESTAPI(registry, db, options) {
5742
6461
  });
5743
6462
  }
5744
6463
 
5745
- export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createHonoApp, createLocalStorage, createRESTAPI, getAppSecret, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
5746
- //# sourceMappingURL=chunk-HXRD4B37.js.map
5747
- //# sourceMappingURL=chunk-HXRD4B37.js.map
6464
+ export { AuditLogger, AuthRoutes, InMemoryAuditLogger, InMemoryRateLimiter, MediaService, createAuditContext2 as createAuditContext, createCloudinaryStorage, createFtpStorage, createHonoApp, createLocalStorage, createRESTAPI, createS3Storage, getAppSecret, getDefaultRegistry, getEncryptionKey, getSessionConfig, init_secret, loadSecrets, resolveProvider, setDbAdapter };
6465
+ //# sourceMappingURL=chunk-IPTZM3VE.js.map
6466
+ //# sourceMappingURL=chunk-IPTZM3VE.js.map