emdash 0.0.0-b → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (661) hide show
  1. package/README.md +87 -43
  2. package/dist/adapters-BLMa4JGD.d.mts +106 -0
  3. package/dist/adapters-BLMa4JGD.d.mts.map +1 -0
  4. package/dist/apply-Bjfq_b4-.mjs +1293 -0
  5. package/dist/apply-Bjfq_b4-.mjs.map +1 -0
  6. package/dist/astro/index.d.mts +51 -0
  7. package/dist/astro/index.d.mts.map +1 -0
  8. package/dist/astro/index.mjs +1336 -0
  9. package/dist/astro/index.mjs.map +1 -0
  10. package/dist/astro/middleware/auth.d.mts +31 -0
  11. package/dist/astro/middleware/auth.d.mts.map +1 -0
  12. package/dist/astro/middleware/auth.mjs +654 -0
  13. package/dist/astro/middleware/auth.mjs.map +1 -0
  14. package/dist/astro/middleware/redirect.d.mts +22 -0
  15. package/dist/astro/middleware/redirect.d.mts.map +1 -0
  16. package/dist/astro/middleware/redirect.mjs +63 -0
  17. package/dist/astro/middleware/redirect.mjs.map +1 -0
  18. package/dist/astro/middleware/request-context.d.mts +18 -0
  19. package/dist/astro/middleware/request-context.d.mts.map +1 -0
  20. package/dist/astro/middleware/request-context.mjs +1310 -0
  21. package/dist/astro/middleware/request-context.mjs.map +1 -0
  22. package/dist/astro/middleware/setup.d.mts +20 -0
  23. package/dist/astro/middleware/setup.d.mts.map +1 -0
  24. package/dist/astro/middleware/setup.mjs +47 -0
  25. package/dist/astro/middleware/setup.mjs.map +1 -0
  26. package/dist/astro/middleware.d.mts +13 -0
  27. package/dist/astro/middleware.d.mts.map +1 -0
  28. package/dist/astro/middleware.mjs +1613 -0
  29. package/dist/astro/middleware.mjs.map +1 -0
  30. package/dist/astro/types.d.mts +250 -0
  31. package/dist/astro/types.d.mts.map +1 -0
  32. package/dist/astro/types.mjs +1 -0
  33. package/dist/base64-MBPo9ozB.mjs +59 -0
  34. package/dist/base64-MBPo9ozB.mjs.map +1 -0
  35. package/dist/byline-CL847F26.mjs +213 -0
  36. package/dist/byline-CL847F26.mjs.map +1 -0
  37. package/dist/bylines-C2a-2TGt.mjs +136 -0
  38. package/dist/bylines-C2a-2TGt.mjs.map +1 -0
  39. package/dist/chunk-ClPoSABd.mjs +21 -0
  40. package/dist/cli/index.d.mts +1 -0
  41. package/dist/cli/index.mjs +3909 -0
  42. package/dist/cli/index.mjs.map +1 -0
  43. package/dist/client/cf-access.d.mts +60 -0
  44. package/dist/client/cf-access.d.mts.map +1 -0
  45. package/dist/client/cf-access.mjs +179 -0
  46. package/dist/client/cf-access.mjs.map +1 -0
  47. package/dist/client/index.d.mts +398 -0
  48. package/dist/client/index.d.mts.map +1 -0
  49. package/dist/client/index.mjs +346 -0
  50. package/dist/client/index.mjs.map +1 -0
  51. package/dist/config-CKE8p9xM.mjs +55 -0
  52. package/dist/config-CKE8p9xM.mjs.map +1 -0
  53. package/dist/connection-B4zVnQIa.mjs +40 -0
  54. package/dist/connection-B4zVnQIa.mjs.map +1 -0
  55. package/dist/content-D6C2WsZC.mjs +824 -0
  56. package/dist/content-D6C2WsZC.mjs.map +1 -0
  57. package/dist/db/index.d.mts +4 -0
  58. package/dist/db/index.mjs +62 -0
  59. package/dist/db/index.mjs.map +1 -0
  60. package/dist/db/libsql.d.mts +11 -0
  61. package/dist/db/libsql.d.mts.map +1 -0
  62. package/dist/db/libsql.mjs +17 -0
  63. package/dist/db/libsql.mjs.map +1 -0
  64. package/dist/db/postgres.d.mts +11 -0
  65. package/dist/db/postgres.d.mts.map +1 -0
  66. package/dist/db/postgres.mjs +30 -0
  67. package/dist/db/postgres.mjs.map +1 -0
  68. package/dist/db/sqlite.d.mts +11 -0
  69. package/dist/db/sqlite.d.mts.map +1 -0
  70. package/dist/db/sqlite.mjs +16 -0
  71. package/dist/db/sqlite.mjs.map +1 -0
  72. package/dist/default-Cyi4aAxu.mjs +81 -0
  73. package/dist/default-Cyi4aAxu.mjs.map +1 -0
  74. package/dist/dialect-helpers-B9uSp2GJ.mjs +90 -0
  75. package/dist/dialect-helpers-B9uSp2GJ.mjs.map +1 -0
  76. package/dist/error-Cxz0tQeO.mjs +27 -0
  77. package/dist/error-Cxz0tQeO.mjs.map +1 -0
  78. package/dist/index-C1xF3OGh.d.mts +4527 -0
  79. package/dist/index-C1xF3OGh.d.mts.map +1 -0
  80. package/dist/index.d.mts +16 -0
  81. package/dist/index.mjs +30 -0
  82. package/dist/load-yOOlckBj.mjs +28 -0
  83. package/dist/load-yOOlckBj.mjs.map +1 -0
  84. package/dist/loader-fz8Q_3EO.mjs +447 -0
  85. package/dist/loader-fz8Q_3EO.mjs.map +1 -0
  86. package/dist/manifest-schema-Dcl0R6nM.mjs +184 -0
  87. package/dist/manifest-schema-Dcl0R6nM.mjs.map +1 -0
  88. package/dist/media/index.d.mts +26 -0
  89. package/dist/media/index.d.mts.map +1 -0
  90. package/dist/media/index.mjs +55 -0
  91. package/dist/media/index.mjs.map +1 -0
  92. package/dist/media/local-runtime.d.mts +39 -0
  93. package/dist/media/local-runtime.d.mts.map +1 -0
  94. package/dist/media/local-runtime.mjs +133 -0
  95. package/dist/media/local-runtime.mjs.map +1 -0
  96. package/dist/media-DqHVh136.mjs +200 -0
  97. package/dist/media-DqHVh136.mjs.map +1 -0
  98. package/dist/mode-C2EzN1uE.mjs +23 -0
  99. package/dist/mode-C2EzN1uE.mjs.map +1 -0
  100. package/dist/page/index.d.mts +140 -0
  101. package/dist/page/index.d.mts.map +1 -0
  102. package/dist/page/index.mjs +416 -0
  103. package/dist/page/index.mjs.map +1 -0
  104. package/dist/placeholder-CmGAmqeO.d.mts +276 -0
  105. package/dist/placeholder-CmGAmqeO.d.mts.map +1 -0
  106. package/dist/placeholder-SmpOx-_v.mjs +243 -0
  107. package/dist/placeholder-SmpOx-_v.mjs.map +1 -0
  108. package/dist/plugin-utils.d.mts +58 -0
  109. package/dist/plugin-utils.d.mts.map +1 -0
  110. package/dist/plugin-utils.mjs +78 -0
  111. package/dist/plugin-utils.mjs.map +1 -0
  112. package/dist/plugins/adapt-sandbox-entry.d.mts +22 -0
  113. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -0
  114. package/dist/plugins/adapt-sandbox-entry.mjs +113 -0
  115. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -0
  116. package/dist/query-CS_iSj34.mjs +460 -0
  117. package/dist/query-CS_iSj34.mjs.map +1 -0
  118. package/dist/redirect-DIfIni3r.mjs +329 -0
  119. package/dist/redirect-DIfIni3r.mjs.map +1 -0
  120. package/dist/registry-D_w5HW4G.mjs +863 -0
  121. package/dist/registry-D_w5HW4G.mjs.map +1 -0
  122. package/dist/request-context.d.mts +49 -0
  123. package/dist/request-context.d.mts.map +1 -0
  124. package/dist/request-context.mjs +43 -0
  125. package/dist/request-context.mjs.map +1 -0
  126. package/dist/runner-C0hCbYnD.mjs +1412 -0
  127. package/dist/runner-C0hCbYnD.mjs.map +1 -0
  128. package/dist/runner-EAtf0ZIe.d.mts +27 -0
  129. package/dist/runner-EAtf0ZIe.d.mts.map +1 -0
  130. package/dist/runtime.d.mts +26 -0
  131. package/dist/runtime.d.mts.map +1 -0
  132. package/dist/runtime.mjs +42 -0
  133. package/dist/runtime.mjs.map +1 -0
  134. package/dist/search-DG603UrT.mjs +9211 -0
  135. package/dist/search-DG603UrT.mjs.map +1 -0
  136. package/dist/seed/index.d.mts +3 -0
  137. package/dist/seed/index.mjs +15 -0
  138. package/dist/seo/index.d.mts +70 -0
  139. package/dist/seo/index.d.mts.map +1 -0
  140. package/dist/seo/index.mjs +70 -0
  141. package/dist/seo/index.mjs.map +1 -0
  142. package/dist/storage/local.d.mts +39 -0
  143. package/dist/storage/local.d.mts.map +1 -0
  144. package/dist/storage/local.mjs +166 -0
  145. package/dist/storage/local.mjs.map +1 -0
  146. package/dist/storage/s3.d.mts +32 -0
  147. package/dist/storage/s3.d.mts.map +1 -0
  148. package/dist/storage/s3.mjs +175 -0
  149. package/dist/storage/s3.mjs.map +1 -0
  150. package/dist/tokens-DpgrkrXK.mjs +171 -0
  151. package/dist/tokens-DpgrkrXK.mjs.map +1 -0
  152. package/dist/transport-BFGblqwG.d.mts +42 -0
  153. package/dist/transport-BFGblqwG.d.mts.map +1 -0
  154. package/dist/transport-yxiQsi8I.mjs +418 -0
  155. package/dist/transport-yxiQsi8I.mjs.map +1 -0
  156. package/dist/types-BRuPJGdV.d.mts +102 -0
  157. package/dist/types-BRuPJGdV.d.mts.map +1 -0
  158. package/dist/types-C4-fAxN3.d.mts +182 -0
  159. package/dist/types-C4-fAxN3.d.mts.map +1 -0
  160. package/dist/types-CMMN0pNg.mjs +31 -0
  161. package/dist/types-CMMN0pNg.mjs.map +1 -0
  162. package/dist/types-CUBbjgmP.mjs +16 -0
  163. package/dist/types-CUBbjgmP.mjs.map +1 -0
  164. package/dist/types-DRjfYOEv.d.mts +426 -0
  165. package/dist/types-DRjfYOEv.d.mts.map +1 -0
  166. package/dist/types-DY5zk5HN.mjs +73 -0
  167. package/dist/types-DY5zk5HN.mjs.map +1 -0
  168. package/dist/types-DaNLHo_T.d.mts +184 -0
  169. package/dist/types-DaNLHo_T.d.mts.map +1 -0
  170. package/dist/types-DvhsUmSJ.d.mts +1111 -0
  171. package/dist/types-DvhsUmSJ.d.mts.map +1 -0
  172. package/dist/validate-CpBtVMsD.d.mts +378 -0
  173. package/dist/validate-CpBtVMsD.d.mts.map +1 -0
  174. package/dist/validate-CqRJb_xU.mjs +97 -0
  175. package/dist/validate-CqRJb_xU.mjs.map +1 -0
  176. package/dist/validate-O7PWmlnq.mjs +328 -0
  177. package/dist/validate-O7PWmlnq.mjs.map +1 -0
  178. package/locals.d.ts +46 -0
  179. package/package.json +233 -19
  180. package/src/api/authorize.ts +63 -0
  181. package/src/api/csrf.ts +48 -0
  182. package/src/api/error.ts +99 -0
  183. package/src/api/errors.ts +445 -0
  184. package/src/api/escape.ts +9 -0
  185. package/src/api/handlers/api-tokens.ts +240 -0
  186. package/src/api/handlers/comments.ts +314 -0
  187. package/src/api/handlers/content.ts +1315 -0
  188. package/src/api/handlers/dashboard.ts +205 -0
  189. package/src/api/handlers/device-flow.ts +684 -0
  190. package/src/api/handlers/index.ts +163 -0
  191. package/src/api/handlers/manifest.ts +158 -0
  192. package/src/api/handlers/marketplace.ts +930 -0
  193. package/src/api/handlers/media.ts +207 -0
  194. package/src/api/handlers/menus.ts +493 -0
  195. package/src/api/handlers/oauth-authorization.ts +429 -0
  196. package/src/api/handlers/oauth-clients.ts +349 -0
  197. package/src/api/handlers/oauth-user-lookup.ts +39 -0
  198. package/src/api/handlers/plugins.ts +254 -0
  199. package/src/api/handlers/redirects.ts +360 -0
  200. package/src/api/handlers/revision.ts +145 -0
  201. package/src/api/handlers/schema.ts +534 -0
  202. package/src/api/handlers/sections.ts +289 -0
  203. package/src/api/handlers/seo.ts +115 -0
  204. package/src/api/handlers/settings.ts +49 -0
  205. package/src/api/handlers/snapshot.ts +350 -0
  206. package/src/api/handlers/taxonomies.ts +523 -0
  207. package/src/api/index.ts +6 -0
  208. package/src/api/openapi/document.ts +2368 -0
  209. package/src/api/openapi/index.ts +1 -0
  210. package/src/api/parse.ts +139 -0
  211. package/src/api/redirect.ts +14 -0
  212. package/src/api/rev.ts +67 -0
  213. package/src/api/schemas/auth.ts +112 -0
  214. package/src/api/schemas/bylines.ts +85 -0
  215. package/src/api/schemas/comments.ts +117 -0
  216. package/src/api/schemas/common.ts +89 -0
  217. package/src/api/schemas/content.ts +191 -0
  218. package/src/api/schemas/import.ts +52 -0
  219. package/src/api/schemas/index.ts +17 -0
  220. package/src/api/schemas/media.ts +116 -0
  221. package/src/api/schemas/menus.ts +111 -0
  222. package/src/api/schemas/redirects.ts +155 -0
  223. package/src/api/schemas/schema.ts +203 -0
  224. package/src/api/schemas/search.ts +63 -0
  225. package/src/api/schemas/sections.ts +67 -0
  226. package/src/api/schemas/settings.ts +63 -0
  227. package/src/api/schemas/setup.ts +37 -0
  228. package/src/api/schemas/taxonomies.ts +113 -0
  229. package/src/api/schemas/users.ts +96 -0
  230. package/src/api/schemas/widgets.ts +80 -0
  231. package/src/api/site-url.ts +25 -0
  232. package/src/api/types.ts +82 -0
  233. package/src/astro/index.ts +27 -0
  234. package/src/astro/integration/index.ts +303 -0
  235. package/src/astro/integration/routes.ts +834 -0
  236. package/src/astro/integration/runtime.ts +338 -0
  237. package/src/astro/integration/virtual-modules.ts +469 -0
  238. package/src/astro/integration/vite-config.ts +335 -0
  239. package/src/astro/middleware/auth.ts +743 -0
  240. package/src/astro/middleware/redirect.ts +89 -0
  241. package/src/astro/middleware/request-context.ts +129 -0
  242. package/src/astro/middleware/setup.ts +89 -0
  243. package/src/astro/middleware.ts +398 -0
  244. package/src/astro/routes/PluginRegistry.tsx +15 -0
  245. package/src/astro/routes/admin.astro +81 -0
  246. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  247. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  248. package/src/astro/routes/api/admin/api-tokens/[id].ts +40 -0
  249. package/src/astro/routes/api/admin/api-tokens/index.ts +68 -0
  250. package/src/astro/routes/api/admin/bylines/[id]/index.ts +87 -0
  251. package/src/astro/routes/api/admin/bylines/index.ts +72 -0
  252. package/src/astro/routes/api/admin/comments/[id]/status.ts +116 -0
  253. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  254. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  255. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  256. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  257. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +91 -0
  258. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  259. package/src/astro/routes/api/admin/oauth-clients/[id].ts +110 -0
  260. package/src/astro/routes/api/admin/oauth-clients/index.ts +71 -0
  261. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +39 -0
  262. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +39 -0
  263. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  264. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +48 -0
  265. package/src/astro/routes/api/admin/plugins/[id]/update.ts +59 -0
  266. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  267. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +61 -0
  268. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  269. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +62 -0
  270. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  271. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  272. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  273. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +61 -0
  274. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  275. package/src/astro/routes/api/admin/users/[id]/disable.ts +69 -0
  276. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  277. package/src/astro/routes/api/admin/users/[id]/index.ts +146 -0
  278. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  279. package/src/astro/routes/api/admin/users/index.ts +66 -0
  280. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  281. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  282. package/src/astro/routes/api/auth/invite/complete.ts +84 -0
  283. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  284. package/src/astro/routes/api/auth/logout.ts +40 -0
  285. package/src/astro/routes/api/auth/magic-link/send.ts +89 -0
  286. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  287. package/src/astro/routes/api/auth/me.ts +60 -0
  288. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +219 -0
  289. package/src/astro/routes/api/auth/oauth/[provider].ts +119 -0
  290. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  291. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  292. package/src/astro/routes/api/auth/passkey/options.ts +82 -0
  293. package/src/astro/routes/api/auth/passkey/register/options.ts +86 -0
  294. package/src/astro/routes/api/auth/passkey/register/verify.ts +115 -0
  295. package/src/astro/routes/api/auth/passkey/verify.ts +66 -0
  296. package/src/astro/routes/api/auth/signup/complete.ts +85 -0
  297. package/src/astro/routes/api/auth/signup/request.ts +77 -0
  298. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  299. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +312 -0
  300. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  301. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +54 -0
  302. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +61 -0
  303. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +33 -0
  304. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  305. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +56 -0
  306. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +54 -0
  307. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  308. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +101 -0
  309. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +140 -0
  310. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +30 -0
  311. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +56 -0
  312. package/src/astro/routes/api/content/[collection]/[id].ts +137 -0
  313. package/src/astro/routes/api/content/[collection]/index.ts +59 -0
  314. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  315. package/src/astro/routes/api/dashboard.ts +32 -0
  316. package/src/astro/routes/api/dev/emails.ts +36 -0
  317. package/src/astro/routes/api/import/probe.ts +47 -0
  318. package/src/astro/routes/api/import/wordpress/analyze.ts +510 -0
  319. package/src/astro/routes/api/import/wordpress/execute.ts +283 -0
  320. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  321. package/src/astro/routes/api/import/wordpress/prepare.ts +181 -0
  322. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +393 -0
  323. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  324. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  325. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +347 -0
  326. package/src/astro/routes/api/manifest.ts +62 -0
  327. package/src/astro/routes/api/mcp.ts +124 -0
  328. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  329. package/src/astro/routes/api/media/[id].ts +145 -0
  330. package/src/astro/routes/api/media/file/[key].ts +79 -0
  331. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +86 -0
  332. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  333. package/src/astro/routes/api/media/providers/index.ts +30 -0
  334. package/src/astro/routes/api/media/upload-url.ts +137 -0
  335. package/src/astro/routes/api/media.ts +190 -0
  336. package/src/astro/routes/api/menus/[name]/items.ts +87 -0
  337. package/src/astro/routes/api/menus/[name]/reorder.ts +33 -0
  338. package/src/astro/routes/api/menus/[name].ts +65 -0
  339. package/src/astro/routes/api/menus/index.ts +47 -0
  340. package/src/astro/routes/api/oauth/authorize.ts +412 -0
  341. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  342. package/src/astro/routes/api/oauth/device/code.ts +51 -0
  343. package/src/astro/routes/api/oauth/device/token.ts +69 -0
  344. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  345. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  346. package/src/astro/routes/api/oauth/token.ts +184 -0
  347. package/src/astro/routes/api/openapi.json.ts +32 -0
  348. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +92 -0
  349. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  350. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  351. package/src/astro/routes/api/redirects/[id].ts +84 -0
  352. package/src/astro/routes/api/redirects/index.ts +52 -0
  353. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  354. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +58 -0
  355. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +76 -0
  356. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +52 -0
  357. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +32 -0
  358. package/src/astro/routes/api/schema/collections/[slug]/index.ts +80 -0
  359. package/src/astro/routes/api/schema/collections/index.ts +47 -0
  360. package/src/astro/routes/api/schema/index.ts +109 -0
  361. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  362. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  363. package/src/astro/routes/api/search/enable.ts +64 -0
  364. package/src/astro/routes/api/search/index.ts +55 -0
  365. package/src/astro/routes/api/search/rebuild.ts +72 -0
  366. package/src/astro/routes/api/search/stats.ts +35 -0
  367. package/src/astro/routes/api/search/suggest.ts +53 -0
  368. package/src/astro/routes/api/sections/[slug].ts +84 -0
  369. package/src/astro/routes/api/sections/index.ts +52 -0
  370. package/src/astro/routes/api/settings/email.ts +150 -0
  371. package/src/astro/routes/api/settings.ts +67 -0
  372. package/src/astro/routes/api/setup/admin-verify.ts +100 -0
  373. package/src/astro/routes/api/setup/admin.ts +94 -0
  374. package/src/astro/routes/api/setup/dev-bypass.ts +199 -0
  375. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  376. package/src/astro/routes/api/setup/index.ts +126 -0
  377. package/src/astro/routes/api/setup/status.ts +122 -0
  378. package/src/astro/routes/api/snapshot.ts +75 -0
  379. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +95 -0
  380. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +69 -0
  381. package/src/astro/routes/api/taxonomies/index.ts +59 -0
  382. package/src/astro/routes/api/themes/preview.ts +77 -0
  383. package/src/astro/routes/api/typegen.ts +114 -0
  384. package/src/astro/routes/api/well-known/auth.ts +68 -0
  385. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +44 -0
  386. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +37 -0
  387. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +68 -0
  388. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +127 -0
  389. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +80 -0
  390. package/src/astro/routes/api/widget-areas/[name].ts +87 -0
  391. package/src/astro/routes/api/widget-areas/index.ts +99 -0
  392. package/src/astro/routes/api/widget-components.ts +22 -0
  393. package/src/astro/routes/robots.txt.ts +77 -0
  394. package/src/astro/routes/sitemap.xml.ts +97 -0
  395. package/src/astro/storage/adapters.ts +74 -0
  396. package/src/astro/storage/index.ts +19 -0
  397. package/src/astro/storage/types.ts +60 -0
  398. package/src/astro/types.ts +346 -0
  399. package/src/auth/api-tokens.ts +25 -0
  400. package/src/auth/challenge-store.ts +80 -0
  401. package/src/auth/mode.ts +96 -0
  402. package/src/auth/oauth-state-store.ts +96 -0
  403. package/src/auth/passkey-config.ts +27 -0
  404. package/src/auth/rate-limit.ts +158 -0
  405. package/src/auth/scopes.ts +33 -0
  406. package/src/auth/types.ts +104 -0
  407. package/src/aws-sdk.d.ts +100 -0
  408. package/src/bylines/index.ts +237 -0
  409. package/src/cleanup.ts +153 -0
  410. package/src/cli/client-factory.ts +100 -0
  411. package/src/cli/commands/auth.ts +46 -0
  412. package/src/cli/commands/bundle-utils.ts +247 -0
  413. package/src/cli/commands/bundle.ts +609 -0
  414. package/src/cli/commands/content.ts +442 -0
  415. package/src/cli/commands/dev.ts +191 -0
  416. package/src/cli/commands/doctor.ts +211 -0
  417. package/src/cli/commands/export-seed.ts +630 -0
  418. package/src/cli/commands/import/wordpress.ts +1056 -0
  419. package/src/cli/commands/init.ts +192 -0
  420. package/src/cli/commands/login.ts +547 -0
  421. package/src/cli/commands/media.ts +165 -0
  422. package/src/cli/commands/menu.ts +67 -0
  423. package/src/cli/commands/plugin-init.ts +291 -0
  424. package/src/cli/commands/plugin-validate.ts +31 -0
  425. package/src/cli/commands/plugin.ts +33 -0
  426. package/src/cli/commands/publish.ts +697 -0
  427. package/src/cli/commands/schema.ts +233 -0
  428. package/src/cli/commands/search-cmd.ts +54 -0
  429. package/src/cli/commands/seed.ts +286 -0
  430. package/src/cli/commands/taxonomy.ts +128 -0
  431. package/src/cli/commands/types.ts +68 -0
  432. package/src/cli/credentials.ts +236 -0
  433. package/src/cli/index.ts +70 -0
  434. package/src/cli/output.ts +75 -0
  435. package/src/cli/wxr/parser.ts +969 -0
  436. package/src/client/cf-access.ts +193 -0
  437. package/src/client/index.ts +854 -0
  438. package/src/client/portable-text.ts +413 -0
  439. package/src/client/transport.ts +200 -0
  440. package/src/comments/moderator.ts +46 -0
  441. package/src/comments/notifications.ts +144 -0
  442. package/src/comments/query.ts +105 -0
  443. package/src/comments/service.ts +213 -0
  444. package/src/components/Break.astro +45 -0
  445. package/src/components/Button.astro +71 -0
  446. package/src/components/Buttons.astro +49 -0
  447. package/src/components/Code.astro +59 -0
  448. package/src/components/Columns.astro +59 -0
  449. package/src/components/CommentForm.astro +315 -0
  450. package/src/components/Comments.astro +232 -0
  451. package/src/components/Cover.astro +128 -0
  452. package/src/components/EmDashBodyEnd.astro +32 -0
  453. package/src/components/EmDashBodyStart.astro +32 -0
  454. package/src/components/EmDashHead.astro +53 -0
  455. package/src/components/EmDashImage.astro +178 -0
  456. package/src/components/EmDashMedia.astro +167 -0
  457. package/src/components/Embed.astro +128 -0
  458. package/src/components/File.astro +122 -0
  459. package/src/components/Gallery.astro +93 -0
  460. package/src/components/HtmlBlock.astro +33 -0
  461. package/src/components/Image.astro +178 -0
  462. package/src/components/InlineEditor.astro +27 -0
  463. package/src/components/InlinePortableTextEditor.tsx +1905 -0
  464. package/src/components/LiveSearch.astro +614 -0
  465. package/src/components/PortableText.astro +51 -0
  466. package/src/components/Pullquote.astro +51 -0
  467. package/src/components/Table.astro +108 -0
  468. package/src/components/WidgetArea.astro +22 -0
  469. package/src/components/WidgetRenderer.astro +72 -0
  470. package/src/components/index.ts +116 -0
  471. package/src/components/marks/Link.astro +31 -0
  472. package/src/components/marks/StrikeThrough.astro +7 -0
  473. package/src/components/marks/Subscript.astro +7 -0
  474. package/src/components/marks/Superscript.astro +7 -0
  475. package/src/components/marks/Underline.astro +7 -0
  476. package/src/components/widgets/Archives.astro +65 -0
  477. package/src/components/widgets/Categories.astro +35 -0
  478. package/src/components/widgets/RecentPosts.astro +51 -0
  479. package/src/components/widgets/Search.astro +18 -0
  480. package/src/components/widgets/Tags.astro +38 -0
  481. package/src/content/converters/index.ts +9 -0
  482. package/src/content/converters/portable-text-to-prosemirror.ts +385 -0
  483. package/src/content/converters/prosemirror-to-portable-text.ts +413 -0
  484. package/src/content/converters/types.ts +120 -0
  485. package/src/content/index.ts +5 -0
  486. package/src/database/connection.ts +67 -0
  487. package/src/database/dialect-helpers.ts +138 -0
  488. package/src/database/index.ts +5 -0
  489. package/src/database/migrations/001_initial.ts +170 -0
  490. package/src/database/migrations/002_media_status.ts +26 -0
  491. package/src/database/migrations/003_schema_registry.ts +79 -0
  492. package/src/database/migrations/004_plugins.ts +62 -0
  493. package/src/database/migrations/005_menus.ts +67 -0
  494. package/src/database/migrations/006_taxonomy_defs.ts +51 -0
  495. package/src/database/migrations/007_widgets.ts +42 -0
  496. package/src/database/migrations/008_auth.ts +194 -0
  497. package/src/database/migrations/009_user_disabled.ts +27 -0
  498. package/src/database/migrations/011_sections.ts +65 -0
  499. package/src/database/migrations/012_search.ts +25 -0
  500. package/src/database/migrations/013_scheduled_publishing.ts +51 -0
  501. package/src/database/migrations/014_draft_revisions.ts +72 -0
  502. package/src/database/migrations/015_indexes.ts +82 -0
  503. package/src/database/migrations/016_api_tokens.ts +89 -0
  504. package/src/database/migrations/017_authorization_codes.ts +45 -0
  505. package/src/database/migrations/018_seo.ts +56 -0
  506. package/src/database/migrations/019_i18n.ts +618 -0
  507. package/src/database/migrations/020_collection_url_pattern.ts +23 -0
  508. package/src/database/migrations/021_remove_section_categories.ts +43 -0
  509. package/src/database/migrations/022_marketplace_plugin_state.ts +46 -0
  510. package/src/database/migrations/023_plugin_metadata.ts +33 -0
  511. package/src/database/migrations/024_media_placeholders.ts +32 -0
  512. package/src/database/migrations/025_oauth_clients.ts +28 -0
  513. package/src/database/migrations/026_cron_tasks.ts +49 -0
  514. package/src/database/migrations/027_comments.ts +87 -0
  515. package/src/database/migrations/028_drop_author_url.ts +9 -0
  516. package/src/database/migrations/029_redirects.ts +67 -0
  517. package/src/database/migrations/030_widen_scheduled_index.ts +48 -0
  518. package/src/database/migrations/031_bylines.ts +90 -0
  519. package/src/database/migrations/032_rate_limits.ts +39 -0
  520. package/src/database/migrations/runner.ts +170 -0
  521. package/src/database/repositories/audit.ts +294 -0
  522. package/src/database/repositories/byline.ts +387 -0
  523. package/src/database/repositories/comment.ts +458 -0
  524. package/src/database/repositories/content.ts +1144 -0
  525. package/src/database/repositories/index.ts +30 -0
  526. package/src/database/repositories/media.ts +347 -0
  527. package/src/database/repositories/options.ts +150 -0
  528. package/src/database/repositories/plugin-storage.ts +373 -0
  529. package/src/database/repositories/redirect.ts +480 -0
  530. package/src/database/repositories/revision.ts +200 -0
  531. package/src/database/repositories/seo.ts +176 -0
  532. package/src/database/repositories/taxonomy.ts +294 -0
  533. package/src/database/repositories/types.ts +132 -0
  534. package/src/database/repositories/user.ts +258 -0
  535. package/src/database/transaction.ts +54 -0
  536. package/src/database/types.ts +501 -0
  537. package/src/database/validate.ts +138 -0
  538. package/src/db/adapters.ts +125 -0
  539. package/src/db/index.ts +37 -0
  540. package/src/db/libsql.ts +23 -0
  541. package/src/db/postgres.ts +30 -0
  542. package/src/db/sqlite.ts +27 -0
  543. package/src/emdash-runtime.ts +2096 -0
  544. package/src/fields/boolean.ts +34 -0
  545. package/src/fields/datetime.ts +44 -0
  546. package/src/fields/file.ts +41 -0
  547. package/src/fields/image.ts +34 -0
  548. package/src/fields/index.ts +42 -0
  549. package/src/fields/integer.ts +50 -0
  550. package/src/fields/json.ts +37 -0
  551. package/src/fields/multiselect.ts +48 -0
  552. package/src/fields/number.ts +52 -0
  553. package/src/fields/portable-text.ts +33 -0
  554. package/src/fields/reference.ts +29 -0
  555. package/src/fields/richtext.ts +31 -0
  556. package/src/fields/select.ts +46 -0
  557. package/src/fields/slug.ts +38 -0
  558. package/src/fields/text.ts +55 -0
  559. package/src/fields/textarea.ts +52 -0
  560. package/src/fields/types.ts +64 -0
  561. package/src/i18n/config.ts +68 -0
  562. package/src/import/index.ts +90 -0
  563. package/src/import/menus.ts +436 -0
  564. package/src/import/registry.ts +111 -0
  565. package/src/import/sections.ts +103 -0
  566. package/src/import/settings.ts +281 -0
  567. package/src/import/sources/wordpress-plugin.ts +641 -0
  568. package/src/import/sources/wordpress-rest.ts +191 -0
  569. package/src/import/sources/wxr.ts +330 -0
  570. package/src/import/ssrf.ts +260 -0
  571. package/src/import/types.ts +418 -0
  572. package/src/import/utils.ts +412 -0
  573. package/src/index.ts +481 -0
  574. package/src/loader.ts +770 -0
  575. package/src/mcp/server.ts +1463 -0
  576. package/src/media/index.ts +32 -0
  577. package/src/media/local-runtime.ts +213 -0
  578. package/src/media/local.ts +46 -0
  579. package/src/media/normalize.ts +190 -0
  580. package/src/media/placeholder.ts +150 -0
  581. package/src/media/provider-loader.ts +78 -0
  582. package/src/media/types.ts +279 -0
  583. package/src/menus/index.ts +324 -0
  584. package/src/menus/types.ts +112 -0
  585. package/src/page/context.ts +93 -0
  586. package/src/page/fragments.ts +89 -0
  587. package/src/page/index.ts +58 -0
  588. package/src/page/jsonld.ts +94 -0
  589. package/src/page/metadata.ts +185 -0
  590. package/src/page/seo-contributions.ts +136 -0
  591. package/src/plugin-utils.ts +80 -0
  592. package/src/plugins/adapt-sandbox-entry.ts +207 -0
  593. package/src/plugins/context.ts +833 -0
  594. package/src/plugins/cron.ts +361 -0
  595. package/src/plugins/define-plugin.ts +259 -0
  596. package/src/plugins/email-console.ts +73 -0
  597. package/src/plugins/email.ts +209 -0
  598. package/src/plugins/hooks.ts +1273 -0
  599. package/src/plugins/index.ts +193 -0
  600. package/src/plugins/manager.ts +595 -0
  601. package/src/plugins/manifest-schema.ts +230 -0
  602. package/src/plugins/marketplace.ts +460 -0
  603. package/src/plugins/request-meta.ts +139 -0
  604. package/src/plugins/routes.ts +302 -0
  605. package/src/plugins/sandbox/index.ts +18 -0
  606. package/src/plugins/sandbox/noop.ts +76 -0
  607. package/src/plugins/sandbox/types.ts +173 -0
  608. package/src/plugins/scheduler/node.ts +122 -0
  609. package/src/plugins/scheduler/piggyback.ts +71 -0
  610. package/src/plugins/scheduler/types.ts +27 -0
  611. package/src/plugins/state.ts +208 -0
  612. package/src/plugins/storage-indexes.ts +326 -0
  613. package/src/plugins/storage-query.ts +240 -0
  614. package/src/plugins/types.ts +1284 -0
  615. package/src/preview/helpers.ts +27 -0
  616. package/src/preview/index.ts +40 -0
  617. package/src/preview/tokens.ts +279 -0
  618. package/src/preview/urls.ts +118 -0
  619. package/src/query.ts +674 -0
  620. package/src/redirects/patterns.ts +224 -0
  621. package/src/request-context.ts +67 -0
  622. package/src/runtime.ts +21 -0
  623. package/src/schema/index.ts +29 -0
  624. package/src/schema/query.ts +44 -0
  625. package/src/schema/registry.ts +965 -0
  626. package/src/schema/types.ts +276 -0
  627. package/src/schema/zod-generator.ts +413 -0
  628. package/src/search/fts-manager.ts +452 -0
  629. package/src/search/index.ts +26 -0
  630. package/src/search/query.ts +396 -0
  631. package/src/search/text-extraction.ts +162 -0
  632. package/src/search/types.ts +114 -0
  633. package/src/sections/index.ts +226 -0
  634. package/src/sections/types.ts +86 -0
  635. package/src/seed/apply.ts +1141 -0
  636. package/src/seed/default.ts +86 -0
  637. package/src/seed/index.ts +28 -0
  638. package/src/seed/load.ts +35 -0
  639. package/src/seed/types.ts +341 -0
  640. package/src/seed/validate.ts +642 -0
  641. package/src/seo/index.ts +179 -0
  642. package/src/settings/index.ts +203 -0
  643. package/src/settings/types.ts +58 -0
  644. package/src/storage/index.ts +28 -0
  645. package/src/storage/local.ts +249 -0
  646. package/src/storage/s3.ts +263 -0
  647. package/src/storage/types.ts +204 -0
  648. package/src/taxonomies/index.ts +309 -0
  649. package/src/taxonomies/types.ts +61 -0
  650. package/src/ui.ts +75 -0
  651. package/src/utils/base64.ts +73 -0
  652. package/src/utils/hash.ts +36 -0
  653. package/src/utils/sanitize.ts +20 -0
  654. package/src/utils/slugify.ts +29 -0
  655. package/src/utils/url.ts +48 -0
  656. package/src/virtual-modules.d.ts +111 -0
  657. package/src/visual-editing/editable.ts +108 -0
  658. package/src/visual-editing/toolbar.ts +1229 -0
  659. package/src/widgets/components.ts +105 -0
  660. package/src/widgets/index.ts +131 -0
  661. package/src/widgets/types.ts +81 -0
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * Seed engine - applies seed files to database
3
+ *
4
+ * This is the core implementation that bootstraps an EmDash site from a seed file.
5
+ * Apply order is critical for foreign keys and references.
6
+ */
7
+
8
+ import { imageSize } from "image-size";
9
+ import type { Kysely } from "kysely";
10
+ import mime from "mime/lite";
11
+ import { ulid } from "ulidx";
12
+
13
+ import { BylineRepository } from "../database/repositories/byline.js";
14
+ import { ContentRepository } from "../database/repositories/content.js";
15
+ import { MediaRepository } from "../database/repositories/media.js";
16
+ import { RedirectRepository } from "../database/repositories/redirect.js";
17
+ import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
18
+ import type { Database } from "../database/types.js";
19
+ import type { MediaValue } from "../fields/types.js";
20
+ import { ssrfSafeFetch, validateExternalUrl } from "../import/ssrf.js";
21
+ import { SchemaRegistry } from "../schema/registry.js";
22
+ import { FTSManager } from "../search/fts-manager.js";
23
+ import { setSiteSettings } from "../settings/index.js";
24
+ import type { Storage } from "../storage/types.js";
25
+ import type {
26
+ SeedFile,
27
+ SeedApplyOptions,
28
+ SeedApplyResult,
29
+ SeedTaxonomyTerm,
30
+ SeedMenuItem,
31
+ SeedWidget,
32
+ SeedMediaReference,
33
+ } from "./types.js";
34
+
35
+ const FILE_EXTENSION_PATTERN = /\.([a-z0-9]+)(?:\?|$)/i;
36
+ import { validateSeed } from "./validate.js";
37
+
38
+ /** Pattern to remove file extensions */
39
+ const EXTENSION_PATTERN = /\.[^.]+$/;
40
+
41
+ /** Pattern to remove query parameters */
42
+ const QUERY_PARAM_PATTERN = /\?.*$/;
43
+
44
+ /** Pattern to remove non-alphanumeric characters (except dash and underscore) */
45
+ const SANITIZE_PATTERN = /[^a-zA-Z0-9_-]/g;
46
+
47
+ /** Pattern to collapse multiple hyphens */
48
+ const MULTIPLE_HYPHENS_PATTERN = /-+/g;
49
+
50
+ /**
51
+ * Apply a seed file to the database
52
+ *
53
+ * This function is idempotent - safe to run multiple times.
54
+ *
55
+ * @param db - Kysely database instance
56
+ * @param seed - Seed file to apply
57
+ * @param options - Application options
58
+ * @returns Result summary
59
+ */
60
+ export async function applySeed(
61
+ db: Kysely<Database>,
62
+ seed: SeedFile,
63
+ options: SeedApplyOptions = {},
64
+ ): Promise<SeedApplyResult> {
65
+ // Validate seed first
66
+ const validation = validateSeed(seed);
67
+ if (!validation.valid) {
68
+ throw new Error(`Invalid seed file:\n${validation.errors.join("\n")}`);
69
+ }
70
+
71
+ const {
72
+ includeContent = false,
73
+ storage,
74
+ skipMediaDownload = false,
75
+ onConflict = "skip",
76
+ } = options;
77
+
78
+ // Result counters
79
+ const result: SeedApplyResult = {
80
+ collections: { created: 0, skipped: 0, updated: 0 },
81
+ fields: { created: 0, skipped: 0, updated: 0 },
82
+ taxonomies: { created: 0, terms: 0 },
83
+ bylines: { created: 0, skipped: 0, updated: 0 },
84
+ menus: { created: 0, items: 0 },
85
+ redirects: { created: 0, skipped: 0, updated: 0 },
86
+ widgetAreas: { created: 0, widgets: 0 },
87
+ sections: { created: 0, skipped: 0, updated: 0 },
88
+ settings: { applied: 0 },
89
+ content: { created: 0, skipped: 0, updated: 0 },
90
+ media: { created: 0, skipped: 0 },
91
+ };
92
+
93
+ // Media context for $media resolution
94
+ const mediaContext: MediaContext = {
95
+ db,
96
+ storage: storage ?? null,
97
+ skipMediaDownload,
98
+ mediaCache: new Map(), // Cache downloaded media by URL to avoid re-downloading
99
+ };
100
+
101
+ // Apply order (critical for foreign keys and references):
102
+ // 1. Site settings
103
+ // 2. Collections + Fields
104
+ // 3. Taxonomy definitions + Terms
105
+ // 4. Content (so menu refs can resolve)
106
+ // 5. Menus + Menu items (can now resolve content refs)
107
+ // 6. Redirects
108
+ // 7. Widget areas + Widgets
109
+
110
+ // Track seed content IDs for reference resolution (shared across content and menus)
111
+ const seedIdMap = new Map<string, string>(); // seed id -> real entry id
112
+ const seedBylineIdMap = new Map<string, string>(); // seed byline id -> real byline id
113
+
114
+ // 1. Site settings
115
+ if (seed.settings) {
116
+ await setSiteSettings(seed.settings, db);
117
+ result.settings.applied = Object.keys(seed.settings).length;
118
+ }
119
+
120
+ // 2-3. Collections and Fields
121
+ if (seed.collections) {
122
+ const registry = new SchemaRegistry(db);
123
+
124
+ for (const collection of seed.collections) {
125
+ // Check if collection exists
126
+ const existing = await registry.getCollection(collection.slug);
127
+
128
+ if (existing) {
129
+ if (onConflict === "error") {
130
+ throw new Error(`Conflict: collection "${collection.slug}" already exists`);
131
+ }
132
+
133
+ if (onConflict === "update") {
134
+ await registry.updateCollection(collection.slug, {
135
+ label: collection.label,
136
+ labelSingular: collection.labelSingular,
137
+ description: collection.description,
138
+ icon: collection.icon,
139
+ supports: collection.supports || [],
140
+ urlPattern: collection.urlPattern,
141
+ commentsEnabled: collection.commentsEnabled,
142
+ });
143
+ result.collections.updated++;
144
+
145
+ // Update or create fields
146
+ for (const field of collection.fields) {
147
+ const existingField = await registry.getField(collection.slug, field.slug);
148
+ if (existingField) {
149
+ await registry.updateField(collection.slug, field.slug, {
150
+ label: field.label,
151
+ required: field.required || false,
152
+ unique: field.unique || false,
153
+ searchable: field.searchable || false,
154
+ defaultValue: field.defaultValue,
155
+ validation: field.validation,
156
+ widget: field.widget,
157
+ options: field.options,
158
+ });
159
+ result.fields.updated++;
160
+ } else {
161
+ await registry.createField(collection.slug, {
162
+ slug: field.slug,
163
+ label: field.label,
164
+ type: field.type,
165
+ required: field.required || false,
166
+ unique: field.unique || false,
167
+ searchable: field.searchable || false,
168
+ defaultValue: field.defaultValue,
169
+ validation: field.validation,
170
+ widget: field.widget,
171
+ options: field.options,
172
+ });
173
+ result.fields.created++;
174
+ }
175
+ }
176
+ continue;
177
+ }
178
+
179
+ // skip
180
+ result.collections.skipped++;
181
+ result.fields.skipped += collection.fields.length;
182
+ continue;
183
+ }
184
+
185
+ // Create collection
186
+ await registry.createCollection({
187
+ slug: collection.slug,
188
+ label: collection.label,
189
+ labelSingular: collection.labelSingular,
190
+ description: collection.description,
191
+ icon: collection.icon,
192
+ supports: collection.supports || [],
193
+ source: "seed",
194
+ urlPattern: collection.urlPattern,
195
+ commentsEnabled: collection.commentsEnabled,
196
+ });
197
+ result.collections.created++;
198
+
199
+ // Create fields
200
+ for (const field of collection.fields) {
201
+ await registry.createField(collection.slug, {
202
+ slug: field.slug,
203
+ label: field.label,
204
+ type: field.type,
205
+ required: field.required || false,
206
+ unique: field.unique || false,
207
+ searchable: field.searchable || false,
208
+ defaultValue: field.defaultValue,
209
+ validation: field.validation,
210
+ widget: field.widget,
211
+ options: field.options,
212
+ });
213
+ result.fields.created++;
214
+ }
215
+ }
216
+ }
217
+
218
+ // 4-5. Taxonomies
219
+ if (seed.taxonomies) {
220
+ for (const taxonomy of seed.taxonomies) {
221
+ // Check if taxonomy definition exists
222
+ const existingDef = await db
223
+ .selectFrom("_emdash_taxonomy_defs")
224
+ .selectAll()
225
+ .where("name", "=", taxonomy.name)
226
+ .executeTakeFirst();
227
+
228
+ if (existingDef) {
229
+ if (onConflict === "error") {
230
+ throw new Error(`Conflict: taxonomy "${taxonomy.name}" already exists`);
231
+ }
232
+ if (onConflict === "update") {
233
+ await db
234
+ .updateTable("_emdash_taxonomy_defs")
235
+ .set({
236
+ label: taxonomy.label,
237
+ label_singular: taxonomy.labelSingular ?? null,
238
+ hierarchical: taxonomy.hierarchical ? 1 : 0,
239
+ collections: JSON.stringify(taxonomy.collections),
240
+ })
241
+ .where("id", "=", existingDef.id)
242
+ .execute();
243
+ // Taxonomy defs don't track an "updated" counter -- just the definition is updated
244
+ }
245
+ // skip: do nothing for the definition
246
+ } else {
247
+ // Create taxonomy definition
248
+ await db
249
+ .insertInto("_emdash_taxonomy_defs")
250
+ .values({
251
+ id: ulid(),
252
+ name: taxonomy.name,
253
+ label: taxonomy.label,
254
+ label_singular: taxonomy.labelSingular ?? null,
255
+ hierarchical: taxonomy.hierarchical ? 1 : 0,
256
+ collections: JSON.stringify(taxonomy.collections),
257
+ })
258
+ .execute();
259
+ result.taxonomies.created++;
260
+ }
261
+
262
+ // Create terms (if provided)
263
+ if (taxonomy.terms && taxonomy.terms.length > 0) {
264
+ const termRepo = new TaxonomyRepository(db);
265
+
266
+ // For hierarchical taxonomies, we need to create parents before children
267
+ if (taxonomy.hierarchical) {
268
+ await applyHierarchicalTerms(termRepo, taxonomy.name, taxonomy.terms, result, onConflict);
269
+ } else {
270
+ // Flat taxonomy - create all terms
271
+ for (const term of taxonomy.terms) {
272
+ const existing = await termRepo.findBySlug(taxonomy.name, term.slug);
273
+ if (existing) {
274
+ if (onConflict === "error") {
275
+ throw new Error(
276
+ `Conflict: taxonomy term "${term.slug}" in "${taxonomy.name}" already exists`,
277
+ );
278
+ }
279
+ if (onConflict === "update") {
280
+ await termRepo.update(existing.id, {
281
+ label: term.label,
282
+ data: term.description ? { description: term.description } : {},
283
+ });
284
+ result.taxonomies.terms++;
285
+ }
286
+ // skip: do nothing
287
+ } else {
288
+ await termRepo.create({
289
+ name: taxonomy.name,
290
+ slug: term.slug,
291
+ label: term.label,
292
+ data: term.description ? { description: term.description } : undefined,
293
+ });
294
+ result.taxonomies.terms++;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ // 6. Bylines
303
+ if (seed.bylines) {
304
+ const bylineRepo = new BylineRepository(db);
305
+ for (const byline of seed.bylines) {
306
+ const existing = await bylineRepo.findBySlug(byline.slug);
307
+ if (existing) {
308
+ if (onConflict === "error") {
309
+ throw new Error(`Conflict: byline "${byline.slug}" already exists`);
310
+ }
311
+
312
+ if (onConflict === "update") {
313
+ await bylineRepo.update(existing.id, {
314
+ displayName: byline.displayName,
315
+ bio: byline.bio ?? null,
316
+ websiteUrl: byline.websiteUrl ?? null,
317
+ isGuest: byline.isGuest,
318
+ });
319
+ seedBylineIdMap.set(byline.id, existing.id);
320
+ result.bylines.updated++;
321
+ continue;
322
+ }
323
+
324
+ // skip
325
+ seedBylineIdMap.set(byline.id, existing.id);
326
+ result.bylines.skipped++;
327
+ continue;
328
+ }
329
+
330
+ const created = await bylineRepo.create({
331
+ slug: byline.slug,
332
+ displayName: byline.displayName,
333
+ bio: byline.bio ?? null,
334
+ websiteUrl: byline.websiteUrl ?? null,
335
+ isGuest: byline.isGuest,
336
+ });
337
+ seedBylineIdMap.set(byline.id, created.id);
338
+ result.bylines.created++;
339
+ }
340
+ }
341
+
342
+ // 7. Content (created before menus so refs can resolve)
343
+ if (includeContent && seed.content) {
344
+ const contentRepo = new ContentRepository(db);
345
+ const bylineRepo = new BylineRepository(db);
346
+
347
+ // Create content entries
348
+ for (const [collectionSlug, entries] of Object.entries(seed.content)) {
349
+ for (const entry of entries) {
350
+ // Check if entry exists (by slug + locale for locale-aware lookup)
351
+ const existing = await contentRepo.findBySlug(collectionSlug, entry.slug, entry.locale);
352
+
353
+ if (existing) {
354
+ if (onConflict === "error") {
355
+ throw new Error(
356
+ `Conflict: content "${entry.slug}" in "${collectionSlug}" already exists`,
357
+ );
358
+ }
359
+
360
+ if (onConflict === "update") {
361
+ // Resolve $ref and $media in data
362
+ const resolvedData = await resolveReferences(
363
+ entry.data,
364
+ seedIdMap,
365
+ mediaContext,
366
+ result,
367
+ );
368
+
369
+ const status = entry.status || "published";
370
+ await contentRepo.update(collectionSlug, existing.id, {
371
+ status,
372
+ data: resolvedData,
373
+ });
374
+
375
+ seedIdMap.set(entry.id, existing.id);
376
+ result.content.updated++;
377
+
378
+ // Update bylines and taxonomy assignments
379
+ await applyContentBylines(
380
+ bylineRepo,
381
+ collectionSlug,
382
+ existing.id,
383
+ entry,
384
+ seedBylineIdMap,
385
+ true,
386
+ );
387
+ await applyContentTaxonomies(db, collectionSlug, existing.id, entry, true);
388
+ continue;
389
+ }
390
+
391
+ // skip
392
+ result.content.skipped++;
393
+ seedIdMap.set(entry.id, existing.id);
394
+ continue;
395
+ }
396
+
397
+ // Resolve $ref and $media in data
398
+ const resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);
399
+
400
+ // Resolve translationOf: map from seed-local ID to real EmDash ID
401
+ let translationOf: string | undefined;
402
+ if (entry.translationOf) {
403
+ const sourceId = seedIdMap.get(entry.translationOf);
404
+ if (!sourceId) {
405
+ console.warn(
406
+ `content.${collectionSlug}: translationOf "${entry.translationOf}" not found (not yet created or missing). Skipping translation link.`,
407
+ );
408
+ } else {
409
+ translationOf = sourceId;
410
+ }
411
+ }
412
+
413
+ // Create entry
414
+ const status = entry.status || "published";
415
+ const created = await contentRepo.create({
416
+ type: collectionSlug,
417
+ slug: entry.slug,
418
+ status,
419
+ data: resolvedData,
420
+ locale: entry.locale,
421
+ translationOf,
422
+ // Set published_at for published content so RSS/Archives work correctly
423
+ publishedAt: status === "published" ? new Date().toISOString() : null,
424
+ });
425
+
426
+ seedIdMap.set(entry.id, created.id);
427
+ result.content.created++;
428
+
429
+ await applyContentBylines(bylineRepo, collectionSlug, created.id, entry, seedBylineIdMap);
430
+ await applyContentTaxonomies(db, collectionSlug, created.id, entry, false);
431
+ }
432
+ }
433
+ }
434
+
435
+ // 8. Menus and Menu Items (after content so refs can resolve)
436
+ if (seed.menus) {
437
+ for (const menu of seed.menus) {
438
+ // Check if menu exists
439
+ const existingMenu = await db
440
+ .selectFrom("_emdash_menus")
441
+ .selectAll()
442
+ .where("name", "=", menu.name)
443
+ .executeTakeFirst();
444
+
445
+ let menuId: string;
446
+
447
+ if (existingMenu) {
448
+ menuId = existingMenu.id;
449
+ // Clear existing items (menus are recreated)
450
+ await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menuId).execute();
451
+ } else {
452
+ // Create menu
453
+ menuId = ulid();
454
+ await db
455
+ .insertInto("_emdash_menus")
456
+ .values({
457
+ id: menuId,
458
+ name: menu.name,
459
+ label: menu.label,
460
+ created_at: new Date().toISOString(),
461
+ updated_at: new Date().toISOString(),
462
+ })
463
+ .execute();
464
+ result.menus.created++;
465
+ }
466
+
467
+ // Create menu items
468
+ const itemCount = await applyMenuItems(
469
+ db,
470
+ menuId,
471
+ menu.items,
472
+ null, // parent_id
473
+ 0, // sort_order
474
+ seedIdMap,
475
+ );
476
+ result.menus.items += itemCount;
477
+ }
478
+ }
479
+
480
+ // 9. Redirects
481
+ if (seed.redirects) {
482
+ const redirectRepo = new RedirectRepository(db);
483
+
484
+ for (const redirect of seed.redirects) {
485
+ const existing = await redirectRepo.findBySource(redirect.source);
486
+ if (existing) {
487
+ if (onConflict === "error") {
488
+ throw new Error(`Conflict: redirect "${redirect.source}" already exists`);
489
+ }
490
+
491
+ if (onConflict === "update") {
492
+ await redirectRepo.update(existing.id, {
493
+ destination: redirect.destination,
494
+ type: redirect.type,
495
+ enabled: redirect.enabled,
496
+ groupName: redirect.groupName,
497
+ });
498
+ result.redirects.updated++;
499
+ continue;
500
+ }
501
+
502
+ // skip
503
+ result.redirects.skipped++;
504
+ continue;
505
+ }
506
+
507
+ await redirectRepo.create({
508
+ source: redirect.source,
509
+ destination: redirect.destination,
510
+ type: redirect.type,
511
+ enabled: redirect.enabled,
512
+ groupName: redirect.groupName,
513
+ });
514
+ result.redirects.created++;
515
+ }
516
+ }
517
+
518
+ // 10. Widget Areas and Widgets
519
+ if (seed.widgetAreas) {
520
+ for (const area of seed.widgetAreas) {
521
+ // Check if area exists
522
+ const existingArea = await db
523
+ .selectFrom("_emdash_widget_areas")
524
+ .selectAll()
525
+ .where("name", "=", area.name)
526
+ .executeTakeFirst();
527
+
528
+ let areaId: string;
529
+
530
+ if (existingArea) {
531
+ areaId = existingArea.id;
532
+ // Clear existing widgets (areas are recreated)
533
+ await db.deleteFrom("_emdash_widgets").where("area_id", "=", areaId).execute();
534
+ } else {
535
+ // Create area
536
+ areaId = ulid();
537
+ await db
538
+ .insertInto("_emdash_widget_areas")
539
+ .values({
540
+ id: areaId,
541
+ name: area.name,
542
+ label: area.label,
543
+ description: area.description ?? null,
544
+ })
545
+ .execute();
546
+ result.widgetAreas.created++;
547
+ }
548
+
549
+ // Create widgets
550
+ for (let i = 0; i < area.widgets.length; i++) {
551
+ const widget = area.widgets[i];
552
+ await applyWidget(db, areaId, widget, i);
553
+ result.widgetAreas.widgets++;
554
+ }
555
+ }
556
+ }
557
+
558
+ // 11. Sections
559
+ if (seed.sections) {
560
+ for (const section of seed.sections) {
561
+ // Check if section exists
562
+ const existing = await db
563
+ .selectFrom("_emdash_sections")
564
+ .select("id")
565
+ .where("slug", "=", section.slug)
566
+ .executeTakeFirst();
567
+
568
+ if (existing) {
569
+ if (onConflict === "error") {
570
+ throw new Error(`Conflict: section "${section.slug}" already exists`);
571
+ }
572
+
573
+ if (onConflict === "update") {
574
+ await db
575
+ .updateTable("_emdash_sections")
576
+ .set({
577
+ title: section.title,
578
+ description: section.description ?? null,
579
+ keywords: section.keywords ? JSON.stringify(section.keywords) : null,
580
+ content: JSON.stringify(section.content),
581
+ source: section.source || "theme",
582
+ updated_at: new Date().toISOString(),
583
+ })
584
+ .where("id", "=", existing.id)
585
+ .execute();
586
+ result.sections.updated++;
587
+ continue;
588
+ }
589
+
590
+ // skip
591
+ result.sections.skipped++;
592
+ continue;
593
+ }
594
+
595
+ const id = ulid();
596
+ const now = new Date().toISOString();
597
+
598
+ await db
599
+ .insertInto("_emdash_sections")
600
+ .values({
601
+ id,
602
+ slug: section.slug,
603
+ title: section.title,
604
+ description: section.description ?? null,
605
+ keywords: section.keywords ? JSON.stringify(section.keywords) : null,
606
+ content: JSON.stringify(section.content),
607
+ preview_media_id: null,
608
+ source: section.source || "theme",
609
+ theme_id: section.source === "theme" ? section.slug : null,
610
+ created_at: now,
611
+ updated_at: now,
612
+ })
613
+ .execute();
614
+
615
+ result.sections.created++;
616
+ }
617
+ }
618
+
619
+ // 11. Enable search for collections that have `search` in supports
620
+ if (seed.collections) {
621
+ const ftsManager = new FTSManager(db);
622
+
623
+ for (const collection of seed.collections) {
624
+ if (collection.supports?.includes("search")) {
625
+ // Check if there are searchable fields
626
+ const searchableFields = await ftsManager.getSearchableFields(collection.slug);
627
+ if (searchableFields.length > 0) {
628
+ try {
629
+ await ftsManager.enableSearch(collection.slug);
630
+ } catch (err) {
631
+ // Log but don't fail - search can be enabled manually later
632
+ console.warn(`Failed to enable search for ${collection.slug}:`, err);
633
+ }
634
+ }
635
+ }
636
+ }
637
+ }
638
+
639
+ return result;
640
+ }
641
+
642
+ /**
643
+ * Apply hierarchical taxonomy terms (parents before children)
644
+ */
645
+ async function applyHierarchicalTerms(
646
+ termRepo: TaxonomyRepository,
647
+ taxonomyName: string,
648
+ terms: SeedTaxonomyTerm[],
649
+ result: SeedApplyResult,
650
+ onConflict: "skip" | "update" | "error" = "skip",
651
+ ): Promise<void> {
652
+ // Map slugs to IDs
653
+ const slugToId = new Map<string, string>();
654
+
655
+ // Multiple passes to handle deep nesting
656
+ let remaining = [...terms];
657
+ let maxPasses = 10; // Prevent infinite loop
658
+
659
+ while (remaining.length > 0 && maxPasses > 0) {
660
+ const processedThisPass: string[] = [];
661
+
662
+ for (const term of remaining) {
663
+ // Check if parent exists (or no parent)
664
+ if (!term.parent || slugToId.has(term.parent)) {
665
+ const parentId = term.parent ? slugToId.get(term.parent) : undefined;
666
+
667
+ const existing = await termRepo.findBySlug(taxonomyName, term.slug);
668
+ if (existing) {
669
+ if (onConflict === "error") {
670
+ throw new Error(
671
+ `Conflict: taxonomy term "${term.slug}" in "${taxonomyName}" already exists`,
672
+ );
673
+ }
674
+ if (onConflict === "update") {
675
+ await termRepo.update(existing.id, {
676
+ label: term.label,
677
+ parentId,
678
+ data: term.description ? { description: term.description } : {},
679
+ });
680
+ result.taxonomies.terms++;
681
+ }
682
+ slugToId.set(term.slug, existing.id);
683
+ } else {
684
+ const created = await termRepo.create({
685
+ name: taxonomyName,
686
+ slug: term.slug,
687
+ label: term.label,
688
+ parentId,
689
+ data: term.description ? { description: term.description } : undefined,
690
+ });
691
+ slugToId.set(term.slug, created.id);
692
+ result.taxonomies.terms++;
693
+ }
694
+
695
+ processedThisPass.push(term.slug);
696
+ }
697
+ }
698
+
699
+ // Remove processed terms
700
+ remaining = remaining.filter((t) => !processedThisPass.includes(t.slug));
701
+ maxPasses--;
702
+ }
703
+
704
+ if (remaining.length > 0) {
705
+ console.warn(`Could not process ${remaining.length} terms due to missing parents`);
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Apply byline credits to a content entry.
711
+ * In update mode, clears existing credits even if the seed has none.
712
+ */
713
+ async function applyContentBylines(
714
+ bylineRepo: BylineRepository,
715
+ collectionSlug: string,
716
+ contentId: string,
717
+ entry: { slug: string; bylines?: Array<{ byline: string; roleLabel?: string }> },
718
+ seedBylineIdMap: Map<string, string>,
719
+ isUpdate = false,
720
+ ): Promise<void> {
721
+ if (!entry.bylines || entry.bylines.length === 0) {
722
+ // In update mode, clear existing bylines when the seed entry has none
723
+ if (isUpdate) {
724
+ await bylineRepo.setContentBylines(collectionSlug, contentId, []);
725
+ }
726
+ return;
727
+ }
728
+
729
+ const credits = entry.bylines
730
+ .map((credit) => {
731
+ const bylineId = seedBylineIdMap.get(credit.byline);
732
+ if (!bylineId) return null;
733
+ return {
734
+ bylineId,
735
+ roleLabel: credit.roleLabel ?? null,
736
+ };
737
+ })
738
+ .filter((credit): credit is { bylineId: string; roleLabel: string | null } => Boolean(credit));
739
+
740
+ if (credits.length !== entry.bylines.length) {
741
+ console.warn(
742
+ `content.${collectionSlug}.${entry.slug}: one or more byline refs could not be resolved`,
743
+ );
744
+ }
745
+
746
+ // In update mode, always call setContentBylines (even with empty credits)
747
+ // to clear stale assignments when all byline refs fail to resolve.
748
+ // In create mode, only call if there are credits to assign.
749
+ if (credits.length > 0 || isUpdate) {
750
+ await bylineRepo.setContentBylines(collectionSlug, contentId, credits);
751
+ }
752
+ }
753
+
754
+ /**
755
+ * Apply taxonomy term assignments to a content entry.
756
+ * In update mode, clears existing assignments before re-attaching.
757
+ */
758
+ async function applyContentTaxonomies(
759
+ db: Kysely<Database>,
760
+ collectionSlug: string,
761
+ contentId: string,
762
+ entry: { taxonomies?: Record<string, string[]> },
763
+ isUpdate: boolean,
764
+ ): Promise<void> {
765
+ // In update mode, clear existing taxonomy assignments first
766
+ if (isUpdate) {
767
+ await db
768
+ .deleteFrom("content_taxonomies")
769
+ .where("collection", "=", collectionSlug)
770
+ .where("entry_id", "=", contentId)
771
+ .execute();
772
+ }
773
+
774
+ if (!entry.taxonomies) return;
775
+
776
+ for (const [taxonomyName, termSlugs] of Object.entries(entry.taxonomies)) {
777
+ const termRepo = new TaxonomyRepository(db);
778
+
779
+ for (const termSlug of termSlugs) {
780
+ const term = await termRepo.findBySlug(taxonomyName, termSlug);
781
+ if (term) {
782
+ await termRepo.attachToEntry(collectionSlug, contentId, term.id);
783
+ }
784
+ }
785
+ }
786
+ }
787
+
788
+ /**
789
+ * Apply menu items recursively
790
+ */
791
+ async function applyMenuItems(
792
+ db: Kysely<Database>,
793
+ menuId: string,
794
+ items: SeedMenuItem[],
795
+ parentId: string | null,
796
+ startOrder: number,
797
+ seedIdMap: Map<string, string>,
798
+ ): Promise<number> {
799
+ let count = 0;
800
+ let order = startOrder;
801
+
802
+ for (const item of items) {
803
+ const itemId = ulid();
804
+
805
+ // Resolve reference if needed
806
+ let referenceId: string | null = null;
807
+ let referenceCollection: string | null = null;
808
+
809
+ if (item.type === "page" || item.type === "post") {
810
+ // Try to resolve from seedIdMap
811
+ if (item.ref && seedIdMap.has(item.ref)) {
812
+ referenceId = seedIdMap.get(item.ref)!;
813
+ // Default to plural collection name (pages/posts) if not specified
814
+ referenceCollection = item.collection || `${item.type}s`;
815
+ }
816
+ // If not in map, the content might not exist yet (will be broken link)
817
+ }
818
+
819
+ // Insert menu item
820
+ await db
821
+ .insertInto("_emdash_menu_items")
822
+ .values({
823
+ id: itemId,
824
+ menu_id: menuId,
825
+ parent_id: parentId,
826
+ sort_order: order,
827
+ type: item.type,
828
+ reference_collection: referenceCollection,
829
+ reference_id: referenceId,
830
+ custom_url: item.url ?? null,
831
+ label: item.label || "",
832
+ title_attr: item.titleAttr ?? null,
833
+ target: item.target ?? null,
834
+ css_classes: item.cssClasses ?? null,
835
+ created_at: new Date().toISOString(),
836
+ })
837
+ .execute();
838
+
839
+ count++;
840
+ order++;
841
+
842
+ // Process children
843
+ if (item.children && item.children.length > 0) {
844
+ const childCount = await applyMenuItems(db, menuId, item.children, itemId, 0, seedIdMap);
845
+ count += childCount;
846
+ }
847
+ }
848
+
849
+ return count;
850
+ }
851
+
852
+ /**
853
+ * Apply a widget
854
+ */
855
+ async function applyWidget(
856
+ db: Kysely<Database>,
857
+ areaId: string,
858
+ widget: SeedWidget,
859
+ sortOrder: number,
860
+ ): Promise<void> {
861
+ await db
862
+ .insertInto("_emdash_widgets")
863
+ .values({
864
+ id: ulid(),
865
+ area_id: areaId,
866
+ sort_order: sortOrder,
867
+ type: widget.type,
868
+ title: widget.title ?? null,
869
+ content: widget.content ? JSON.stringify(widget.content) : null,
870
+ menu_name: widget.menuName ?? null,
871
+ component_id: widget.componentId ?? null,
872
+ component_props: widget.props ? JSON.stringify(widget.props) : null,
873
+ })
874
+ .execute();
875
+ }
876
+
877
+ /**
878
+ * Context for media resolution during seed application
879
+ */
880
+ interface MediaContext {
881
+ db: Kysely<Database>;
882
+ storage: Storage | null;
883
+ skipMediaDownload: boolean;
884
+ mediaCache: Map<string, MediaValue>; // URL -> resolved MediaValue
885
+ }
886
+
887
+ /**
888
+ * Type guard for $media reference
889
+ */
890
+ function isSeedMediaReference(value: unknown): value is SeedMediaReference {
891
+ if (typeof value !== "object" || value === null || !("$media" in value)) {
892
+ return false;
893
+ }
894
+ const media = (value as Record<string, unknown>).$media;
895
+ return (
896
+ typeof media === "object" &&
897
+ media !== null &&
898
+ "url" in media &&
899
+ typeof (media as Record<string, unknown>).url === "string"
900
+ );
901
+ }
902
+
903
+ /**
904
+ * Resolve $ref: and $media references in content data
905
+ */
906
+ async function resolveReferences(
907
+ data: Record<string, unknown>,
908
+ seedIdMap: Map<string, string>,
909
+ mediaContext: MediaContext,
910
+ result: SeedApplyResult,
911
+ ): Promise<Record<string, unknown>> {
912
+ const resolved: Record<string, unknown> = {};
913
+
914
+ for (const [key, value] of Object.entries(data)) {
915
+ resolved[key] = await resolveValue(value, seedIdMap, mediaContext, result);
916
+ }
917
+
918
+ return resolved;
919
+ }
920
+
921
+ /**
922
+ * Resolve a single value recursively
923
+ */
924
+ async function resolveValue(
925
+ value: unknown,
926
+ seedIdMap: Map<string, string>,
927
+ mediaContext: MediaContext,
928
+ result: SeedApplyResult,
929
+ ): Promise<unknown> {
930
+ // Handle $ref: syntax
931
+ if (typeof value === "string" && value.startsWith("$ref:")) {
932
+ const seedId = value.slice(5);
933
+ return seedIdMap.get(seedId) ?? value; // Return unresolved if not found
934
+ }
935
+
936
+ // Handle $media syntax
937
+ if (isSeedMediaReference(value)) {
938
+ return resolveMedia(value, mediaContext, result);
939
+ }
940
+
941
+ // Handle arrays
942
+ if (Array.isArray(value)) {
943
+ return Promise.all(value.map((item) => resolveValue(item, seedIdMap, mediaContext, result)));
944
+ }
945
+
946
+ // Handle objects recursively
947
+ if (typeof value === "object" && value !== null) {
948
+ const resolved: Record<string, unknown> = {};
949
+ for (const [k, v] of Object.entries(value)) {
950
+ resolved[k] = await resolveValue(v, seedIdMap, mediaContext, result);
951
+ }
952
+ return resolved;
953
+ }
954
+
955
+ return value;
956
+ }
957
+
958
+ /**
959
+ * Resolve a $media reference by downloading and uploading the media
960
+ */
961
+ async function resolveMedia(
962
+ ref: SeedMediaReference,
963
+ ctx: MediaContext,
964
+ result: SeedApplyResult,
965
+ ): Promise<MediaValue | null> {
966
+ const { url, alt, filename, caption } = ref.$media;
967
+
968
+ // Check cache first
969
+ const cached = ctx.mediaCache.get(url);
970
+ if (cached) {
971
+ result.media.skipped++;
972
+ return { ...cached, alt: alt ?? cached.alt };
973
+ }
974
+
975
+ // When skipMediaDownload is set, resolve $media to an external URL reference
976
+ // without downloading or storing anything. Used by playground mode.
977
+ if (ctx.skipMediaDownload) {
978
+ const mediaValue: MediaValue = {
979
+ provider: "external",
980
+ id: ulid(),
981
+ src: url,
982
+ alt: alt ?? undefined,
983
+ filename: filename ?? undefined,
984
+ };
985
+ ctx.mediaCache.set(url, mediaValue);
986
+ result.media.created++;
987
+ return mediaValue;
988
+ }
989
+
990
+ // Storage is required for $media resolution
991
+ if (!ctx.storage) {
992
+ console.warn(`Skipping $media reference (no storage configured): ${url}`);
993
+ result.media.skipped++;
994
+ return null;
995
+ }
996
+
997
+ try {
998
+ // SSRF protection: validate URL before downloading
999
+ validateExternalUrl(url);
1000
+
1001
+ // Download the media (ssrfSafeFetch re-validates redirect targets)
1002
+ console.log(` 📥 Downloading: ${url}`);
1003
+ const response = await ssrfSafeFetch(url, {
1004
+ headers: {
1005
+ // Some services like Unsplash require a user-agent
1006
+ "User-Agent": "EmDash-CMS/1.0",
1007
+ },
1008
+ });
1009
+
1010
+ if (!response.ok) {
1011
+ console.warn(` ⚠️ Failed to download ${url}: ${response.status}`);
1012
+ result.media.skipped++;
1013
+ return null;
1014
+ }
1015
+
1016
+ // Get content type and determine extension
1017
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
1018
+ const ext = getExtensionFromContentType(contentType) || getExtensionFromUrl(url) || ".bin";
1019
+
1020
+ // Generate filename and storage key
1021
+ const id = ulid();
1022
+ const finalFilename = filename || generateFilename(url, ext);
1023
+ const storageKey = `${id}${ext}`;
1024
+
1025
+ // Get the body as buffer
1026
+ const arrayBuffer = await response.arrayBuffer();
1027
+ const body = new Uint8Array(arrayBuffer);
1028
+
1029
+ // Get image dimensions if it's an image
1030
+ let width: number | undefined;
1031
+ let height: number | undefined;
1032
+ if (contentType.startsWith("image/")) {
1033
+ const dimensions = getImageDimensions(body);
1034
+ width = dimensions?.width;
1035
+ height = dimensions?.height;
1036
+ }
1037
+
1038
+ // Upload to storage
1039
+ await ctx.storage.upload({
1040
+ key: storageKey,
1041
+ body,
1042
+ contentType,
1043
+ });
1044
+
1045
+ // Create media record
1046
+ const mediaRepo = new MediaRepository(ctx.db);
1047
+ await mediaRepo.create({
1048
+ filename: finalFilename,
1049
+ mimeType: contentType,
1050
+ size: body.length,
1051
+ width,
1052
+ height,
1053
+ alt,
1054
+ caption,
1055
+ storageKey,
1056
+ status: "ready",
1057
+ });
1058
+
1059
+ // Create the MediaValue - only store id, URL is built at runtime by EmDashMedia
1060
+ const mediaValue: MediaValue = {
1061
+ provider: "local",
1062
+ id,
1063
+ alt: alt ?? undefined,
1064
+ width,
1065
+ height,
1066
+ mimeType: contentType,
1067
+ filename: finalFilename,
1068
+ meta: { storageKey },
1069
+ };
1070
+
1071
+ // Cache for reuse
1072
+ ctx.mediaCache.set(url, mediaValue);
1073
+ result.media.created++;
1074
+
1075
+ console.log(` ✅ Uploaded: ${finalFilename}`);
1076
+ return mediaValue;
1077
+ } catch (error) {
1078
+ console.warn(
1079
+ ` ⚠️ Error processing $media ${url}:`,
1080
+ error instanceof Error ? error.message : error,
1081
+ );
1082
+ result.media.skipped++;
1083
+ return null;
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Get file extension from content type
1089
+ */
1090
+ function getExtensionFromContentType(contentType: string): string | null {
1091
+ // Handle content-type with parameters like "image/jpeg; charset=utf-8"
1092
+ const baseMime = contentType.split(";")[0].trim();
1093
+ const ext = mime.getExtension(baseMime);
1094
+ return ext ? `.${ext}` : null;
1095
+ }
1096
+
1097
+ /**
1098
+ * Get file extension from URL
1099
+ */
1100
+ function getExtensionFromUrl(url: string): string | null {
1101
+ try {
1102
+ const pathname = new URL(url).pathname;
1103
+ const match = pathname.match(FILE_EXTENSION_PATTERN);
1104
+ return match ? `.${match[1]}` : null;
1105
+ } catch {
1106
+ return null;
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * Generate a filename from URL
1112
+ */
1113
+ function generateFilename(url: string, ext: string): string {
1114
+ try {
1115
+ const pathname = new URL(url).pathname;
1116
+ const basename = pathname.split("/").pop() || "media";
1117
+ // Remove any existing extension and query params
1118
+ const name = basename.replace(EXTENSION_PATTERN, "").replace(QUERY_PARAM_PATTERN, "");
1119
+ // Sanitize: only alphanumeric, dash, underscore
1120
+ const sanitized = name.replace(SANITIZE_PATTERN, "-").replace(MULTIPLE_HYPHENS_PATTERN, "-");
1121
+ return `${sanitized || "media"}${ext}`;
1122
+ } catch {
1123
+ return `media${ext}`;
1124
+ }
1125
+ }
1126
+
1127
+ /**
1128
+ * Get image dimensions from buffer using image-size.
1129
+ * Supports PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, and more.
1130
+ */
1131
+ function getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {
1132
+ try {
1133
+ const result = imageSize(buffer);
1134
+ if (result.width != null && result.height != null) {
1135
+ return { width: result.width, height: result.height };
1136
+ }
1137
+ return null;
1138
+ } catch {
1139
+ return null;
1140
+ }
1141
+ }