dineway 0.1.3 → 0.1.5

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 (296) hide show
  1. package/README.md +6 -3
  2. package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
  3. package/dist/astro/index.d.mts +18 -9
  4. package/dist/astro/index.mjs +238 -16
  5. package/dist/astro/middleware/auth.d.mts +16 -5
  6. package/dist/astro/middleware/auth.mjs +74 -37
  7. package/dist/astro/middleware/redirect.mjs +24 -8
  8. package/dist/astro/middleware/request-context.mjs +18 -5
  9. package/dist/astro/middleware/setup.mjs +1 -1
  10. package/dist/astro/middleware.mjs +411 -169
  11. package/dist/astro/types.d.mts +25 -8
  12. package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
  13. package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
  14. package/dist/cache-BdSY-gQN.mjs +42 -0
  15. package/dist/chunks--4F8ddV4.mjs +18 -0
  16. package/dist/cli/index.mjs +935 -15
  17. package/dist/client/external-auth-headers.d.mts +1 -1
  18. package/dist/client/index.d.mts +11 -3
  19. package/dist/client/index.mjs +4 -3
  20. package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
  21. package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
  22. package/dist/database/instrumentation.d.mts +34 -0
  23. package/dist/database/instrumentation.mjs +53 -0
  24. package/dist/db/index.d.mts +3 -3
  25. package/dist/db/index.mjs +2 -2
  26. package/dist/db/libsql.d.mts +1 -1
  27. package/dist/db/libsql.mjs +11 -5
  28. package/dist/db/postgres.d.mts +1 -1
  29. package/dist/db/sqlite.d.mts +1 -1
  30. package/dist/db/sqlite.mjs +7 -1
  31. package/dist/db-errors-CEqD7qH9.mjs +23 -0
  32. package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
  33. package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
  34. package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
  35. package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
  36. package/dist/index.d.mts +11 -11
  37. package/dist/index.mjs +24 -22
  38. package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
  39. package/dist/media/index.d.mts +1 -1
  40. package/dist/media/index.mjs +1 -1
  41. package/dist/media/local-runtime.d.mts +7 -7
  42. package/dist/page/index.d.mts +10 -2
  43. package/dist/page/index.mjs +22 -1
  44. package/dist/patterns-CrCYkMBb.mjs +92 -0
  45. package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
  46. package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
  47. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  48. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  49. package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
  50. package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
  51. package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
  52. package/dist/request-cache-Dk5qPSOx.mjs +66 -0
  53. package/dist/request-context.d.mts +4 -16
  54. package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
  55. package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
  56. package/dist/runtime.d.mts +6 -6
  57. package/dist/runtime.mjs +2 -2
  58. package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
  59. package/dist/seed/index.d.mts +2 -2
  60. package/dist/seed/index.mjs +11 -10
  61. package/dist/seo/index.d.mts +1 -1
  62. package/dist/storage/local.d.mts +1 -1
  63. package/dist/storage/local.mjs +1 -1
  64. package/dist/storage/s3.d.mts +11 -3
  65. package/dist/storage/s3.mjs +78 -15
  66. package/dist/taxonomies-1s5PaS_8.mjs +266 -0
  67. package/dist/transaction-Cn2rjY78.mjs +27 -0
  68. package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
  69. package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
  70. package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
  71. package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
  72. package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
  73. package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
  74. package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
  75. package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
  76. package/dist/version-BKXPsfmJ.mjs +6 -0
  77. package/package.json +53 -39
  78. package/src/astro/routes/PluginRegistry.tsx +21 -0
  79. package/src/astro/routes/admin.astro +99 -0
  80. package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
  81. package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
  82. package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
  83. package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
  84. package/src/astro/routes/api/admin/briefing.ts +76 -0
  85. package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
  86. package/src/astro/routes/api/admin/bylines/index.ts +74 -0
  87. package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
  88. package/src/astro/routes/api/admin/comments/[id].ts +64 -0
  89. package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
  90. package/src/astro/routes/api/admin/comments/counts.ts +30 -0
  91. package/src/astro/routes/api/admin/comments/index.ts +46 -0
  92. package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
  93. package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
  94. package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
  95. package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
  96. package/src/astro/routes/api/admin/context/diff.ts +35 -0
  97. package/src/astro/routes/api/admin/context/index.ts +69 -0
  98. package/src/astro/routes/api/admin/context/stale.ts +35 -0
  99. package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
  100. package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
  101. package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
  102. package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
  103. package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
  104. package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
  105. package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
  106. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
  107. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
  108. package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
  109. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
  110. package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
  111. package/src/astro/routes/api/admin/plugins/index.ts +32 -0
  112. package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
  113. package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
  114. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
  115. package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
  116. package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
  117. package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
  118. package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
  119. package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
  120. package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
  121. package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
  122. package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
  123. package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
  124. package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
  125. package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
  126. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
  127. package/src/astro/routes/api/admin/users/index.ts +66 -0
  128. package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
  129. package/src/astro/routes/api/auth/invite/accept.ts +52 -0
  130. package/src/astro/routes/api/auth/invite/complete.ts +86 -0
  131. package/src/astro/routes/api/auth/invite/index.ts +99 -0
  132. package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
  133. package/src/astro/routes/api/auth/logout.ts +40 -0
  134. package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
  135. package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
  136. package/src/astro/routes/api/auth/me.ts +60 -0
  137. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
  138. package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
  139. package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
  140. package/src/astro/routes/api/auth/passkey/index.ts +54 -0
  141. package/src/astro/routes/api/auth/passkey/options.ts +85 -0
  142. package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
  143. package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
  144. package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
  145. package/src/astro/routes/api/auth/signup/complete.ts +87 -0
  146. package/src/astro/routes/api/auth/signup/request.ts +89 -0
  147. package/src/astro/routes/api/auth/signup/verify.ts +53 -0
  148. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
  149. package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
  150. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
  151. package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
  152. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
  153. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
  154. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
  155. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
  156. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
  157. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
  158. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
  159. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
  160. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
  161. package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
  162. package/src/astro/routes/api/content/[collection]/index.ts +103 -0
  163. package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
  164. package/src/astro/routes/api/dashboard.ts +32 -0
  165. package/src/astro/routes/api/dev/emails.ts +36 -0
  166. package/src/astro/routes/api/health.ts +54 -0
  167. package/src/astro/routes/api/import/probe.ts +47 -0
  168. package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
  169. package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
  170. package/src/astro/routes/api/import/wordpress/media.ts +338 -0
  171. package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
  172. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
  173. package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
  174. package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
  175. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
  176. package/src/astro/routes/api/manifest.ts +75 -0
  177. package/src/astro/routes/api/mcp.ts +125 -0
  178. package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
  179. package/src/astro/routes/api/media/[id].ts +145 -0
  180. package/src/astro/routes/api/media/file/[...key].ts +79 -0
  181. package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
  182. package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
  183. package/src/astro/routes/api/media/providers/index.ts +30 -0
  184. package/src/astro/routes/api/media/upload-url.ts +146 -0
  185. package/src/astro/routes/api/media.ts +204 -0
  186. package/src/astro/routes/api/menus/[name]/items.ts +206 -0
  187. package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
  188. package/src/astro/routes/api/menus/[name].ts +145 -0
  189. package/src/astro/routes/api/menus/index.ts +91 -0
  190. package/src/astro/routes/api/oauth/authorize.ts +430 -0
  191. package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
  192. package/src/astro/routes/api/oauth/device/code.ts +56 -0
  193. package/src/astro/routes/api/oauth/device/token.ts +70 -0
  194. package/src/astro/routes/api/oauth/register.ts +182 -0
  195. package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
  196. package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
  197. package/src/astro/routes/api/oauth/token.ts +195 -0
  198. package/src/astro/routes/api/openapi.json.ts +33 -0
  199. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
  200. package/src/astro/routes/api/redirects/404s/index.ts +72 -0
  201. package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
  202. package/src/astro/routes/api/redirects/[id].ts +183 -0
  203. package/src/astro/routes/api/redirects/index.ts +100 -0
  204. package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
  205. package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
  206. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
  207. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
  208. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
  209. package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
  210. package/src/astro/routes/api/schema/collections/index.ts +61 -0
  211. package/src/astro/routes/api/schema/index.ts +109 -0
  212. package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
  213. package/src/astro/routes/api/schema/orphans/index.ts +26 -0
  214. package/src/astro/routes/api/search/enable.ts +64 -0
  215. package/src/astro/routes/api/search/index.ts +52 -0
  216. package/src/astro/routes/api/search/rebuild.ts +72 -0
  217. package/src/astro/routes/api/search/stats.ts +35 -0
  218. package/src/astro/routes/api/search/suggest.ts +50 -0
  219. package/src/astro/routes/api/sections/[slug].ts +203 -0
  220. package/src/astro/routes/api/sections/index.ts +107 -0
  221. package/src/astro/routes/api/settings/email.ts +150 -0
  222. package/src/astro/routes/api/settings.ts +116 -0
  223. package/src/astro/routes/api/setup/admin-verify.ts +122 -0
  224. package/src/astro/routes/api/setup/admin.ts +104 -0
  225. package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
  226. package/src/astro/routes/api/setup/dev-reset.ts +40 -0
  227. package/src/astro/routes/api/setup/index.ts +128 -0
  228. package/src/astro/routes/api/setup/status.ts +122 -0
  229. package/src/astro/routes/api/snapshot.ts +76 -0
  230. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
  231. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
  232. package/src/astro/routes/api/taxonomies/index.ts +114 -0
  233. package/src/astro/routes/api/themes/preview.ts +78 -0
  234. package/src/astro/routes/api/typegen.ts +114 -0
  235. package/src/astro/routes/api/well-known/auth.ts +71 -0
  236. package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
  237. package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
  238. package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
  239. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
  240. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
  241. package/src/astro/routes/api/widget-areas/[name].ts +135 -0
  242. package/src/astro/routes/api/widget-areas/index.ts +149 -0
  243. package/src/astro/routes/api/widget-components.ts +22 -0
  244. package/src/astro/routes/robots.txt.ts +81 -0
  245. package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
  246. package/src/astro/routes/sitemap.xml.ts +92 -0
  247. package/src/components/Break.astro +45 -0
  248. package/src/components/Button.astro +71 -0
  249. package/src/components/Buttons.astro +49 -0
  250. package/src/components/Code.astro +59 -0
  251. package/src/components/Columns.astro +59 -0
  252. package/src/components/CommentForm.astro +315 -0
  253. package/src/components/Comments.astro +232 -0
  254. package/src/components/Cover.astro +128 -0
  255. package/src/components/DinewayBodyEnd.astro +32 -0
  256. package/src/components/DinewayBodyStart.astro +32 -0
  257. package/src/components/DinewayHead.astro +61 -0
  258. package/src/components/DinewayImage.astro +178 -0
  259. package/src/components/DinewayMedia.astro +167 -0
  260. package/src/components/Embed.astro +128 -0
  261. package/src/components/File.astro +122 -0
  262. package/src/components/Gallery.astro +93 -0
  263. package/src/components/HtmlBlock.astro +33 -0
  264. package/src/components/Image.astro +178 -0
  265. package/src/components/InlineEditor.astro +27 -0
  266. package/src/components/InlinePortableTextEditor.tsx +1937 -0
  267. package/src/components/LiveSearch.astro +614 -0
  268. package/src/components/PortableText.astro +51 -0
  269. package/src/components/Pullquote.astro +51 -0
  270. package/src/components/Table.astro +135 -0
  271. package/src/components/WidgetArea.astro +22 -0
  272. package/src/components/WidgetRenderer.astro +72 -0
  273. package/src/components/index.ts +106 -0
  274. package/src/components/marks/Link.astro +31 -0
  275. package/src/components/marks/StrikeThrough.astro +7 -0
  276. package/src/components/marks/Subscript.astro +7 -0
  277. package/src/components/marks/Superscript.astro +7 -0
  278. package/src/components/marks/Underline.astro +7 -0
  279. package/src/components/marks.ts +19 -0
  280. package/src/components/widgets/Archives.astro +65 -0
  281. package/src/components/widgets/Categories.astro +35 -0
  282. package/src/components/widgets/RecentPosts.astro +51 -0
  283. package/src/components/widgets/Search.astro +18 -0
  284. package/src/components/widgets/Tags.astro +38 -0
  285. package/src/ui.ts +75 -0
  286. package/LICENSE +0 -9
  287. /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
  288. /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
  289. /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
  290. /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
  291. /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
  292. /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
  293. /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
  294. /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
  295. /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
  296. /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
@@ -0,0 +1,330 @@
1
+ /**
2
+ * WordPress WXR execute import endpoint
3
+ *
4
+ * POST /_dineway/api/import/wordpress/execute
5
+ *
6
+ * Accepts WXR file and import configuration, imports content into the database.
7
+ */
8
+
9
+ import { gutenbergToPortableText } from "@dineway-ai/gutenberg-to-portable-text";
10
+ import type { APIRoute } from "astro";
11
+ import {
12
+ parseWxrString,
13
+ ContentRepository,
14
+ importReusableBlocksAsSections,
15
+ type WxrPost,
16
+ parseWxrDate,
17
+ } from "dineway";
18
+
19
+ import { requirePerm } from "#api/authorize.js";
20
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
21
+ import {
22
+ ensureWorkflowHitlRouteRequest,
23
+ hitlRequiredRouteError,
24
+ resolveHitlRouteActor,
25
+ } from "#api/hitl-route-helpers.js";
26
+ import { BylineRepository } from "#db/repositories/byline.js";
27
+ import { resolveImportByline } from "#import/utils.js";
28
+ import { sanitizeWordPressImportSlug } from "#import/wordpress-slugs.js";
29
+ import { RiskPolicyEvaluator, WordPressImportHitlPayloadBuilder } from "#site-context/index.js";
30
+ import type { DinewayHandlers, DinewayManifest } from "#types";
31
+ import { slugify } from "#utils/slugify.js";
32
+
33
+ export const prerender = false;
34
+
35
+ export interface ImportConfig {
36
+ /** Map WordPress post types to Dineway collections */
37
+ postTypeMappings: Record<
38
+ string,
39
+ {
40
+ collection: string;
41
+ enabled: boolean;
42
+ }
43
+ >;
44
+ /** Whether to skip items that already exist (by slug) */
45
+ skipExisting: boolean;
46
+ /** Whether to import reusable blocks (wp_block) as sections */
47
+ importSections?: boolean;
48
+ /** Author mappings (WP author login -> Dineway user ID) */
49
+ authorMappings?: Record<string, string | null>;
50
+ /** BCP 47 locale for all imported items. When omitted, defaults to defaultLocale. */
51
+ locale?: string;
52
+ }
53
+
54
+ export interface ImportResult {
55
+ success: boolean;
56
+ imported: number;
57
+ skipped: number;
58
+ errors: Array<{ title: string; error: string }>;
59
+ byCollection: Record<string, number>;
60
+ /** Sections import results (if enabled) */
61
+ sections?: {
62
+ created: number;
63
+ skipped: number;
64
+ };
65
+ }
66
+
67
+ export const POST: APIRoute = async ({ request, locals }) => {
68
+ const { dineway, dinewayManifest, user } = locals;
69
+
70
+ const denied = requirePerm(user, "import:execute");
71
+ if (denied) return denied;
72
+
73
+ if (!dineway?.handleContentCreate) {
74
+ return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
75
+ }
76
+
77
+ try {
78
+ const formData = await request.formData();
79
+ const fileEntry = formData.get("file");
80
+ const file = fileEntry instanceof File ? fileEntry : null;
81
+ const configEntry = formData.get("config");
82
+ const configJson = typeof configEntry === "string" ? configEntry : null;
83
+ const hitlRequestEntry = formData.get("hitlRequestId");
84
+ const hitlRequestId =
85
+ typeof hitlRequestEntry === "string" && hitlRequestEntry.length > 0
86
+ ? hitlRequestEntry
87
+ : undefined;
88
+
89
+ if (!file) {
90
+ return apiError("VALIDATION_ERROR", "No file provided", 400);
91
+ }
92
+
93
+ if (!configJson) {
94
+ return apiError("VALIDATION_ERROR", "No config provided", 400);
95
+ }
96
+
97
+ const config: ImportConfig = JSON.parse(configJson);
98
+ const actor = resolveHitlRouteActor(locals);
99
+ const evaluator = new RiskPolicyEvaluator({
100
+ db: dineway.db,
101
+ handlers: dineway,
102
+ });
103
+ let text: string;
104
+ if (evaluator.requiresWorkflowHitl(actor.identity)) {
105
+ const fileBuffer = await file.arrayBuffer();
106
+ const action = await new WordPressImportHitlPayloadBuilder().buildExecuteRequest({
107
+ fileName: file.name,
108
+ fileBuffer,
109
+ config,
110
+ });
111
+ const decision = await evaluator.evaluateWorkflowHitl({
112
+ actor: actor.identity,
113
+ hitlRequestId,
114
+ action,
115
+ });
116
+ if (!decision.allowed) {
117
+ const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
118
+ return hitlRequiredRouteError(decision, ensured);
119
+ }
120
+ text = new TextDecoder().decode(fileBuffer);
121
+ } else {
122
+ text = await file.text();
123
+ }
124
+
125
+ const wxr = await parseWxrString(text);
126
+
127
+ // Build attachment ID -> URL map for featured images
128
+ const attachmentMap = new Map<string, string>();
129
+ for (const att of wxr.attachments) {
130
+ if (att.id && att.url) {
131
+ attachmentMap.set(String(att.id), att.url);
132
+ }
133
+ }
134
+
135
+ // Build author login -> display name map
136
+ const authorDisplayNames = new Map<string, string>();
137
+ for (const author of wxr.authors) {
138
+ if (!author.login) continue;
139
+ authorDisplayNames.set(author.login, author.displayName || author.login);
140
+ }
141
+
142
+ // Import content (locale from config scopes all items)
143
+ const result = await importContent(
144
+ wxr.posts,
145
+ config,
146
+ dineway,
147
+ dinewayManifest,
148
+ attachmentMap,
149
+ config.locale,
150
+ authorDisplayNames,
151
+ );
152
+
153
+ // Import reusable blocks as sections (if enabled)
154
+ if (config.importSections !== false) {
155
+ const sectionsResult = await importReusableBlocksAsSections(wxr.posts, dineway.db);
156
+ result.sections = {
157
+ created: sectionsResult.sectionsCreated,
158
+ skipped: sectionsResult.sectionsSkipped,
159
+ };
160
+ // Add section errors to main errors array
161
+ result.errors.push(...sectionsResult.errors);
162
+ if (sectionsResult.errors.length > 0) {
163
+ result.success = false;
164
+ }
165
+ }
166
+
167
+ return apiSuccess(result);
168
+ } catch (error) {
169
+ return handleError(error, "Failed to import content", "WXR_IMPORT_ERROR");
170
+ }
171
+ };
172
+
173
+ async function importContent(
174
+ posts: WxrPost[],
175
+ config: ImportConfig,
176
+ dineway: DinewayHandlers,
177
+ manifest: DinewayManifest,
178
+ attachmentMap: Map<string, string>,
179
+ locale?: string,
180
+ authorDisplayNames?: Map<string, string>,
181
+ ): Promise<ImportResult> {
182
+ const result: ImportResult = {
183
+ success: true,
184
+ imported: 0,
185
+ skipped: 0,
186
+ errors: [],
187
+ byCollection: {},
188
+ };
189
+
190
+ // Create content repository for checking existing items
191
+ const contentRepo = new ContentRepository(dineway.db);
192
+ const bylineRepo = new BylineRepository(dineway.db);
193
+ const bylineCache = new Map<string, string>();
194
+
195
+ for (const post of posts) {
196
+ const postType = post.postType || "post";
197
+ const mapping = config.postTypeMappings[postType];
198
+
199
+ // Skip if not mapped or disabled
200
+ if (!mapping || !mapping.enabled) {
201
+ result.skipped++;
202
+ continue;
203
+ }
204
+
205
+ // Defensive: mapping.collection is already sanitized by prepare, but the user
206
+ // could manually edit the import config between prepare and execute.
207
+ const collection = sanitizeWordPressImportSlug(mapping.collection);
208
+
209
+ // Check if collection exists in manifest
210
+ if (!manifest?.collections[collection]) {
211
+ result.errors.push({
212
+ title: post.title || "Untitled",
213
+ error: `Collection "${collection}" does not exist`,
214
+ });
215
+ continue;
216
+ }
217
+
218
+ try {
219
+ // Convert content to Portable Text
220
+ const content = post.content ? gutenbergToPortableText(post.content) : [];
221
+
222
+ // Generate slug from post name or title
223
+ const slug = post.postName || slugify(post.title || `post-${post.id || Date.now()}`);
224
+
225
+ // Check if already exists (idempotency)
226
+ if (config.skipExisting) {
227
+ const existing = await contentRepo.findBySlug(collection, slug);
228
+ if (existing) {
229
+ result.skipped++;
230
+ continue;
231
+ }
232
+ }
233
+
234
+ // Map WordPress status to Dineway status
235
+ const status = mapStatus(post.status);
236
+
237
+ // Build data object with required fields
238
+ const data: Record<string, unknown> = {
239
+ title: post.title || "Untitled",
240
+ content,
241
+ excerpt: post.excerpt || undefined,
242
+ };
243
+
244
+ // Only add featured_image if the collection has this field and we have a value
245
+ const collectionSchema = manifest.collections[collection];
246
+ const hasFeaturedImageField = collectionSchema?.fields
247
+ ? "featured_image" in collectionSchema.fields
248
+ : false;
249
+ if (hasFeaturedImageField) {
250
+ const thumbnailId = post.meta.get("_thumbnail_id");
251
+ const featuredImage = thumbnailId ? attachmentMap.get(String(thumbnailId)) : undefined;
252
+ if (featuredImage) {
253
+ data.featured_image = featuredImage;
254
+ }
255
+ }
256
+
257
+ // Resolve author ID from mappings
258
+ let authorId: string | undefined;
259
+ if (config.authorMappings && post.creator) {
260
+ const mappedUserId = config.authorMappings[post.creator];
261
+ if (mappedUserId !== undefined && mappedUserId !== null) {
262
+ authorId = mappedUserId;
263
+ }
264
+ }
265
+
266
+ const bylineId = await resolveImportByline(
267
+ post.creator,
268
+ authorDisplayNames?.get(post.creator ?? "") ?? post.creator,
269
+ authorId,
270
+ bylineRepo,
271
+ bylineCache,
272
+ );
273
+
274
+ // Preserve original WordPress dates using the shared WXR date parser.
275
+ // Fallback chain: postDateGmt (UTC) → pubDate (RFC 2822) → postDate (site-local).
276
+ const parsedDate = parseWxrDate(post.postDateGmt, post.pubDate, post.postDate);
277
+ const createdAt = parsedDate ? parsedDate.toISOString() : undefined;
278
+ const publishedAt = status === "published" && createdAt ? createdAt : undefined;
279
+
280
+ // Create the content item
281
+ const createResult = await dineway.handleContentCreate(collection, {
282
+ data,
283
+ slug,
284
+ status,
285
+ authorId,
286
+ bylines: bylineId ? [{ bylineId }] : undefined,
287
+ locale,
288
+ createdAt,
289
+ publishedAt,
290
+ });
291
+
292
+ if (createResult.success) {
293
+ result.imported++;
294
+ result.byCollection[collection] = (result.byCollection[collection] || 0) + 1;
295
+ } else {
296
+ result.errors.push({
297
+ title: post.title || "Untitled",
298
+ error:
299
+ typeof createResult.error === "object" && createResult.error !== null
300
+ ? (createResult.error as { message?: string }).message || "Unknown error"
301
+ : String(createResult.error),
302
+ });
303
+ }
304
+ } catch (error) {
305
+ console.error(`Import error for "${post.title || "Untitled"}":`, error);
306
+ result.errors.push({
307
+ title: post.title || "Untitled",
308
+ error: error instanceof Error && error.message ? error.message : "Failed to import item",
309
+ });
310
+ }
311
+ }
312
+
313
+ result.success = result.errors.length === 0;
314
+ return result;
315
+ }
316
+
317
+ function mapStatus(wpStatus: string | undefined): string {
318
+ switch (wpStatus) {
319
+ case "publish":
320
+ return "published";
321
+ case "draft":
322
+ return "draft";
323
+ case "pending":
324
+ return "draft";
325
+ case "private":
326
+ return "draft";
327
+ default:
328
+ return "draft";
329
+ }
330
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * WordPress media import endpoint
3
+ *
4
+ * POST /_dineway/api/import/wordpress/media
5
+ *
6
+ * Downloads media attachments from WordPress URLs and uploads to Dineway storage.
7
+ * Streams progress updates as newline-delimited JSON (NDJSON).
8
+ * Each line is either a progress update or the final result.
9
+ */
10
+
11
+ import * as path from "node:path";
12
+
13
+ import type { APIRoute } from "astro";
14
+ import { MediaRepository, computeContentHash } from "dineway";
15
+ import mime from "mime/lite";
16
+ import { ulid } from "ulidx";
17
+
18
+ import { requirePerm } from "#api/authorize.js";
19
+ import { apiError, apiSuccess, handleError } from "#api/error.js";
20
+ import { isParseError, parseBody } from "#api/parse.js";
21
+ import { wpMediaImportBody } from "#api/schemas.js";
22
+ import { validateExternalUrl, ssrfSafeFetch, SsrfError } from "#import/ssrf.js";
23
+ import type { DinewayHandlers } from "#types";
24
+
25
+ import type { AttachmentInfo } from "./analyze.js";
26
+
27
+ export const prerender = false;
28
+
29
+ /** Progress update sent during streaming */
30
+ export interface MediaImportProgress {
31
+ type: "progress";
32
+ current: number;
33
+ total: number;
34
+ filename?: string;
35
+ status: "downloading" | "uploading" | "done" | "skipped" | "failed";
36
+ error?: string;
37
+ }
38
+
39
+ /** Final result sent at end of stream */
40
+ export interface MediaImportResult {
41
+ type?: "result";
42
+ /** Successfully imported items */
43
+ imported: Array<{
44
+ wpId?: number;
45
+ originalUrl: string;
46
+ newUrl: string;
47
+ mediaId: string;
48
+ }>;
49
+ /** Failed items */
50
+ failed: Array<{
51
+ wpId?: number;
52
+ originalUrl: string;
53
+ error: string;
54
+ }>;
55
+ /** Map of old URLs to new URLs (for content rewriting) */
56
+ urlMap: Record<string, string>;
57
+ }
58
+
59
+ export const POST: APIRoute = async ({ request, locals }) => {
60
+ const { dineway, user } = locals;
61
+
62
+ const denied = requirePerm(user, "import:execute");
63
+ if (denied) return denied;
64
+
65
+ if (!dineway?.storage) {
66
+ return apiError("NO_STORAGE", "Storage not configured. Media import requires storage.", 501);
67
+ }
68
+
69
+ if (!dineway?.db) {
70
+ return apiError("NO_DB", "Database not initialized", 500);
71
+ }
72
+
73
+ try {
74
+ const body = await parseBody(request, wpMediaImportBody);
75
+ if (isParseError(body)) return body;
76
+
77
+ const attachments = body.attachments as AttachmentInfo[];
78
+
79
+ // Check if streaming is requested (default: true)
80
+ const shouldStream = body.stream !== false;
81
+
82
+ if (shouldStream) {
83
+ // Stream progress updates as NDJSON
84
+ const stream = new ReadableStream({
85
+ async start(controller) {
86
+ const encoder = new TextEncoder();
87
+ const sendProgress = (progress: MediaImportProgress) => {
88
+ controller.enqueue(encoder.encode(JSON.stringify(progress) + "\n"));
89
+ };
90
+
91
+ const result = await importMediaWithProgress(
92
+ attachments,
93
+ dineway.db,
94
+ dineway.storage,
95
+ request.url,
96
+ sendProgress,
97
+ );
98
+
99
+ // Send final result
100
+ controller.enqueue(encoder.encode(JSON.stringify({ ...result, type: "result" }) + "\n"));
101
+ controller.close();
102
+ },
103
+ });
104
+
105
+ return new Response(stream, {
106
+ status: 200,
107
+ headers: {
108
+ "Content-Type": "application/x-ndjson",
109
+ "Cache-Control": "private, no-store",
110
+ "Transfer-Encoding": "chunked",
111
+ },
112
+ });
113
+ }
114
+
115
+ // Non-streaming mode
116
+ const result = await importMediaWithProgress(
117
+ attachments,
118
+ dineway.db,
119
+ dineway.storage,
120
+ request.url,
121
+ () => {}, // No-op progress callback
122
+ );
123
+
124
+ return apiSuccess(result);
125
+ } catch (error) {
126
+ return handleError(error, "Failed to import media", "IMPORT_ERROR");
127
+ }
128
+ };
129
+
130
+ async function importMediaWithProgress(
131
+ attachments: AttachmentInfo[],
132
+ db: NonNullable<DinewayHandlers["db"]>,
133
+ storage: NonNullable<DinewayHandlers["storage"]>,
134
+ requestUrl: string,
135
+ onProgress: (progress: MediaImportProgress) => void,
136
+ ): Promise<MediaImportResult> {
137
+ const repo = new MediaRepository(db);
138
+ const url = new URL(requestUrl);
139
+ const baseUrl = `${url.protocol}//${url.host}`;
140
+ const total = attachments.length;
141
+
142
+ const result: MediaImportResult = {
143
+ imported: [],
144
+ failed: [],
145
+ urlMap: {},
146
+ };
147
+
148
+ for (let i = 0; i < attachments.length; i++) {
149
+ const attachment = attachments[i];
150
+ const current = i + 1;
151
+ const filename = attachment.filename || `file-${attachment.id}`;
152
+
153
+ if (!attachment.url) {
154
+ result.failed.push({
155
+ wpId: attachment.id,
156
+ originalUrl: "",
157
+ error: "No URL provided",
158
+ });
159
+ onProgress({
160
+ type: "progress",
161
+ current,
162
+ total,
163
+ filename,
164
+ status: "failed",
165
+ error: "No URL provided",
166
+ });
167
+ continue;
168
+ }
169
+
170
+ try {
171
+ // SSRF: validate URL before fetching
172
+ try {
173
+ validateExternalUrl(attachment.url);
174
+ } catch (e) {
175
+ const msg = e instanceof SsrfError ? e.message : "Invalid URL";
176
+ result.failed.push({
177
+ wpId: attachment.id,
178
+ originalUrl: attachment.url,
179
+ error: `Blocked: ${msg}`,
180
+ });
181
+ onProgress({
182
+ type: "progress",
183
+ current,
184
+ total,
185
+ filename,
186
+ status: "failed",
187
+ error: `Blocked: ${msg}`,
188
+ });
189
+ continue;
190
+ }
191
+
192
+ // Report downloading
193
+ onProgress({
194
+ type: "progress",
195
+ current,
196
+ total,
197
+ filename,
198
+ status: "downloading",
199
+ });
200
+
201
+ // Download from WordPress (ssrfSafeFetch re-validates redirect targets)
202
+ const response = await ssrfSafeFetch(attachment.url, {
203
+ headers: {
204
+ "User-Agent": "Dineway-Importer/1.0",
205
+ },
206
+ });
207
+
208
+ if (!response.ok) {
209
+ result.failed.push({
210
+ wpId: attachment.id,
211
+ originalUrl: attachment.url,
212
+ error: `HTTP ${response.status}: ${response.statusText}`,
213
+ });
214
+ onProgress({
215
+ type: "progress",
216
+ current,
217
+ total,
218
+ filename,
219
+ status: "failed",
220
+ error: `HTTP ${response.status}`,
221
+ });
222
+ continue;
223
+ }
224
+
225
+ // Get content type from response or guess from filename
226
+ const contentType =
227
+ response.headers.get("content-type") || attachment.mimeType || "application/octet-stream";
228
+
229
+ // Get the file data
230
+ const buffer = await response.arrayBuffer();
231
+ const size = buffer.byteLength;
232
+
233
+ // Compute content hash for deduplication
234
+ const contentHash = await computeContentHash(buffer);
235
+
236
+ // Check if we already have this exact content
237
+ const existing = await repo.findByContentHash(contentHash);
238
+ if (existing) {
239
+ // Same content already exists - reuse it
240
+ const existingUrl = `${baseUrl}/_dineway/api/media/file/${existing.storageKey}`;
241
+ result.urlMap[attachment.url] = existingUrl;
242
+ result.imported.push({
243
+ wpId: attachment.id,
244
+ originalUrl: attachment.url,
245
+ newUrl: existingUrl,
246
+ mediaId: existing.id,
247
+ });
248
+ onProgress({
249
+ type: "progress",
250
+ current,
251
+ total,
252
+ filename,
253
+ status: "skipped",
254
+ });
255
+ continue;
256
+ }
257
+
258
+ // Report uploading
259
+ onProgress({
260
+ type: "progress",
261
+ current,
262
+ total,
263
+ filename,
264
+ status: "uploading",
265
+ });
266
+
267
+ // Generate storage key
268
+ const id = ulid();
269
+ const ext = attachment.filename
270
+ ? path.extname(attachment.filename)
271
+ : getExtensionFromMimeType(contentType);
272
+ const storageKey = `${id}${ext}`;
273
+
274
+ // Upload to storage
275
+ await storage.upload({
276
+ key: storageKey,
277
+ body: new Uint8Array(buffer),
278
+ contentType,
279
+ });
280
+
281
+ // Create media record with content hash
282
+ const mediaItem = await repo.create({
283
+ filename: attachment.filename || `media-${attachment.id}${ext}`,
284
+ mimeType: contentType,
285
+ size,
286
+ storageKey,
287
+ contentHash,
288
+ width: undefined,
289
+ height: undefined,
290
+ });
291
+
292
+ // Build the new URL
293
+ const newUrl = `${baseUrl}/_dineway/api/media/file/${storageKey}`;
294
+
295
+ result.imported.push({
296
+ wpId: attachment.id,
297
+ originalUrl: attachment.url,
298
+ newUrl,
299
+ mediaId: mediaItem.id,
300
+ });
301
+
302
+ // Add to URL map
303
+ result.urlMap[attachment.url] = newUrl;
304
+
305
+ // Report done
306
+ onProgress({
307
+ type: "progress",
308
+ current,
309
+ total,
310
+ filename,
311
+ status: "done",
312
+ });
313
+ } catch (error) {
314
+ console.error(`Media import error for "${filename}":`, error);
315
+ const errorMsg = "Failed to import media";
316
+ result.failed.push({
317
+ wpId: attachment.id,
318
+ originalUrl: attachment.url,
319
+ error: errorMsg,
320
+ });
321
+ onProgress({
322
+ type: "progress",
323
+ current,
324
+ total,
325
+ filename,
326
+ status: "failed",
327
+ error: errorMsg,
328
+ });
329
+ }
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ function getExtensionFromMimeType(mimeType: string): string {
336
+ const ext = mime.getExtension(mimeType);
337
+ return ext ? `.${ext}` : "";
338
+ }