emdash 0.8.0 → 0.10.0

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 (317) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
  4. package/dist/apply-UsrFuO7l.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +118 -4
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +14 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +15 -10
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +8 -5
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +70 -121
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +25 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
  26. package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
  27. package/dist/bylines-esI7ioa9.mjs +113 -0
  28. package/dist/bylines-esI7ioa9.mjs.map +1 -0
  29. package/dist/cache-fTzxgMFJ.mjs +65 -0
  30. package/dist/cache-fTzxgMFJ.mjs.map +1 -0
  31. package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
  32. package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
  33. package/dist/cli/index.mjs +456 -90
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/client/cf-access.d.mts +1 -1
  36. package/dist/client/index.d.mts +1 -1
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/client/index.mjs.map +1 -1
  39. package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
  40. package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
  41. package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
  42. package/dist/content-C7G4QXkK.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/libsql.d.mts.map +1 -1
  47. package/dist/db/libsql.mjs +7 -2
  48. package/dist/db/libsql.mjs.map +1 -1
  49. package/dist/db/postgres.d.mts +1 -1
  50. package/dist/db/sqlite.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts.map +1 -1
  52. package/dist/db/sqlite.mjs +8 -3
  53. package/dist/db/sqlite.mjs.map +1 -1
  54. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  55. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  56. package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
  57. package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  58. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  59. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  60. package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
  61. package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  62. package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
  63. package/dist/index-DjPMOfO0.d.mts.map +1 -0
  64. package/dist/index.d.mts +11 -11
  65. package/dist/index.mjs +27 -24
  66. package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
  67. package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
  68. package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
  69. package/dist/loader-Bx2_9-5e.mjs.map +1 -0
  70. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
  71. package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
  72. package/dist/media/index.d.mts +1 -1
  73. package/dist/media/index.mjs +1 -1
  74. package/dist/media/local-runtime.d.mts +7 -7
  75. package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
  76. package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  77. package/dist/options-nPxWnrya.mjs +117 -0
  78. package/dist/options-nPxWnrya.mjs.map +1 -0
  79. package/dist/page/index.d.mts +2 -2
  80. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  81. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  82. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  83. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  84. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  85. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  86. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  87. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  88. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  89. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  90. package/dist/public-url-B1AxbbbQ.mjs +51 -0
  91. package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
  92. package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
  93. package/dist/query-Bo-msrmu.mjs.map +1 -0
  94. package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
  95. package/dist/redirect-C5H7VGIX.mjs.map +1 -0
  96. package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
  97. package/dist/registry-Beb7wxFc.mjs.map +1 -0
  98. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
  99. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
  100. package/dist/runner-Clwe4Mme.d.mts +44 -0
  101. package/dist/runner-Clwe4Mme.d.mts.map +1 -0
  102. package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
  103. package/dist/runner-DMnlIkh4.mjs.map +1 -0
  104. package/dist/runtime.d.mts +6 -6
  105. package/dist/runtime.mjs +2 -2
  106. package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
  107. package/dist/search-DkN-BqsS.mjs.map +1 -0
  108. package/dist/secrets-CZ8rxLX3.mjs +314 -0
  109. package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
  110. package/dist/seed/index.d.mts +2 -2
  111. package/dist/seed/index.mjs +13 -11
  112. package/dist/seo/index.d.mts +1 -1
  113. package/dist/storage/local.d.mts +1 -1
  114. package/dist/storage/local.mjs +1 -1
  115. package/dist/storage/s3.d.mts +1 -1
  116. package/dist/storage/s3.mjs +1 -1
  117. package/dist/taxonomies-CTtewrSQ.mjs +407 -0
  118. package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
  119. package/dist/taxonomy-DSxx2K2L.mjs +218 -0
  120. package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
  121. package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
  122. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  123. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  124. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  125. package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  127. package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
  128. package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  129. package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
  130. package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  131. package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
  132. package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  133. package/dist/types-CoO6mpV3.mjs +68 -0
  134. package/dist/types-CoO6mpV3.mjs.map +1 -0
  135. package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
  136. package/dist/types-D19uBYWn.d.mts.map +1 -0
  137. package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
  138. package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
  139. package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
  140. package/dist/types-Dtx1mSMX.d.mts.map +1 -0
  141. package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
  142. package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
  143. package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
  144. package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  145. package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
  146. package/dist/validate-CBIbxM3L.mjs.map +1 -0
  147. package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
  148. package/dist/validate-DHGwADqO.d.mts.map +1 -0
  149. package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
  150. package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
  151. package/dist/version-CMD42IRC.mjs +7 -0
  152. package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
  153. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
  154. package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
  155. package/locals.d.ts +1 -6
  156. package/package.json +9 -8
  157. package/src/api/handlers/comments.ts +6 -4
  158. package/src/api/handlers/content.ts +40 -1
  159. package/src/api/handlers/dashboard.ts +29 -36
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/menus.ts +256 -75
  163. package/src/api/handlers/oauth-authorization.ts +72 -33
  164. package/src/api/handlers/revision.ts +23 -14
  165. package/src/api/handlers/taxonomies.ts +273 -100
  166. package/src/api/public-url.ts +48 -2
  167. package/src/api/schemas/comments.ts +2 -2
  168. package/src/api/schemas/common.ts +7 -0
  169. package/src/api/schemas/content.ts +17 -0
  170. package/src/api/schemas/menus.ts +23 -0
  171. package/src/api/schemas/sections.ts +3 -3
  172. package/src/api/schemas/taxonomies.ts +39 -0
  173. package/src/api/schemas/users.ts +1 -1
  174. package/src/api/types.ts +5 -1
  175. package/src/astro/integration/index.ts +17 -0
  176. package/src/astro/integration/routes.ts +10 -0
  177. package/src/astro/integration/runtime.ts +30 -0
  178. package/src/astro/integration/virtual-modules.ts +32 -2
  179. package/src/astro/integration/vite-config.ts +6 -1
  180. package/src/astro/middleware/auth.ts +13 -6
  181. package/src/astro/middleware/redirect.ts +29 -16
  182. package/src/astro/middleware/request-context.ts +15 -5
  183. package/src/astro/middleware.ts +23 -9
  184. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  185. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  186. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  187. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  188. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  190. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  191. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  192. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  193. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  194. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  195. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  196. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  197. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  198. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  199. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  200. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  201. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  202. package/src/astro/routes/api/manifest.ts +62 -45
  203. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  204. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  205. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  206. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  207. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  208. package/src/astro/routes/api/menus/[name].ts +19 -10
  209. package/src/astro/routes/api/menus/index.ts +9 -6
  210. package/src/astro/routes/api/openapi.json.ts +27 -10
  211. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  212. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  213. package/src/astro/routes/api/redirects/[id].ts +10 -4
  214. package/src/astro/routes/api/redirects/index.ts +7 -3
  215. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  216. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  217. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  218. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  219. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  220. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  221. package/src/astro/routes/api/search/index.ts +10 -2
  222. package/src/astro/routes/api/sections/[slug].ts +10 -4
  223. package/src/astro/routes/api/sections/index.ts +7 -3
  224. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  225. package/src/astro/routes/api/snapshot.ts +44 -18
  226. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  227. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  228. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  229. package/src/astro/routes/api/taxonomies/index.ts +9 -7
  230. package/src/astro/routes/api/themes/preview.ts +11 -5
  231. package/src/astro/types.ts +23 -3
  232. package/src/auth/allowed-origins.ts +168 -0
  233. package/src/auth/passkey-config.ts +35 -13
  234. package/src/bylines/index.ts +37 -88
  235. package/src/cli/commands/auth.ts +28 -6
  236. package/src/cli/commands/bundle-utils.ts +11 -2
  237. package/src/cli/commands/bundle.ts +28 -8
  238. package/src/cli/commands/content.ts +13 -0
  239. package/src/cli/commands/export-seed.ts +82 -21
  240. package/src/cli/commands/login.ts +8 -1
  241. package/src/cli/commands/plugin-init.ts +216 -90
  242. package/src/cli/commands/publish.ts +24 -0
  243. package/src/cli/commands/secrets.ts +183 -0
  244. package/src/cli/credentials.ts +1 -1
  245. package/src/cli/index.ts +5 -1
  246. package/src/client/index.ts +4 -4
  247. package/src/client/transport.ts +17 -7
  248. package/src/components/Break.astro +2 -2
  249. package/src/components/EmDashHead.astro +18 -13
  250. package/src/components/Embed.astro +1 -1
  251. package/src/components/Gallery.astro +1 -1
  252. package/src/components/Image.astro +1 -1
  253. package/src/components/InlinePortableTextEditor.tsx +104 -18
  254. package/src/config/secrets.ts +528 -0
  255. package/src/database/dialect-helpers.ts +50 -0
  256. package/src/database/migrations/034_published_at_index.ts +1 -1
  257. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  258. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  259. package/src/database/migrations/runner.ts +158 -23
  260. package/src/database/repositories/content.ts +47 -12
  261. package/src/database/repositories/redirect.ts +14 -3
  262. package/src/database/repositories/taxonomy.ts +212 -82
  263. package/src/database/types.ts +10 -2
  264. package/src/db/libsql.ts +1 -3
  265. package/src/db/sqlite.ts +2 -5
  266. package/src/emdash-runtime.ts +84 -159
  267. package/src/i18n/resolve.ts +37 -0
  268. package/src/index.ts +9 -0
  269. package/src/loader.ts +73 -3
  270. package/src/mcp/server.ts +180 -54
  271. package/src/menus/index.ts +143 -124
  272. package/src/menus/types.ts +15 -1
  273. package/src/page/site-identity.ts +58 -0
  274. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  275. package/src/plugins/context.ts +13 -10
  276. package/src/plugins/define-plugin.ts +40 -12
  277. package/src/plugins/hooks.ts +23 -19
  278. package/src/plugins/index.ts +9 -0
  279. package/src/plugins/manifest-schema.ts +37 -2
  280. package/src/plugins/types.ts +151 -11
  281. package/src/preview/urls.ts +23 -3
  282. package/src/query.ts +148 -5
  283. package/src/redirects/cache.ts +38 -18
  284. package/src/schema/registry.ts +56 -0
  285. package/src/schema/zod-generator.ts +39 -7
  286. package/src/seed/apply.ts +142 -54
  287. package/src/seed/types.ts +14 -1
  288. package/src/seed/validate.ts +27 -13
  289. package/src/settings/index.ts +80 -6
  290. package/src/settings/types.ts +23 -1
  291. package/src/taxonomies/index.ts +237 -210
  292. package/src/taxonomies/types.ts +10 -0
  293. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  294. package/dist/bylines-CRNsVG88.mjs +0 -157
  295. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  296. package/dist/cache-BkKBuIvS.mjs +0 -56
  297. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  298. package/dist/chunk-ClPoSABd.mjs +0 -21
  299. package/dist/content-BcQPYxdV.mjs.map +0 -1
  300. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  301. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  302. package/dist/loader-CndGj8kM.mjs.map +0 -1
  303. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  304. package/dist/query-fqEdLFms.mjs.map +0 -1
  305. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  306. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  307. package/dist/runner-OURCaApa.d.mts +0 -34
  308. package/dist/runner-OURCaApa.d.mts.map +0 -1
  309. package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
  310. package/dist/search-BoZYFuUk.mjs.map +0 -1
  311. package/dist/taxonomies-B4IAshV8.mjs +0 -308
  312. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  313. package/dist/types-CS8FIX7L.d.mts.map +0 -1
  314. package/dist/types-i36XcA_X.d.mts.map +0 -1
  315. package/dist/validate-CxVsLehf.mjs.map +0 -1
  316. package/dist/validate-DHxmpFJt.d.mts.map +0 -1
  317. package/dist/version-Bbq8TCrz.mjs +0 -7
@@ -9,7 +9,12 @@
9
9
  export interface PasskeyConfig {
10
10
  rpName: string;
11
11
  rpId: string;
12
- origin: string;
12
+ /**
13
+ * Accepted client-data origins. First entry is the canonical/preferred origin;
14
+ * additional entries support multi-origin deployments (e.g. apex + preview
15
+ * subdomain sharing the same `rpId`). See `allowedOrigins` parameter.
16
+ */
17
+ origins: string[];
13
18
  }
14
19
 
15
20
  /**
@@ -18,10 +23,22 @@ export interface PasskeyConfig {
18
23
  * @param url The request URL (typically `new URL(Astro.request.url)` or `new URL(request.url)`)
19
24
  * @param siteName Optional site name for rpName (defaults to hostname from `url` or public origin)
20
25
  * @param siteUrl Optional browser-facing origin (see `EmDashConfig.siteUrl`).
21
- * When set, **origin** and **rpId** are taken from this URL so they match WebAuthn `clientData.origin`.
26
+ * When set, the canonical **origin** and **rpId** are taken from this URL.
27
+ * @param allowedOrigins Optional list of additional accepted origins for verification.
28
+ * Each must share `rpId` with the canonical origin (WebAuthn requirement).
29
+ * Typical use: apex + preview subdomain on the same registrable domain.
22
30
  * @throws If `siteUrl` is non-empty but not parseable by `new URL()`.
23
31
  */
24
- export function getPasskeyConfig(url: URL, siteName?: string, siteUrl?: string): PasskeyConfig {
32
+ export function getPasskeyConfig(
33
+ url: URL,
34
+ siteName?: string,
35
+ siteUrl?: string,
36
+ allowedOrigins?: string[],
37
+ ): PasskeyConfig {
38
+ let rpName: string;
39
+ let rpId: string;
40
+ let canonicalOrigin: string;
41
+
25
42
  if (siteUrl) {
26
43
  let publicUrl: URL;
27
44
  try {
@@ -29,16 +46,21 @@ export function getPasskeyConfig(url: URL, siteName?: string, siteUrl?: string):
29
46
  } catch (e) {
30
47
  throw new Error(`Invalid siteUrl: "${siteUrl}"`, { cause: e });
31
48
  }
32
- return {
33
- rpName: siteName || publicUrl.hostname,
34
- rpId: publicUrl.hostname,
35
- origin: publicUrl.origin,
36
- };
49
+ rpName = siteName || publicUrl.hostname;
50
+ rpId = publicUrl.hostname;
51
+ canonicalOrigin = publicUrl.origin;
52
+ } else {
53
+ rpName = siteName || url.hostname;
54
+ rpId = url.hostname;
55
+ canonicalOrigin = url.origin;
56
+ }
57
+
58
+ const origins = [canonicalOrigin];
59
+ if (allowedOrigins) {
60
+ for (const extra of allowedOrigins) {
61
+ if (extra && !origins.includes(extra)) origins.push(extra);
62
+ }
37
63
  }
38
64
 
39
- return {
40
- rpName: siteName || url.hostname,
41
- rpId: url.hostname,
42
- origin: url.origin,
43
- };
65
+ return { rpName, rpId, origins };
44
66
  }
@@ -12,7 +12,6 @@ import { BylineRepository } from "../database/repositories/byline.js";
12
12
  import type { BylineSummary, ContentBylineCredit } from "../database/repositories/types.js";
13
13
  import { validateIdentifier } from "../database/validate.js";
14
14
  import { getDb } from "../loader.js";
15
- import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
16
15
  import { isMissingTableError } from "../utils/db-errors.js";
17
16
 
18
17
  /**
@@ -73,15 +72,11 @@ export async function getBylineBySlug(slug: string): Promise<BylineSummary | nul
73
72
  * but the entry has an `authorId`, falls back to the user-linked byline
74
73
  * (marked as source: "inferred").
75
74
  *
76
- * @example
77
- * ```ts
78
- * import { getEntryBylines } from "emdash";
79
- *
80
- * const bylines = await getEntryBylines("posts", post.data.id);
81
- * for (const credit of bylines) {
82
- * console.log(credit.byline.displayName, credit.roleLabel);
83
- * }
84
- * ```
75
+ * Internal: not re-exported from the `emdash` package entry point. Every
76
+ * entry returned by `getEmDashCollection` / `getEmDashEntry` already has
77
+ * `data.bylines` populated by `hydrateEntryBylines` (which uses the batch
78
+ * helper `getBylinesForEntries` directly). Site code should read those
79
+ * fields rather than calling this function.
85
80
  */
86
81
  export async function getEntryBylines(
87
82
  collection: string,
@@ -108,55 +103,55 @@ export async function getEntryBylines(
108
103
  return [];
109
104
  }
110
105
 
106
+ /**
107
+ * An entry reference for batch byline lookups.
108
+ *
109
+ * `authorId` is read directly from the row when computing the inferred-byline
110
+ * fallback — passing it in avoids a redundant `SELECT id, author_id` against
111
+ * the content table after every list/entry fetch.
112
+ */
113
+ export interface BylineEntry {
114
+ id: string;
115
+ authorId: string | null;
116
+ }
117
+
111
118
  /**
112
119
  * Batch-fetch byline credits for multiple content entries in a single query.
113
120
  *
114
- * This is more efficient than calling getEntryBylines for each entry
115
- * when you need bylines for a list of entries (e.g., a blog index page).
121
+ * Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every
122
+ * entry returned from `getEmDashCollection` / `getEmDashEntry` already has
123
+ * `data.bylines` populated. Site code should rely on that eager hydration
124
+ * rather than calling this directly -- this function is not re-exported
125
+ * from the `emdash` package entry point.
116
126
  *
117
127
  * @param collection - The collection slug (e.g., "posts")
118
- * @param entryIds - Array of entry IDs
128
+ * @param entries - Entry id + authorId pairs (authorId is already on the row)
119
129
  * @returns Map from entry ID to array of byline credits
120
- *
121
- * @example
122
- * ```ts
123
- * import { getBylinesForEntries, getEmDashCollection } from "emdash";
124
- *
125
- * const { entries } = await getEmDashCollection("posts");
126
- * const ids = entries.map(e => e.data.id);
127
- * const bylinesMap = await getBylinesForEntries("posts", ids);
128
- *
129
- * for (const entry of entries) {
130
- * const bylines = bylinesMap.get(entry.data.id) ?? [];
131
- * // render bylines
132
- * }
133
- * ```
134
130
  */
135
131
  export async function getBylinesForEntries(
136
132
  collection: string,
137
- entryIds: string[],
133
+ entries: BylineEntry[],
138
134
  ): Promise<Map<string, ContentBylineCredit[]>> {
139
135
  validateIdentifier(collection, "collection");
140
136
  const result = new Map<string, ContentBylineCredit[]>();
141
137
 
142
- // Initialize all entry IDs with empty arrays
143
- for (const id of entryIds) {
138
+ for (const { id } of entries) {
144
139
  result.set(id, []);
145
140
  }
146
141
 
147
- if (entryIds.length === 0) {
142
+ if (entries.length === 0) {
148
143
  return result;
149
144
  }
150
145
 
151
146
  const db = await getDb();
152
147
  const repo = new BylineRepository(db);
148
+ const entryIds = entries.map((e) => e.id);
153
149
 
154
- // 1. Batch fetch all explicit byline credits. Sites with no bylines
155
- // get an empty map back for one query the previous "has any bylines"
156
- // probe traded an extra round-trip on every request to save that one
157
- // query on empty sites, which is exactly backwards for the common case.
158
- // Pre-migration databases (bylines table missing) fall through to the
159
- // `isMissingTableError` catch below and return empty results.
150
+ // Sites with no bylines get an empty map back for one query — the previous
151
+ // "has any bylines" probe traded an extra round-trip on every request to
152
+ // save that one query on empty sites, which is exactly backwards for the
153
+ // common case. Pre-migration databases (bylines table missing) fall
154
+ // through to the `isMissingTableError` catch below and return empty.
160
155
  let bylinesMap;
161
156
  try {
162
157
  bylinesMap = await repo.getContentBylinesMany(collection, entryIds);
@@ -165,32 +160,17 @@ export async function getBylinesForEntries(
165
160
  throw error;
166
161
  }
167
162
 
168
- // 2. Collect entry IDs that need fallback lookup
169
- const fallbackEntryIds: string[] = [];
170
- const needsFallback: Map<string, string> = new Map(); // entryId -> authorId
171
-
172
- for (const id of entryIds) {
173
- if (!bylinesMap.has(id)) {
174
- // Need to check author_id for this entry — but we only have the IDs,
175
- // so batch-fetch them from the content table
176
- fallbackEntryIds.push(id);
163
+ const needsFallback = new Map<string, string>();
164
+ for (const { id, authorId } of entries) {
165
+ if (!bylinesMap.has(id) && authorId) {
166
+ needsFallback.set(id, authorId);
177
167
  }
178
168
  }
179
169
 
180
- // Batch-fetch author_ids for entries that need fallback
181
- if (fallbackEntryIds.length > 0) {
182
- const authorMap = await getAuthorIds(db, collection, fallbackEntryIds);
183
- for (const [entryId, authorId] of authorMap) {
184
- needsFallback.set(entryId, authorId);
185
- }
186
- }
187
-
188
- // 3. Batch fetch user-linked bylines for fallback
189
170
  const uniqueAuthorIds = [...new Set(needsFallback.values())];
190
171
  const authorBylineMap = await repo.findByUserIds(uniqueAuthorIds);
191
172
 
192
- // 4. Assign results
193
- for (const id of entryIds) {
173
+ for (const { id } of entries) {
194
174
  const explicit = bylinesMap.get(id);
195
175
  if (explicit && explicit.length > 0) {
196
176
  result.set(
@@ -205,11 +185,8 @@ export async function getBylinesForEntries(
205
185
  const fallback = authorBylineMap.get(authorId);
206
186
  if (fallback) {
207
187
  result.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: "inferred" }]);
208
- continue;
209
188
  }
210
189
  }
211
-
212
- // Already initialized with empty array
213
190
  }
214
191
 
215
192
  return result;
@@ -235,31 +212,3 @@ async function getAuthorId(
235
212
 
236
213
  return result.rows[0]?.author_id ?? null;
237
214
  }
238
-
239
- /**
240
- * Batch-fetch author_ids for multiple content entries.
241
- * Returns Map<entryId, authorId> (only entries with non-null author_id).
242
- */
243
- async function getAuthorIds(
244
- db: Awaited<ReturnType<typeof getDb>>,
245
- collection: string,
246
- entryIds: string[],
247
- ): Promise<Map<string, string>> {
248
- validateIdentifier(collection, "collection");
249
- const tableName = `ec_${collection}`;
250
-
251
- const map = new Map<string, string>();
252
- for (const chunk of chunks(entryIds, SQL_BATCH_SIZE)) {
253
- const result = await sql<{ id: string; author_id: string | null }>`
254
- SELECT id, author_id FROM ${sql.ref(tableName)}
255
- WHERE id IN (${sql.join(chunk.map((id) => sql`${id}`))})
256
- `.execute(db);
257
-
258
- for (const row of result.rows) {
259
- if (row.author_id) {
260
- map.set(row.id, row.author_id);
261
- }
262
- }
263
- }
264
- return map;
265
- }
@@ -1,5 +1,22 @@
1
1
  /**
2
- * Auth CLI commands
2
+ * Auth CLI commands (deprecated)
3
+ *
4
+ * Kept as a deprecated alias for backwards compatibility. The original
5
+ * `emdash auth secret` was documented in published docs and is plausibly
6
+ * scripted in user CI (e.g. `npx emdash auth secret >> .env`). Removing
7
+ * it outright would break those scripts on minor-version upgrade.
8
+ *
9
+ * The command still emits an `EMDASH_AUTH_SECRET=<32-byte-base64url>`
10
+ * line, unchanged. `EMDASH_AUTH_SECRET` itself is now legacy: it's only
11
+ * read as a fallback source for the commenter-IP hash salt so installs
12
+ * upgrading from a prior version keep stable IP hashes (and therefore
13
+ * stable rate-limit buckets). New installs don't need to set it.
14
+ *
15
+ * The deprecation note steers users toward `emdash secrets generate`
16
+ * (which emits a different, versioned `emdash_enc_v1_*` value for
17
+ * `EMDASH_ENCRYPTION_KEY` — used to encrypt plugin secrets at rest).
18
+ *
19
+ * Will be removed in a future minor.
3
20
  */
4
21
 
5
22
  import { defineCommand } from "citty";
@@ -8,9 +25,6 @@ import pc from "picocolors";
8
25
 
9
26
  import { encodeBase64url } from "../../utils/base64.js";
10
27
 
11
- /**
12
- * Generate a cryptographically secure auth secret
13
- */
14
28
  function generateAuthSecret(): string {
15
29
  const bytes = new Uint8Array(32);
16
30
  crypto.getRandomValues(bytes);
@@ -20,11 +34,13 @@ function generateAuthSecret(): string {
20
34
  const secretCommand = defineCommand({
21
35
  meta: {
22
36
  name: "secret",
23
- description: "Generate a secure auth secret",
37
+ description: "[DEPRECATED] Generate a value for legacy EMDASH_AUTH_SECRET",
24
38
  },
25
39
  run() {
26
40
  const secret = generateAuthSecret();
27
41
 
42
+ // Match the original behavior verbatim: pretty-printed name=value
43
+ // on stdout (most scripts piped this to a file expecting that shape).
28
44
  consola.log("");
29
45
  consola.log(pc.bold("Generated auth secret:"));
30
46
  consola.log("");
@@ -32,13 +48,19 @@ const secretCommand = defineCommand({
32
48
  consola.log("");
33
49
  consola.log(pc.dim("Add this to your environment variables."));
34
50
  consola.log("");
51
+ // Deprecation note on stderr so it doesn't break stdout consumers.
52
+ process.stderr.write(
53
+ `${pc.yellow("Note:")} ${pc.bold("emdash auth secret")} is deprecated and will be removed in a future minor. ` +
54
+ `${pc.cyan("EMDASH_AUTH_SECRET")} itself is now optional — it's only used as a legacy fallback for the commenter-IP hash salt. ` +
55
+ `For encrypting plugin secrets at rest, use ${pc.bold("emdash secrets generate")} (a different secret: ${pc.cyan("EMDASH_ENCRYPTION_KEY")}).\n`,
56
+ );
35
57
  },
36
58
  });
37
59
 
38
60
  export const authCommand = defineCommand({
39
61
  meta: {
40
62
  name: "auth",
41
- description: "Authentication utilities",
63
+ description: "[DEPRECATED] Authentication utilities (use `emdash secrets` for new flows)",
42
64
  },
43
65
  subCommands: {
44
66
  secret: secretCommand,
@@ -30,8 +30,17 @@ export const ICON_SIZE = 256;
30
30
 
31
31
  // ── Regex patterns (module-scope to avoid re-compilation) ────────────────────
32
32
 
33
- /** Matches require("node:xxx") / require("xxx") / import("node:xxx") in bundled output */
34
- const NODE_BUILTIN_IMPORT_RE = /(?:import|require)\s*\(?["'](?:node:)?([a-z_]+)["']\)?/g;
33
+ /**
34
+ * Matches Node.js built-in imports in bundled output:
35
+ * - require("node:xxx") / require("xxx")
36
+ * - import("node:xxx") / import("xxx")
37
+ * - import X from "node:xxx" / import { X } from "node:xxx"
38
+ * - import * as X from "node:xxx"
39
+ * - export { X } from "node:xxx"
40
+ * Captures the base module name (e.g. "fs" from "node:fs/promises").
41
+ */
42
+ const NODE_BUILTIN_IMPORT_RE =
43
+ /(?:import|export|require)\s*(?:\(|[^(]*?\bfrom\s+)["'](?:node:)?([a-z_]+)(?:\/[^"']*)?\s*["']\)?/g;
35
44
  const LEADING_DOT_SLASH_RE = /^\.\//;
36
45
  const DIST_PREFIX_RE = /^dist\//;
37
46
  const MJS_EXT_RE = /\.m?js$/;
@@ -20,6 +20,7 @@ import { resolve, join, extname, basename } from "node:path";
20
20
  import { defineCommand } from "citty";
21
21
  import consola from "consola";
22
22
 
23
+ import { CAPABILITY_RENAMES, isDeprecatedCapability } from "../../plugins/types.js";
23
24
  import type { ResolvedPlugin } from "../../plugins/types.js";
24
25
  import {
25
26
  fileExists,
@@ -524,20 +525,39 @@ export const bundleCommand = defineCommand({
524
525
  }
525
526
  }
526
527
 
527
- // Check capabilities warnings
528
- if (manifest.capabilities.includes("network:fetch:any")) {
528
+ // Check capabilities warnings — use canonical names. Deprecated
529
+ // names are accepted (and warned about separately below) so we
530
+ // also check the legacy aliases here for the duration of the
531
+ // deprecation window.
532
+ const declaresUnrestricted =
533
+ manifest.capabilities.includes("network:request:unrestricted") ||
534
+ manifest.capabilities.includes("network:fetch:any");
535
+ const declaresHostRestricted =
536
+ manifest.capabilities.includes("network:request") ||
537
+ manifest.capabilities.includes("network:fetch");
538
+ if (declaresUnrestricted) {
529
539
  consola.warn(
530
- "Plugin declares unrestricted network access (network:fetch:any) — it can make requests to any host",
540
+ "Plugin declares unrestricted network access (network:request:unrestricted) — it can make requests to any host",
531
541
  );
532
- } else if (
533
- manifest.capabilities.includes("network:fetch") &&
534
- manifest.allowedHosts.length === 0
535
- ) {
542
+ } else if (declaresHostRestricted && manifest.allowedHosts.length === 0) {
536
543
  consola.warn(
537
- "Plugin declares network:fetch capability but no allowedHosts — all fetch requests will be blocked",
544
+ "Plugin declares network:request capability but no allowedHosts — all requests will be blocked",
538
545
  );
539
546
  }
540
547
 
548
+ // Warn for each deprecated capability used. The warning points
549
+ // to the new name so the fix is mechanical. We continue (not
550
+ // error) here — the hard fail lives in `publish` so authors
551
+ // can still build and test locally.
552
+ const deprecatedCaps = manifest.capabilities.filter(isDeprecatedCapability);
553
+ if (deprecatedCaps.length > 0) {
554
+ consola.warn("Plugin uses deprecated capability names. Rename them before publishing:");
555
+ for (const cap of deprecatedCaps) {
556
+ const replacement = CAPABILITY_RENAMES[cap];
557
+ consola.warn(` ${cap} → ${replacement}`);
558
+ }
559
+ }
560
+
541
561
  // Check for features that won't work in sandboxed mode
542
562
  if (
543
563
  resolvedPlugin.admin?.portableTextBlocks &&
@@ -9,6 +9,7 @@ import { readFile } from "node:fs/promises";
9
9
  import { defineCommand } from "citty";
10
10
  import { consola } from "consola";
11
11
 
12
+ import { convertDataForRead } from "../../client/portable-text.js";
12
13
  import { connectionArgs, createClientFromArgs } from "../client-factory.js";
13
14
  import { configureOutputMode, output } from "../output.js";
14
15
 
@@ -144,6 +145,13 @@ const getCommand = defineCommand({
144
145
  const comparison = await client.compare(args.collection, args.id);
145
146
  if (comparison.hasChanges && comparison.draft) {
146
147
  item.data = comparison.draft;
148
+ // The comparison endpoint returns raw PT data. Apply the same
149
+ // PT-to-markdown conversion that `client.get` does, unless --raw.
150
+ if (!args.raw && item.data) {
151
+ const col = await client.collection(args.collection);
152
+ const fields = col.fields.map((f) => ({ slug: f.slug, type: f.type }));
153
+ item.data = convertDataForRead(item.data, fields, false);
154
+ }
147
155
  }
148
156
  }
149
157
 
@@ -278,6 +286,7 @@ const deleteCommand = defineCommand({
278
286
  try {
279
287
  const client = createClientFromArgs(args);
280
288
  await client.delete(args.collection, args.id);
289
+ output({ success: true }, args);
281
290
  consola.success(`Deleted ${args.collection}/${args.id}`);
282
291
  } catch (error) {
283
292
  consola.error(error instanceof Error ? error.message : "Unknown error");
@@ -306,6 +315,7 @@ const publishCommand = defineCommand({
306
315
  try {
307
316
  const client = createClientFromArgs(args);
308
317
  await client.publish(args.collection, args.id);
318
+ output({ success: true }, args);
309
319
  consola.success(`Published ${args.collection}/${args.id}`);
310
320
  } catch (error) {
311
321
  consola.error(error instanceof Error ? error.message : "Unknown error");
@@ -334,6 +344,7 @@ const unpublishCommand = defineCommand({
334
344
  try {
335
345
  const client = createClientFromArgs(args);
336
346
  await client.unpublish(args.collection, args.id);
347
+ output({ success: true }, args);
337
348
  consola.success(`Unpublished ${args.collection}/${args.id}`);
338
349
  } catch (error) {
339
350
  consola.error(error instanceof Error ? error.message : "Unknown error");
@@ -367,6 +378,7 @@ const scheduleCommand = defineCommand({
367
378
  try {
368
379
  const client = createClientFromArgs(args);
369
380
  await client.schedule(args.collection, args.id, { at: args.at });
381
+ output({ success: true }, args);
370
382
  consola.success(`Scheduled ${args.collection}/${args.id} for ${args.at}`);
371
383
  } catch (error) {
372
384
  consola.error(error instanceof Error ? error.message : "Unknown error");
@@ -395,6 +407,7 @@ const restoreCommand = defineCommand({
395
407
  try {
396
408
  const client = createClientFromArgs(args);
397
409
  await client.restore(args.collection, args.id);
410
+ output({ success: true }, args);
398
411
  consola.success(`Restored ${args.collection}/${args.id}`);
399
412
  } catch (error) {
400
413
  consola.error(error instanceof Error ? error.message : "Unknown error");
@@ -212,41 +212,69 @@ async function exportCollections(db: Kysely<Database>): Promise<SeedCollection[]
212
212
  * Export taxonomy definitions and terms
213
213
  */
214
214
  async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
215
- // Get taxonomy definitions
216
- const defs = await db.selectFrom("_emdash_taxonomy_defs").selectAll().execute();
215
+ const i18nEnabled = isI18nEnabled();
216
+
217
+ // Mirrors the content export pattern: one entry per (name, locale), stable
218
+ // seed-local id, translations linked via `translationOf` to the anchor's id.
219
+ const defs = await db
220
+ .selectFrom("_emdash_taxonomy_defs")
221
+ .selectAll()
222
+ .orderBy(["name", "locale"])
223
+ .execute();
217
224
 
218
225
  const result: SeedTaxonomy[] = [];
219
226
  const termRepo = new TaxonomyRepository(db);
220
227
 
228
+ // translation_group -> seed-local id of first def we emitted in that group.
229
+ const defGroupToSeedId = new Map<string, string>();
230
+
221
231
  for (const def of defs) {
222
- // Get terms for this taxonomy
223
- const terms = await termRepo.findByName(def.name);
232
+ const defSeedId =
233
+ i18nEnabled && def.locale ? `tax:${def.name}:${def.locale}` : `tax:${def.name}`;
224
234
 
225
- // Build term tree for hierarchical taxonomies
226
- const seedTerms: SeedTaxonomyTerm[] = [];
235
+ // Terms in this def's locale.
236
+ const terms = await termRepo.findByName(def.name, { locale: def.locale });
227
237
 
228
- // First, create a map of id -> slug for parent resolution
238
+ // id -> slug for parent resolution within this locale.
229
239
  const idToSlug = new Map<string, string>();
230
- for (const term of terms) {
231
- idToSlug.set(term.id, term.slug);
232
- }
240
+ for (const term of terms) idToSlug.set(term.id, term.slug);
241
+
242
+ // translation_group -> seed id of the anchor term.
243
+ const termGroupToSeedId = new Map<string, string>();
233
244
 
245
+ const seedTerms: SeedTaxonomyTerm[] = [];
234
246
  for (const term of terms) {
247
+ const termSeedId =
248
+ i18nEnabled && term.locale
249
+ ? `term:${def.name}:${term.slug}:${term.locale}`
250
+ : `term:${def.name}:${term.slug}`;
251
+
235
252
  const seedTerm: SeedTaxonomyTerm = {
253
+ id: termSeedId,
236
254
  slug: term.slug,
237
255
  label: term.label,
238
256
  description: typeof term.data?.description === "string" ? term.data.description : undefined,
239
257
  };
240
258
 
241
- // Resolve parent slug
242
- if (term.parentId) {
243
- seedTerm.parent = idToSlug.get(term.parentId);
259
+ if (term.parentId) seedTerm.parent = idToSlug.get(term.parentId);
260
+
261
+ if (i18nEnabled && term.locale) {
262
+ seedTerm.locale = term.locale;
263
+ if (term.translationGroup) {
264
+ const anchor = termGroupToSeedId.get(term.translationGroup);
265
+ if (anchor) seedTerm.translationOf = anchor;
266
+ else termGroupToSeedId.set(term.translationGroup, termSeedId);
267
+ }
244
268
  }
245
269
 
246
270
  seedTerms.push(seedTerm);
247
271
  }
248
272
 
273
+ // Anchors first so import can resolve `translationOf`.
274
+ seedTerms.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
275
+
249
276
  const taxonomy: SeedTaxonomy = {
277
+ id: defSeedId,
250
278
  name: def.name,
251
279
  label: def.label,
252
280
  labelSingular: def.label_singular || undefined,
@@ -254,13 +282,23 @@ async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
254
282
  collections: def.collections ? JSON.parse(def.collections) : [],
255
283
  };
256
284
 
257
- if (seedTerms.length > 0) {
258
- taxonomy.terms = seedTerms;
285
+ if (i18nEnabled && def.locale) {
286
+ taxonomy.locale = def.locale;
287
+ if (def.translation_group) {
288
+ const anchor = defGroupToSeedId.get(def.translation_group);
289
+ if (anchor) taxonomy.translationOf = anchor;
290
+ else defGroupToSeedId.set(def.translation_group, defSeedId);
291
+ }
259
292
  }
260
293
 
294
+ if (seedTerms.length > 0) taxonomy.terms = seedTerms;
295
+
261
296
  result.push(taxonomy);
262
297
  }
263
298
 
299
+ // Anchors first at def level too.
300
+ result.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
301
+
264
302
  return result;
265
303
  }
266
304
 
@@ -268,13 +306,22 @@ async function exportTaxonomies(db: Kysely<Database>): Promise<SeedTaxonomy[]> {
268
306
  * Export menus with their items
269
307
  */
270
308
  async function exportMenus(db: Kysely<Database>): Promise<SeedMenu[]> {
271
- // Get all menus
272
- const menus = await db.selectFrom("_emdash_menus").selectAll().execute();
309
+ const i18nEnabled = isI18nEnabled();
310
+
311
+ const menus = await db
312
+ .selectFrom("_emdash_menus")
313
+ .selectAll()
314
+ .orderBy(["name", "locale"])
315
+ .execute();
273
316
 
274
317
  const result: SeedMenu[] = [];
318
+ // translation_group -> seed-local id of the anchor menu in that group.
319
+ const groupToSeedId = new Map<string, string>();
275
320
 
276
321
  for (const menu of menus) {
277
- // Get menu items
322
+ const seedId =
323
+ i18nEnabled && menu.locale ? `menu:${menu.name}:${menu.locale}` : `menu:${menu.name}`;
324
+
278
325
  const items = await db
279
326
  .selectFrom("_emdash_menu_items")
280
327
  .selectAll()
@@ -282,16 +329,30 @@ async function exportMenus(db: Kysely<Database>): Promise<SeedMenu[]> {
282
329
  .orderBy("sort_order", "asc")
283
330
  .execute();
284
331
 
285
- // Build item tree
286
332
  const seedItems = buildMenuItemTree(items);
287
333
 
288
- result.push({
334
+ const seedMenu: SeedMenu = {
335
+ id: seedId,
289
336
  name: menu.name,
290
337
  label: menu.label,
291
338
  items: seedItems,
292
- });
339
+ };
340
+
341
+ if (i18nEnabled && menu.locale) {
342
+ seedMenu.locale = menu.locale;
343
+ if (menu.translation_group) {
344
+ const anchor = groupToSeedId.get(menu.translation_group);
345
+ if (anchor) seedMenu.translationOf = anchor;
346
+ else groupToSeedId.set(menu.translation_group, seedId);
347
+ }
348
+ }
349
+
350
+ result.push(seedMenu);
293
351
  }
294
352
 
353
+ // Anchors first so import can resolve `translationOf`.
354
+ result.sort((a, b) => Number(!!a.translationOf) - Number(!!b.translationOf));
355
+
295
356
  return result;
296
357
  }
297
358
 
@@ -459,7 +459,14 @@ export const whoamiCommand = defineCommand({
459
459
  },
460
460
  );
461
461
  if (refreshRes.ok) {
462
- const refreshed = (await refreshRes.json()) as TokenResponse;
462
+ const json = (await refreshRes.json()) as Record<string, unknown>;
463
+ // Token endpoint wraps response in { data: ... } via apiSuccess.
464
+ // Handle both wrapped and bare shapes for robustness.
465
+ const refreshed = (
466
+ json.data && typeof json.data === "object" && "access_token" in json.data
467
+ ? json.data
468
+ : json
469
+ ) as TokenResponse;
463
470
  token = refreshed.access_token;
464
471
  saveCredentials(baseUrl, {
465
472
  ...cred,