qarpeo 0.0.0-alpha.10

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 (599) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/dist/chunks/auth-schema.mjs +219 -0
  4. package/dist/chunks/wrangler.mjs +783 -0
  5. package/dist/cli/index.mjs +1085 -0
  6. package/dist/dev/env.d.ts +12 -0
  7. package/dist/dev/wrangler.d.ts +18 -0
  8. package/dist/module.d.ts +16 -0
  9. package/dist/module.mjs +4436 -0
  10. package/dist/runtime/ai.d.ts +25 -0
  11. package/dist/runtime/ai.mjs +106 -0
  12. package/dist/runtime/analytics.d.ts +24 -0
  13. package/dist/runtime/analytics.mjs +38 -0
  14. package/dist/runtime/app/components/BlobImage.vue +100 -0
  15. package/dist/runtime/app/components/CMSBlocks.vue +93 -0
  16. package/dist/runtime/app/components/CMSContent.vue +23 -0
  17. package/dist/runtime/app/components/CMSImage.vue +49 -0
  18. package/dist/runtime/app/components/CMSLocaleSwitcher.vue +110 -0
  19. package/dist/runtime/app/components/CMSPreviewBanner.vue +79 -0
  20. package/dist/runtime/app/components/CMSVersionBanner.vue +49 -0
  21. package/dist/runtime/app/components/CMSVersionSwitcher.vue +126 -0
  22. package/dist/runtime/app/components/ContentRenderer.vue +80 -0
  23. package/dist/runtime/app/components/FileUpload.vue +196 -0
  24. package/dist/runtime/app/components/ImpersonationBanner.vue +57 -0
  25. package/dist/runtime/app/components/SignedIn.vue +13 -0
  26. package/dist/runtime/app/components/SignedOut.vue +13 -0
  27. package/dist/runtime/app/components/shiki.css +34 -0
  28. package/dist/runtime/app/composables/analytics.d.ts +125 -0
  29. package/dist/runtime/app/composables/analytics.mjs +81 -0
  30. package/dist/runtime/app/composables/api-tokens.d.ts +49 -0
  31. package/dist/runtime/app/composables/api-tokens.mjs +61 -0
  32. package/dist/runtime/app/composables/auth.d.ts +50 -0
  33. package/dist/runtime/app/composables/auth.mjs +101 -0
  34. package/dist/runtime/app/composables/chat.d.ts +69 -0
  35. package/dist/runtime/app/composables/chat.mjs +159 -0
  36. package/dist/runtime/app/composables/cms-preview.d.ts +33 -0
  37. package/dist/runtime/app/composables/cms-preview.mjs +83 -0
  38. package/dist/runtime/app/composables/cms.d.ts +307 -0
  39. package/dist/runtime/app/composables/cms.mjs +355 -0
  40. package/dist/runtime/app/composables/docs-version.d.ts +35 -0
  41. package/dist/runtime/app/composables/docs-version.mjs +70 -0
  42. package/dist/runtime/app/composables/impersonation.d.ts +14 -0
  43. package/dist/runtime/app/composables/impersonation.mjs +44 -0
  44. package/dist/runtime/app/composables/notifications.d.ts +35 -0
  45. package/dist/runtime/app/composables/notifications.mjs +139 -0
  46. package/dist/runtime/app/composables/organization.d.ts +127 -0
  47. package/dist/runtime/app/composables/organization.mjs +138 -0
  48. package/dist/runtime/app/composables/passkey.d.ts +6 -0
  49. package/dist/runtime/app/composables/passkey.mjs +105 -0
  50. package/dist/runtime/app/composables/passwordless.d.ts +7 -0
  51. package/dist/runtime/app/composables/passwordless.mjs +67 -0
  52. package/dist/runtime/app/composables/payments.d.ts +43 -0
  53. package/dist/runtime/app/composables/payments.mjs +74 -0
  54. package/dist/runtime/app/composables/realtime.d.ts +96 -0
  55. package/dist/runtime/app/composables/realtime.mjs +432 -0
  56. package/dist/runtime/app/composables/site.d.ts +35 -0
  57. package/dist/runtime/app/composables/site.mjs +36 -0
  58. package/dist/runtime/app/composables/storage.d.ts +60 -0
  59. package/dist/runtime/app/composables/storage.mjs +294 -0
  60. package/dist/runtime/app/composables/tenant.d.ts +82 -0
  61. package/dist/runtime/app/composables/tenant.mjs +120 -0
  62. package/dist/runtime/app/composables/turnstile.d.ts +3 -0
  63. package/dist/runtime/app/composables/turnstile.mjs +10 -0
  64. package/dist/runtime/app/composables/two-factor.d.ts +12 -0
  65. package/dist/runtime/app/composables/two-factor.mjs +65 -0
  66. package/dist/runtime/app/middleware/auth.global.d.ts +2 -0
  67. package/dist/runtime/app/middleware/auth.global.mjs +44 -0
  68. package/dist/runtime/app/pages/cms.vue +2249 -0
  69. package/dist/runtime/app/pages/devtools/agents.vue +717 -0
  70. package/dist/runtime/app/pages/devtools/analytics.vue +618 -0
  71. package/dist/runtime/app/pages/devtools/auth.vue +351 -0
  72. package/dist/runtime/app/pages/devtools/blob.vue +745 -0
  73. package/dist/runtime/app/pages/devtools/db.vue +546 -0
  74. package/dist/runtime/app/pages/devtools/domains.vue +772 -0
  75. package/dist/runtime/app/pages/devtools/kv.vue +829 -0
  76. package/dist/runtime/app/pages/devtools/logs.vue +655 -0
  77. package/dist/runtime/app/pages/devtools/mail.vue +389 -0
  78. package/dist/runtime/app/pages/devtools/overview.vue +390 -0
  79. package/dist/runtime/app/pages/devtools/settings.vue +560 -0
  80. package/dist/runtime/app/pages/devtools.vue +165 -0
  81. package/dist/runtime/app/plugins/seo.d.ts +2 -0
  82. package/dist/runtime/app/plugins/seo.mjs +105 -0
  83. package/dist/runtime/app/plugins/shortcuts.client.d.ts +6 -0
  84. package/dist/runtime/app/plugins/shortcuts.client.mjs +19 -0
  85. package/dist/runtime/app/public/sw-notifications.mjs +52 -0
  86. package/dist/runtime/auth.d.ts +25 -0
  87. package/dist/runtime/auth.mjs +11 -0
  88. package/dist/runtime/bindings.d.ts +27 -0
  89. package/dist/runtime/bindings.mjs +38 -0
  90. package/dist/runtime/blob.d.ts +26 -0
  91. package/dist/runtime/blob.mjs +334 -0
  92. package/dist/runtime/bots.d.ts +1 -0
  93. package/dist/runtime/bots.mjs +1 -0
  94. package/dist/runtime/browser.d.ts +21 -0
  95. package/dist/runtime/browser.mjs +64 -0
  96. package/dist/runtime/cache.d.ts +19 -0
  97. package/dist/runtime/cache.mjs +12 -0
  98. package/dist/runtime/cms.d.ts +2 -0
  99. package/dist/runtime/cms.mjs +0 -0
  100. package/dist/runtime/db.d.ts +21 -0
  101. package/dist/runtime/db.mjs +36 -0
  102. package/dist/runtime/drizzle.d.ts +11 -0
  103. package/dist/runtime/drizzle.mjs +18 -0
  104. package/dist/runtime/email.d.ts +51 -0
  105. package/dist/runtime/email.mjs +245 -0
  106. package/dist/runtime/hyperdrive.d.ts +21 -0
  107. package/dist/runtime/hyperdrive.mjs +49 -0
  108. package/dist/runtime/index.d.ts +23 -0
  109. package/dist/runtime/index.mjs +23 -0
  110. package/dist/runtime/kv.d.ts +16 -0
  111. package/dist/runtime/kv.mjs +126 -0
  112. package/dist/runtime/payments.d.ts +2 -0
  113. package/dist/runtime/payments.mjs +0 -0
  114. package/dist/runtime/queue.d.ts +21 -0
  115. package/dist/runtime/queue.mjs +41 -0
  116. package/dist/runtime/rag.d.ts +1 -0
  117. package/dist/runtime/rag.mjs +1 -0
  118. package/dist/runtime/ratelimit.d.ts +21 -0
  119. package/dist/runtime/ratelimit.mjs +38 -0
  120. package/dist/runtime/realtime.d.ts +6 -0
  121. package/dist/runtime/realtime.mjs +13 -0
  122. package/dist/runtime/server/db/cms-schema.d.ts +651 -0
  123. package/dist/runtime/server/db/cms-schema.mjs +74 -0
  124. package/dist/runtime/server/durable-objects/agent.d.ts +14 -0
  125. package/dist/runtime/server/durable-objects/agent.mjs +571 -0
  126. package/dist/runtime/server/durable-objects/room.d.ts +34 -0
  127. package/dist/runtime/server/durable-objects/room.mjs +514 -0
  128. package/dist/runtime/server/middleware/auth.d.ts +2 -0
  129. package/dist/runtime/server/middleware/auth.mjs +108 -0
  130. package/dist/runtime/server/middleware/devtools.d.ts +2 -0
  131. package/dist/runtime/server/middleware/devtools.mjs +61 -0
  132. package/dist/runtime/server/middleware/ratelimit.d.ts +2 -0
  133. package/dist/runtime/server/middleware/ratelimit.mjs +49 -0
  134. package/dist/runtime/server/middleware/tenancy.d.ts +6 -0
  135. package/dist/runtime/server/middleware/tenancy.mjs +15 -0
  136. package/dist/runtime/server/middleware/tracker.d.ts +7 -0
  137. package/dist/runtime/server/middleware/tracker.mjs +9 -0
  138. package/dist/runtime/server/plugins/agents.d.ts +7 -0
  139. package/dist/runtime/server/plugins/agents.mjs +3 -0
  140. package/dist/runtime/server/plugins/cms.d.ts +7 -0
  141. package/dist/runtime/server/plugins/cms.mjs +16 -0
  142. package/dist/runtime/server/plugins/dev-logs.d.ts +1 -0
  143. package/dist/runtime/server/plugins/dev-logs.mjs +58 -0
  144. package/dist/runtime/server/plugins/dev-proxy.d.ts +3 -0
  145. package/dist/runtime/server/plugins/dev-proxy.mjs +21 -0
  146. package/dist/runtime/server/plugins/env.d.ts +6 -0
  147. package/dist/runtime/server/plugins/env.mjs +17 -0
  148. package/dist/runtime/server/plugins/queue.d.ts +6 -0
  149. package/dist/runtime/server/plugins/queue.mjs +8 -0
  150. package/dist/runtime/server/routes/__qarpeo/agents/chat.post.d.ts +20 -0
  151. package/dist/runtime/server/routes/__qarpeo/agents/chat.post.mjs +110 -0
  152. package/dist/runtime/server/routes/__qarpeo/agents/clear.post.d.ts +7 -0
  153. package/dist/runtime/server/routes/__qarpeo/agents/clear.post.mjs +15 -0
  154. package/dist/runtime/server/routes/__qarpeo/agents/create.post.d.ts +17 -0
  155. package/dist/runtime/server/routes/__qarpeo/agents/create.post.mjs +47 -0
  156. package/dist/runtime/server/routes/__qarpeo/agents/history.get.d.ts +15 -0
  157. package/dist/runtime/server/routes/__qarpeo/agents/history.get.mjs +24 -0
  158. package/dist/runtime/server/routes/__qarpeo/agents/list.get.d.ts +5 -0
  159. package/dist/runtime/server/routes/__qarpeo/agents/list.get.mjs +63 -0
  160. package/dist/runtime/server/routes/__qarpeo/analytics/events.get.d.ts +64 -0
  161. package/dist/runtime/server/routes/__qarpeo/analytics/events.get.mjs +55 -0
  162. package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.d.ts +30 -0
  163. package/dist/runtime/server/routes/__qarpeo/analytics/summary.get.mjs +73 -0
  164. package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.d.ts +6 -0
  165. package/dist/runtime/server/routes/__qarpeo/auth/revoke-session.post.mjs +26 -0
  166. package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.d.ts +6 -0
  167. package/dist/runtime/server/routes/__qarpeo/auth/sessions.get.mjs +46 -0
  168. package/dist/runtime/server/routes/__qarpeo/auth/users.get.d.ts +7 -0
  169. package/dist/runtime/server/routes/__qarpeo/auth/users.get.mjs +65 -0
  170. package/dist/runtime/server/routes/__qarpeo/blob/files.get.d.ts +22 -0
  171. package/dist/runtime/server/routes/__qarpeo/blob/files.get.mjs +36 -0
  172. package/dist/runtime/server/routes/__qarpeo/db/query.post.d.ts +23 -0
  173. package/dist/runtime/server/routes/__qarpeo/db/query.post.mjs +34 -0
  174. package/dist/runtime/server/routes/__qarpeo/db/tables.get.d.ts +31 -0
  175. package/dist/runtime/server/routes/__qarpeo/db/tables.get.mjs +81 -0
  176. package/dist/runtime/server/routes/__qarpeo/kv/delete.post.d.ts +11 -0
  177. package/dist/runtime/server/routes/__qarpeo/kv/delete.post.mjs +18 -0
  178. package/dist/runtime/server/routes/__qarpeo/kv/get.get.d.ts +11 -0
  179. package/dist/runtime/server/routes/__qarpeo/kv/get.get.mjs +20 -0
  180. package/dist/runtime/server/routes/__qarpeo/kv/keys.get.d.ts +8 -0
  181. package/dist/runtime/server/routes/__qarpeo/kv/keys.get.mjs +27 -0
  182. package/dist/runtime/server/routes/__qarpeo/kv/set.post.d.ts +14 -0
  183. package/dist/runtime/server/routes/__qarpeo/kv/set.post.mjs +24 -0
  184. package/dist/runtime/server/routes/__qarpeo/logs.delete.d.ts +5 -0
  185. package/dist/runtime/server/routes/__qarpeo/logs.delete.mjs +6 -0
  186. package/dist/runtime/server/routes/__qarpeo/logs.get.d.ts +5 -0
  187. package/dist/runtime/server/routes/__qarpeo/logs.get.mjs +26 -0
  188. package/dist/runtime/server/routes/__qarpeo/logs.post.d.ts +6 -0
  189. package/dist/runtime/server/routes/__qarpeo/logs.post.mjs +17 -0
  190. package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.d.ts +5 -0
  191. package/dist/runtime/server/routes/__qarpeo/mail/bounce.post.mjs +37 -0
  192. package/dist/runtime/server/routes/__qarpeo/mail/clear.post.d.ts +4 -0
  193. package/dist/runtime/server/routes/__qarpeo/mail/clear.post.mjs +6 -0
  194. package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.d.ts +5 -0
  195. package/dist/runtime/server/routes/__qarpeo/mail/inbox.get.mjs +7 -0
  196. package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.d.ts +4 -0
  197. package/dist/runtime/server/routes/__qarpeo/mail/send-test.post.mjs +29 -0
  198. package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.d.ts +2 -0
  199. package/dist/runtime/server/routes/__qarpeo/mail/track/click/[id].get.mjs +23 -0
  200. package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.d.ts +2 -0
  201. package/dist/runtime/server/routes/__qarpeo/mail/track/open/[id].get.mjs +31 -0
  202. package/dist/runtime/server/routes/__qarpeo/overview/health.get.d.ts +8 -0
  203. package/dist/runtime/server/routes/__qarpeo/overview/health.get.mjs +46 -0
  204. package/dist/runtime/server/routes/__qarpeo/overview.get.d.ts +32 -0
  205. package/dist/runtime/server/routes/__qarpeo/overview.get.mjs +38 -0
  206. package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.d.ts +11 -0
  207. package/dist/runtime/server/routes/__qarpeo/settings/reset-dev-data.post.mjs +84 -0
  208. package/dist/runtime/server/routes/__qarpeo/settings.get.d.ts +124 -0
  209. package/dist/runtime/server/routes/__qarpeo/settings.get.mjs +145 -0
  210. package/dist/runtime/server/routes/__qarpeo/tenants/create.post.d.ts +7 -0
  211. package/dist/runtime/server/routes/__qarpeo/tenants/create.post.mjs +26 -0
  212. package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.d.ts +6 -0
  213. package/dist/runtime/server/routes/__qarpeo/tenants/domain-add.post.mjs +29 -0
  214. package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.d.ts +7 -0
  215. package/dist/runtime/server/routes/__qarpeo/tenants/domain-delete.post.mjs +21 -0
  216. package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.d.ts +9 -0
  217. package/dist/runtime/server/routes/__qarpeo/tenants/domain-verify.post.mjs +21 -0
  218. package/dist/runtime/server/routes/__qarpeo/tenants/list.get.d.ts +15 -0
  219. package/dist/runtime/server/routes/__qarpeo/tenants/list.get.mjs +22 -0
  220. package/dist/runtime/server/routes/agents/[agent]/chat.post.d.ts +2 -0
  221. package/dist/runtime/server/routes/agents/[agent]/chat.post.mjs +35 -0
  222. package/dist/runtime/server/routes/agents/[agent]/clear.post.d.ts +4 -0
  223. package/dist/runtime/server/routes/agents/[agent]/clear.post.mjs +27 -0
  224. package/dist/runtime/server/routes/agents/[agent]/history.get.d.ts +4 -0
  225. package/dist/runtime/server/routes/agents/[agent]/history.get.mjs +27 -0
  226. package/dist/runtime/server/routes/analytics/events.get.d.ts +3 -0
  227. package/dist/runtime/server/routes/analytics/events.get.mjs +17 -0
  228. package/dist/runtime/server/routes/analytics/pageviews.get.d.ts +3 -0
  229. package/dist/runtime/server/routes/analytics/pageviews.get.mjs +15 -0
  230. package/dist/runtime/server/routes/analytics/summary.get.d.ts +6 -0
  231. package/dist/runtime/server/routes/analytics/summary.get.mjs +15 -0
  232. package/dist/runtime/server/routes/atom.xml.get.d.ts +2 -0
  233. package/dist/runtime/server/routes/atom.xml.get.mjs +76 -0
  234. package/dist/runtime/server/routes/auth/2fa/disable.post.d.ts +4 -0
  235. package/dist/runtime/server/routes/auth/2fa/disable.post.mjs +44 -0
  236. package/dist/runtime/server/routes/auth/2fa/setup.post.d.ts +6 -0
  237. package/dist/runtime/server/routes/auth/2fa/setup.post.mjs +32 -0
  238. package/dist/runtime/server/routes/auth/2fa/verify.post.d.ts +6 -0
  239. package/dist/runtime/server/routes/auth/2fa/verify.post.mjs +92 -0
  240. package/dist/runtime/server/routes/auth/[provider]/callback.d.ts +1 -0
  241. package/dist/runtime/server/routes/auth/[provider]/callback.mjs +1 -0
  242. package/dist/runtime/server/routes/auth/[provider].d.ts +3 -0
  243. package/dist/runtime/server/routes/auth/[provider].mjs +95 -0
  244. package/dist/runtime/server/routes/auth/account.delete.d.ts +5 -0
  245. package/dist/runtime/server/routes/auth/account.delete.mjs +52 -0
  246. package/dist/runtime/server/routes/auth/impersonate/start.post.d.ts +5 -0
  247. package/dist/runtime/server/routes/auth/impersonate/start.post.mjs +14 -0
  248. package/dist/runtime/server/routes/auth/impersonate/stop.post.d.ts +5 -0
  249. package/dist/runtime/server/routes/auth/impersonate/stop.post.mjs +5 -0
  250. package/dist/runtime/server/routes/auth/logout-all.post.d.ts +4 -0
  251. package/dist/runtime/server/routes/auth/logout-all.post.mjs +16 -0
  252. package/dist/runtime/server/routes/auth/logout-others.post.d.ts +4 -0
  253. package/dist/runtime/server/routes/auth/logout-others.post.mjs +11 -0
  254. package/dist/runtime/server/routes/auth/logout.post.d.ts +4 -0
  255. package/dist/runtime/server/routes/auth/logout.post.mjs +17 -0
  256. package/dist/runtime/server/routes/auth/magic-link/send.post.d.ts +10 -0
  257. package/dist/runtime/server/routes/auth/magic-link/send.post.mjs +29 -0
  258. package/dist/runtime/server/routes/auth/magic-link/verify.get.d.ts +2 -0
  259. package/dist/runtime/server/routes/auth/magic-link/verify.get.mjs +81 -0
  260. package/dist/runtime/server/routes/auth/organization/create.post.d.ts +5 -0
  261. package/dist/runtime/server/routes/auth/organization/create.post.mjs +10 -0
  262. package/dist/runtime/server/routes/auth/organization/invite.post.d.ts +9 -0
  263. package/dist/runtime/server/routes/auth/organization/invite.post.mjs +27 -0
  264. package/dist/runtime/server/routes/auth/organization/join.post.d.ts +6 -0
  265. package/dist/runtime/server/routes/auth/organization/join.post.mjs +15 -0
  266. package/dist/runtime/server/routes/auth/organization/list.get.d.ts +5 -0
  267. package/dist/runtime/server/routes/auth/organization/list.get.mjs +7 -0
  268. package/dist/runtime/server/routes/auth/organization/members/[id].delete.d.ts +4 -0
  269. package/dist/runtime/server/routes/auth/organization/members/[id].delete.mjs +33 -0
  270. package/dist/runtime/server/routes/auth/organization/members/index.get.d.ts +6 -0
  271. package/dist/runtime/server/routes/auth/organization/members/index.get.mjs +18 -0
  272. package/dist/runtime/server/routes/auth/organization/mfa.post.d.ts +6 -0
  273. package/dist/runtime/server/routes/auth/organization/mfa.post.mjs +33 -0
  274. package/dist/runtime/server/routes/auth/organization/sso/[id].delete.d.ts +4 -0
  275. package/dist/runtime/server/routes/auth/organization/sso/[id].delete.mjs +28 -0
  276. package/dist/runtime/server/routes/auth/organization/sso/index.get.d.ts +2 -0
  277. package/dist/runtime/server/routes/auth/organization/sso/index.get.mjs +23 -0
  278. package/dist/runtime/server/routes/auth/organization/sso/index.post.d.ts +5 -0
  279. package/dist/runtime/server/routes/auth/organization/sso/index.post.mjs +84 -0
  280. package/dist/runtime/server/routes/auth/organization/switch.post.d.ts +6 -0
  281. package/dist/runtime/server/routes/auth/organization/switch.post.mjs +14 -0
  282. package/dist/runtime/server/routes/auth/otp/send.post.d.ts +10 -0
  283. package/dist/runtime/server/routes/auth/otp/send.post.mjs +29 -0
  284. package/dist/runtime/server/routes/auth/otp/verify.post.d.ts +6 -0
  285. package/dist/runtime/server/routes/auth/otp/verify.post.mjs +95 -0
  286. package/dist/runtime/server/routes/auth/sessions/[id].delete.d.ts +5 -0
  287. package/dist/runtime/server/routes/auth/sessions/[id].delete.mjs +24 -0
  288. package/dist/runtime/server/routes/auth/sessions.get.d.ts +2 -0
  289. package/dist/runtime/server/routes/auth/sessions.get.mjs +11 -0
  290. package/dist/runtime/server/routes/auth/sso/login.get.d.ts +2 -0
  291. package/dist/runtime/server/routes/auth/sso/login.get.mjs +94 -0
  292. package/dist/runtime/server/routes/auth/sso/oidc/callback.get.d.ts +2 -0
  293. package/dist/runtime/server/routes/auth/sso/oidc/callback.get.mjs +113 -0
  294. package/dist/runtime/server/routes/auth/sso/saml/acs.post.d.ts +2 -0
  295. package/dist/runtime/server/routes/auth/sso/saml/acs.post.mjs +95 -0
  296. package/dist/runtime/server/routes/auth/tokens/[id].delete.d.ts +4 -0
  297. package/dist/runtime/server/routes/auth/tokens/[id].delete.mjs +13 -0
  298. package/dist/runtime/server/routes/auth/tokens/create.post.d.ts +9 -0
  299. package/dist/runtime/server/routes/auth/tokens/create.post.mjs +17 -0
  300. package/dist/runtime/server/routes/auth/tokens/index.get.d.ts +9 -0
  301. package/dist/runtime/server/routes/auth/tokens/index.get.mjs +5 -0
  302. package/dist/runtime/server/routes/auth/turnstile/verify.post.d.ts +2 -0
  303. package/dist/runtime/server/routes/auth/turnstile/verify.post.mjs +15 -0
  304. package/dist/runtime/server/routes/auth/user.get.d.ts +2 -0
  305. package/dist/runtime/server/routes/auth/user.get.mjs +6 -0
  306. package/dist/runtime/server/routes/auth/webauthn/login/options.post.d.ts +2 -0
  307. package/dist/runtime/server/routes/auth/webauthn/login/options.post.mjs +17 -0
  308. package/dist/runtime/server/routes/auth/webauthn/login/verify.post.d.ts +6 -0
  309. package/dist/runtime/server/routes/auth/webauthn/login/verify.post.mjs +127 -0
  310. package/dist/runtime/server/routes/auth/webauthn/register/options.post.d.ts +2 -0
  311. package/dist/runtime/server/routes/auth/webauthn/register/options.post.mjs +47 -0
  312. package/dist/runtime/server/routes/auth/webauthn/register/verify.post.d.ts +5 -0
  313. package/dist/runtime/server/routes/auth/webauthn/register/verify.post.mjs +88 -0
  314. package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.d.ts +8 -0
  315. package/dist/runtime/server/routes/cms/[collection]/[id]/preview-token.post.mjs +27 -0
  316. package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.d.ts +6 -0
  317. package/dist/runtime/server/routes/cms/[collection]/[id]/publish.post.mjs +19 -0
  318. package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.d.ts +6 -0
  319. package/dist/runtime/server/routes/cms/[collection]/[id]/review.post.mjs +33 -0
  320. package/dist/runtime/server/routes/cms/[collection]/[id].delete.d.ts +4 -0
  321. package/dist/runtime/server/routes/cms/[collection]/[id].delete.mjs +17 -0
  322. package/dist/runtime/server/routes/cms/[collection]/[id].put.d.ts +4 -0
  323. package/dist/runtime/server/routes/cms/[collection]/[id].put.mjs +18 -0
  324. package/dist/runtime/server/routes/cms/[collection]/[slug].get.d.ts +4 -0
  325. package/dist/runtime/server/routes/cms/[collection]/[slug].get.mjs +19 -0
  326. package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.d.ts +2 -0
  327. package/dist/runtime/server/routes/cms/[collection]/atom.xml.get.mjs +69 -0
  328. package/dist/runtime/server/routes/cms/[collection]/by-path.get.d.ts +8 -0
  329. package/dist/runtime/server/routes/cms/[collection]/by-path.get.mjs +23 -0
  330. package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.d.ts +2 -0
  331. package/dist/runtime/server/routes/cms/[collection]/feed.xml.get.mjs +65 -0
  332. package/dist/runtime/server/routes/cms/[collection]/index.get.d.ts +7 -0
  333. package/dist/runtime/server/routes/cms/[collection]/index.get.mjs +15 -0
  334. package/dist/runtime/server/routes/cms/[collection]/index.post.d.ts +4 -0
  335. package/dist/runtime/server/routes/cms/[collection]/index.post.mjs +23 -0
  336. package/dist/runtime/server/routes/cms/[collection]/references.get.d.ts +19 -0
  337. package/dist/runtime/server/routes/cms/[collection]/references.get.mjs +47 -0
  338. package/dist/runtime/server/routes/cms/[collection]/surroundings.get.d.ts +10 -0
  339. package/dist/runtime/server/routes/cms/[collection]/surroundings.get.mjs +12 -0
  340. package/dist/runtime/server/routes/cms/collections/[id].delete.d.ts +4 -0
  341. package/dist/runtime/server/routes/cms/collections/[id].delete.mjs +10 -0
  342. package/dist/runtime/server/routes/cms/collections/[id].put.d.ts +4 -0
  343. package/dist/runtime/server/routes/cms/collections/[id].put.mjs +11 -0
  344. package/dist/runtime/server/routes/cms/collections/index.get.d.ts +4 -0
  345. package/dist/runtime/server/routes/cms/collections/index.get.mjs +6 -0
  346. package/dist/runtime/server/routes/cms/collections/index.post.d.ts +4 -0
  347. package/dist/runtime/server/routes/cms/collections/index.post.mjs +19 -0
  348. package/dist/runtime/server/routes/cms/components/index.get.d.ts +17 -0
  349. package/dist/runtime/server/routes/cms/components/index.get.mjs +16 -0
  350. package/dist/runtime/server/routes/cms/deploy.post.d.ts +4 -0
  351. package/dist/runtime/server/routes/cms/deploy.post.mjs +46 -0
  352. package/dist/runtime/server/routes/cms/export.post.d.ts +10 -0
  353. package/dist/runtime/server/routes/cms/export.post.mjs +55 -0
  354. package/dist/runtime/server/routes/cms/navigation.get.d.ts +8 -0
  355. package/dist/runtime/server/routes/cms/navigation.get.mjs +36 -0
  356. package/dist/runtime/server/routes/cms/preview/disable.get.d.ts +9 -0
  357. package/dist/runtime/server/routes/cms/preview/disable.get.mjs +9 -0
  358. package/dist/runtime/server/routes/cms/preview/enable.get.d.ts +14 -0
  359. package/dist/runtime/server/routes/cms/preview/enable.get.mjs +42 -0
  360. package/dist/runtime/server/routes/cms/publish-scheduled.post.d.ts +11 -0
  361. package/dist/runtime/server/routes/cms/publish-scheduled.post.mjs +35 -0
  362. package/dist/runtime/server/routes/cms/search.get.d.ts +13 -0
  363. package/dist/runtime/server/routes/cms/search.get.mjs +137 -0
  364. package/dist/runtime/server/routes/cms/sync.post.d.ts +7 -0
  365. package/dist/runtime/server/routes/cms/sync.post.mjs +18 -0
  366. package/dist/runtime/server/routes/cms/webhooks/[provider].post.d.ts +150 -0
  367. package/dist/runtime/server/routes/cms/webhooks/[provider].post.mjs +213 -0
  368. package/dist/runtime/server/routes/docs/search.get.d.ts +15 -0
  369. package/dist/runtime/server/routes/docs/search.get.mjs +20 -0
  370. package/dist/runtime/server/routes/feed.xml.get.d.ts +2 -0
  371. package/dist/runtime/server/routes/feed.xml.get.mjs +73 -0
  372. package/dist/runtime/server/routes/mail/bounce.post.d.ts +14 -0
  373. package/dist/runtime/server/routes/mail/bounce.post.mjs +56 -0
  374. package/dist/runtime/server/routes/mail/inbox/[id].get.d.ts +2 -0
  375. package/dist/runtime/server/routes/mail/inbox/[id].get.mjs +9 -0
  376. package/dist/runtime/server/routes/mail/inbox.get.d.ts +4 -0
  377. package/dist/runtime/server/routes/mail/inbox.get.mjs +7 -0
  378. package/dist/runtime/server/routes/mail/suppressions/[email].delete.d.ts +11 -0
  379. package/dist/runtime/server/routes/mail/suppressions/[email].delete.mjs +18 -0
  380. package/dist/runtime/server/routes/mail/suppressions/index.get.d.ts +12 -0
  381. package/dist/runtime/server/routes/mail/suppressions/index.get.mjs +23 -0
  382. package/dist/runtime/server/routes/mail/suppressions/index.post.d.ts +10 -0
  383. package/dist/runtime/server/routes/mail/suppressions/index.post.mjs +23 -0
  384. package/dist/runtime/server/routes/mail/unsubscribe.d.ts +13 -0
  385. package/dist/runtime/server/routes/mail/unsubscribe.mjs +91 -0
  386. package/dist/runtime/server/routes/mail/webhooks.post.d.ts +12 -0
  387. package/dist/runtime/server/routes/mail/webhooks.post.mjs +71 -0
  388. package/dist/runtime/server/routes/notifications/[id]/read.post.d.ts +4 -0
  389. package/dist/runtime/server/routes/notifications/[id]/read.post.mjs +13 -0
  390. package/dist/runtime/server/routes/notifications/[id].delete.d.ts +4 -0
  391. package/dist/runtime/server/routes/notifications/[id].delete.mjs +13 -0
  392. package/dist/runtime/server/routes/notifications/[id].get.d.ts +2 -0
  393. package/dist/runtime/server/routes/notifications/[id].get.mjs +15 -0
  394. package/dist/runtime/server/routes/notifications/index.get.d.ts +6 -0
  395. package/dist/runtime/server/routes/notifications/index.get.mjs +18 -0
  396. package/dist/runtime/server/routes/notifications/push/subscribe.post.d.ts +4 -0
  397. package/dist/runtime/server/routes/notifications/push/subscribe.post.mjs +20 -0
  398. package/dist/runtime/server/routes/notifications/push/unsubscribe.post.d.ts +4 -0
  399. package/dist/runtime/server/routes/notifications/push/unsubscribe.post.mjs +15 -0
  400. package/dist/runtime/server/routes/notifications/read-all.post.d.ts +4 -0
  401. package/dist/runtime/server/routes/notifications/read-all.post.mjs +11 -0
  402. package/dist/runtime/server/routes/payments/cancel.post.d.ts +7 -0
  403. package/dist/runtime/server/routes/payments/cancel.post.mjs +9 -0
  404. package/dist/runtime/server/routes/payments/change-plan.post.d.ts +7 -0
  405. package/dist/runtime/server/routes/payments/change-plan.post.mjs +12 -0
  406. package/dist/runtime/server/routes/payments/checkout.post.d.ts +9 -0
  407. package/dist/runtime/server/routes/payments/checkout.post.mjs +16 -0
  408. package/dist/runtime/server/routes/payments/pause.post.d.ts +7 -0
  409. package/dist/runtime/server/routes/payments/pause.post.mjs +9 -0
  410. package/dist/runtime/server/routes/payments/portal.post.d.ts +5 -0
  411. package/dist/runtime/server/routes/payments/portal.post.mjs +9 -0
  412. package/dist/runtime/server/routes/payments/subscription.get.d.ts +6 -0
  413. package/dist/runtime/server/routes/payments/subscription.get.mjs +9 -0
  414. package/dist/runtime/server/routes/payments/usage.get.d.ts +12 -0
  415. package/dist/runtime/server/routes/payments/usage.get.mjs +18 -0
  416. package/dist/runtime/server/routes/payments/usage.post.d.ts +6 -0
  417. package/dist/runtime/server/routes/payments/usage.post.mjs +24 -0
  418. package/dist/runtime/server/routes/payments/webhook.post.d.ts +14 -0
  419. package/dist/runtime/server/routes/payments/webhook.post.mjs +21 -0
  420. package/dist/runtime/server/routes/realtime/broadcast.post.d.ts +4 -0
  421. package/dist/runtime/server/routes/realtime/broadcast.post.mjs +26 -0
  422. package/dist/runtime/server/routes/realtime/history.delete.d.ts +13 -0
  423. package/dist/runtime/server/routes/realtime/history.delete.mjs +12 -0
  424. package/dist/runtime/server/routes/realtime/history.get.d.ts +16 -0
  425. package/dist/runtime/server/routes/realtime/history.get.mjs +15 -0
  426. package/dist/runtime/server/routes/realtime/presence.get.d.ts +6 -0
  427. package/dist/runtime/server/routes/realtime/presence.get.mjs +27 -0
  428. package/dist/runtime/server/routes/realtime/state.get.d.ts +5 -0
  429. package/dist/runtime/server/routes/realtime/state.get.mjs +26 -0
  430. package/dist/runtime/server/routes/realtime/state.post.d.ts +6 -0
  431. package/dist/runtime/server/routes/realtime/state.post.mjs +37 -0
  432. package/dist/runtime/server/routes/realtime/ws.get.d.ts +2 -0
  433. package/dist/runtime/server/routes/realtime/ws.get.mjs +73 -0
  434. package/dist/runtime/server/routes/robots.txt.get.d.ts +2 -0
  435. package/dist/runtime/server/routes/robots.txt.get.mjs +41 -0
  436. package/dist/runtime/server/routes/rss.xml.get.d.ts +2 -0
  437. package/dist/runtime/server/routes/rss.xml.get.mjs +2 -0
  438. package/dist/runtime/server/routes/sitemap.xml.get.d.ts +2 -0
  439. package/dist/runtime/server/routes/sitemap.xml.get.mjs +84 -0
  440. package/dist/runtime/server/routes/storage/file/[...key].delete.d.ts +4 -0
  441. package/dist/runtime/server/routes/storage/file/[...key].delete.mjs +23 -0
  442. package/dist/runtime/server/routes/storage/file/[...key].get.d.ts +2 -0
  443. package/dist/runtime/server/routes/storage/file/[...key].get.mjs +23 -0
  444. package/dist/runtime/server/routes/storage/files.get.d.ts +14 -0
  445. package/dist/runtime/server/routes/storage/files.get.mjs +11 -0
  446. package/dist/runtime/server/routes/storage/presigned.post.d.ts +8 -0
  447. package/dist/runtime/server/routes/storage/presigned.post.mjs +39 -0
  448. package/dist/runtime/server/routes/storage/sign.post.d.ts +6 -0
  449. package/dist/runtime/server/routes/storage/sign.post.mjs +24 -0
  450. package/dist/runtime/server/routes/storage/upload.post.d.ts +22 -0
  451. package/dist/runtime/server/routes/storage/upload.post.mjs +96 -0
  452. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.d.ts +8 -0
  453. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/index.delete.mjs +39 -0
  454. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.d.ts +8 -0
  455. package/dist/runtime/server/routes/tenants/[tenant]/domains/[domain]/verify.post.mjs +39 -0
  456. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.d.ts +8 -0
  457. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.get.mjs +20 -0
  458. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.d.ts +8 -0
  459. package/dist/runtime/server/routes/tenants/[tenant]/domains/index.post.mjs +44 -0
  460. package/dist/runtime/server/routes/tenants/current.get.d.ts +8 -0
  461. package/dist/runtime/server/routes/tenants/current.get.mjs +6 -0
  462. package/dist/runtime/server/tasks/cms/publish.d.ts +4 -0
  463. package/dist/runtime/server/tasks/cms/publish.mjs +18 -0
  464. package/dist/runtime/server/utils/agent-store.d.ts +24 -0
  465. package/dist/runtime/server/utils/agent-store.mjs +53 -0
  466. package/dist/runtime/server/utils/agents-registry-fallback.d.ts +1 -0
  467. package/dist/runtime/server/utils/agents-registry-fallback.mjs +1 -0
  468. package/dist/runtime/server/utils/ai.d.ts +119 -0
  469. package/dist/runtime/server/utils/ai.mjs +607 -0
  470. package/dist/runtime/server/utils/analytics-query.d.ts +109 -0
  471. package/dist/runtime/server/utils/analytics-query.mjs +234 -0
  472. package/dist/runtime/server/utils/auth.d.ts +60 -0
  473. package/dist/runtime/server/utils/auth.mjs +361 -0
  474. package/dist/runtime/server/utils/blob-store.d.ts +13 -0
  475. package/dist/runtime/server/utils/blob-store.mjs +60 -0
  476. package/dist/runtime/server/utils/bots.d.ts +21 -0
  477. package/dist/runtime/server/utils/bots.mjs +491 -0
  478. package/dist/runtime/server/utils/cache.d.ts +33 -0
  479. package/dist/runtime/server/utils/cache.mjs +128 -0
  480. package/dist/runtime/server/utils/cms-events.d.ts +17 -0
  481. package/dist/runtime/server/utils/cms-events.mjs +80 -0
  482. package/dist/runtime/server/utils/cms-filter.d.ts +3 -0
  483. package/dist/runtime/server/utils/cms-filter.mjs +34 -0
  484. package/dist/runtime/server/utils/cms.d.ts +278 -0
  485. package/dist/runtime/server/utils/cms.mjs +3268 -0
  486. package/dist/runtime/server/utils/config.d.ts +27 -0
  487. package/dist/runtime/server/utils/config.mjs +38 -0
  488. package/dist/runtime/server/utils/context.d.ts +13 -0
  489. package/dist/runtime/server/utils/context.mjs +98 -0
  490. package/dist/runtime/server/utils/crypto.d.ts +16 -0
  491. package/dist/runtime/server/utils/crypto.mjs +56 -0
  492. package/dist/runtime/server/utils/dev-logs.d.ts +21 -0
  493. package/dist/runtime/server/utils/dev-logs.mjs +41 -0
  494. package/dist/runtime/server/utils/dev-proxy.d.ts +1 -0
  495. package/dist/runtime/server/utils/dev-proxy.mjs +69 -0
  496. package/dist/runtime/server/utils/domains.d.ts +46 -0
  497. package/dist/runtime/server/utils/domains.mjs +318 -0
  498. package/dist/runtime/server/utils/drizzle.d.ts +10 -0
  499. package/dist/runtime/server/utils/drizzle.mjs +36 -0
  500. package/dist/runtime/server/utils/email-templates.d.ts +44 -0
  501. package/dist/runtime/server/utils/email-templates.mjs +373 -0
  502. package/dist/runtime/server/utils/feed.d.ts +42 -0
  503. package/dist/runtime/server/utils/feed.mjs +161 -0
  504. package/dist/runtime/server/utils/impersonation.d.ts +67 -0
  505. package/dist/runtime/server/utils/impersonation.mjs +298 -0
  506. package/dist/runtime/server/utils/jobs.d.ts +70 -0
  507. package/dist/runtime/server/utils/jobs.mjs +125 -0
  508. package/dist/runtime/server/utils/kv-store.d.ts +9 -0
  509. package/dist/runtime/server/utils/kv-store.mjs +32 -0
  510. package/dist/runtime/server/utils/mail-suppression.d.ts +62 -0
  511. package/dist/runtime/server/utils/mail-suppression.mjs +328 -0
  512. package/dist/runtime/server/utils/mail.d.ts +90 -0
  513. package/dist/runtime/server/utils/mail.mjs +366 -0
  514. package/dist/runtime/server/utils/notifications.d.ts +51 -0
  515. package/dist/runtime/server/utils/notifications.mjs +215 -0
  516. package/dist/runtime/server/utils/organization.d.ts +54 -0
  517. package/dist/runtime/server/utils/organization.mjs +354 -0
  518. package/dist/runtime/server/utils/passwordless.d.ts +48 -0
  519. package/dist/runtime/server/utils/passwordless.mjs +179 -0
  520. package/dist/runtime/server/utils/payments.d.ts +121 -0
  521. package/dist/runtime/server/utils/payments.mjs +1067 -0
  522. package/dist/runtime/server/utils/queue-fallback.d.ts +2 -0
  523. package/dist/runtime/server/utils/queue-fallback.mjs +2 -0
  524. package/dist/runtime/server/utils/queue.d.ts +4 -0
  525. package/dist/runtime/server/utils/queue.mjs +3 -0
  526. package/dist/runtime/server/utils/rag.d.ts +25 -0
  527. package/dist/runtime/server/utils/rag.mjs +333 -0
  528. package/dist/runtime/server/utils/ratelimit.d.ts +45 -0
  529. package/dist/runtime/server/utils/ratelimit.mjs +182 -0
  530. package/dist/runtime/server/utils/realtime.d.ts +110 -0
  531. package/dist/runtime/server/utils/realtime.mjs +197 -0
  532. package/dist/runtime/server/utils/schema-fallback.d.ts +3 -0
  533. package/dist/runtime/server/utils/schema-fallback.mjs +2 -0
  534. package/dist/runtime/server/utils/session.d.ts +72 -0
  535. package/dist/runtime/server/utils/session.mjs +287 -0
  536. package/dist/runtime/server/utils/sso.d.ts +49 -0
  537. package/dist/runtime/server/utils/sso.mjs +131 -0
  538. package/dist/runtime/server/utils/storage.d.ts +111 -0
  539. package/dist/runtime/server/utils/storage.mjs +654 -0
  540. package/dist/runtime/server/utils/tenant.d.ts +44 -0
  541. package/dist/runtime/server/utils/tenant.mjs +285 -0
  542. package/dist/runtime/server/utils/totp.d.ts +33 -0
  543. package/dist/runtime/server/utils/totp.mjs +112 -0
  544. package/dist/runtime/server/utils/tracker.d.ts +72 -0
  545. package/dist/runtime/server/utils/tracker.mjs +139 -0
  546. package/dist/runtime/server/utils/turnstile.d.ts +17 -0
  547. package/dist/runtime/server/utils/turnstile.mjs +68 -0
  548. package/dist/runtime/server/utils/vapid.d.ts +35 -0
  549. package/dist/runtime/server/utils/vapid.mjs +211 -0
  550. package/dist/runtime/storage.d.ts +1 -0
  551. package/dist/runtime/storage.mjs +0 -0
  552. package/dist/runtime/types/ai.d.ts +68 -0
  553. package/dist/runtime/types/ai.mjs +0 -0
  554. package/dist/runtime/types/analytics.d.ts +30 -0
  555. package/dist/runtime/types/analytics.mjs +0 -0
  556. package/dist/runtime/types/auth-globals.d.ts +14 -0
  557. package/dist/runtime/types/auth.d.ts +153 -0
  558. package/dist/runtime/types/auth.mjs +0 -0
  559. package/dist/runtime/types/browser.d.ts +29 -0
  560. package/dist/runtime/types/browser.mjs +0 -0
  561. package/dist/runtime/types/d1.d.ts +44 -0
  562. package/dist/runtime/types/d1.mjs +0 -0
  563. package/dist/runtime/types/drizzle.d.ts +13 -0
  564. package/dist/runtime/types/drizzle.mjs +0 -0
  565. package/dist/runtime/types/durable-object.d.ts +51 -0
  566. package/dist/runtime/types/durable-object.mjs +0 -0
  567. package/dist/runtime/types/email.d.ts +66 -0
  568. package/dist/runtime/types/email.mjs +0 -0
  569. package/dist/runtime/types/hyperdrive.d.ts +18 -0
  570. package/dist/runtime/types/hyperdrive.mjs +0 -0
  571. package/dist/runtime/types/imports.d.ts +58 -0
  572. package/dist/runtime/types/index.d.ts +156 -0
  573. package/dist/runtime/types/index.mjs +13 -0
  574. package/dist/runtime/types/queue.d.ts +67 -0
  575. package/dist/runtime/types/queue.mjs +0 -0
  576. package/dist/runtime/types/r2.d.ts +108 -0
  577. package/dist/runtime/types/r2.mjs +0 -0
  578. package/dist/runtime/types/ratelimit.d.ts +21 -0
  579. package/dist/runtime/types/ratelimit.mjs +0 -0
  580. package/dist/runtime/types/vectorize.d.ts +74 -0
  581. package/dist/runtime/types/vectorize.mjs +0 -0
  582. package/dist/runtime/vectorize.d.ts +21 -0
  583. package/dist/runtime/vectorize.mjs +53 -0
  584. package/dist/types.d.ts +2750 -0
  585. package/dist/types.mjs +1 -0
  586. package/dist/utils/agents.d.ts +8 -0
  587. package/dist/utils/auth-schema.d.ts +11 -0
  588. package/dist/utils/bindings.d.ts +16 -0
  589. package/dist/utils/cms-schema.d.ts +1 -0
  590. package/dist/utils/cms-sources.d.ts +62 -0
  591. package/dist/utils/drizzle.d.ts +42 -0
  592. package/dist/utils/jobs.d.ts +8 -0
  593. package/dist/utils/local-db.d.ts +28 -0
  594. package/dist/utils/notifications-schema.d.ts +15 -0
  595. package/dist/utils/payments-schema.d.ts +11 -0
  596. package/dist/utils/pkg.d.ts +15 -0
  597. package/dist/utils/queue.d.ts +8 -0
  598. package/dist/utils/tenants-schema.d.ts +11 -0
  599. package/package.json +104 -0
@@ -0,0 +1,3268 @@
1
+ import { parseMarkdown as comarkParseMarkdown } from "comark";
2
+ import { render as comarkRender } from "comark/render";
3
+ import { createError, useRuntimeConfig, getCookie } from "nuxt/server";
4
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
5
+ import { getD1Binding } from "../../db.mjs";
6
+ import { getKVBinding } from "../../kv.mjs";
7
+ import { fireCMSWebhooks, fireCMSDeployTrigger } from "./cms-events.mjs";
8
+ import { getQarpeoConfig } from "./config.mjs";
9
+ import { getAuthContext } from "./context.mjs";
10
+ function getCMSDb(event) {
11
+ return getD1Binding("DB", event);
12
+ }
13
+ function nanoid(size = 21) {
14
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
15
+ const bytes = crypto.getRandomValues(new Uint8Array(size));
16
+ return Array.from(bytes, (b) => chars[b % chars.length]).join("");
17
+ }
18
+ export function slugifyHeading(text) {
19
+ return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/^-+|-+$/g, "");
20
+ }
21
+ export function stripOrderPrefix(segment) {
22
+ return segment.replace(/^\d+\./, "");
23
+ }
24
+ export function isGroupSegment(segment) {
25
+ const stripped = stripOrderPrefix(segment);
26
+ return /^\([^)]+\)$/.test(stripped);
27
+ }
28
+ export function extractGroupTitle(segment) {
29
+ const stripped = stripOrderPrefix(segment);
30
+ const inner = stripped.replace(/^\(|\)$/g, "");
31
+ return inner.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
32
+ }
33
+ export function extractOrderPrefix(segment) {
34
+ const m = segment.match(/^(\d+)\./);
35
+ return m ? parseInt(m[1], 10) : void 0;
36
+ }
37
+ export function computeEntryPath(filePathRelative, collectionId) {
38
+ const normalized = filePathRelative.replace(/\\/g, "/").replace(/\.md$/, "");
39
+ const segments = normalized.split("/").filter(Boolean);
40
+ const cleaned = segments.filter((seg) => !isGroupSegment(seg)).map(stripOrderPrefix).filter(Boolean);
41
+ if (cleaned[cleaned.length - 1] === "index") {
42
+ cleaned.pop();
43
+ }
44
+ if (collectionId && collectionId !== "content" && cleaned[0] !== collectionId) {
45
+ cleaned.unshift(collectionId);
46
+ }
47
+ if (cleaned.length === 0) return "/";
48
+ return "/" + cleaned.join("/");
49
+ }
50
+ export function matchPathPatterns(entryPath, patterns) {
51
+ if (!patterns) return false;
52
+ const list = Array.isArray(patterns) ? patterns : [patterns];
53
+ if (!list.length) return false;
54
+ const path = ("/" + entryPath.replace(/\\/g, "/").replace(/^\/+/, "")).replace(/\/+$/, "") || "/";
55
+ let matched = false;
56
+ for (const raw of list) {
57
+ const isNeg = raw.startsWith("!");
58
+ const rawPattern = isNeg ? raw.slice(1) : raw;
59
+ const pattern = ("/" + rawPattern.replace(/\\/g, "/").replace(/^\/+/, "")).replace(/\/+$/, "") || "/";
60
+ let isMatch = false;
61
+ if (pattern.endsWith("/**")) {
62
+ const base = pattern.slice(0, -3) || "/";
63
+ isMatch = path === base || path.startsWith(base + "/");
64
+ } else if (pattern.includes("*") || pattern.includes("?")) {
65
+ const regexStr = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, (m) => "\\" + m).replace(/\*\*/g, ".*").replace(/(?<!\.)\*/g, "[^/]*").replace(/\?/g, "[^/]") + "$";
66
+ isMatch = new RegExp(regexStr).test(path);
67
+ } else {
68
+ isMatch = path === pattern;
69
+ }
70
+ if (isMatch) {
71
+ if (isNeg) return false;
72
+ matched = true;
73
+ }
74
+ }
75
+ return matched;
76
+ }
77
+ export function resolveCollectionForPath(entryPath, collectionsConfig, fallbackId = "content") {
78
+ if (collectionsConfig) {
79
+ for (const [colId, colConfig] of Object.entries(collectionsConfig)) {
80
+ if (colConfig.path && matchPathPatterns(entryPath, colConfig.path)) {
81
+ return colId;
82
+ }
83
+ }
84
+ }
85
+ return fallbackId;
86
+ }
87
+ export function extractTOC(md) {
88
+ const lines = md.split("\n");
89
+ const flat = [];
90
+ for (const line of lines) {
91
+ const m = line.match(/^(#{2,6})\s+(.+)/);
92
+ if (!m) continue;
93
+ const depth = m[1].length;
94
+ const rawText = m[2].trim().replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[`*_~]/g, "");
95
+ flat.push({ id: slugifyHeading(rawText), text: rawText, depth });
96
+ }
97
+ const result = [];
98
+ let current2 = null;
99
+ for (const item of flat) {
100
+ if (item.depth === 2) {
101
+ current2 = { ...item, children: [] };
102
+ result.push(current2);
103
+ } else if (current2 && item.depth >= 3) {
104
+ current2.children = current2.children || [];
105
+ current2.children.push(item);
106
+ } else {
107
+ result.push(item);
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ function parseInlineProps(propsStr) {
113
+ const inner = propsStr.replace(/^\{|\}$/g, "");
114
+ const result = {};
115
+ const re = /(\w+)="([^"]*)"/g;
116
+ let m;
117
+ while ((m = re.exec(inner)) !== null) {
118
+ result[m[1]] = m[2];
119
+ }
120
+ return result;
121
+ }
122
+ function escapeAttr(val) {
123
+ return val.replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
124
+ }
125
+ export function preprocessMDC(md) {
126
+ let out = md;
127
+ out = out.replace(
128
+ /:::(\w[\w-]*)(\{[^}]*\})?\s*\n([\s\S]*?):::/g,
129
+ (_match, type, propsStr, body) => {
130
+ const attrs = parseInlineProps(propsStr || "");
131
+ const cls = `mdc-${type}`;
132
+ const attrStr = Object.entries(attrs).map(([k, v]) => ` data-${k}="${escapeAttr(v)}"`).join("");
133
+ return `<div class="${cls}"${attrStr}>
134
+ ${body.trim()}
135
+ </div>`;
136
+ }
137
+ );
138
+ out = out.replace(/::(\w[\w-]*)\s*\n([\s\S]*?)::/g, (_match, type, body) => {
139
+ return `<div class="mdc-${type}">
140
+ ${body.trim()}
141
+ </div>`;
142
+ });
143
+ return out;
144
+ }
145
+ export function getCMSConfig(event) {
146
+ const rc = useRuntimeConfig(event);
147
+ const cfg = rc?.qarpeo?.cms || {};
148
+ return {
149
+ admin: cfg.admin !== false,
150
+ path: cfg.path || "/cms",
151
+ apiPath: cfg.apiPath || "/api/cms",
152
+ contentDir: cfg.contentDir || "content",
153
+ hybrid: cfg.hybrid !== false,
154
+ roles: cfg.roles || ["admin", "editor"],
155
+ revisions: cfg.revisions || { max: 10 },
156
+ cache: cfg.cache !== false,
157
+ collections: cfg.collections || {},
158
+ components: cfg.components || {},
159
+ sources: cfg.sources || [],
160
+ exclude: cfg.exclude || void 0,
161
+ github: cfg.github || void 0,
162
+ deploy: cfg.deploy || void 0,
163
+ webhooks: cfg.webhooks || [],
164
+ webhookSecret: cfg.webhookSecret || void 0,
165
+ approval: cfg.approval || void 0,
166
+ scheduledPublish: cfg.scheduledPublish ?? false,
167
+ shiki: cfg.shiki !== void 0 ? cfg.shiki : true,
168
+ plugins: cfg.plugins || {},
169
+ locales: cfg.locales || ["en"],
170
+ defaultLocale: cfg.defaultLocale || "en"
171
+ };
172
+ }
173
+ export function resolveCollectionApproval(collectionConfig, globalApproval) {
174
+ const raw = collectionConfig?.approval ?? globalApproval;
175
+ if (!raw) return null;
176
+ if (typeof raw === "object") {
177
+ return {
178
+ roles: raw.roles && raw.roles.length > 0 ? raw.roles : ["admin"],
179
+ allowSelfApproval: raw.allowSelfApproval ?? false
180
+ };
181
+ }
182
+ return {
183
+ roles: ["admin"],
184
+ allowSelfApproval: false
185
+ };
186
+ }
187
+ const _cachedPluginsMap = /* @__PURE__ */ new Map();
188
+ async function _resolveMarkdownPlugins(event) {
189
+ const rc = useRuntimeConfig(event);
190
+ const pluginCfg = getQarpeoConfig(event).cms?.plugins || rc?.public?.qarpeo?.cms?.plugins || {};
191
+ const cacheKey = JSON.stringify(pluginCfg);
192
+ if (_cachedPluginsMap.has(cacheKey)) return _cachedPluginsMap.get(cacheKey);
193
+ const plugins = [];
194
+ if (pluginCfg.shiki !== false) {
195
+ try {
196
+ const shikiMod = await import("comark/plugins/shiki");
197
+ const shikiPlugin = shikiMod.default;
198
+ const shikiOptions = typeof pluginCfg.shiki === "object" ? pluginCfg.shiki : {};
199
+ plugins.push(shikiPlugin(shikiOptions));
200
+ } catch {
201
+ }
202
+ }
203
+ _cachedPluginsMap.set(cacheKey, plugins);
204
+ return plugins;
205
+ }
206
+ export async function parseMarkdownAST(md, event) {
207
+ if (!md) return { nodes: [], frontmatter: {}, meta: {} };
208
+ try {
209
+ const plugins = await _resolveMarkdownPlugins(event);
210
+ return await comarkParseMarkdown(md, { plugins });
211
+ } catch (err) {
212
+ console.error("[qarpeo:cms] parseMarkdownAST failed:", err);
213
+ return { nodes: [], frontmatter: {}, meta: {} };
214
+ }
215
+ }
216
+ export async function processMarkdown(md, event) {
217
+ if (!md) return { ast: { nodes: [], frontmatter: {}, meta: {} }, html: "" };
218
+ try {
219
+ const plugins = await _resolveMarkdownPlugins(event);
220
+ const ast = await comarkParseMarkdown(md, { plugins });
221
+ const html = (await comarkRender(ast, { format: "text/html" })).trim();
222
+ return { ast, html };
223
+ } catch {
224
+ return { ast: { nodes: [], frontmatter: {}, meta: {} }, html: md };
225
+ }
226
+ }
227
+ export async function renderMarkdown(md, event) {
228
+ if (!md) return "";
229
+ try {
230
+ const res = await processMarkdown(md, event);
231
+ return res.html;
232
+ } catch {
233
+ return md;
234
+ }
235
+ }
236
+ export async function renderPageMarkdown(md) {
237
+ return renderMarkdown(md);
238
+ }
239
+ export function parseFrontmatter(raw) {
240
+ const clean = raw ? raw.replace(/^\uFEFF/, "") : "";
241
+ const fmMatch = clean.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)/);
242
+ if (!fmMatch) {
243
+ return { data: {}, content: clean };
244
+ }
245
+ const yamlBlock = fmMatch[1] || "";
246
+ const body = fmMatch[2] || "";
247
+ let data = {};
248
+ try {
249
+ const parsed = parseYaml(yamlBlock);
250
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
251
+ data = parsed;
252
+ }
253
+ } catch (err) {
254
+ console.warn("[qarpeo:cms] Warning: failed to parse YAML frontmatter:", err);
255
+ }
256
+ return { data, content: body };
257
+ }
258
+ export function formatFrontmatter(data, body) {
259
+ const cleanData = {};
260
+ for (const [k, v] of Object.entries(data)) {
261
+ if (v === void 0 || v === null || k.startsWith("_")) continue;
262
+ cleanData[k] = v;
263
+ }
264
+ const yamlStr = Object.keys(cleanData).length > 0 ? stringifyYaml(cleanData).trim() : "";
265
+ if (!yamlStr) {
266
+ return body;
267
+ }
268
+ return `---
269
+ ${yamlStr}
270
+ ---
271
+
272
+ ${body}`;
273
+ }
274
+ export function formatEntryAsMarkdown(entry) {
275
+ const e = entry;
276
+ let metadata = {};
277
+ try {
278
+ if (e.metadata && typeof e.metadata === "object") {
279
+ metadata = e.metadata;
280
+ } else if (e.metadataJson || e.metadata_json) {
281
+ metadata = JSON.parse(e.metadataJson || e.metadata_json);
282
+ }
283
+ } catch {
284
+ }
285
+ const cleanMetadata = {};
286
+ for (const [k, v] of Object.entries(metadata)) {
287
+ if (!k.startsWith("_")) {
288
+ cleanMetadata[k] = v;
289
+ }
290
+ }
291
+ const serializedMetadata = serializeReferencesForFrontmatter(cleanMetadata);
292
+ const rawTags = e.tags;
293
+ const tags = Array.isArray(rawTags) ? rawTags : typeof rawTags === "string" && rawTags.length > 0 ? rawTags.split(",").map((t) => t.trim()) : void 0;
294
+ const fmData = {
295
+ title: e.title,
296
+ slug: e.slug,
297
+ ...e.description ? { description: e.description } : {},
298
+ ...e.coverImage || e.cover_image ? { coverImage: e.coverImage || e.cover_image } : {},
299
+ ...tags && tags.length > 0 ? { tags } : {},
300
+ ...typeof e.order === "number" && e.order !== 0 ? { order: e.order } : {},
301
+ ...e.status && e.status !== "published" ? { status: e.status } : {},
302
+ ...serializedMetadata
303
+ };
304
+ const body = e.contentMd ?? e.content_md ?? "";
305
+ return formatFrontmatter(fmData, body);
306
+ }
307
+ function toBase64(str) {
308
+ if (typeof Buffer !== "undefined") {
309
+ return Buffer.from(str, "utf8").toString("base64");
310
+ }
311
+ const bytes = new TextEncoder().encode(str);
312
+ let binary = "";
313
+ for (let i = 0; i < bytes.byteLength; i++) {
314
+ binary += String.fromCharCode(bytes[i]);
315
+ }
316
+ return btoa(binary);
317
+ }
318
+ export async function syncEntryToGit(event, collectionId, entry, action) {
319
+ const config = getCMSConfig(event);
320
+ const colConfig = config.collections[collectionId] || {};
321
+ const mode = colConfig.mode || config.mode || "db";
322
+ if (mode !== "git-sync") {
323
+ return null;
324
+ }
325
+ const result = {};
326
+ const e = entry;
327
+ let rawFilePath = e.filePath || e.file_path;
328
+ if (!rawFilePath) {
329
+ const targetDir = colConfig.directory || `${config.contentDir}/${collectionId}`;
330
+ rawFilePath = `${targetDir}/${e.slug}.md`;
331
+ }
332
+ let resolvedFsPath = rawFilePath;
333
+ let fileContent = "";
334
+ try {
335
+ const fs = await import("node:fs/promises");
336
+ const pathMod = await import("node:path");
337
+ resolvedFsPath = pathMod.isAbsolute(rawFilePath) ? rawFilePath : pathMod.resolve(process.cwd(), rawFilePath);
338
+ if (action === "delete") {
339
+ const exists = await fs.stat(resolvedFsPath).catch(() => null);
340
+ if (exists) {
341
+ await fs.unlink(resolvedFsPath);
342
+ result.localFs = true;
343
+ }
344
+ } else {
345
+ fileContent = formatEntryAsMarkdown(entry);
346
+ await fs.mkdir(pathMod.dirname(resolvedFsPath), { recursive: true });
347
+ await fs.writeFile(resolvedFsPath, fileContent, "utf8");
348
+ result.localFs = true;
349
+ const contentHash = await computeContentHash(fileContent);
350
+ result.filePath = resolvedFsPath;
351
+ result.contentHash = contentHash;
352
+ if (e.id) {
353
+ const db = getCMSDb(event);
354
+ await db.prepare(
355
+ "UPDATE _qarpeo_cms_entries SET file_path = ?, content_hash = ?, source = 'file' WHERE id = ?"
356
+ ).bind(resolvedFsPath, contentHash, e.id).run();
357
+ }
358
+ }
359
+ } catch (err) {
360
+ }
361
+ const ghConfig = {
362
+ ...config.github || {},
363
+ ...colConfig.github || {}
364
+ };
365
+ const repo = ghConfig.repo || (typeof process !== "undefined" ? process.env.GITHUB_REPOSITORY : void 0);
366
+ const token = ghConfig.token || (typeof process !== "undefined" ? process.env.GITHUB_TOKEN || process.env.GH_TOKEN : void 0);
367
+ if (repo && token) {
368
+ try {
369
+ const [owner, repoName] = repo.split("/");
370
+ if (!owner || !repoName) {
371
+ console.warn(`[qarpeo:cms] Invalid GitHub repo format: "${repo}". Expected "owner/repo".`);
372
+ } else {
373
+ let gitPath = resolvedFsPath.replace(/\\/g, "/");
374
+ const match = gitPath.match(/(?:^|[\\/])((?:content|docs)[\\/].*)$/i);
375
+ if (match) {
376
+ gitPath = match[1];
377
+ } else if (colConfig.directory) {
378
+ gitPath = `${colConfig.directory}/${entry.slug}.md`;
379
+ } else {
380
+ gitPath = `${config.contentDir}/${collectionId}/${entry.slug}.md`;
381
+ }
382
+ gitPath = gitPath.replace(/\\/g, "/").replace(/^\/+/, "");
383
+ const baseBranch = ghConfig.branch || "main";
384
+ const actionVerb = action === "create" ? "create" : action === "delete" ? "delete" : "update";
385
+ const template = ghConfig.commitMessage || "content({collection}): {action} {slug}";
386
+ const commitMsg = template.replace("{collection}", collectionId).replace("{slug}", entry.slug).replace("{action}", actionVerb);
387
+ const headers = {
388
+ Accept: "application/vnd.github.v3+json",
389
+ Authorization: `Bearer ${token}`,
390
+ "User-Agent": "qarpeo-cms",
391
+ "Content-Type": "application/json"
392
+ };
393
+ let existingSha;
394
+ try {
395
+ const checkRes = await fetch(
396
+ `https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}?ref=${baseBranch}`,
397
+ { headers }
398
+ );
399
+ if (checkRes.ok) {
400
+ const checkData = await checkRes.json();
401
+ existingSha = checkData.sha;
402
+ }
403
+ } catch {
404
+ }
405
+ let targetBranch = baseBranch;
406
+ let prUrl;
407
+ if (ghConfig.pr) {
408
+ try {
409
+ const refRes = await fetch(
410
+ `https://api.github.com/repos/${owner}/${repoName}/git/ref/heads/${ghConfig.prBase || baseBranch}`,
411
+ { headers }
412
+ );
413
+ if (refRes.ok) {
414
+ const refData = await refRes.json();
415
+ const baseSha = refData.object?.sha;
416
+ const newBranch = `${ghConfig.branchPrefix || "content/"}${collectionId}-${entry.slug}-${Date.now().toString(36)}`;
417
+ const createBranchRes = await fetch(
418
+ `https://api.github.com/repos/${owner}/${repoName}/git/refs`,
419
+ {
420
+ method: "POST",
421
+ headers,
422
+ body: JSON.stringify({
423
+ ref: `refs/heads/${newBranch}`,
424
+ sha: baseSha
425
+ })
426
+ }
427
+ );
428
+ if (createBranchRes.ok) {
429
+ targetBranch = newBranch;
430
+ }
431
+ }
432
+ } catch {
433
+ }
434
+ }
435
+ let commitSha;
436
+ if (action === "delete") {
437
+ if (existingSha) {
438
+ const delRes = await fetch(
439
+ `https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}`,
440
+ {
441
+ method: "DELETE",
442
+ headers,
443
+ body: JSON.stringify({
444
+ message: commitMsg,
445
+ sha: existingSha,
446
+ branch: targetBranch
447
+ })
448
+ }
449
+ );
450
+ if (delRes.ok) {
451
+ const delData = await delRes.json();
452
+ commitSha = delData.commit?.sha;
453
+ }
454
+ }
455
+ } else {
456
+ if (!fileContent) {
457
+ fileContent = formatEntryAsMarkdown(entry);
458
+ }
459
+ const base64Content = toBase64(fileContent);
460
+ const putRes = await fetch(
461
+ `https://api.github.com/repos/${owner}/${repoName}/contents/${gitPath}`,
462
+ {
463
+ method: "PUT",
464
+ headers,
465
+ body: JSON.stringify({
466
+ message: commitMsg,
467
+ content: base64Content,
468
+ branch: targetBranch,
469
+ ...existingSha ? { sha: existingSha } : {}
470
+ })
471
+ }
472
+ );
473
+ if (putRes.ok) {
474
+ const putData = await putRes.json();
475
+ commitSha = putData.commit?.sha;
476
+ } else {
477
+ const errData = await putRes.json().catch(() => ({}));
478
+ console.warn(
479
+ `[qarpeo:cms] GitHub commit failed (${putRes.status}):`,
480
+ errData?.message || putRes.statusText
481
+ );
482
+ }
483
+ }
484
+ if (ghConfig.pr && targetBranch !== baseBranch) {
485
+ try {
486
+ const prRes = await fetch(`https://api.github.com/repos/${owner}/${repoName}/pulls`, {
487
+ method: "POST",
488
+ headers,
489
+ body: JSON.stringify({
490
+ title: commitMsg,
491
+ head: targetBranch,
492
+ base: ghConfig.prBase || baseBranch,
493
+ body: `Automated content change from Qarpeo CMS for \`${collectionId}/${entry.slug}\`.`
494
+ })
495
+ });
496
+ if (prRes.ok) {
497
+ const prData = await prRes.json();
498
+ prUrl = prData.html_url;
499
+ }
500
+ } catch {
501
+ }
502
+ }
503
+ result.github = {
504
+ ok: true,
505
+ commitSha,
506
+ prUrl
507
+ };
508
+ }
509
+ } catch (err) {
510
+ console.warn(`[qarpeo:cms] GitHub sync error:`, err?.message || err);
511
+ result.github = {
512
+ ok: false,
513
+ error: err?.message || String(err)
514
+ };
515
+ }
516
+ }
517
+ return result;
518
+ }
519
+ export async function computeContentHash(text) {
520
+ const enc = new TextEncoder();
521
+ const buf = await crypto.subtle.digest("SHA-256", enc.encode(text));
522
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
523
+ }
524
+ export function getCMSTenantId(event) {
525
+ const tenantId = event?.context?.tenant?.id || event?.context?.tenantId || event?.context?.organizationId || event?.context?.orgId || null;
526
+ return tenantId ? String(tenantId) : null;
527
+ }
528
+ export function getCMSTenantPrefix(event) {
529
+ const tenantId = getCMSTenantId(event);
530
+ return tenantId ? `${tenantId}:` : "";
531
+ }
532
+ export async function getCMSCollection(event, collectionId) {
533
+ const config = getCMSConfig(event);
534
+ const tenantPrefix = getCMSTenantPrefix(event);
535
+ if (config.cache) {
536
+ try {
537
+ const kv = getKVBinding(event);
538
+ const cached = await kv.get(`${tenantPrefix}cms:schema:${collectionId}`, {
539
+ type: "json"
540
+ });
541
+ if (cached) return cached;
542
+ } catch {
543
+ }
544
+ }
545
+ const db = getCMSDb(event);
546
+ const row = await db.prepare("SELECT * FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
547
+ if (!row) return null;
548
+ const collection = rowToCollection(row);
549
+ if (config.cache) {
550
+ try {
551
+ const kv = getKVBinding(event);
552
+ await kv.put(`${tenantPrefix}cms:schema:${collectionId}`, JSON.stringify(collection), {
553
+ expirationTtl: 300
554
+ });
555
+ } catch {
556
+ }
557
+ }
558
+ return collection;
559
+ }
560
+ function rowToCollection(row, config) {
561
+ let fields;
562
+ try {
563
+ fields = JSON.parse(row.schema_json || "{}");
564
+ } catch {
565
+ fields = {};
566
+ }
567
+ const id = row.id;
568
+ const resolvedConfig = config || getCMSConfig();
569
+ const colCfg = resolvedConfig.collections?.[id];
570
+ return {
571
+ id,
572
+ name: row.name,
573
+ description: row.description,
574
+ schemaJson: row.schema_json,
575
+ fields,
576
+ source: row.source,
577
+ order: row.order ?? 0,
578
+ singleton: colCfg?.singleton,
579
+ tenantId: row.tenant_id || void 0,
580
+ createdAt: row.created_at,
581
+ updatedAt: row.updated_at
582
+ };
583
+ }
584
+ export async function listCMSCollections(event) {
585
+ await ensureCMSSynced(event);
586
+ const db = getCMSDb(event);
587
+ const results = await db.prepare('SELECT * FROM _qarpeo_cms_collections ORDER BY "order" ASC, name ASC').all();
588
+ return (results.results || []).map(
589
+ (r) => rowToCollection(r)
590
+ );
591
+ }
592
+ export async function createCMSCollection(event, data) {
593
+ const db = getCMSDb(event);
594
+ const now = Date.now();
595
+ const schemaJson = JSON.stringify(data.fields || {});
596
+ await db.prepare(
597
+ "INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, created_at, updated_at) VALUES (?, ?, ?, ?, 'db', ?, ?)"
598
+ ).bind(data.id, data.name, data.description ?? null, schemaJson, now, now).run();
599
+ return {
600
+ id: data.id,
601
+ name: data.name,
602
+ description: data.description ?? null,
603
+ schemaJson,
604
+ fields: data.fields,
605
+ source: "db",
606
+ createdAt: now,
607
+ updatedAt: now
608
+ };
609
+ }
610
+ export async function updateCMSCollection(event, id, data) {
611
+ const db = getCMSDb(event);
612
+ const now = Date.now();
613
+ const schemaJson = JSON.stringify(data.fields || {});
614
+ await db.prepare(
615
+ "UPDATE _qarpeo_cms_collections SET name = COALESCE(?, name), description = COALESCE(?, description), schema_json = ?, updated_at = ? WHERE id = ?"
616
+ ).bind(data.name ?? null, data.description ?? null, schemaJson, now, id).run();
617
+ try {
618
+ const kv = getKVBinding(event);
619
+ const tenantPrefix = getCMSTenantPrefix(event);
620
+ await kv.delete(`${tenantPrefix}cms:schema:${id}`);
621
+ } catch {
622
+ }
623
+ }
624
+ export async function deleteCMSCollection(event, id) {
625
+ const db = getCMSDb(event);
626
+ const row = await db.prepare("SELECT source FROM _qarpeo_cms_collections WHERE id = ?").bind(id).first();
627
+ if (!row) throw createError({ statusCode: 404, message: `Collection '${id}' not found` });
628
+ if (row.source === "config") {
629
+ throw createError({
630
+ statusCode: 403,
631
+ message: "Cannot delete a config-seeded collection from the API. Remove it from nuxt.config.ts instead."
632
+ });
633
+ }
634
+ await db.prepare("DELETE FROM _qarpeo_cms_collections WHERE id = ?").bind(id).run();
635
+ try {
636
+ const kv = getKVBinding(event);
637
+ const tenantPrefix = getCMSTenantPrefix(event);
638
+ await kv.delete(`${tenantPrefix}cms:schema:${id}`);
639
+ } catch {
640
+ }
641
+ }
642
+ function rowToEntry(row) {
643
+ let metadata;
644
+ try {
645
+ metadata = JSON.parse(row.metadata_json || "{}");
646
+ } catch {
647
+ metadata = {};
648
+ }
649
+ const toc = metadata?._toc || void 0;
650
+ const nav = metadata?._nav || void 0;
651
+ const tocObj = toc || { links: [] };
652
+ const ast = metadata?._ast || void 0;
653
+ if (metadata) {
654
+ delete metadata._ast;
655
+ delete metadata._toc;
656
+ delete metadata._nav;
657
+ }
658
+ const body = ast && Array.isArray(ast.nodes) ? {
659
+ ...ast,
660
+ toc: tocObj
661
+ } : {
662
+ value: row.content_md || "",
663
+ content: row.content_md || "",
664
+ toc: tocObj
665
+ };
666
+ return {
667
+ ...metadata,
668
+ id: row.id,
669
+ collectionId: row.collection_id,
670
+ slug: row.slug,
671
+ path: row.path || void 0,
672
+ title: row.title,
673
+ description: row.description,
674
+ coverImage: row.cover_image,
675
+ tags: row.tags,
676
+ order: row.order,
677
+ contentMd: row.content_md,
678
+ contentHtml: row.content_html,
679
+ body,
680
+ toc: toc || { links: [] },
681
+ navigation: nav,
682
+ metadataJson: row.metadata_json,
683
+ metadata,
684
+ status: row.status,
685
+ source: row.source,
686
+ filePath: row.file_path,
687
+ contentHash: row.content_hash,
688
+ authorId: row.author_id,
689
+ publishedAt: row.published_at,
690
+ tenantId: row.tenant_id || void 0,
691
+ locale: row.locale || "en",
692
+ version: row.version || "latest",
693
+ createdAt: row.created_at,
694
+ updatedAt: row.updated_at
695
+ };
696
+ }
697
+ function isReferenceValue(v) {
698
+ return !!v && typeof v === "object" && !Array.isArray(v) && typeof v.collectionId === "string" && typeof v.slug === "string";
699
+ }
700
+ export async function getCollectionFields(event, collectionId) {
701
+ const config = getCMSConfig(event);
702
+ const configFields = config.collections?.[collectionId]?.fields;
703
+ let dbFields;
704
+ try {
705
+ const db = getCMSDb(event);
706
+ const row = await db.prepare("SELECT schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
707
+ if (row?.schema_json) {
708
+ dbFields = JSON.parse(row.schema_json);
709
+ }
710
+ } catch {
711
+ }
712
+ return { ...configFields || {}, ...dbFields || {} };
713
+ }
714
+ async function fetchEntryRef(event, collectionId, slugOrId) {
715
+ const db = getCMSDb(event);
716
+ const tenantId = getCMSTenantId(event);
717
+ const row = await db.prepare(
718
+ `SELECT id, collection_id, slug, title FROM _qarpeo_cms_entries
719
+ WHERE collection_id = ? AND (slug = ? OR id = ?)
720
+ ${tenantId ? "AND (tenant_id = ? OR tenant_id IS NULL)" : ""}
721
+ LIMIT 1`
722
+ ).bind(
723
+ ...tenantId ? [collectionId, slugOrId, slugOrId, tenantId] : [collectionId, slugOrId, slugOrId]
724
+ ).first();
725
+ if (!row) return null;
726
+ return {
727
+ id: row.id,
728
+ collectionId: row.collection_id,
729
+ slug: row.slug,
730
+ title: row.title
731
+ };
732
+ }
733
+ async function resolveReferenceInput(event, input, defaultTargetCollectionId, filter, fieldName) {
734
+ if (input == null || input === "") {
735
+ throw createError({ statusCode: 400, message: "Reference value is required" });
736
+ }
737
+ let collectionId;
738
+ let slugOrId;
739
+ if (typeof input === "string") {
740
+ const trimmed = input.trim();
741
+ if (!trimmed) {
742
+ throw createError({ statusCode: 400, message: "Reference value is required" });
743
+ }
744
+ const slashIdx = trimmed.indexOf("/");
745
+ if (slashIdx > 0) {
746
+ collectionId = trimmed.slice(0, slashIdx);
747
+ slugOrId = trimmed.slice(slashIdx + 1);
748
+ } else {
749
+ collectionId = defaultTargetCollectionId;
750
+ slugOrId = trimmed;
751
+ }
752
+ } else if (isReferenceValue(input)) {
753
+ collectionId = input.collectionId;
754
+ slugOrId = input.id || input.slug;
755
+ } else {
756
+ throw createError({
757
+ statusCode: 400,
758
+ message: 'Invalid reference value: expected an id, "collection/slug", or a reference object'
759
+ });
760
+ }
761
+ if (!collectionId || !slugOrId) {
762
+ throw createError({
763
+ statusCode: 400,
764
+ message: "Reference value is missing collection or slug/id"
765
+ });
766
+ }
767
+ const resolved = await fetchEntryRef(event, collectionId, slugOrId);
768
+ if (!resolved) {
769
+ throw createError({
770
+ statusCode: 400,
771
+ message: `Referenced entry "${slugOrId}" not found in collection "${collectionId}"`
772
+ });
773
+ }
774
+ if (filter && Object.keys(filter).length > 0) {
775
+ const fullEntry = await getCMSEntry(event, collectionId, resolved.id, { preview: true });
776
+ if (fullEntry) {
777
+ for (const [key, expected] of Object.entries(filter)) {
778
+ const actual = fullEntry.metadata?.[key];
779
+ if (actual !== expected) {
780
+ const label = fieldName ? `reference field "${fieldName}"` : "reference";
781
+ throw createError({
782
+ statusCode: 400,
783
+ message: `The ${label} target "${resolved.slug}" does not match the required filter (${key} must be ${String(expected)})`
784
+ });
785
+ }
786
+ }
787
+ }
788
+ }
789
+ return resolved;
790
+ }
791
+ async function validateAndNormalizeReferenceFields(event, collectionId, metadata) {
792
+ const fields = await getCollectionFields(event, collectionId);
793
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
794
+ if (fieldDef.type !== "reference" || !fieldDef.reference) continue;
795
+ const refOpts = fieldDef.reference;
796
+ const multiple = refOpts.multiple === true;
797
+ const value = metadata[fieldName];
798
+ if (value == null || value === "") {
799
+ if (fieldDef.required) {
800
+ throw createError({
801
+ statusCode: 400,
802
+ message: `Reference field "${fieldName}" is required`
803
+ });
804
+ }
805
+ if (multiple) metadata[fieldName] = [];
806
+ continue;
807
+ }
808
+ if (multiple) {
809
+ const items = Array.isArray(value) ? value : [value];
810
+ const resolved = [];
811
+ for (const item of items) {
812
+ if (item == null || item === "") continue;
813
+ resolved.push(
814
+ await resolveReferenceInput(event, item, refOpts.to, refOpts.filter, fieldName)
815
+ );
816
+ }
817
+ metadata[fieldName] = resolved;
818
+ } else {
819
+ metadata[fieldName] = await resolveReferenceInput(
820
+ event,
821
+ value,
822
+ refOpts.to,
823
+ refOpts.filter,
824
+ fieldName
825
+ );
826
+ }
827
+ }
828
+ return metadata;
829
+ }
830
+ function resolveComponentDefinition(fieldDef, config) {
831
+ if (fieldDef.type !== "component") return null;
832
+ const compOpts = fieldDef.component;
833
+ if (!compOpts) return null;
834
+ if (typeof compOpts === "string") {
835
+ return config.components?.[compOpts] ?? null;
836
+ }
837
+ if (compOpts.name) {
838
+ return config.components?.[compOpts.name] ?? null;
839
+ }
840
+ if (compOpts.fields) {
841
+ return { fields: compOpts.fields };
842
+ }
843
+ return null;
844
+ }
845
+ async function validateAndNormalizeComponentFields(event, collectionId, metadata) {
846
+ const config = getCMSConfig(event);
847
+ const fields = await getCollectionFields(event, collectionId);
848
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
849
+ if (fieldDef.type !== "component") continue;
850
+ const compDef = resolveComponentDefinition(fieldDef, config);
851
+ if (!compDef) continue;
852
+ const compOpts = typeof fieldDef.component === "object" ? fieldDef.component : null;
853
+ const isRepeatable = fieldDef.repeatable || (compOpts?.repeatable ?? false);
854
+ const value = metadata[fieldName];
855
+ if (value == null || value === "") {
856
+ if (fieldDef.required) {
857
+ throw createError({
858
+ statusCode: 400,
859
+ message: `Component field "${fieldName}" is required`
860
+ });
861
+ }
862
+ if (isRepeatable) metadata[fieldName] = [];
863
+ continue;
864
+ }
865
+ if (isRepeatable && !Array.isArray(value)) {
866
+ metadata[fieldName] = [value];
867
+ }
868
+ if (isRepeatable && compOpts?.max != null && Array.isArray(metadata[fieldName])) {
869
+ const arr = metadata[fieldName];
870
+ if (arr.length > compOpts.max) {
871
+ throw createError({
872
+ statusCode: 400,
873
+ message: `Component field "${fieldName}" exceeds maximum of ${compOpts.max} items`
874
+ });
875
+ }
876
+ }
877
+ if (isRepeatable && compOpts?.min != null && Array.isArray(metadata[fieldName])) {
878
+ const arr = metadata[fieldName];
879
+ if (arr.length < compOpts.min) {
880
+ throw createError({
881
+ statusCode: 400,
882
+ message: `Component field "${fieldName}" requires at least ${compOpts.min} items`
883
+ });
884
+ }
885
+ }
886
+ }
887
+ return metadata;
888
+ }
889
+ export function defineCMSComponent(def) {
890
+ return def;
891
+ }
892
+ export async function resolveReference(event, ref) {
893
+ if (!ref || !ref.id && !ref.slug) return null;
894
+ const collectionId = ref.collectionId;
895
+ if (!collectionId) return null;
896
+ const slugOrId = ref.id || ref.slug;
897
+ if (!slugOrId) return null;
898
+ return getCMSEntry(event, collectionId, slugOrId);
899
+ }
900
+ async function populateReferenceFields(event, entry, populate) {
901
+ if (!entry.metadata || Object.keys(entry.metadata).length === 0) return entry;
902
+ const fields = await getCollectionFields(event, entry.collectionId);
903
+ const populatedMeta = { ...entry.metadata };
904
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
905
+ if (fieldDef.type !== "reference") continue;
906
+ if (Array.isArray(populate) && !populate.includes(fieldName)) continue;
907
+ const value = populatedMeta[fieldName];
908
+ if (value == null) continue;
909
+ if (Array.isArray(value)) {
910
+ populatedMeta[fieldName] = await Promise.all(
911
+ value.filter(isReferenceValue).map(async (r) => ({
912
+ ...r,
913
+ entry: await resolveReference(event, r)
914
+ }))
915
+ );
916
+ } else if (isReferenceValue(value)) {
917
+ populatedMeta[fieldName] = { ...value, entry: await resolveReference(event, value) };
918
+ }
919
+ }
920
+ return { ...entry, metadata: populatedMeta };
921
+ }
922
+ const REFERENCE_PATH_RE = /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+$/;
923
+ function serializeReferencesForFrontmatter(metadata) {
924
+ const out = {};
925
+ for (const [k, v] of Object.entries(metadata)) {
926
+ if (isReferenceValue(v)) {
927
+ out[k] = `${v.collectionId}/${v.slug}`;
928
+ } else if (Array.isArray(v) && v.length > 0 && v.every((item) => isReferenceValue(item))) {
929
+ out[k] = v.map(
930
+ (item) => `${item.collectionId}/${item.slug}`
931
+ );
932
+ } else {
933
+ out[k] = v;
934
+ }
935
+ }
936
+ return out;
937
+ }
938
+ async function resolveReferenceFieldsFromFrontmatter(event, collectionId, fm) {
939
+ const fields = await getCollectionFields(event, collectionId);
940
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
941
+ if (fieldDef.type !== "reference" || !fieldDef.reference) continue;
942
+ const refOpts = fieldDef.reference;
943
+ const multiple = refOpts.multiple === true;
944
+ const value = fm[fieldName];
945
+ if (value == null) continue;
946
+ const resolveString = async (str) => {
947
+ if (!REFERENCE_PATH_RE.test(str)) return null;
948
+ const slashIdx = str.indexOf("/");
949
+ const refCollectionId = str.slice(0, slashIdx);
950
+ const refSlug = str.slice(slashIdx + 1);
951
+ return fetchEntryRef(event, refCollectionId, refSlug);
952
+ };
953
+ if (multiple) {
954
+ const items = Array.isArray(value) ? value : [value];
955
+ const resolved = [];
956
+ for (const item of items) {
957
+ if (typeof item === "string") {
958
+ const r = await resolveString(item);
959
+ if (r) resolved.push(r);
960
+ } else if (isReferenceValue(item)) {
961
+ resolved.push(item);
962
+ }
963
+ }
964
+ if (resolved.length > 0) fm[fieldName] = resolved;
965
+ } else if (typeof value === "string" && REFERENCE_PATH_RE.test(value)) {
966
+ const r = await resolveString(value);
967
+ if (r) fm[fieldName] = r;
968
+ }
969
+ }
970
+ }
971
+ function runCMSBackgroundTask(event, task) {
972
+ const ctx = event?.context?.cloudflare?.context || event?.node?.req?.context?.cloudflare?.context || event?.context?.waitUntil;
973
+ if (ctx && typeof ctx.waitUntil === "function") {
974
+ ctx.waitUntil(
975
+ task.catch((err) => console.warn("[qarpeo:cms:cache] Background task error:", err))
976
+ );
977
+ } else if (typeof ctx === "function") {
978
+ ctx(task.catch((err) => console.warn("[qarpeo:cms:cache] Background task error:", err)));
979
+ } else {
980
+ task.catch(() => {
981
+ });
982
+ }
983
+ }
984
+ async function getCachedEntry(event, collectionId, slugOrId) {
985
+ const config = getCMSConfig(event);
986
+ if (!config.cache) return null;
987
+ try {
988
+ const kv = getKVBinding(event);
989
+ const tenantPrefix = getCMSTenantPrefix(event);
990
+ const cached = await kv.get(`${tenantPrefix}cms:entry:${collectionId}:${slugOrId}`, {
991
+ type: "json"
992
+ });
993
+ return cached;
994
+ } catch {
995
+ return null;
996
+ }
997
+ }
998
+ async function setCachedEntry(event, collectionId, slugOrId, entry) {
999
+ const config = getCMSConfig(event);
1000
+ if (!config.cache) return;
1001
+ const tenantPrefix = getCMSTenantPrefix(event);
1002
+ const task = (async () => {
1003
+ try {
1004
+ const kv = getKVBinding(event);
1005
+ await kv.put(`${tenantPrefix}cms:entry:${collectionId}:${slugOrId}`, JSON.stringify(entry), {
1006
+ expirationTtl: 60
1007
+ });
1008
+ } catch {
1009
+ }
1010
+ })();
1011
+ runCMSBackgroundTask(event, task);
1012
+ }
1013
+ async function getCachedList(event, key) {
1014
+ const config = getCMSConfig(event);
1015
+ if (!config.cache) return null;
1016
+ try {
1017
+ const kv = getKVBinding(event);
1018
+ const tenantPrefix = getCMSTenantPrefix(event);
1019
+ const cached = await kv.get(`${tenantPrefix}${key}`, { type: "json" });
1020
+ return cached;
1021
+ } catch {
1022
+ return null;
1023
+ }
1024
+ }
1025
+ async function setCachedList(event, key, data) {
1026
+ const config = getCMSConfig(event);
1027
+ if (!config.cache) return;
1028
+ const tenantPrefix = getCMSTenantPrefix(event);
1029
+ const task = (async () => {
1030
+ try {
1031
+ const kv = getKVBinding(event);
1032
+ await kv.put(`${tenantPrefix}${key}`, JSON.stringify(data), { expirationTtl: 30 });
1033
+ } catch {
1034
+ }
1035
+ })();
1036
+ runCMSBackgroundTask(event, task);
1037
+ }
1038
+ export async function invalidateCMSEntryCache(event, collectionId, slug) {
1039
+ const config = getCMSConfig(event);
1040
+ if (!config.cache) return;
1041
+ const tenantPrefix = getCMSTenantPrefix(event);
1042
+ const task = (async () => {
1043
+ try {
1044
+ const kv = getKVBinding(event);
1045
+ if (slug) {
1046
+ await kv.delete(`${tenantPrefix}cms:entry:${collectionId}:${slug}`);
1047
+ }
1048
+ const listRes = await kv.list({ prefix: `${tenantPrefix}cms:list:${collectionId}:` });
1049
+ if (listRes?.keys?.length) {
1050
+ await Promise.all(listRes.keys.map((k) => kv.delete(k.name)));
1051
+ }
1052
+ } catch {
1053
+ }
1054
+ })();
1055
+ runCMSBackgroundTask(event, task);
1056
+ }
1057
+ export async function verifyGitHubSignature(rawBody, signatureHeader, secret) {
1058
+ if (!signatureHeader || !secret) return false;
1059
+ const cleanSig = signatureHeader.replace(/^sha256=/, "").trim();
1060
+ try {
1061
+ const key = await crypto.subtle.importKey(
1062
+ "raw",
1063
+ new TextEncoder().encode(secret),
1064
+ { name: "HMAC", hash: "SHA-256" },
1065
+ false,
1066
+ ["sign"]
1067
+ );
1068
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));
1069
+ const expected = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1070
+ if (expected.length !== cleanSig.length) return false;
1071
+ let diff = 0;
1072
+ for (let i = 0; i < expected.length; i++) {
1073
+ diff |= expected.charCodeAt(i) ^ cleanSig.charCodeAt(i);
1074
+ }
1075
+ return diff === 0;
1076
+ } catch {
1077
+ return false;
1078
+ }
1079
+ }
1080
+ export async function queryCMSEntries(event, collectionId, opts = {}) {
1081
+ const status = opts.status || "published";
1082
+ const isCacheable = status === "published";
1083
+ const listCacheKey = isCacheable && !opts.populate ? `cms:list:${collectionId}:${opts.page || 1}:${opts.limit || 10}:${opts.sort || ""}:${opts.tag || ""}:${opts.where ? JSON.stringify(opts.where) : ""}` : null;
1084
+ if (listCacheKey) {
1085
+ const cached = await getCachedList(event, listCacheKey);
1086
+ if (cached) return cached;
1087
+ }
1088
+ const db = getCMSDb(event);
1089
+ const page = Math.max(1, opts.page || 1);
1090
+ const limit = Math.min(100, opts.limit || 10);
1091
+ const offset = (page - 1) * limit;
1092
+ const conditions = ["collection_id = ?"];
1093
+ const binds = [collectionId];
1094
+ const tenantId = getCMSTenantId(event);
1095
+ if (tenantId) {
1096
+ conditions.push("(tenant_id = ? OR tenant_id IS NULL)");
1097
+ binds.push(tenantId);
1098
+ }
1099
+ if (status !== "all") {
1100
+ conditions.push("status = ?");
1101
+ binds.push(status);
1102
+ }
1103
+ if (opts.tag) {
1104
+ conditions.push("(tags LIKE ? OR tags LIKE ? OR tags LIKE ? OR tags = ?)");
1105
+ binds.push(`${opts.tag},%`, `%,${opts.tag},%`, `%,${opts.tag}`, opts.tag);
1106
+ }
1107
+ if (opts.where && typeof opts.where === "object") {
1108
+ for (const [key, value] of Object.entries(opts.where)) {
1109
+ if (value === void 0 || !/^[a-zA-Z0-9_]+$/.test(key)) continue;
1110
+ const jsonPath = `$.${key}`;
1111
+ if (value === null) {
1112
+ conditions.push("json_extract(metadata_json, ?) IS NULL");
1113
+ binds.push(jsonPath);
1114
+ } else if (typeof value === "boolean") {
1115
+ conditions.push("json_extract(metadata_json, ?) = ?");
1116
+ binds.push(jsonPath, value ? 1 : 0);
1117
+ } else {
1118
+ conditions.push("json_extract(metadata_json, ?) = ?");
1119
+ binds.push(jsonPath, value);
1120
+ }
1121
+ }
1122
+ }
1123
+ const where = `WHERE ${conditions.join(" AND ")}`;
1124
+ const [sortField, sortDir] = (opts.sort || "published_at:desc").split(":");
1125
+ const safeSortField = ["published_at", "created_at", "updated_at", "title", "order"].includes(
1126
+ sortField
1127
+ ) ? sortField : "published_at";
1128
+ const safeDir = sortDir === "asc" ? "ASC" : "DESC";
1129
+ const [countResult, dataResult] = await Promise.all([
1130
+ db.prepare(`SELECT COUNT(*) as total FROM _qarpeo_cms_entries ${where}`).bind(...binds).first(),
1131
+ db.prepare(
1132
+ `SELECT * FROM _qarpeo_cms_entries ${where} ORDER BY ${safeSortField} ${safeDir} LIMIT ? OFFSET ?`
1133
+ ).bind(...binds, limit, offset).all()
1134
+ ]);
1135
+ const total = countResult?.total ?? 0;
1136
+ const entries = (dataResult.results || []).map(
1137
+ rowToEntry
1138
+ );
1139
+ const populatedEntries = opts.populate ? await Promise.all(entries.map((e) => populateReferenceFields(event, e, opts.populate))) : entries;
1140
+ const result = { entries: populatedEntries, total, page, limit };
1141
+ if (listCacheKey) {
1142
+ await setCachedList(event, listCacheKey, result);
1143
+ }
1144
+ return result;
1145
+ }
1146
+ export async function getCMSEntry(event, collectionId, slugOrId, opts = {}) {
1147
+ const db = getCMSDb(event);
1148
+ let allowDraft = false;
1149
+ const previewCookie = event ? getCookie(event, "qarpeo_preview") : null;
1150
+ let cookieObj = null;
1151
+ if (previewCookie) {
1152
+ try {
1153
+ cookieObj = typeof previewCookie === "string" ? JSON.parse(previewCookie) : previewCookie;
1154
+ } catch {
1155
+ }
1156
+ }
1157
+ const isPreviewRequested = opts.preview || Boolean(cookieObj);
1158
+ if (isPreviewRequested) {
1159
+ const auth = await getAuthContext(event).catch(() => null);
1160
+ const config = getCMSConfig(event);
1161
+ if (auth?.user && config.roles.some((r) => r === auth.role || auth.isAdmin)) {
1162
+ allowDraft = true;
1163
+ }
1164
+ if (!allowDraft) {
1165
+ const token = opts.previewToken || cookieObj?.token;
1166
+ const expires = opts.previewExpires || cookieObj?.expires;
1167
+ if (token && expires) {
1168
+ allowDraft = await verifyPreviewToken(
1169
+ event,
1170
+ collectionId,
1171
+ slugOrId,
1172
+ token,
1173
+ expires
1174
+ );
1175
+ }
1176
+ }
1177
+ }
1178
+ if (!allowDraft) {
1179
+ const cached = await getCachedEntry(event, collectionId, slugOrId);
1180
+ if (cached) return cached;
1181
+ }
1182
+ const statusClause = allowDraft ? "AND status IN ('draft', 'published', 'scheduled')" : "AND status = 'published'";
1183
+ const statusOrder = allowDraft ? "ORDER BY CASE WHEN status = 'draft' THEN 1 WHEN status = 'scheduled' THEN 2 ELSE 3 END" : "";
1184
+ const tenantId = getCMSTenantId(event);
1185
+ const tenantClause = tenantId ? "AND (tenant_id = ? OR tenant_id IS NULL)" : "";
1186
+ const binds = tenantId ? [collectionId, slugOrId, slugOrId, tenantId] : [collectionId, slugOrId, slugOrId];
1187
+ const row = await db.prepare(
1188
+ `SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (slug = ? OR id = ?) ${statusClause} ${tenantClause} ${statusOrder} LIMIT 1`
1189
+ ).bind(...binds).first();
1190
+ const entry = row ? rowToEntry(row) : null;
1191
+ if (entry && (!entry.body || !Array.isArray(entry.body.nodes)) && entry.contentMd) {
1192
+ const ast = await parseMarkdownAST(entry.contentMd, event);
1193
+ entry.body = {
1194
+ ...ast,
1195
+ value: entry.contentMd,
1196
+ content: entry.contentMd,
1197
+ toc: entry.toc || { links: [] }
1198
+ };
1199
+ }
1200
+ if (entry && !allowDraft) {
1201
+ await setCachedEntry(event, collectionId, slugOrId, entry);
1202
+ if (entry.slug && entry.slug !== slugOrId) {
1203
+ await setCachedEntry(event, collectionId, entry.slug, entry);
1204
+ }
1205
+ }
1206
+ if (entry && opts.populate) {
1207
+ return populateReferenceFields(event, entry, opts.populate);
1208
+ }
1209
+ return entry;
1210
+ }
1211
+ export async function createCMSEntry(event, collectionId, data) {
1212
+ const db = getCMSDb(event);
1213
+ const cmsConfig = getCMSConfig(event);
1214
+ const isSingleton = cmsConfig.collections?.[collectionId]?.singleton;
1215
+ if (isSingleton) {
1216
+ const existing = await db.prepare("SELECT COUNT(*) as total FROM _qarpeo_cms_entries WHERE collection_id = ?").bind(collectionId).first();
1217
+ if (existing && existing.total > 0) {
1218
+ throw createError({
1219
+ statusCode: 409,
1220
+ message: `Collection '${collectionId}' is a singleton and already has an entry.`
1221
+ });
1222
+ }
1223
+ }
1224
+ const id = nanoid();
1225
+ const now = Date.now();
1226
+ const { ast, html: contentHtml } = await processMarkdown(data.contentMd);
1227
+ const metadata = data.metadata || {};
1228
+ if (ast) {
1229
+ ;
1230
+ metadata._ast = ast;
1231
+ }
1232
+ await validateAndNormalizeReferenceFields(event, collectionId, metadata);
1233
+ await validateAndNormalizeComponentFields(event, collectionId, metadata);
1234
+ const metadataJson = JSON.stringify(metadata);
1235
+ const publishedAt = data.status === "published" ? data.publishedAt ?? now : data.status === "scheduled" && data.publishedAt ? data.publishedAt : data.publishedAt ?? null;
1236
+ const tenantId = getCMSTenantId(event);
1237
+ await db.prepare(
1238
+ `INSERT INTO _qarpeo_cms_entries
1239
+ (id, collection_id, slug, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, author_id, published_at, created_at, updated_at, tenant_id)
1240
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'db', ?, ?, ?, ?, ?)`
1241
+ ).bind(
1242
+ id,
1243
+ collectionId,
1244
+ data.slug,
1245
+ data.title,
1246
+ data.description ?? null,
1247
+ data.coverImage ?? null,
1248
+ data.tags ?? null,
1249
+ data.order ?? 0,
1250
+ data.contentMd,
1251
+ contentHtml,
1252
+ metadataJson,
1253
+ data.status || "draft",
1254
+ data.authorId ?? null,
1255
+ publishedAt,
1256
+ now,
1257
+ now,
1258
+ tenantId
1259
+ ).run();
1260
+ const entry = {
1261
+ id,
1262
+ collectionId,
1263
+ slug: data.slug,
1264
+ title: data.title,
1265
+ description: data.description ?? null,
1266
+ coverImage: data.coverImage ?? null,
1267
+ tags: data.tags ?? null,
1268
+ order: data.order ?? 0,
1269
+ contentMd: data.contentMd,
1270
+ contentHtml,
1271
+ metadataJson,
1272
+ metadata,
1273
+ body: ast ? { ...ast, value: data.contentMd, content: data.contentMd } : void 0,
1274
+ status: data.status || "draft",
1275
+ source: "db",
1276
+ filePath: null,
1277
+ contentHash: null,
1278
+ authorId: data.authorId ?? null,
1279
+ publishedAt,
1280
+ tenantId: tenantId ?? null,
1281
+ createdAt: now,
1282
+ updatedAt: now
1283
+ };
1284
+ const gitResult = await syncEntryToGit(event, collectionId, entry, "create");
1285
+ if (gitResult?.filePath) {
1286
+ entry.filePath = gitResult.filePath;
1287
+ entry.contentHash = gitResult.contentHash || null;
1288
+ entry.source = "file";
1289
+ }
1290
+ await invalidateCMSEntryCache(event, collectionId, data.slug);
1291
+ fireCMSWebhooks(getCMSConfig(event), collectionId, "create", entry).catch(console.warn);
1292
+ return entry;
1293
+ }
1294
+ export async function updateCMSEntry(event, entryId, data) {
1295
+ const db = getCMSDb(event);
1296
+ const now = Date.now();
1297
+ const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1298
+ if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
1299
+ const tenantId = getCMSTenantId(event);
1300
+ if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
1301
+ throw createError({
1302
+ statusCode: 403,
1303
+ message: "Forbidden: Cannot modify an entry belonging to another tenant"
1304
+ });
1305
+ }
1306
+ const contentMd = data.contentMd ?? existing.content_md;
1307
+ let contentHtml = existing.content_html;
1308
+ let newAst = null;
1309
+ if (data.contentMd) {
1310
+ const res = await processMarkdown(data.contentMd);
1311
+ contentHtml = res.html;
1312
+ newAst = res.ast;
1313
+ }
1314
+ let metadataObj = data.metadata;
1315
+ if (!metadataObj && existing.metadata_json) {
1316
+ try {
1317
+ metadataObj = JSON.parse(existing.metadata_json);
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ if (metadataObj && newAst) {
1322
+ ;
1323
+ metadataObj._ast = newAst;
1324
+ }
1325
+ if (metadataObj && data.metadata) {
1326
+ await validateAndNormalizeReferenceFields(event, existing.collection_id, metadataObj);
1327
+ await validateAndNormalizeComponentFields(event, existing.collection_id, metadataObj);
1328
+ }
1329
+ const metadataJson = metadataObj ? JSON.stringify(metadataObj) : existing.metadata_json;
1330
+ const status = data.status ?? existing.status;
1331
+ let publishedAt;
1332
+ if (data.publishedAt !== void 0) {
1333
+ publishedAt = data.publishedAt;
1334
+ } else if (status === "published") {
1335
+ publishedAt = existing.published_at ?? now;
1336
+ } else if (status === "scheduled") {
1337
+ publishedAt = existing.published_at ?? now;
1338
+ } else {
1339
+ publishedAt = existing.published_at;
1340
+ }
1341
+ await db.prepare(
1342
+ "INSERT INTO _qarpeo_cms_revisions (id, entry_id, title, content_md, metadata_json, author_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1343
+ ).bind(
1344
+ nanoid(),
1345
+ entryId,
1346
+ existing.title,
1347
+ existing.content_md,
1348
+ existing.metadata_json,
1349
+ existing.author_id,
1350
+ now
1351
+ ).run();
1352
+ await db.prepare(
1353
+ `UPDATE _qarpeo_cms_entries SET
1354
+ slug = COALESCE(?, slug), title = COALESCE(?, title),
1355
+ description = COALESCE(?, description), cover_image = COALESCE(?, cover_image),
1356
+ tags = COALESCE(?, tags), "order" = COALESCE(?, "order"),
1357
+ content_md = ?, content_html = ?, metadata_json = ?,
1358
+ status = ?, source = 'db', published_at = ?, updated_at = ?
1359
+ WHERE id = ?`
1360
+ ).bind(
1361
+ data.slug ?? null,
1362
+ data.title ?? null,
1363
+ data.description ?? null,
1364
+ data.coverImage ?? null,
1365
+ data.tags ?? null,
1366
+ data.order ?? null,
1367
+ contentMd,
1368
+ contentHtml,
1369
+ metadataJson,
1370
+ status,
1371
+ publishedAt,
1372
+ now,
1373
+ entryId
1374
+ ).run();
1375
+ const config = getCMSConfig(event);
1376
+ const maxRevisions = typeof config.revisions === "object" ? config.revisions.max ?? 10 : 10;
1377
+ await pruneRevisions(event, entryId, maxRevisions);
1378
+ const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1379
+ const updatedEntry = rowToEntry(updated);
1380
+ const gitResult = await syncEntryToGit(event, updatedEntry.collectionId, updatedEntry, "update");
1381
+ if (gitResult?.filePath) {
1382
+ updatedEntry.filePath = gitResult.filePath;
1383
+ updatedEntry.contentHash = gitResult.contentHash || null;
1384
+ updatedEntry.source = "file";
1385
+ }
1386
+ await invalidateCMSEntryCache(event, updatedEntry.collectionId, updatedEntry.slug);
1387
+ fireCMSWebhooks(getCMSConfig(event), updatedEntry.collectionId, "update", updatedEntry).catch(
1388
+ console.warn
1389
+ );
1390
+ return updatedEntry;
1391
+ }
1392
+ async function nullifyOrphanedReferences(event, deletedEntryId) {
1393
+ const db = getCMSDb(event);
1394
+ try {
1395
+ const tenantId = getCMSTenantId(event);
1396
+ const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
1397
+ const binds = tenantId ? [`%"id":"${deletedEntryId}"%`, tenantId] : [`%"id":"${deletedEntryId}"%`];
1398
+ const rows = (await db.prepare(
1399
+ `SELECT id, collection_id, slug, metadata_json FROM _qarpeo_cms_entries
1400
+ WHERE json_extract(metadata_json, '$') IS NOT NULL
1401
+ AND metadata_json LIKE ?${tenantClause}`
1402
+ ).bind(...binds).all()).results;
1403
+ for (const row of rows) {
1404
+ let meta;
1405
+ try {
1406
+ meta = JSON.parse(row.metadata_json);
1407
+ } catch {
1408
+ continue;
1409
+ }
1410
+ let changed = false;
1411
+ for (const [key, value] of Object.entries(meta)) {
1412
+ if (isReferenceValue(value) && value.id === deletedEntryId) {
1413
+ meta[key] = null;
1414
+ changed = true;
1415
+ } else if (Array.isArray(value)) {
1416
+ const filtered = value.filter(
1417
+ (item) => !(isReferenceValue(item) && item.id === deletedEntryId)
1418
+ );
1419
+ if (filtered.length !== value.length) {
1420
+ meta[key] = filtered;
1421
+ changed = true;
1422
+ }
1423
+ }
1424
+ }
1425
+ if (changed) {
1426
+ await db.prepare("UPDATE _qarpeo_cms_entries SET metadata_json = ? WHERE id = ?").bind(JSON.stringify(meta), row.id).run();
1427
+ await invalidateCMSEntryCache(event, row.collection_id, row.slug);
1428
+ }
1429
+ }
1430
+ } catch (err) {
1431
+ console.warn("[qarpeo:cms] nullifyOrphanedReferences failed:", err);
1432
+ }
1433
+ }
1434
+ export async function deleteCMSEntry(event, entryId) {
1435
+ const db = getCMSDb(event);
1436
+ const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1437
+ if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
1438
+ const tenantId = getCMSTenantId(event);
1439
+ if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
1440
+ throw createError({
1441
+ statusCode: 403,
1442
+ message: "Forbidden: Cannot delete an entry belonging to another tenant"
1443
+ });
1444
+ }
1445
+ await db.batch([
1446
+ db.prepare("DELETE FROM _qarpeo_cms_revisions WHERE entry_id = ?").bind(entryId),
1447
+ db.prepare("DELETE FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId)
1448
+ ]);
1449
+ if (existing) {
1450
+ await nullifyOrphanedReferences(event, entryId);
1451
+ await invalidateCMSEntryCache(event, existing.collection_id, existing.slug);
1452
+ await syncEntryToGit(event, existing.collection_id, rowToEntry(existing), "delete");
1453
+ fireCMSWebhooks(
1454
+ getCMSConfig(event),
1455
+ existing.collection_id,
1456
+ "delete",
1457
+ rowToEntry(existing)
1458
+ ).catch(console.warn);
1459
+ }
1460
+ }
1461
+ export async function publishCMSEntry(event, entryId, targetStatus, targetPublishedAt) {
1462
+ const db = getCMSDb(event);
1463
+ const now = Date.now();
1464
+ const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1465
+ if (!existing) {
1466
+ throw createError({ statusCode: 404, message: "Entry not found" });
1467
+ }
1468
+ const tenantId = getCMSTenantId(event);
1469
+ if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
1470
+ throw createError({
1471
+ statusCode: 403,
1472
+ message: "Forbidden: Cannot publish an entry belonging to another tenant"
1473
+ });
1474
+ }
1475
+ const publishedAt = targetStatus === "published" ? targetPublishedAt ?? existing.published_at ?? now : targetStatus === "scheduled" ? targetPublishedAt ?? existing.published_at ?? now : null;
1476
+ const collectionId = existing.collection_id;
1477
+ const config = getCMSConfig(event);
1478
+ const colConfig = config.collections[collectionId] || {};
1479
+ const approval = resolveCollectionApproval(colConfig, config.approval);
1480
+ let metadataJson = existing.metadata_json || "{}";
1481
+ if ((targetStatus === "published" || targetStatus === "scheduled") && approval) {
1482
+ const auth = await getAuthContext(event);
1483
+ const userRole = auth?.role || "user";
1484
+ const isAuthorized = auth?.isAdmin || approval.roles?.includes(userRole);
1485
+ if (!isAuthorized) {
1486
+ throw createError({
1487
+ statusCode: 403,
1488
+ message: `${targetStatus === "scheduled" ? "Scheduling" : "Publishing"} in "${collectionId}" requires one of the following approver roles: ${approval.roles?.join(", ")}.`
1489
+ });
1490
+ }
1491
+ if (!approval.allowSelfApproval && existing.author_id && auth?.user?.id === existing.author_id) {
1492
+ throw createError({
1493
+ statusCode: 403,
1494
+ message: `Self-approval is disabled for "${collectionId}". Another team member must approve this entry.`
1495
+ });
1496
+ }
1497
+ let metaObj = {};
1498
+ try {
1499
+ metaObj = JSON.parse(metadataJson);
1500
+ } catch {
1501
+ }
1502
+ metaObj._approval = {
1503
+ ...metaObj._approval || {},
1504
+ approvedBy: auth?.user?.id || null,
1505
+ approvedByName: auth?.user?.name || auth?.user?.email || "Admin",
1506
+ approvedAt: now
1507
+ };
1508
+ metadataJson = JSON.stringify(metaObj);
1509
+ }
1510
+ await db.prepare(
1511
+ "UPDATE _qarpeo_cms_entries SET status = ?, published_at = ?, metadata_json = ?, updated_at = ? WHERE id = ?"
1512
+ ).bind(targetStatus, publishedAt, metadataJson, now, entryId).run();
1513
+ const updatedRow = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1514
+ if (updatedRow) {
1515
+ const updated = rowToEntry(updatedRow);
1516
+ await syncEntryToGit(
1517
+ event,
1518
+ updated.collectionId,
1519
+ updated,
1520
+ targetStatus === "published" ? "publish" : "update"
1521
+ );
1522
+ const webhookEvent = targetStatus === "published" ? "publish" : targetStatus === "scheduled" ? "update" : "unpublish";
1523
+ fireCMSWebhooks(config, updated.collectionId, webhookEvent, updated).catch(console.warn);
1524
+ if (targetStatus === "published" || targetStatus === "draft") {
1525
+ fireCMSDeployTrigger(
1526
+ config,
1527
+ updated.collectionId,
1528
+ targetStatus === "published" ? "publish" : "unpublish"
1529
+ ).catch(console.warn);
1530
+ }
1531
+ }
1532
+ }
1533
+ export async function publishDueScheduledEntries(event) {
1534
+ const db = getCMSDb(event);
1535
+ const now = Date.now();
1536
+ const config = getCMSConfig(event);
1537
+ const rows = await db.prepare(
1538
+ "SELECT * FROM _qarpeo_cms_entries WHERE status = 'scheduled' AND published_at IS NOT NULL AND published_at <= ?"
1539
+ ).bind(now).all();
1540
+ const entriesToPublish = rows.results || [];
1541
+ const publishedEntries = [];
1542
+ for (const row of entriesToPublish) {
1543
+ const entryId = row.id;
1544
+ const collectionId = row.collection_id;
1545
+ await db.prepare("UPDATE _qarpeo_cms_entries SET status = 'published', updated_at = ? WHERE id = ?").bind(now, entryId).run();
1546
+ const updatedRow = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1547
+ if (updatedRow) {
1548
+ const entry = rowToEntry(updatedRow);
1549
+ publishedEntries.push(entry);
1550
+ await syncEntryToGit(event, collectionId, entry, "publish").catch(console.warn);
1551
+ fireCMSWebhooks(config, collectionId, "publish", entry).catch(console.warn);
1552
+ fireCMSDeployTrigger(config, collectionId, "publish").catch(console.warn);
1553
+ }
1554
+ }
1555
+ return publishedEntries;
1556
+ }
1557
+ export async function submitCMSEntryForReview(event, entryId, note) {
1558
+ const db = getCMSDb(event);
1559
+ const now = Date.now();
1560
+ const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1561
+ if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
1562
+ const tenantId = getCMSTenantId(event);
1563
+ if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
1564
+ throw createError({
1565
+ statusCode: 403,
1566
+ message: "Forbidden: Cannot modify an entry belonging to another tenant"
1567
+ });
1568
+ }
1569
+ const auth = await getAuthContext(event);
1570
+ let metaObj = {};
1571
+ try {
1572
+ metaObj = JSON.parse(existing.metadata_json || "{}");
1573
+ } catch {
1574
+ }
1575
+ metaObj._approval = {
1576
+ ...metaObj._approval || {},
1577
+ submittedBy: auth?.user?.id || null,
1578
+ submittedByName: auth?.user?.name || auth?.user?.email || "Editor",
1579
+ submittedAt: now,
1580
+ submissionNote: note || null
1581
+ };
1582
+ const metadataJson = JSON.stringify(metaObj);
1583
+ await db.prepare(
1584
+ "INSERT INTO _qarpeo_cms_revisions (id, entry_id, title, content_md, metadata_json, author_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1585
+ ).bind(
1586
+ nanoid(),
1587
+ entryId,
1588
+ existing.title,
1589
+ existing.content_md,
1590
+ metadataJson,
1591
+ auth?.user?.id || existing.author_id,
1592
+ now
1593
+ ).run();
1594
+ await db.prepare(
1595
+ "UPDATE _qarpeo_cms_entries SET status = 'pending_review', metadata_json = ?, updated_at = ? WHERE id = ?"
1596
+ ).bind(metadataJson, now, entryId).run();
1597
+ const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1598
+ return rowToEntry(updated);
1599
+ }
1600
+ export async function rejectCMSEntryReview(event, entryId, reason) {
1601
+ const db = getCMSDb(event);
1602
+ const now = Date.now();
1603
+ const existing = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1604
+ if (!existing) throw createError({ statusCode: 404, message: "Entry not found" });
1605
+ const tenantId = getCMSTenantId(event);
1606
+ if (tenantId && existing.tenant_id && existing.tenant_id !== tenantId) {
1607
+ throw createError({
1608
+ statusCode: 403,
1609
+ message: "Forbidden: Cannot modify an entry belonging to another tenant"
1610
+ });
1611
+ }
1612
+ const collectionId = existing.collection_id;
1613
+ const config = getCMSConfig(event);
1614
+ const colConfig = config.collections[collectionId] || {};
1615
+ const approval = resolveCollectionApproval(colConfig, config.approval);
1616
+ const auth = await getAuthContext(event);
1617
+ if (approval) {
1618
+ const userRole = auth?.role || "user";
1619
+ const isAuthorized = auth?.isAdmin || approval.roles?.includes(userRole);
1620
+ if (!isAuthorized) {
1621
+ throw createError({
1622
+ statusCode: 403,
1623
+ message: `Only reviewers with roles [${approval.roles?.join(", ")}] can request changes on entries in "${collectionId}".`
1624
+ });
1625
+ }
1626
+ }
1627
+ let metaObj = {};
1628
+ try {
1629
+ metaObj = JSON.parse(existing.metadata_json || "{}");
1630
+ } catch {
1631
+ }
1632
+ metaObj._approval = {
1633
+ ...metaObj._approval || {},
1634
+ rejectedBy: auth?.user?.id || null,
1635
+ rejectedByName: auth?.user?.name || auth?.user?.email || "Reviewer",
1636
+ rejectedAt: now,
1637
+ rejectionReason: reason || null
1638
+ };
1639
+ const metadataJson = JSON.stringify(metaObj);
1640
+ await db.prepare(
1641
+ "UPDATE _qarpeo_cms_entries SET status = 'draft', metadata_json = ?, updated_at = ? WHERE id = ?"
1642
+ ).bind(metadataJson, now, entryId).run();
1643
+ const updated = await db.prepare("SELECT * FROM _qarpeo_cms_entries WHERE id = ?").bind(entryId).first();
1644
+ return rowToEntry(updated);
1645
+ }
1646
+ export async function pruneRevisions(event, entryId, max) {
1647
+ const db = getCMSDb(event);
1648
+ await db.prepare(
1649
+ `DELETE FROM _qarpeo_cms_revisions WHERE entry_id = ? AND id NOT IN (
1650
+ SELECT id FROM _qarpeo_cms_revisions WHERE entry_id = ? ORDER BY created_at DESC LIMIT ?
1651
+ )`
1652
+ ).bind(entryId, entryId, max).run();
1653
+ }
1654
+ async function getCMSSecret(event) {
1655
+ const rc = useRuntimeConfig(event);
1656
+ const secret = rc.appSecret || rc.qarpeo?.sessionPassword || rc.sessionPassword || rc.session?.password || globalThis.process?.env?.NUXT_APP_SECRET || globalThis.process?.env?.NUXT_SESSION_PASSWORD || "qarpeo-cms-preview-secret";
1657
+ const enc = new TextEncoder();
1658
+ return crypto.subtle.importKey(
1659
+ "raw",
1660
+ enc.encode(secret),
1661
+ { name: "HMAC", hash: "SHA-256" },
1662
+ false,
1663
+ ["sign", "verify"]
1664
+ );
1665
+ }
1666
+ export async function generatePreviewToken(event, collection, slugOrId, ttlMs = 24 * 60 * 60 * 1e3) {
1667
+ const expires = Date.now() + ttlMs;
1668
+ const key = await getCMSSecret(event);
1669
+ const msg = new TextEncoder().encode(`${collection}:${slugOrId}:${expires}`);
1670
+ const sig = await crypto.subtle.sign("HMAC", key, msg);
1671
+ const token = btoa(String.fromCharCode(...new Uint8Array(sig)));
1672
+ return { token, expires };
1673
+ }
1674
+ export async function verifyPreviewToken(event, collection, slugOrId, token, expiresStr) {
1675
+ try {
1676
+ const expires = typeof expiresStr === "number" ? expiresStr : parseInt(String(expiresStr), 10);
1677
+ if (Number.isNaN(expires) || Date.now() > expires) return false;
1678
+ const key = await getCMSSecret(event);
1679
+ const sigBytes = Uint8Array.from(atob(token), (c) => c.charCodeAt(0));
1680
+ const msg = new TextEncoder().encode(`${collection}:${slugOrId}:${expires}`);
1681
+ const isValid = await crypto.subtle.verify("HMAC", key, sigBytes, msg);
1682
+ if (isValid) return true;
1683
+ try {
1684
+ const db = getCMSDb(event);
1685
+ const row = await db.prepare(
1686
+ "SELECT id, slug FROM _qarpeo_cms_entries WHERE collection_id = ? AND (id = ? OR slug = ?) LIMIT 1"
1687
+ ).bind(collection, slugOrId, slugOrId).first();
1688
+ if (row) {
1689
+ const alt = row.id === slugOrId ? row.slug : row.id;
1690
+ if (alt) {
1691
+ const altMsg = new TextEncoder().encode(`${collection}:${alt}:${expires}`);
1692
+ return await crypto.subtle.verify("HMAC", key, sigBytes, altMsg);
1693
+ }
1694
+ }
1695
+ } catch {
1696
+ }
1697
+ return false;
1698
+ } catch {
1699
+ return false;
1700
+ }
1701
+ }
1702
+ export async function ensureCMSTables(event) {
1703
+ const db = getCMSDb(event);
1704
+ const statements = [
1705
+ `CREATE TABLE IF NOT EXISTS _qarpeo_cms_collections (id TEXT PRIMARY KEY, tenant_id TEXT, name TEXT NOT NULL, description TEXT, schema_json TEXT NOT NULL DEFAULT '{}', source TEXT NOT NULL DEFAULT 'db', "order" INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);`,
1706
+ `CREATE TABLE IF NOT EXISTS _qarpeo_cms_entries (id TEXT PRIMARY KEY, tenant_id TEXT, collection_id TEXT NOT NULL, slug TEXT NOT NULL, title TEXT NOT NULL, description TEXT, cover_image TEXT, tags TEXT, "order" INTEGER NOT NULL DEFAULT 0, content_md TEXT NOT NULL DEFAULT '', content_html TEXT NOT NULL DEFAULT '', metadata_json TEXT DEFAULT '{}', status TEXT NOT NULL DEFAULT 'draft', source TEXT NOT NULL DEFAULT 'db', path TEXT, file_path TEXT, content_hash TEXT, author_id TEXT, locale TEXT NOT NULL DEFAULT 'en', version TEXT NOT NULL DEFAULT 'latest', published_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL);`,
1707
+ `CREATE TABLE IF NOT EXISTS _qarpeo_cms_revisions (id TEXT PRIMARY KEY, entry_id TEXT NOT NULL, title TEXT NOT NULL, content_md TEXT NOT NULL, metadata_json TEXT DEFAULT '{}', author_id TEXT, created_at INTEGER NOT NULL);`,
1708
+ // Matching Drizzle schema indexes
1709
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_collection_idx ON _qarpeo_cms_entries(collection_id);`,
1710
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_tenant_idx ON _qarpeo_cms_entries(tenant_id, collection_id);`,
1711
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_slug_idx ON _qarpeo_cms_entries(collection_id, slug);`,
1712
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_status_idx ON _qarpeo_cms_entries(status);`,
1713
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_order_idx ON _qarpeo_cms_entries(collection_id, "order");`,
1714
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_idx ON _qarpeo_cms_entries(collection_id, path);`,
1715
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_locale_idx ON _qarpeo_cms_entries(collection_id, locale);`,
1716
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_locale_idx ON _qarpeo_cms_entries(path, locale);`,
1717
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_version_idx ON _qarpeo_cms_entries(collection_id, version);`,
1718
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_path_version_idx ON _qarpeo_cms_entries(path, version);`,
1719
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_published_at_idx ON _qarpeo_cms_entries(published_at);`,
1720
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_entries_list_idx ON _qarpeo_cms_entries(collection_id, status, published_at);`,
1721
+ `CREATE INDEX IF NOT EXISTS _qarpeo_cms_revisions_entry_idx ON _qarpeo_cms_revisions(entry_id, created_at);`
1722
+ ];
1723
+ for (const sql of statements) {
1724
+ try {
1725
+ await db.exec(sql);
1726
+ } catch (err) {
1727
+ console.warn("[qarpeo:cms] ensureCMSTables warning on statement:", sql, err);
1728
+ }
1729
+ }
1730
+ try {
1731
+ await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN path TEXT;");
1732
+ } catch {
1733
+ }
1734
+ try {
1735
+ await db.exec(
1736
+ 'ALTER TABLE _qarpeo_cms_collections ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;'
1737
+ );
1738
+ } catch {
1739
+ }
1740
+ try {
1741
+ await db.exec('ALTER TABLE _qarpeo_cms_entries ADD COLUMN "order" INTEGER NOT NULL DEFAULT 0;');
1742
+ } catch {
1743
+ }
1744
+ try {
1745
+ await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN tenant_id TEXT;");
1746
+ } catch {
1747
+ }
1748
+ try {
1749
+ await db.exec("ALTER TABLE _qarpeo_cms_collections ADD COLUMN tenant_id TEXT;");
1750
+ } catch {
1751
+ }
1752
+ try {
1753
+ await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN locale TEXT NOT NULL DEFAULT 'en';");
1754
+ } catch {
1755
+ }
1756
+ try {
1757
+ await db.exec("ALTER TABLE _qarpeo_cms_entries ADD COLUMN version TEXT NOT NULL DEFAULT 'latest';");
1758
+ } catch {
1759
+ }
1760
+ try {
1761
+ await db.exec(
1762
+ "CREATE VIRTUAL TABLE IF NOT EXISTS _qarpeo_cms_entries_fts USING fts5(id UNINDEXED, slug, title, description, content_md, tags, tokenize = 'unicode61');"
1763
+ );
1764
+ } catch (err) {
1765
+ console.warn("[qarpeo:cms] ensureCMSTables FTS virtual table notice:", err);
1766
+ }
1767
+ try {
1768
+ await db.exec(
1769
+ "CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_ai AFTER INSERT ON _qarpeo_cms_entries BEGIN INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) VALUES (new.id, new.slug, new.title, coalesce(new.description, ''), new.content_md, coalesce(new.tags, '')); END;"
1770
+ );
1771
+ } catch {
1772
+ }
1773
+ try {
1774
+ await db.exec(
1775
+ "CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_ad AFTER DELETE ON _qarpeo_cms_entries BEGIN DELETE FROM _qarpeo_cms_entries_fts WHERE id = old.id; END;"
1776
+ );
1777
+ } catch {
1778
+ }
1779
+ try {
1780
+ await db.exec(
1781
+ "CREATE TRIGGER IF NOT EXISTS _qarpeo_cms_entries_fts_au AFTER UPDATE ON _qarpeo_cms_entries BEGIN DELETE FROM _qarpeo_cms_entries_fts WHERE id = old.id; INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) VALUES (new.id, new.slug, new.title, coalesce(new.description, ''), new.content_md, coalesce(new.tags, '')); END;"
1782
+ );
1783
+ } catch {
1784
+ }
1785
+ try {
1786
+ await db.exec(
1787
+ "INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) SELECT e.id, e.slug, e.title, coalesce(e.description, ''), e.content_md, coalesce(e.tags, '') FROM _qarpeo_cms_entries e WHERE NOT EXISTS (SELECT 1 FROM _qarpeo_cms_entries_fts fts WHERE fts.id = e.id);"
1788
+ );
1789
+ } catch {
1790
+ }
1791
+ }
1792
+ let lastSyncTime = 0;
1793
+ let syncPromise = null;
1794
+ let hasInitialSynced = false;
1795
+ async function _runCMSSync(event) {
1796
+ try {
1797
+ if (!globalThis.__QARPEO_DEV_ENV__) {
1798
+ try {
1799
+ const { getOrInitDevProxy } = await import("./dev-proxy.mjs");
1800
+ const proxy = await getOrInitDevProxy();
1801
+ if (proxy?.env) {
1802
+ ;
1803
+ globalThis.__QARPEO_DEV_ENV__ = proxy.env;
1804
+ }
1805
+ } catch (proxyErr) {
1806
+ console.warn("[qarpeo:cms] Could not init dev proxy in ensureCMSSynced:", proxyErr);
1807
+ }
1808
+ }
1809
+ await ensureCMSTables(event);
1810
+ const config = getCMSConfig(event);
1811
+ await seedCollectionsFromConfig(event, config.collections);
1812
+ await syncLocalContentFiles(
1813
+ event,
1814
+ config.contentDir,
1815
+ config.collections,
1816
+ config.sources
1817
+ );
1818
+ } catch (err) {
1819
+ console.error("[qarpeo:cms] Auto-sync failed:", err);
1820
+ }
1821
+ }
1822
+ let prodTablesChecked = false;
1823
+ export async function ensureCMSSynced(event) {
1824
+ if (process.env.NODE_ENV !== "production" || import.meta.dev) {
1825
+ if (!hasInitialSynced) {
1826
+ if (!syncPromise) {
1827
+ syncPromise = _runCMSSync(event).finally(() => {
1828
+ hasInitialSynced = true;
1829
+ lastSyncTime = Date.now();
1830
+ syncPromise = null;
1831
+ });
1832
+ }
1833
+ await syncPromise;
1834
+ return;
1835
+ }
1836
+ if (syncPromise) {
1837
+ await syncPromise;
1838
+ return;
1839
+ }
1840
+ const now = Date.now();
1841
+ if (now - lastSyncTime > 3e4) {
1842
+ lastSyncTime = now;
1843
+ syncPromise = _runCMSSync(event).finally(() => {
1844
+ syncPromise = null;
1845
+ });
1846
+ await syncPromise;
1847
+ }
1848
+ } else {
1849
+ if (!prodTablesChecked) {
1850
+ prodTablesChecked = true;
1851
+ try {
1852
+ await ensureCMSTables(event);
1853
+ } catch (err) {
1854
+ console.warn("[qarpeo:cms] Production ensureCMSTables check failed:", err);
1855
+ }
1856
+ }
1857
+ }
1858
+ }
1859
+ export async function seedCollectionsFromConfig(event, collectionsConfig) {
1860
+ const db = getCMSDb(event);
1861
+ const now = Date.now();
1862
+ for (const [id, cfg] of Object.entries(collectionsConfig)) {
1863
+ const schemaJson = JSON.stringify(cfg.fields || {});
1864
+ const colName = cfg.name || id.charAt(0).toUpperCase() + id.slice(1).replace(/-/g, " ");
1865
+ const existing = await db.prepare("SELECT id, source, schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(id).first();
1866
+ if (!existing) {
1867
+ await db.prepare(
1868
+ "INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, created_at, updated_at) VALUES (?, ?, ?, ?, 'config', ?, ?)"
1869
+ ).bind(id, colName, cfg.description ?? null, schemaJson, now, now).run();
1870
+ } else if (existing.source === "config" && existing.schema_json !== schemaJson) {
1871
+ await db.prepare(
1872
+ "UPDATE _qarpeo_cms_collections SET name = ?, description = ?, schema_json = ?, updated_at = ? WHERE id = ?"
1873
+ ).bind(colName, cfg.description ?? null, schemaJson, now, id).run();
1874
+ try {
1875
+ const kv = getKVBinding(event);
1876
+ const tenantPrefix = getCMSTenantPrefix(event);
1877
+ await kv.delete(`${tenantPrefix}cms:schema:${id}`);
1878
+ } catch {
1879
+ }
1880
+ }
1881
+ }
1882
+ }
1883
+ import { matchesSourceFilters, matchesGlobFilter } from "./cms-filter.mjs";
1884
+ export { matchesSourceFilters, matchesGlobFilter };
1885
+ export async function syncLocalContentFiles(event, contentDir, collectionsConfig = {}, sources = []) {
1886
+ let readdir, readFile, existsSync;
1887
+ try {
1888
+ const fs = await import("node:fs/promises");
1889
+ const fsSync = await import("node:fs");
1890
+ readdir = (dir) => fs.readdir(dir, { withFileTypes: true });
1891
+ readFile = (p) => fs.readFile(p, "utf8");
1892
+ existsSync = fsSync.existsSync;
1893
+ } catch {
1894
+ return { synced: 0, skipped: 0, inserted: 0 };
1895
+ }
1896
+ async function readNavJson(dir) {
1897
+ const navJsonPath = `${dir}/.navigation.json`;
1898
+ if (existsSync(navJsonPath)) {
1899
+ try {
1900
+ const raw = await readFile(navJsonPath);
1901
+ const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
1902
+ return JSON.parse(clean);
1903
+ } catch {
1904
+ }
1905
+ }
1906
+ const navYmlPath = `${dir}/.navigation.yml`;
1907
+ if (existsSync(navYmlPath)) {
1908
+ try {
1909
+ const raw = await readFile(navYmlPath);
1910
+ const { parse: parseYaml2 } = await import("yaml");
1911
+ return parseYaml2(raw) || {};
1912
+ } catch {
1913
+ }
1914
+ }
1915
+ return {};
1916
+ }
1917
+ async function readSchemaJson(dir) {
1918
+ const schemaPath = `${dir}/.schema.json`;
1919
+ if (existsSync(schemaPath)) {
1920
+ try {
1921
+ const raw = await readFile(schemaPath);
1922
+ const clean = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
1923
+ const parsed = JSON.parse(clean);
1924
+ return {
1925
+ scope: parsed.scope || "cascade",
1926
+ fields: parsed.fields || {}
1927
+ };
1928
+ } catch {
1929
+ }
1930
+ }
1931
+ return null;
1932
+ }
1933
+ const db = getCMSDb(event);
1934
+ const cmsConfig = getCMSConfig(event);
1935
+ let synced = 0, skipped = 0, inserted = 0;
1936
+ const now = Date.now();
1937
+ try {
1938
+ const existingFileRows = await db.prepare("SELECT id, file_path FROM _qarpeo_cms_entries WHERE source = 'file'").all();
1939
+ for (const r of existingFileRows?.results || []) {
1940
+ if (r.file_path && !existsSync(r.file_path)) {
1941
+ await db.prepare("DELETE FROM _qarpeo_cms_entries WHERE id = ?").bind(r.id).run();
1942
+ }
1943
+ }
1944
+ } catch {
1945
+ }
1946
+ async function syncSource(source) {
1947
+ if (!source?.resolvedDir || !existsSync(source.resolvedDir)) return;
1948
+ let orderCounter = 0;
1949
+ const processDir = async (dir, relBase, sectionNavMeta) => {
1950
+ let entries;
1951
+ try {
1952
+ entries = await readdir(dir);
1953
+ } catch {
1954
+ return;
1955
+ }
1956
+ entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
1957
+ let navMeta = await readNavJson(dir);
1958
+ if (navMeta.private === true || navMeta.draft === true || navMeta.exclude === true) {
1959
+ return;
1960
+ }
1961
+ const isTopLevelSection = Boolean(relBase && !relBase.includes("/"));
1962
+ const currentSectionNavMeta = sectionNavMeta || (isTopLevelSection ? {
1963
+ title: navMeta.title || stripOrderPrefix(relBase).charAt(0).toUpperCase() + stripOrderPrefix(relBase).slice(1).replace(/-/g, " "),
1964
+ icon: navMeta.icon,
1965
+ order: navMeta.order ?? extractOrderPrefix(relBase),
1966
+ navigation: navMeta.navigation,
1967
+ hidden: navMeta.hidden
1968
+ } : void 0);
1969
+ for (const entry of entries) {
1970
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
1971
+ const subRel = relBase ? `${relBase}/${entry.name}` : entry.name;
1972
+ if (entry.isDirectory()) {
1973
+ await processDir(`${dir}/${entry.name}`, subRel, currentSectionNavMeta);
1974
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1975
+ const combinedExclude = [
1976
+ ...Array.isArray(cmsConfig?.exclude) ? cmsConfig.exclude : cmsConfig?.exclude ? [cmsConfig.exclude] : [],
1977
+ ...Array.isArray(source.exclude) ? source.exclude : source.exclude ? [source.exclude] : []
1978
+ ];
1979
+ if (!matchesSourceFilters(subRel, source.include, combinedExclude)) continue;
1980
+ const filePath = `${dir}/${entry.name}`;
1981
+ const rawContent = await readFile(filePath);
1982
+ const fileHash = await computeContentHash(rawContent);
1983
+ const { data: fm, content: body } = parseFrontmatter(rawContent);
1984
+ const rawRelSegments = subRel.replace(/\.md$/, "").split("/").filter(Boolean);
1985
+ const isReadme = entry.name.toLowerCase() === "readme.md";
1986
+ const isIndex = entry.name.replace(/\.md$/, "") === "index" || entry.name.replace(/\.md$/, "") === "01.index" || isReadme;
1987
+ const groupSegment = rawRelSegments.slice(0, -1).find(isGroupSegment);
1988
+ let groupMeta;
1989
+ if (groupSegment) {
1990
+ const groupOrder = extractOrderPrefix(groupSegment);
1991
+ const defaultGroupTitle = extractGroupTitle(groupSegment);
1992
+ const groupKey = stripOrderPrefix(groupSegment).replace(/^\(|\)$/g, "");
1993
+ const currentDirName = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop();
1994
+ const isGroupDir = currentDirName === groupSegment;
1995
+ groupMeta = {
1996
+ key: groupKey,
1997
+ title: isGroupDir && navMeta.title ? navMeta.title : defaultGroupTitle,
1998
+ icon: isGroupDir && navMeta.icon ? navMeta.icon : void 0,
1999
+ order: groupOrder
2000
+ };
2001
+ }
2002
+ const cleanedRelSegments = rawRelSegments.filter((seg) => !isGroupSegment(seg)).map(stripOrderPrefix).filter(Boolean);
2003
+ if (isIndex && cleanedRelSegments.length > 0) {
2004
+ cleanedRelSegments.pop();
2005
+ }
2006
+ const prefixSegments = (source.normalizedPrefix || "/").split("/").filter(Boolean);
2007
+ const combinedSegments = [...prefixSegments, ...cleanedRelSegments];
2008
+ const entryPath = "/" + combinedSegments.join("/");
2009
+ const defaultCollectionId = prefixSegments[0] || "content";
2010
+ const collectionId = resolveCollectionForPath(
2011
+ entryPath,
2012
+ collectionsConfig,
2013
+ defaultCollectionId
2014
+ );
2015
+ const slug = fm.slug || cleanedRelSegments[cleanedRelSegments.length - 1] || prefixSegments[prefixSegments.length - 1] || "index";
2016
+ const title = fm.title || slug;
2017
+ const srcOrder = typeof source.order === "number" ? source.order : 0;
2018
+ const existingCol = await db.prepare("SELECT id FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
2019
+ if (!existingCol) {
2020
+ const colName = collectionId.charAt(0).toUpperCase() + collectionId.slice(1).replace(/-/g, " ");
2021
+ await db.prepare(
2022
+ `INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, "order", created_at, updated_at) VALUES (?, ?, ?, '{}', 'file', ?, ?, ?)`
2023
+ ).bind(
2024
+ collectionId,
2025
+ colName,
2026
+ `Auto-discovered ${colName} collection`,
2027
+ srcOrder,
2028
+ now,
2029
+ now
2030
+ ).run();
2031
+ }
2032
+ const toc = extractTOC(body);
2033
+ const { ast, html: contentHtml } = await processMarkdown(body);
2034
+ const description = fm.description || null;
2035
+ const coverImage = fm.coverImage || fm.cover_image || null;
2036
+ const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
2037
+ const folderNum = rawRelSegments.length > 1 ? extractOrderPrefix(rawRelSegments[0]) : void 0;
2038
+ const effectiveFolderNum = folderNum ?? (typeof currentSectionNavMeta?.order === "number" ? currentSectionNavMeta.order : void 0);
2039
+ const folderOrder = effectiveFolderNum != null ? effectiveFolderNum * 1e3 : 5e4;
2040
+ let subfolderOffset = 0;
2041
+ if (rawRelSegments.length > 2) {
2042
+ const subDir = rawRelSegments[1];
2043
+ const subDirNum = extractOrderPrefix(subDir);
2044
+ const subOrder = subDirNum ?? (typeof groupMeta?.order === "number" ? groupMeta.order : typeof navMeta?.order === "number" ? navMeta.order : void 0);
2045
+ subfolderOffset = 500 + (subOrder != null ? subOrder * 50 : 100);
2046
+ }
2047
+ const fileNum = extractOrderPrefix(entry.name);
2048
+ const itemOrder = typeof fm.order === "number" ? fm.order : isIndex ? 0 : fileNum ?? (subfolderOffset > 0 ? 10 + orderCounter++ : 100 + orderCounter++);
2049
+ const order = srcOrder * 1e5 + folderOrder + subfolderOffset + itemOrder;
2050
+ const itemTitle = fm.navigation?.title || fm.title || slug;
2051
+ const {
2052
+ slug: _s,
2053
+ title: _t,
2054
+ description: _d,
2055
+ coverImage: _ci,
2056
+ tags: _ta,
2057
+ order: _o,
2058
+ ...rest
2059
+ } = fm;
2060
+ void _s;
2061
+ void _t;
2062
+ void _d;
2063
+ void _ci;
2064
+ void _ta;
2065
+ void _o;
2066
+ const effectiveSection = currentSectionNavMeta?.title ? { title: currentSectionNavMeta.title, icon: currentSectionNavMeta.icon } : navMeta.title ? { title: navMeta.title, icon: navMeta.icon } : void 0;
2067
+ const isDraft = Boolean(
2068
+ fm.draft === true || fm.private === true || fm.status === "draft" || navMeta.draft === true || navMeta.private === true
2069
+ );
2070
+ const entryStatus = isDraft ? "draft" : "published";
2071
+ const resolvedRest = { ...rest };
2072
+ try {
2073
+ await resolveReferenceFieldsFromFrontmatter(event, collectionId, resolvedRest);
2074
+ } catch {
2075
+ }
2076
+ const metadataJson = JSON.stringify({
2077
+ ...resolvedRest,
2078
+ navigation: fm.navigation,
2079
+ hidden: fm.hidden,
2080
+ draft: isDraft,
2081
+ _ast: ast,
2082
+ _toc: { links: toc },
2083
+ _nav: navMeta,
2084
+ _section: effectiveSection,
2085
+ _sectionOrder: effectiveFolderNum,
2086
+ _group: groupMeta,
2087
+ _itemTitle: itemTitle,
2088
+ _isIndex: isIndex,
2089
+ _source: source.from || "unknown",
2090
+ _sourceLabel: source.label || source.from || "source",
2091
+ _sourcePriority: source.priority ?? 0,
2092
+ _sourceOrder: source.order ?? srcOrder
2093
+ });
2094
+ const existing = await db.prepare(
2095
+ "SELECT id, source, content_hash, metadata_json, file_path, status FROM _qarpeo_cms_entries WHERE collection_id = ? AND path = ?"
2096
+ ).bind(collectionId, entryPath).first();
2097
+ if (!existing) {
2098
+ const id = nanoid();
2099
+ await db.prepare(
2100
+ `INSERT INTO _qarpeo_cms_entries
2101
+ (id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
2102
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'file', ?, ?, ?, ?, ?)`
2103
+ ).bind(
2104
+ id,
2105
+ collectionId,
2106
+ slug,
2107
+ entryPath,
2108
+ title,
2109
+ description,
2110
+ coverImage,
2111
+ tags,
2112
+ order,
2113
+ body,
2114
+ contentHtml,
2115
+ metadataJson,
2116
+ entryStatus,
2117
+ filePath,
2118
+ fileHash,
2119
+ isDraft ? null : now,
2120
+ now,
2121
+ now
2122
+ ).run();
2123
+ inserted++;
2124
+ } else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson || existing.file_path !== filePath || existing.status !== entryStatus)) {
2125
+ await db.prepare(
2126
+ `UPDATE _qarpeo_cms_entries SET
2127
+ status = ?, slug = ?, path = ?, title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
2128
+ content_md = ?, content_html = ?, metadata_json = ?,
2129
+ file_path = ?, content_hash = ?, source = 'file', updated_at = ?
2130
+ WHERE id = ?`
2131
+ ).bind(
2132
+ entryStatus,
2133
+ slug,
2134
+ entryPath,
2135
+ title,
2136
+ description,
2137
+ coverImage,
2138
+ tags,
2139
+ order,
2140
+ body,
2141
+ contentHtml,
2142
+ metadataJson,
2143
+ filePath,
2144
+ fileHash,
2145
+ now,
2146
+ existing.id
2147
+ ).run();
2148
+ synced++;
2149
+ } else {
2150
+ skipped++;
2151
+ }
2152
+ }
2153
+ }
2154
+ };
2155
+ await processDir(source.resolvedDir, "");
2156
+ }
2157
+ const lowPrioritySources = (sources || []).filter((s) => (s.priority ?? 0) < 10);
2158
+ const highPrioritySources = (sources || []).filter((s) => (s.priority ?? 0) >= 10);
2159
+ for (const src of lowPrioritySources) {
2160
+ await syncSource(src);
2161
+ }
2162
+ const targetCollections = {};
2163
+ for (const [id, cfg] of Object.entries(collectionsConfig)) {
2164
+ targetCollections[id] = {
2165
+ directory: cfg.directory || `${contentDir}/${id}`,
2166
+ name: cfg.name,
2167
+ type: cfg.type || "content",
2168
+ order: typeof cfg.order === "number" ? cfg.order : 0
2169
+ };
2170
+ }
2171
+ const rootMdFiles = [];
2172
+ try {
2173
+ const rootItems = await readdir(contentDir);
2174
+ for (const item of rootItems) {
2175
+ if (item.isDirectory()) {
2176
+ const colId = stripOrderPrefix(item.name).toLowerCase();
2177
+ if (!targetCollections[colId]) {
2178
+ targetCollections[colId] = {
2179
+ directory: `${contentDir}/${item.name}`,
2180
+ name: item.name.charAt(0).toUpperCase() + item.name.slice(1).replace(/-/g, " "),
2181
+ type: "content",
2182
+ order: extractOrderPrefix(item.name) ?? 0
2183
+ };
2184
+ }
2185
+ } else if (item.isFile() && (item.name.endsWith(".md") || item.name.endsWith(".json") || item.name.endsWith(".yml") || item.name.endsWith(".yaml"))) {
2186
+ if (item.name.endsWith(".md")) {
2187
+ rootMdFiles.push(item);
2188
+ } else {
2189
+ const colId = item.name.replace(/\.(json|ya?ml)$/, "").toLowerCase();
2190
+ if (!targetCollections[colId]) {
2191
+ targetCollections[colId] = {
2192
+ directory: `${contentDir}/${item.name}`,
2193
+ name: colId.charAt(0).toUpperCase() + colId.slice(1).replace(/-/g, " "),
2194
+ type: "content",
2195
+ order: 0,
2196
+ isSingleDataFile: true
2197
+ };
2198
+ }
2199
+ }
2200
+ }
2201
+ }
2202
+ } catch {
2203
+ return { synced: 0, skipped: 0, inserted: 0 };
2204
+ }
2205
+ if (rootMdFiles.length > 0 && !targetCollections["content"]) {
2206
+ targetCollections["content"] = {
2207
+ directory: contentDir,
2208
+ name: "Content",
2209
+ type: "page",
2210
+ order: 100
2211
+ };
2212
+ }
2213
+ for (const [collectionId, meta] of Object.entries(targetCollections)) {
2214
+ const isPageType = meta.type === "page";
2215
+ const folderSchema = await readSchemaJson(meta.directory);
2216
+ const initialFields = folderSchema?.fields ? JSON.stringify(folderSchema.fields) : "{}";
2217
+ const existingCol = await db.prepare("SELECT id, source, schema_json FROM _qarpeo_cms_collections WHERE id = ?").bind(collectionId).first();
2218
+ if (!existingCol) {
2219
+ const colName = meta.name || collectionId.charAt(0).toUpperCase() + collectionId.slice(1).replace(/-/g, " ");
2220
+ await db.prepare(
2221
+ `INSERT INTO _qarpeo_cms_collections (id, name, description, schema_json, source, "order", created_at, updated_at) VALUES (?, ?, ?, ?, 'file', ?, ?, ?)`
2222
+ ).bind(
2223
+ collectionId,
2224
+ colName,
2225
+ `Auto-discovered ${colName} collection`,
2226
+ initialFields,
2227
+ meta.order,
2228
+ now,
2229
+ now
2230
+ ).run();
2231
+ } else if (folderSchema?.fields && existingCol.schema_json !== initialFields && existingCol.source === "file") {
2232
+ await db.prepare("UPDATE _qarpeo_cms_collections SET schema_json = ?, updated_at = ? WHERE id = ?").bind(initialFields, now, collectionId).run();
2233
+ }
2234
+ if (meta.isSingleDataFile) {
2235
+ const filePath = meta.directory;
2236
+ let rawContent = "";
2237
+ try {
2238
+ rawContent = await readFile(filePath);
2239
+ } catch {
2240
+ continue;
2241
+ }
2242
+ const fileHash = await computeContentHash(rawContent);
2243
+ let parsed = {};
2244
+ if (filePath.endsWith(".json")) {
2245
+ try {
2246
+ parsed = JSON.parse(rawContent);
2247
+ } catch {
2248
+ }
2249
+ } else {
2250
+ try {
2251
+ const { parse: parseYaml2 } = await import("yaml");
2252
+ parsed = parseYaml2(rawContent) || {};
2253
+ } catch {
2254
+ }
2255
+ }
2256
+ const slug = parsed.slug || collectionId;
2257
+ const title = parsed.title || slug;
2258
+ const description = parsed.description || null;
2259
+ const entryPath = `/${collectionId}`;
2260
+ const metadataJson = JSON.stringify(parsed);
2261
+ const existing = await db.prepare(
2262
+ "SELECT id, source, content_hash, metadata_json, file_path FROM _qarpeo_cms_entries WHERE collection_id = ? AND slug = ?"
2263
+ ).bind(collectionId, slug).first();
2264
+ if (!existing) {
2265
+ const id = nanoid();
2266
+ await db.prepare(
2267
+ `INSERT INTO _qarpeo_cms_entries
2268
+ (id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
2269
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, '', '', ?, 'published', 'file', ?, ?, ?, ?, ?)`
2270
+ ).bind(
2271
+ id,
2272
+ collectionId,
2273
+ slug,
2274
+ entryPath,
2275
+ title,
2276
+ description,
2277
+ null,
2278
+ null,
2279
+ metadataJson,
2280
+ filePath,
2281
+ fileHash,
2282
+ now,
2283
+ now,
2284
+ now
2285
+ ).run();
2286
+ inserted++;
2287
+ } else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson)) {
2288
+ await db.prepare(
2289
+ `UPDATE _qarpeo_cms_entries SET
2290
+ slug = ?, path = ?, title = ?, description = ?,
2291
+ metadata_json = ?, file_path = ?, content_hash = ?, source = 'file', updated_at = ?
2292
+ WHERE id = ?`
2293
+ ).bind(
2294
+ slug,
2295
+ entryPath,
2296
+ title,
2297
+ description,
2298
+ metadataJson,
2299
+ filePath,
2300
+ fileHash,
2301
+ now,
2302
+ existing.id
2303
+ ).run();
2304
+ synced++;
2305
+ } else {
2306
+ skipped++;
2307
+ }
2308
+ continue;
2309
+ }
2310
+ if (isPageType) {
2311
+ let orderCounter = 0;
2312
+ const processDir = async (dir, relBase) => {
2313
+ let entries;
2314
+ try {
2315
+ entries = await readdir(dir);
2316
+ } catch {
2317
+ return;
2318
+ }
2319
+ entries.sort((a, b) => a.name.localeCompare(b.name, void 0, { numeric: true }));
2320
+ for (const entry of entries) {
2321
+ if (entry.name.startsWith(".")) continue;
2322
+ if (entry.isDirectory()) {
2323
+ if (collectionId === "content") continue;
2324
+ await processDir(`${dir}/${entry.name}`, `${relBase}/${entry.name}`);
2325
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
2326
+ const filePath = `${dir}/${entry.name}`;
2327
+ const rawContent = await readFile(filePath);
2328
+ const fileHash = await computeContentHash(rawContent);
2329
+ const { data: fm, content: body } = parseFrontmatter(rawContent);
2330
+ const relFilePath = `${relBase}/${entry.name}`.replace(/^\//, "");
2331
+ const slug = fm.slug || stripOrderPrefix(entry.name.replace(/\.md$/, ""));
2332
+ const title = fm.title || slug;
2333
+ const navJson = await readNavJson(dir);
2334
+ const navMeta = {
2335
+ title: navJson.title,
2336
+ icon: navJson.icon
2337
+ };
2338
+ const rawRelSegments = relFilePath.replace(/\.md$/, "").split("/").filter(Boolean);
2339
+ const groupSegment = rawRelSegments.slice(0, -1).find(isGroupSegment);
2340
+ let groupMeta;
2341
+ if (groupSegment) {
2342
+ const groupOrder = extractOrderPrefix(groupSegment);
2343
+ const defaultGroupTitle = extractGroupTitle(groupSegment);
2344
+ const groupKey = stripOrderPrefix(groupSegment).replace(/^\(|\)$/g, "");
2345
+ const currentDirName = dir.replace(/\\/g, "/").split("/").filter(Boolean).pop();
2346
+ const isGroupDir = currentDirName === groupSegment;
2347
+ groupMeta = {
2348
+ key: groupKey,
2349
+ title: isGroupDir && navMeta.title ? navMeta.title : defaultGroupTitle,
2350
+ icon: isGroupDir && navMeta.icon ? navMeta.icon : void 0,
2351
+ order: groupOrder
2352
+ };
2353
+ }
2354
+ const entryPath = computeEntryPath(relFilePath, collectionId);
2355
+ const targetCollectionId = resolveCollectionForPath(
2356
+ entryPath,
2357
+ collectionsConfig,
2358
+ collectionId
2359
+ );
2360
+ const toc = extractTOC(body);
2361
+ const { ast, html: contentHtml } = await processMarkdown(body);
2362
+ const description = fm.description || null;
2363
+ const coverImage = fm.coverImage || fm.cover_image || null;
2364
+ const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
2365
+ const order = fm.order ?? orderCounter++;
2366
+ const isIndex = entry.name.replace(/\.md$/, "") === "01.index" || entry.name.replace(/\.md$/, "") === "index";
2367
+ const itemTitle = fm.navigation?.title || fm.title || slug;
2368
+ const {
2369
+ slug: _s,
2370
+ title: _t,
2371
+ description: _d,
2372
+ coverImage: _ci,
2373
+ tags: _ta,
2374
+ order: _o,
2375
+ ...rest
2376
+ } = fm;
2377
+ void _s;
2378
+ void _t;
2379
+ void _d;
2380
+ void _ci;
2381
+ void _ta;
2382
+ void _o;
2383
+ const metadataJson = JSON.stringify({
2384
+ ...rest,
2385
+ _ast: ast,
2386
+ _toc: { links: toc },
2387
+ _nav: navMeta,
2388
+ _section: navMeta.title ? { title: navMeta.title, icon: navMeta.icon } : void 0,
2389
+ _group: groupMeta,
2390
+ _itemTitle: itemTitle,
2391
+ _isIndex: isIndex
2392
+ });
2393
+ const existing = await db.prepare(
2394
+ "SELECT id, source, content_hash, metadata_json, file_path FROM _qarpeo_cms_entries WHERE collection_id = ? AND path = ?"
2395
+ ).bind(targetCollectionId, entryPath).first();
2396
+ if (!existing) {
2397
+ const id = nanoid();
2398
+ await db.prepare(
2399
+ `INSERT INTO _qarpeo_cms_entries
2400
+ (id, collection_id, slug, path, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
2401
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', 'file', ?, ?, ?, ?, ?)`
2402
+ ).bind(
2403
+ id,
2404
+ targetCollectionId,
2405
+ slug,
2406
+ entryPath,
2407
+ title,
2408
+ description,
2409
+ coverImage,
2410
+ tags,
2411
+ order,
2412
+ body,
2413
+ contentHtml,
2414
+ metadataJson,
2415
+ filePath,
2416
+ fileHash,
2417
+ now,
2418
+ now,
2419
+ now
2420
+ ).run();
2421
+ inserted++;
2422
+ } else if (existing.source === "file" && (existing.content_hash !== fileHash || existing.metadata_json !== metadataJson || existing.file_path !== filePath)) {
2423
+ await db.prepare(
2424
+ `UPDATE _qarpeo_cms_entries SET
2425
+ slug = ?, path = ?, title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
2426
+ content_md = ?, content_html = ?, metadata_json = ?,
2427
+ file_path = ?, content_hash = ?, source = 'file', updated_at = ?
2428
+ WHERE id = ?`
2429
+ ).bind(
2430
+ slug,
2431
+ entryPath,
2432
+ title,
2433
+ description,
2434
+ coverImage,
2435
+ tags,
2436
+ order,
2437
+ body,
2438
+ contentHtml,
2439
+ metadataJson,
2440
+ filePath,
2441
+ fileHash,
2442
+ now,
2443
+ existing.id
2444
+ ).run();
2445
+ synced++;
2446
+ } else {
2447
+ skipped++;
2448
+ }
2449
+ }
2450
+ }
2451
+ };
2452
+ await processDir(meta.directory, "");
2453
+ } else {
2454
+ let entries;
2455
+ try {
2456
+ entries = await readdir(meta.directory);
2457
+ } catch {
2458
+ continue;
2459
+ }
2460
+ for (const entry of entries) {
2461
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
2462
+ const filePath = `${meta.directory}/${entry.name}`;
2463
+ const rawContent = await readFile(filePath);
2464
+ const fileHash = await computeContentHash(rawContent);
2465
+ const { data: fm, content: body } = parseFrontmatter(rawContent);
2466
+ const slug = fm.slug || entry.name.replace(/\.md$/, "");
2467
+ const title = fm.title || slug;
2468
+ const { ast, html: contentHtml } = await processMarkdown(body);
2469
+ const description = fm.description || null;
2470
+ const coverImage = fm.coverImage || fm.cover_image || null;
2471
+ const tags = Array.isArray(fm.tags) ? fm.tags.join(",") : fm.tags || null;
2472
+ const order = fm.order || 0;
2473
+ const {
2474
+ slug: _s,
2475
+ title: _t,
2476
+ description: _d,
2477
+ coverImage: _ci,
2478
+ tags: _ta,
2479
+ order: _o,
2480
+ ...rest
2481
+ } = fm;
2482
+ void _s;
2483
+ void _t;
2484
+ void _d;
2485
+ void _ci;
2486
+ void _ta;
2487
+ void _o;
2488
+ const metadataJson = JSON.stringify({
2489
+ ...rest,
2490
+ _ast: ast,
2491
+ _source: "local",
2492
+ _sourceLabel: "content",
2493
+ _sourcePriority: 10
2494
+ });
2495
+ const existing = await db.prepare(
2496
+ "SELECT id, source, content_hash FROM _qarpeo_cms_entries WHERE collection_id = ? AND slug = ?"
2497
+ ).bind(collectionId, slug).first();
2498
+ if (!existing) {
2499
+ const id = nanoid();
2500
+ await db.prepare(
2501
+ `INSERT INTO _qarpeo_cms_entries
2502
+ (id, collection_id, slug, title, description, cover_image, tags, "order", content_md, content_html, metadata_json, status, source, file_path, content_hash, published_at, created_at, updated_at)
2503
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', 'file', ?, ?, ?, ?, ?)`
2504
+ ).bind(
2505
+ id,
2506
+ collectionId,
2507
+ slug,
2508
+ title,
2509
+ description,
2510
+ coverImage,
2511
+ tags,
2512
+ order,
2513
+ body,
2514
+ contentHtml,
2515
+ metadataJson,
2516
+ filePath,
2517
+ fileHash,
2518
+ now,
2519
+ now,
2520
+ now
2521
+ ).run();
2522
+ inserted++;
2523
+ } else if (existing.source === "file" && existing.content_hash !== fileHash) {
2524
+ await db.prepare(
2525
+ `UPDATE _qarpeo_cms_entries SET
2526
+ title = ?, description = ?, cover_image = ?, tags = ?, "order" = ?,
2527
+ content_md = ?, content_html = ?, metadata_json = ?,
2528
+ file_path = ?, content_hash = ?, source = 'file', updated_at = ?
2529
+ WHERE id = ?`
2530
+ ).bind(
2531
+ title,
2532
+ description,
2533
+ coverImage,
2534
+ tags,
2535
+ order,
2536
+ body,
2537
+ contentHtml,
2538
+ metadataJson,
2539
+ filePath,
2540
+ fileHash,
2541
+ now,
2542
+ existing.id
2543
+ ).run();
2544
+ synced++;
2545
+ } else {
2546
+ skipped++;
2547
+ }
2548
+ }
2549
+ }
2550
+ }
2551
+ for (const src of highPrioritySources) {
2552
+ await syncSource(src);
2553
+ }
2554
+ return { synced, skipped, inserted };
2555
+ }
2556
+ export async function getCMSEntryByPath(event, collectionId, path, locale, options = {}) {
2557
+ await ensureCMSSynced(event);
2558
+ const db = getCMSDb(event);
2559
+ const cleanPath = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2560
+ const tenantId = getCMSTenantId(event);
2561
+ const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
2562
+ let allowDraft = false;
2563
+ const previewCookie = event ? getCookie(event, "qarpeo_preview") : null;
2564
+ let cookieObj = null;
2565
+ if (previewCookie) {
2566
+ try {
2567
+ cookieObj = typeof previewCookie === "string" ? JSON.parse(previewCookie) : previewCookie;
2568
+ } catch {
2569
+ }
2570
+ }
2571
+ const isPreviewRequested = options.preview || Boolean(cookieObj);
2572
+ if (isPreviewRequested) {
2573
+ const auth = await getAuthContext(event).catch(() => null);
2574
+ const config = getCMSConfig(event);
2575
+ if (auth?.user && config.roles.some((r) => r === auth.role || auth.isAdmin)) {
2576
+ allowDraft = true;
2577
+ }
2578
+ if (!allowDraft) {
2579
+ const token = options.previewToken || cookieObj?.token;
2580
+ const expires = options.previewExpires || cookieObj?.expires;
2581
+ if (token && expires) {
2582
+ const candidate = await db.prepare(
2583
+ `SELECT id, slug, collection_id FROM _qarpeo_cms_entries WHERE (collection_id = ? OR ? = 'content') AND (path = ? OR path = ?) ${tenantClause} LIMIT 1`
2584
+ ).bind(collectionId, collectionId, cleanPath, `${cleanPath}/`).first();
2585
+ if (candidate) {
2586
+ allowDraft = await verifyPreviewToken(
2587
+ event,
2588
+ candidate.collection_id,
2589
+ candidate.id,
2590
+ token,
2591
+ expires
2592
+ ) || await verifyPreviewToken(
2593
+ event,
2594
+ candidate.collection_id,
2595
+ candidate.slug,
2596
+ token,
2597
+ expires
2598
+ );
2599
+ }
2600
+ }
2601
+ }
2602
+ }
2603
+ const statusFilter = allowDraft ? "status IN ('draft', 'published', 'scheduled')" : "status = 'published'";
2604
+ const statusOrder = allowDraft ? "CASE WHEN status = 'draft' THEN 1 WHEN status = 'scheduled' THEN 2 ELSE 3 END," : "";
2605
+ let row = null;
2606
+ if (locale) {
2607
+ const bindsLocale = tenantId ? [collectionId, cleanPath, `${cleanPath}/`, locale, tenantId] : [collectionId, cleanPath, `${cleanPath}/`, locale];
2608
+ row = await db.prepare(
2609
+ `SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND locale = ? AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
2610
+ ).bind(...bindsLocale).first();
2611
+ }
2612
+ const binds1 = tenantId ? [collectionId, cleanPath, `${cleanPath}/`, tenantId] : [collectionId, cleanPath, `${cleanPath}/`];
2613
+ if (!row) {
2614
+ row = await db.prepare(
2615
+ `SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} CASE WHEN locale = 'en' THEN 1 ELSE 2 END LIMIT 1`
2616
+ ).bind(...binds1).first();
2617
+ }
2618
+ if (!row && cleanPath.startsWith(`/${collectionId}`)) {
2619
+ const unPrefixed = cleanPath.slice(`/${collectionId}`.length) || "/";
2620
+ const binds2 = tenantId ? [collectionId, unPrefixed, `${unPrefixed}/`, tenantId] : [collectionId, unPrefixed, `${unPrefixed}/`];
2621
+ row = await db.prepare(
2622
+ `SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
2623
+ ).bind(...binds2).first();
2624
+ }
2625
+ if (!row) {
2626
+ const binds3 = tenantId ? [cleanPath, `${cleanPath}/`, tenantId] : [cleanPath, `${cleanPath}/`];
2627
+ row = await db.prepare(
2628
+ `SELECT * FROM _qarpeo_cms_entries WHERE (path = ? OR path = ?) AND ${statusFilter}${tenantClause} ORDER BY ${statusOrder} created_at DESC LIMIT 1`
2629
+ ).bind(...binds3).first();
2630
+ }
2631
+ if (!row) return null;
2632
+ const entry = rowToEntry(row);
2633
+ if ((!entry.body || !Array.isArray(entry.body.nodes)) && entry.contentMd) {
2634
+ const ast = await parseMarkdownAST(entry.contentMd);
2635
+ entry.body = {
2636
+ ...ast,
2637
+ value: entry.contentMd,
2638
+ content: entry.contentMd,
2639
+ toc: entry.toc || { links: [] }
2640
+ };
2641
+ }
2642
+ return entry;
2643
+ }
2644
+ export async function getCMSEntrySurroundings(event, collectionId, path, fields = []) {
2645
+ await ensureCMSSynced(event);
2646
+ const db = getCMSDb(event);
2647
+ const cleanPath = path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
2648
+ const pick = (entry) => {
2649
+ if (!entry) return null;
2650
+ if (!fields.length) return entry;
2651
+ const partial = { id: entry.id, slug: entry.slug, path: entry.path, title: entry.title };
2652
+ for (const f of fields) {
2653
+ if (f in entry) partial[f] = entry[f];
2654
+ }
2655
+ return partial;
2656
+ };
2657
+ try {
2658
+ const navTree = await getCMSNavigationTree(event, collectionId);
2659
+ const orderedPaths = [];
2660
+ const seenPaths = /* @__PURE__ */ new Set();
2661
+ const collectPaths = (items) => {
2662
+ for (const item of items) {
2663
+ if (item.path && item.type !== "group") {
2664
+ const norm = item.path.length > 1 && item.path.endsWith("/") ? item.path.slice(0, -1) : item.path;
2665
+ if (!seenPaths.has(norm)) {
2666
+ seenPaths.add(norm);
2667
+ orderedPaths.push(norm);
2668
+ }
2669
+ }
2670
+ if (item.children?.length) {
2671
+ collectPaths(item.children);
2672
+ }
2673
+ }
2674
+ };
2675
+ collectPaths(navTree);
2676
+ let idx2 = orderedPaths.indexOf(cleanPath);
2677
+ if (idx2 === -1) {
2678
+ idx2 = orderedPaths.indexOf(`${cleanPath}/`);
2679
+ }
2680
+ if (idx2 !== -1) {
2681
+ const prevPath = idx2 > 0 ? orderedPaths[idx2 - 1] : null;
2682
+ const nextPath = idx2 < orderedPaths.length - 1 ? orderedPaths[idx2 + 1] : null;
2683
+ const [prevEntry, nextEntry] = await Promise.all([
2684
+ prevPath ? getCMSEntryByPath(event, collectionId, prevPath) : Promise.resolve(null),
2685
+ nextPath ? getCMSEntryByPath(event, collectionId, nextPath) : Promise.resolve(null)
2686
+ ]);
2687
+ return [pick(prevEntry), pick(nextEntry)];
2688
+ }
2689
+ } catch (err) {
2690
+ console.warn("[qarpeo:cms] getCMSEntrySurroundings navTree lookup failed:", err);
2691
+ }
2692
+ const tenantId = getCMSTenantId(event);
2693
+ const tenantClause = tenantId ? " AND (tenant_id = ? OR tenant_id IS NULL)" : "";
2694
+ const binds = tenantId ? [collectionId, tenantId] : [collectionId];
2695
+ const all = await db.prepare(
2696
+ `SELECT * FROM _qarpeo_cms_entries WHERE collection_id = ? AND status = 'published'${tenantClause} ORDER BY "order" ASC`
2697
+ ).bind(...binds).all();
2698
+ const rows = (all.results || []).map(rowToEntry);
2699
+ const idx = rows.findIndex((e) => e.path === cleanPath || e.path === `${cleanPath}/`);
2700
+ if (idx === -1) return [null, null];
2701
+ return [pick(rows[idx - 1]), pick(rows[idx + 1])];
2702
+ }
2703
+ function pruneTreeLevels(items, currentLevel, maxLevel) {
2704
+ if (currentLevel >= maxLevel) {
2705
+ for (const item of items) {
2706
+ delete item.children;
2707
+ }
2708
+ return items;
2709
+ }
2710
+ for (const item of items) {
2711
+ if (item.children) {
2712
+ pruneTreeLevels(item.children, currentLevel + 1, maxLevel);
2713
+ }
2714
+ }
2715
+ return items;
2716
+ }
2717
+ const CMS_FIRST_CLASS_COLUMNS = /* @__PURE__ */ new Set([
2718
+ "id",
2719
+ "slug",
2720
+ "path",
2721
+ "title",
2722
+ "description",
2723
+ "tags",
2724
+ "status",
2725
+ "source"
2726
+ ]);
2727
+ function matchesWhereFilter(row, meta, where) {
2728
+ return Object.entries(where).every(([key, expected]) => {
2729
+ if (CMS_FIRST_CLASS_COLUMNS.has(key)) {
2730
+ const actual2 = row[key] ?? null;
2731
+ return actual2 === expected;
2732
+ }
2733
+ const actual = meta[key] !== void 0 ? meta[key] : null;
2734
+ return actual === expected;
2735
+ });
2736
+ }
2737
+ export async function getCMSNavigationTree(event, collectionId, options) {
2738
+ await ensureCMSSynced(event);
2739
+ const db = getCMSDb(event);
2740
+ if (collectionId === "content") {
2741
+ const config = getCMSConfig(event);
2742
+ const configuredKeys = Object.keys(config.collections || {});
2743
+ const collections = await listCMSCollections(event);
2744
+ const valid = collections.filter((c) => {
2745
+ if (c.id.startsWith("_") || c.id === "content") return false;
2746
+ const colConf = config.collections?.[c.id];
2747
+ if (colConf && (colConf.navigation === false || colConf.hidden === true)) {
2748
+ return false;
2749
+ }
2750
+ return true;
2751
+ });
2752
+ if (configuredKeys.length) {
2753
+ valid.sort((a, b) => {
2754
+ const ia = configuredKeys.indexOf(a.id);
2755
+ const ib = configuredKeys.indexOf(b.id);
2756
+ if (ia !== -1 && ib !== -1) return ia - ib;
2757
+ if (ia !== -1) return -1;
2758
+ if (ib !== -1) return 1;
2759
+ return (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name);
2760
+ });
2761
+ } else {
2762
+ valid.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name));
2763
+ }
2764
+ let items = valid.map((c) => ({
2765
+ title: c.name,
2766
+ path: `/${c.id}`
2767
+ }));
2768
+ const targetStatus2 = options?.status || "published";
2769
+ const navRootTenantId = getCMSTenantId(event);
2770
+ let rootSql = `SELECT id, slug, path, title, "order", metadata_json, status FROM _qarpeo_cms_entries WHERE collection_id = 'content'`;
2771
+ const rootBinds = [];
2772
+ if (targetStatus2 !== "all") {
2773
+ rootSql += " AND status = ?";
2774
+ rootBinds.push(targetStatus2);
2775
+ }
2776
+ if (navRootTenantId) {
2777
+ rootSql += " AND (tenant_id = ? OR tenant_id IS NULL)";
2778
+ rootBinds.push(navRootTenantId);
2779
+ }
2780
+ rootSql += ' ORDER BY "order" ASC';
2781
+ const rootEntries = await db.prepare(rootSql).bind(...rootBinds).all();
2782
+ const rows2 = rootEntries.results || [];
2783
+ for (const r of rows2) {
2784
+ let meta = {};
2785
+ try {
2786
+ meta = JSON.parse(r.metadata_json || "{}");
2787
+ } catch {
2788
+ }
2789
+ if (meta.navigation === false || meta.hidden === true || meta.navigation?.hidden === true || meta._nav?.navigation === false || meta._nav?.hidden === true) {
2790
+ continue;
2791
+ }
2792
+ if (options?.where && !matchesWhereFilter(r, meta, options.where)) {
2793
+ continue;
2794
+ }
2795
+ const entryPath = r.path || (r.slug === "index" ? "/" : `/${r.slug}`);
2796
+ const title = meta.navigation?.title || meta._itemTitle || r.title;
2797
+ const icon = meta.navigation?.icon || meta._nav?.icon;
2798
+ items.push({
2799
+ title,
2800
+ path: entryPath,
2801
+ to: entryPath,
2802
+ icon
2803
+ });
2804
+ }
2805
+ if (options?.skip != null || options?.limit != null) {
2806
+ const skip = options?.skip ?? 0;
2807
+ const limit = options?.limit ?? items.length;
2808
+ items = items.slice(skip, skip + limit);
2809
+ }
2810
+ return items;
2811
+ }
2812
+ const targetStatus = options?.status || "published";
2813
+ const navTenantId = getCMSTenantId(event);
2814
+ let sql = 'SELECT id, slug, path, title, "order", metadata_json, status FROM _qarpeo_cms_entries WHERE collection_id = ?';
2815
+ const binds = [collectionId];
2816
+ if (targetStatus !== "all") {
2817
+ sql += " AND status = ?";
2818
+ binds.push(targetStatus);
2819
+ }
2820
+ if (navTenantId) {
2821
+ sql += " AND (tenant_id = ? OR tenant_id IS NULL)";
2822
+ binds.push(navTenantId);
2823
+ }
2824
+ if (options?.locale) {
2825
+ sql += " AND (locale = ? OR locale = 'en' OR locale IS NULL)";
2826
+ binds.push(options.locale);
2827
+ }
2828
+ if (options?.path) {
2829
+ const searchPath = ("/" + options.path.replace(/\\/g, "/").replace(/^\/+/, "")).replace(
2830
+ /\/+$/,
2831
+ ""
2832
+ );
2833
+ sql += " AND (path = ? OR path LIKE ?)";
2834
+ binds.push(searchPath, `${searchPath}/%`);
2835
+ }
2836
+ sql += ' ORDER BY "order" ASC';
2837
+ const all = await db.prepare(sql).bind(...binds).all();
2838
+ const rows = all.results || [];
2839
+ const sectionsMap = /* @__PURE__ */ new Map();
2840
+ for (const r of rows) {
2841
+ let meta = {};
2842
+ try {
2843
+ meta = JSON.parse(r.metadata_json || "{}");
2844
+ } catch {
2845
+ }
2846
+ const sectionMeta = meta._section || {};
2847
+ const isHiddenFromNav = meta.navigation === false || meta.hidden === true || meta.navigation?.hidden === true || meta._nav?.navigation === false || meta._nav?.hidden === true || sectionMeta?.navigation === false || sectionMeta?.hidden === true;
2848
+ if (isHiddenFromNav) {
2849
+ continue;
2850
+ }
2851
+ if (options?.where && !matchesWhereFilter(r, meta, options.where)) {
2852
+ continue;
2853
+ }
2854
+ const entryPath = r.path || `/${collectionId}/${r.slug}`;
2855
+ const segments = entryPath.split("/").filter(Boolean);
2856
+ const sectionPath = segments.length >= 2 ? `/${segments[0]}/${segments[1]}` : entryPath;
2857
+ const defaultSectionTitle = segments.length >= 2 ? segments[1].charAt(0).toUpperCase() + segments[1].slice(1).replace(/-/g, " ") : r.title;
2858
+ if (!sectionsMap.has(sectionPath)) {
2859
+ sectionsMap.set(sectionPath, {
2860
+ title: sectionMeta.title || defaultSectionTitle,
2861
+ path: sectionPath,
2862
+ icon: sectionMeta.icon,
2863
+ children: [],
2864
+ _order: meta._sectionOrder ?? (r.order ? Math.floor(r.order / 1e3) : 500)
2865
+ });
2866
+ }
2867
+ const section = sectionsMap.get(sectionPath);
2868
+ const itemTitle = meta._itemTitle || r.title;
2869
+ if (sectionMeta.title) {
2870
+ section.title = sectionMeta.title;
2871
+ if (sectionMeta.icon) section.icon = sectionMeta.icon;
2872
+ }
2873
+ const isSubFeature = segments.length >= 4 && !meta._isIndex && segments[0] === "docs" && segments[1] === "features";
2874
+ let targetParent = section;
2875
+ if (isSubFeature) {
2876
+ const subFeaturePath = `/${segments[0]}/${segments[1]}/${segments[2]}`;
2877
+ let subFeatureNode = section.children?.find((c) => c.path === subFeaturePath);
2878
+ if (!subFeatureNode) {
2879
+ const subFeatureTitle = segments[2].charAt(0).toUpperCase() + segments[2].slice(1).replace(/-/g, " ");
2880
+ const newNode = {
2881
+ title: subFeatureTitle,
2882
+ path: subFeaturePath,
2883
+ children: []
2884
+ };
2885
+ newNode._groupOrder = 500;
2886
+ section.children = section.children || [];
2887
+ section.children.push(newNode);
2888
+ subFeatureNode = newNode;
2889
+ }
2890
+ if (!subFeatureNode.children) {
2891
+ subFeatureNode.children = [];
2892
+ }
2893
+ targetParent = subFeatureNode;
2894
+ }
2895
+ if (meta._group) {
2896
+ const groupKey = meta._group.key;
2897
+ let groupItem = targetParent.children?.find((c) => c._groupKey === groupKey);
2898
+ if (!groupItem) {
2899
+ groupItem = {
2900
+ title: meta._group.title,
2901
+ icon: meta._group.icon,
2902
+ type: "group",
2903
+ children: [],
2904
+ _groupKey: groupKey,
2905
+ _groupOrder: meta._group.order ?? 999
2906
+ };
2907
+ targetParent.children = targetParent.children || [];
2908
+ targetParent.children.push(groupItem);
2909
+ }
2910
+ const gi = groupItem;
2911
+ gi.children = gi.children || [];
2912
+ gi.children.push({
2913
+ title: itemTitle,
2914
+ path: entryPath,
2915
+ icon: meta.navigation?.icon || meta._nav?.icon
2916
+ });
2917
+ continue;
2918
+ }
2919
+ const minDirectLen = isSubFeature ? 4 : 3;
2920
+ if (segments.length >= minDirectLen) {
2921
+ if (segments.length === minDirectLen) {
2922
+ targetParent.children = targetParent.children || [];
2923
+ if (meta._isIndex && targetParent !== section) {
2924
+ targetParent.title = itemTitle;
2925
+ if (meta.navigation?.icon || meta._nav?.icon) {
2926
+ targetParent.icon = meta.navigation?.icon || meta._nav?.icon;
2927
+ }
2928
+ continue;
2929
+ }
2930
+ targetParent.children.push({
2931
+ title: itemTitle,
2932
+ path: entryPath,
2933
+ icon: meta.navigation?.icon || meta._nav?.icon
2934
+ });
2935
+ } else {
2936
+ const subGroupKey = segments.slice(0, segments.length - 1).join("/");
2937
+ let subGroup = targetParent.children?.find((c) => c._subGroupKey === subGroupKey);
2938
+ if (!subGroup) {
2939
+ const subTitle = meta._nav?.title || sectionMeta.title || segments[segments.length - 2].charAt(0).toUpperCase() + segments[segments.length - 2].slice(1);
2940
+ const subOrder = typeof meta._nav?.order === "number" ? 10 + meta._nav.order : 800;
2941
+ subGroup = {
2942
+ title: subTitle,
2943
+ icon: meta._nav?.icon || sectionMeta.icon,
2944
+ type: "group",
2945
+ children: [],
2946
+ _subGroupKey: subGroupKey,
2947
+ _groupOrder: subOrder
2948
+ };
2949
+ targetParent.children = targetParent.children || [];
2950
+ targetParent.children.push(subGroup);
2951
+ }
2952
+ if (subGroup) {
2953
+ subGroup.children = subGroup.children || [];
2954
+ subGroup.children.push({
2955
+ title: itemTitle,
2956
+ path: entryPath,
2957
+ icon: meta.navigation?.icon || meta._nav?.icon
2958
+ });
2959
+ }
2960
+ }
2961
+ } else {
2962
+ targetParent.children = targetParent.children || [];
2963
+ targetParent.children.unshift({
2964
+ title: itemTitle,
2965
+ path: entryPath,
2966
+ icon: meta.navigation?.icon || meta._nav?.icon
2967
+ });
2968
+ }
2969
+ }
2970
+ function cleanAndSortNode(item) {
2971
+ if (item.path && !item.to) {
2972
+ item.to = item.path;
2973
+ }
2974
+ if (item.children) {
2975
+ item.children.sort((a, b) => {
2976
+ if (a.path === item.path) return -1;
2977
+ if (b.path === item.path) return 1;
2978
+ const orderA = a._groupOrder ?? 500;
2979
+ const orderB = b._groupOrder ?? 500;
2980
+ if (orderA !== orderB) return orderA - orderB;
2981
+ return 0;
2982
+ });
2983
+ for (const c of item.children) {
2984
+ if (c.path && !c.to) {
2985
+ c.to = c.path;
2986
+ }
2987
+ delete c._groupKey;
2988
+ delete c._groupOrder;
2989
+ delete c._subGroupKey;
2990
+ cleanAndSortNode(c);
2991
+ }
2992
+ }
2993
+ }
2994
+ for (const s of sectionsMap.values()) {
2995
+ cleanAndSortNode(s);
2996
+ }
2997
+ const sectionsList = Array.from(sectionsMap.values());
2998
+ sectionsList.sort((a, b) => {
2999
+ const orderA = a._order ?? 500;
3000
+ const orderB = b._order ?? 500;
3001
+ if (orderA !== orderB) return orderA - orderB;
3002
+ return a.title.localeCompare(b.title);
3003
+ });
3004
+ for (const s of sectionsList) {
3005
+ if (s.path && !s.to) {
3006
+ s.to = s.path;
3007
+ }
3008
+ delete s._order;
3009
+ }
3010
+ let result = sectionsList;
3011
+ if (options?.unwrap) {
3012
+ if (result.length === 1 && result[0].children?.length) {
3013
+ result = result[0].children;
3014
+ } else {
3015
+ const collectionRoot = result.find((item) => item.path === `/${collectionId}`);
3016
+ if (collectionRoot?.children?.length) {
3017
+ result = collectionRoot.children;
3018
+ }
3019
+ }
3020
+ }
3021
+ if (options?.path) {
3022
+ const searchPath = ("/" + options.path.replace(/\\/g, "/").replace(/^\/+/, "")).replace(
3023
+ /\/+$/,
3024
+ ""
3025
+ );
3026
+ const matchedSection = sectionsList.find((s) => s.path === searchPath);
3027
+ if (matchedSection && matchedSection.children?.length) {
3028
+ result = matchedSection.children;
3029
+ } else {
3030
+ const findNode = (items) => {
3031
+ for (const item of items) {
3032
+ if (item.path === searchPath) return item;
3033
+ if (item.children) {
3034
+ const found = findNode(item.children);
3035
+ if (found) return found;
3036
+ }
3037
+ }
3038
+ };
3039
+ const deepNode = findNode(sectionsList);
3040
+ if (deepNode) {
3041
+ result = deepNode.children?.length ? deepNode.children : [deepNode];
3042
+ }
3043
+ }
3044
+ }
3045
+ if (options?.levels != null && options.levels > 0) {
3046
+ pruneTreeLevels(result, 1, options.levels);
3047
+ }
3048
+ if (options?.skip != null || options?.limit != null) {
3049
+ const skip = options?.skip ?? 0;
3050
+ const limit = options?.limit ?? result.length;
3051
+ result = result.slice(skip, skip + limit);
3052
+ }
3053
+ return result;
3054
+ }
3055
+ export function buildFtsQuery(query, operator = "AND") {
3056
+ if (!query) return "";
3057
+ const clean = query.replace(/[^\p{L}\p{N}_\s]/gu, " ").trim();
3058
+ const rawTokens = clean.split(/\s+/).filter(Boolean);
3059
+ if (!rawTokens.length) return "";
3060
+ const reserved = /* @__PURE__ */ new Set(["and", "or", "not", "near"]);
3061
+ const tokens = rawTokens.filter((t) => !reserved.has(t.toLowerCase()));
3062
+ const effectiveTokens = tokens.length > 0 ? tokens : rawTokens;
3063
+ return effectiveTokens.map((t) => `"${t.replace(/"/g, '""')}"*`).join(` ${operator} `);
3064
+ }
3065
+ export async function rebuildCMSFTS(event) {
3066
+ const db = getCMSDb(event);
3067
+ try {
3068
+ await db.exec("DELETE FROM _qarpeo_cms_entries_fts;");
3069
+ await db.exec(
3070
+ "INSERT INTO _qarpeo_cms_entries_fts(id, slug, title, description, content_md, tags) SELECT id, slug, title, coalesce(description, ''), content_md, coalesce(tags, '') FROM _qarpeo_cms_entries;"
3071
+ );
3072
+ const res = await db.prepare("SELECT COUNT(*) as total FROM _qarpeo_cms_entries_fts").first();
3073
+ return { total: Number(res?.total || 0) };
3074
+ } catch (err) {
3075
+ console.warn("[qarpeo:cms] rebuildCMSFTS failed:", err);
3076
+ return { total: 0 };
3077
+ }
3078
+ }
3079
+ export async function searchDocs(event, options = {}) {
3080
+ await ensureCMSSynced(event);
3081
+ const db = getCMSDb(event);
3082
+ const q = (options.q || "").trim();
3083
+ if (!q) {
3084
+ return { results: [], total: 0, query: "", mode: "fts" };
3085
+ }
3086
+ const limit = Math.min(100, Math.max(1, options.limit || 20));
3087
+ const locale = options.locale;
3088
+ const tenantId = getCMSTenantId(event);
3089
+ const words = q.toLowerCase().split(/\s+/).filter(Boolean);
3090
+ let rows = [];
3091
+ let modeUsed = "fts";
3092
+ if (options.mode !== "like") {
3093
+ try {
3094
+ const ftsQuery = buildFtsQuery(q, "AND");
3095
+ if (ftsQuery) {
3096
+ let ftsSql = `
3097
+ SELECT e.id, e.collection_id, e.slug, e.path, e.title, e.description, e.content_md, e.metadata_json, e.locale, fts.rank as fts_rank
3098
+ FROM _qarpeo_cms_entries e
3099
+ JOIN _qarpeo_cms_entries_fts fts ON e.id = fts.id
3100
+ WHERE fts MATCH ? AND e.status = 'published'
3101
+ `;
3102
+ const ftsBinds = [ftsQuery];
3103
+ if (tenantId) {
3104
+ ftsSql += " AND (e.tenant_id = ? OR e.tenant_id IS NULL)";
3105
+ ftsBinds.push(tenantId);
3106
+ }
3107
+ if (options.collection) {
3108
+ ftsSql += " AND e.collection_id = ?";
3109
+ ftsBinds.push(options.collection);
3110
+ } else {
3111
+ ftsSql += " AND (e.collection_id = 'docs' OR e.collection_id = 'content' OR e.path LIKE '/docs%' OR e.collection_id LIKE '%doc%')";
3112
+ }
3113
+ if (locale) {
3114
+ ftsSql += " AND (e.locale = ? OR e.locale = 'en')";
3115
+ ftsBinds.push(locale);
3116
+ }
3117
+ if (options.version && options.version !== "all") {
3118
+ if (options.version === "latest") {
3119
+ ftsSql += " AND (e.version = 'latest' OR e.version IS NULL OR e.path NOT LIKE '%/docs/v%')";
3120
+ } else {
3121
+ ftsSql += " AND (e.version = ? OR e.path LIKE ?)";
3122
+ ftsBinds.push(options.version, `%/docs/${options.version}%`);
3123
+ }
3124
+ }
3125
+ ftsSql += " ORDER BY fts.rank LIMIT 50";
3126
+ const res = await db.prepare(ftsSql).bind(...ftsBinds).all();
3127
+ rows = res.results || [];
3128
+ if (rows.length === 0 && words.length > 1) {
3129
+ const ftsOrQuery = buildFtsQuery(q, "OR");
3130
+ if (ftsOrQuery && ftsOrQuery !== ftsQuery) {
3131
+ const orBinds = [...ftsBinds];
3132
+ orBinds[0] = ftsOrQuery;
3133
+ const orRes = await db.prepare(ftsSql).bind(...orBinds).all();
3134
+ rows = orRes.results || [];
3135
+ }
3136
+ }
3137
+ }
3138
+ } catch {
3139
+ rows = [];
3140
+ }
3141
+ }
3142
+ if (rows.length === 0) {
3143
+ modeUsed = "like";
3144
+ const searchPattern = `%${words.join("%")}%`;
3145
+ let sql = `
3146
+ SELECT id, collection_id, slug, path, title, description, content_md, metadata_json, locale
3147
+ FROM _qarpeo_cms_entries
3148
+ WHERE status = 'published'
3149
+ `;
3150
+ const binds = [];
3151
+ if (tenantId) {
3152
+ sql += " AND (tenant_id = ? OR tenant_id IS NULL)";
3153
+ binds.push(tenantId);
3154
+ }
3155
+ if (options.collection) {
3156
+ sql += " AND collection_id = ?";
3157
+ binds.push(options.collection);
3158
+ } else {
3159
+ sql += " AND (collection_id = 'docs' OR collection_id = 'content' OR path LIKE '/docs%' OR collection_id LIKE '%doc%')";
3160
+ }
3161
+ if (locale) {
3162
+ sql += " AND (locale = ? OR locale = 'en')";
3163
+ binds.push(locale);
3164
+ }
3165
+ if (options.version && options.version !== "all") {
3166
+ if (options.version === "latest") {
3167
+ sql += " AND (version = 'latest' OR version IS NULL OR path NOT LIKE '%/docs/v%')";
3168
+ } else {
3169
+ sql += " AND (version = ? OR path LIKE ?)";
3170
+ binds.push(options.version, `%/docs/${options.version}%`);
3171
+ }
3172
+ }
3173
+ sql += " AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ? OR LOWER(content_md) LIKE ? OR LOWER(tags) LIKE ?)";
3174
+ binds.push(searchPattern, searchPattern, searchPattern, searchPattern);
3175
+ sql += " LIMIT 50";
3176
+ const res = await db.prepare(sql).bind(...binds).all();
3177
+ rows = res.results || [];
3178
+ }
3179
+ const sections = [];
3180
+ for (const row of rows) {
3181
+ const pageTitle = row.title || "Documentation";
3182
+ const docPath = row.path || `/${row.collection_id}/${row.slug}`;
3183
+ const md = row.content_md || "";
3184
+ const headingLines = md.split("\n");
3185
+ const rawSections = [];
3186
+ let currentSection = {
3187
+ heading: pageTitle,
3188
+ level: 1,
3189
+ anchor: "",
3190
+ lines: []
3191
+ };
3192
+ rawSections.push(currentSection);
3193
+ for (const line of headingLines) {
3194
+ const match = line.match(/^(#{1,4})\s+(.+)$/);
3195
+ if (match) {
3196
+ const level = match[1].length;
3197
+ const headingText = match[2].trim().replace(/\s*\{#.*\}\s*$/, "");
3198
+ const slugAnchor = headingText.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-");
3199
+ currentSection = {
3200
+ heading: headingText,
3201
+ level,
3202
+ anchor: `#${slugAnchor}`,
3203
+ lines: []
3204
+ };
3205
+ rawSections.push(currentSection);
3206
+ } else {
3207
+ currentSection.lines.push(line);
3208
+ }
3209
+ }
3210
+ for (const sec of rawSections) {
3211
+ const rawText = sec.lines.join(" ");
3212
+ const cleanContent = rawText.replace(/```[\s\S]*?```/g, " ").replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/[*_~#]/g, "").replace(/\s+/g, " ").trim();
3213
+ const textLower = cleanContent.toLowerCase();
3214
+ const headingLower = sec.heading.toLowerCase();
3215
+ const pageTitleLower = pageTitle.toLowerCase();
3216
+ let score = 0;
3217
+ let matchIdx = -1;
3218
+ for (const w of words) {
3219
+ if (headingLower.includes(w)) {
3220
+ score += 15;
3221
+ }
3222
+ if (pageTitleLower.includes(w)) {
3223
+ score += 8;
3224
+ }
3225
+ const idx = textLower.indexOf(w);
3226
+ if (idx !== -1) {
3227
+ score += 5;
3228
+ if (matchIdx === -1 || idx < matchIdx) {
3229
+ matchIdx = idx;
3230
+ }
3231
+ }
3232
+ }
3233
+ if (score === 0 && row.fts_rank !== void 0 && sec === rawSections[0]) {
3234
+ score = 10;
3235
+ }
3236
+ if (score > 0) {
3237
+ let excerpt = "";
3238
+ if (matchIdx !== -1) {
3239
+ const start = Math.max(0, matchIdx - 40);
3240
+ const end = Math.min(cleanContent.length, matchIdx + 110);
3241
+ excerpt = (start > 0 ? "... " : "") + cleanContent.slice(start, end).trim() + (end < cleanContent.length ? " ..." : "");
3242
+ } else {
3243
+ excerpt = cleanContent.slice(0, 140) + (cleanContent.length > 140 ? " ..." : "");
3244
+ }
3245
+ const titles = sec.heading !== pageTitle ? [pageTitle, sec.heading] : [pageTitle];
3246
+ sections.push({
3247
+ id: `${docPath}${sec.anchor}`,
3248
+ path: docPath,
3249
+ title: sec.heading,
3250
+ pageTitle,
3251
+ titles,
3252
+ content: excerpt || row.description || pageTitle,
3253
+ section: sec.anchor || void 0,
3254
+ locale: row.locale || "en",
3255
+ level: sec.level || 1,
3256
+ score
3257
+ });
3258
+ }
3259
+ }
3260
+ }
3261
+ sections.sort((a, b) => b.score - a.score);
3262
+ return {
3263
+ results: sections.slice(0, limit),
3264
+ total: sections.length,
3265
+ query: q,
3266
+ mode: modeUsed
3267
+ };
3268
+ }