emdash 0.8.0 → 0.9.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 (286) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-DoNJiveC.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-BzltprvY.mjs} +85 -135
  4. package/dist/apply-BzltprvY.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 +110 -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 +16 -59
  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 +17 -12
  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 +9 -6
  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 +72 -124
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +26 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
  26. package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
  27. package/dist/{byline-Chbr2GoP.mjs → byline-BSaNL1w7.mjs} +4 -4
  28. package/dist/{byline-Chbr2GoP.mjs.map → byline-BSaNL1w7.mjs.map} +1 -1
  29. package/dist/bylines-CvJ3PYz2.mjs +113 -0
  30. package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
  31. package/dist/cache-C6N_hhN7.mjs +65 -0
  32. package/dist/cache-C6N_hhN7.mjs.map +1 -0
  33. package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
  34. package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
  35. package/dist/cli/index.mjs +224 -30
  36. package/dist/cli/index.mjs.map +1 -1
  37. package/dist/client/cf-access.d.mts +1 -1
  38. package/dist/client/index.d.mts +1 -1
  39. package/dist/client/index.mjs +3 -3
  40. package/dist/client/index.mjs.map +1 -1
  41. package/dist/{config-BXwuX8Bx.mjs → config-BI0V3ICQ.mjs} +1 -1
  42. package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
  43. package/dist/{content-BcQPYxdV.mjs → content-8lOYF0pr.mjs} +32 -15
  44. package/dist/{content-BcQPYxdV.mjs.map → content-8lOYF0pr.mjs.map} +1 -1
  45. package/dist/db/index.d.mts +3 -3
  46. package/dist/db/index.mjs +2 -2
  47. package/dist/db/libsql.d.mts +1 -1
  48. package/dist/db/libsql.d.mts.map +1 -1
  49. package/dist/db/libsql.mjs +7 -2
  50. package/dist/db/libsql.mjs.map +1 -1
  51. package/dist/db/postgres.d.mts +1 -1
  52. package/dist/db/sqlite.d.mts +1 -1
  53. package/dist/db/sqlite.d.mts.map +1 -1
  54. package/dist/db/sqlite.mjs +8 -3
  55. package/dist/db/sqlite.mjs.map +1 -1
  56. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-WRezodiz.mjs} +1 -1
  57. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
  58. package/dist/{default-DCVqE5ib.mjs → default-D8ksjWhO.mjs} +1 -1
  59. package/dist/{default-DCVqE5ib.mjs.map → default-D8ksjWhO.mjs.map} +1 -1
  60. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  61. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  62. package/dist/{error-zG5T1UGA.mjs → error-D_-tqP-I.mjs} +1 -1
  63. package/dist/{error-zG5T1UGA.mjs.map → error-D_-tqP-I.mjs.map} +1 -1
  64. package/dist/{index-DIb-CzNx.d.mts → index-BFRaVcD6.d.mts} +94 -34
  65. package/dist/index-BFRaVcD6.d.mts.map +1 -0
  66. package/dist/index.d.mts +11 -11
  67. package/dist/index.mjs +29 -27
  68. package/dist/{load-CyEoextb.mjs → load-DDqMMvZL.mjs} +2 -2
  69. package/dist/{load-CyEoextb.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
  70. package/dist/{loader-CndGj8kM.mjs → loader-CKLbBnhK.mjs} +27 -7
  71. package/dist/loader-CKLbBnhK.mjs.map +1 -0
  72. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-DqWNC3lM.mjs} +33 -3
  73. package/dist/manifest-schema-DqWNC3lM.mjs.map +1 -0
  74. package/dist/media/index.d.mts +1 -1
  75. package/dist/media/index.mjs +1 -1
  76. package/dist/media/local-runtime.d.mts +7 -7
  77. package/dist/media/local-runtime.mjs +3 -3
  78. package/dist/{media-D8FbNsl0.mjs → media-BW32b4gi.mjs} +2 -2
  79. package/dist/{media-D8FbNsl0.mjs.map → media-BW32b4gi.mjs.map} +1 -1
  80. package/dist/{mode-BnAOqItE.mjs → mode-ier8jbBk.mjs} +1 -1
  81. package/dist/{mode-BnAOqItE.mjs.map → mode-ier8jbBk.mjs.map} +1 -1
  82. package/dist/options-BVp3UsTS.mjs +117 -0
  83. package/dist/options-BVp3UsTS.mjs.map +1 -0
  84. package/dist/page/index.d.mts +2 -2
  85. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
  86. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
  87. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
  88. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-CIJejMlK.mjs.map} +1 -1
  89. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  90. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  91. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  92. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  93. package/dist/public-url-DByxYjUw.mjs +51 -0
  94. package/dist/public-url-DByxYjUw.mjs.map +1 -0
  95. package/dist/{query-fqEdLFms.mjs → query-Cg9ZKRQ0.mjs} +114 -16
  96. package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
  97. package/dist/{redirect-D_pshWdf.mjs → redirect-BhUBKRc1.mjs} +11 -6
  98. package/dist/redirect-BhUBKRc1.mjs.map +1 -0
  99. package/dist/{registry-C3Mr0ODu.mjs → registry-Dw70ChxB.mjs} +38 -4
  100. package/dist/registry-Dw70ChxB.mjs.map +1 -0
  101. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-B-bmkipQ.mjs} +1 -1
  102. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-B-bmkipQ.mjs.map} +1 -1
  103. package/dist/runner-Bnoj7vjK.d.mts +44 -0
  104. package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
  105. package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
  106. package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
  107. package/dist/runtime.d.mts +6 -6
  108. package/dist/runtime.mjs +4 -4
  109. package/dist/{search-BoZYFuUk.mjs → search-dOGEccMa.mjs} +129 -83
  110. package/dist/search-dOGEccMa.mjs.map +1 -0
  111. package/dist/secrets-CW3reAnU.mjs +314 -0
  112. package/dist/secrets-CW3reAnU.mjs.map +1 -0
  113. package/dist/seed/index.d.mts +2 -2
  114. package/dist/seed/index.mjs +15 -14
  115. package/dist/seo/index.d.mts +1 -1
  116. package/dist/storage/local.d.mts +1 -1
  117. package/dist/storage/local.mjs +1 -1
  118. package/dist/storage/s3.d.mts +1 -1
  119. package/dist/storage/s3.mjs +1 -1
  120. package/dist/{taxonomies-B4IAshV8.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
  121. package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
  122. package/dist/{tokens-D9vnZqYS.mjs → tokens-D7zMmWi2.mjs} +2 -2
  123. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
  124. package/dist/{transport-C9ugt2Nr.mjs → transport-BeMCmin1.mjs} +6 -5
  125. package/dist/{transport-C9ugt2Nr.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts → transport-DNEfeMaU.d.mts} +1 -1
  127. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
  128. package/dist/types-4fVtCIm0.mjs +68 -0
  129. package/dist/types-4fVtCIm0.mjs.map +1 -0
  130. package/dist/{types-BmPPSUEx.d.mts → types-BSyXeCFW.d.mts} +24 -2
  131. package/dist/{types-BmPPSUEx.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
  132. package/dist/{types-i36XcA_X.d.mts → types-BuBIptGk.d.mts} +65 -134
  133. package/dist/types-BuBIptGk.d.mts.map +1 -0
  134. package/dist/{types-CgqmmMJB.mjs → types-CDbKp7ND.mjs} +1 -1
  135. package/dist/{types-CgqmmMJB.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
  136. package/dist/{types-Bm1dn-q3.mjs → types-CIOg5AR8.mjs} +1 -1
  137. package/dist/{types-Bm1dn-q3.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
  138. package/dist/{types-BrA0xf5I.d.mts → types-CJsYGpco.d.mts} +1 -1
  139. package/dist/{types-BrA0xf5I.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
  140. package/dist/{types-BIgulNsW.mjs → types-CRxNbK-Z.mjs} +2 -2
  141. package/dist/{types-BIgulNsW.mjs.map → types-CRxNbK-Z.mjs.map} +1 -1
  142. package/dist/{types-CS8FIX7L.d.mts → types-CrtWgIvl.d.mts} +1 -1
  143. package/dist/{types-CS8FIX7L.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
  144. package/dist/{types-DIMwPFub.d.mts → types-M78DQ1lx.d.mts} +1 -1
  145. package/dist/{types-DIMwPFub.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
  146. package/dist/{validate-CxVsLehf.mjs → validate-Baqf0slj.mjs} +3 -3
  147. package/dist/{validate-CxVsLehf.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
  148. package/dist/{validate-DHxmpFJt.d.mts → validate-BfQh_C_y.d.mts} +4 -4
  149. package/dist/{validate-DHxmpFJt.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
  150. package/dist/{validation-C-ZpN2GI.mjs → validation-BfEI7tNe.mjs} +6 -6
  151. package/dist/{validation-C-ZpN2GI.mjs.map → validation-BfEI7tNe.mjs.map} +1 -1
  152. package/dist/version-DoxrVdYf.mjs +7 -0
  153. package/dist/{version-Bbq8TCrz.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
  154. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-CC0xNe_K.mjs} +4 -4
  155. package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
  156. package/locals.d.ts +1 -6
  157. package/package.json +9 -8
  158. package/src/api/handlers/comments.ts +6 -4
  159. package/src/api/handlers/content.ts +29 -1
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/oauth-authorization.ts +72 -33
  163. package/src/api/handlers/revision.ts +23 -14
  164. package/src/api/handlers/taxonomies.ts +3 -6
  165. package/src/api/public-url.ts +48 -2
  166. package/src/api/schemas/comments.ts +2 -2
  167. package/src/api/schemas/content.ts +17 -0
  168. package/src/api/schemas/sections.ts +3 -3
  169. package/src/api/schemas/users.ts +1 -1
  170. package/src/api/types.ts +5 -1
  171. package/src/astro/integration/index.ts +17 -0
  172. package/src/astro/integration/runtime.ts +30 -0
  173. package/src/astro/integration/virtual-modules.ts +32 -2
  174. package/src/astro/integration/vite-config.ts +6 -1
  175. package/src/astro/middleware/auth.ts +13 -6
  176. package/src/astro/middleware/redirect.ts +29 -16
  177. package/src/astro/middleware/request-context.ts +15 -5
  178. package/src/astro/middleware.ts +23 -9
  179. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  180. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  181. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  182. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  183. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  184. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  185. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  186. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  187. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  188. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  190. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  191. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  192. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  193. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  194. package/src/astro/routes/api/manifest.ts +62 -45
  195. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  196. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  197. package/src/astro/routes/api/openapi.json.ts +27 -10
  198. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  199. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  200. package/src/astro/routes/api/redirects/[id].ts +10 -4
  201. package/src/astro/routes/api/redirects/index.ts +7 -3
  202. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  203. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  204. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  205. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  206. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  207. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  208. package/src/astro/routes/api/search/index.ts +10 -2
  209. package/src/astro/routes/api/sections/[slug].ts +10 -4
  210. package/src/astro/routes/api/sections/index.ts +7 -3
  211. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  212. package/src/astro/routes/api/snapshot.ts +44 -18
  213. package/src/astro/routes/api/taxonomies/index.ts +0 -1
  214. package/src/astro/routes/api/themes/preview.ts +11 -5
  215. package/src/astro/types.ts +23 -3
  216. package/src/auth/allowed-origins.ts +168 -0
  217. package/src/auth/passkey-config.ts +35 -13
  218. package/src/bylines/index.ts +37 -88
  219. package/src/cli/commands/auth.ts +28 -6
  220. package/src/cli/commands/bundle-utils.ts +11 -2
  221. package/src/cli/commands/bundle.ts +28 -8
  222. package/src/cli/commands/content.ts +13 -0
  223. package/src/cli/commands/login.ts +8 -1
  224. package/src/cli/commands/publish.ts +24 -0
  225. package/src/cli/commands/secrets.ts +183 -0
  226. package/src/cli/credentials.ts +1 -1
  227. package/src/cli/index.ts +5 -1
  228. package/src/client/index.ts +4 -4
  229. package/src/client/transport.ts +17 -7
  230. package/src/components/Break.astro +2 -2
  231. package/src/components/EmDashHead.astro +18 -13
  232. package/src/components/Embed.astro +1 -1
  233. package/src/components/Gallery.astro +1 -1
  234. package/src/components/Image.astro +1 -1
  235. package/src/components/InlinePortableTextEditor.tsx +104 -18
  236. package/src/config/secrets.ts +528 -0
  237. package/src/database/dialect-helpers.ts +50 -0
  238. package/src/database/migrations/034_published_at_index.ts +1 -1
  239. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  240. package/src/database/migrations/runner.ts +156 -23
  241. package/src/database/repositories/content.ts +36 -12
  242. package/src/database/repositories/redirect.ts +14 -3
  243. package/src/database/repositories/taxonomy.ts +26 -0
  244. package/src/db/libsql.ts +1 -3
  245. package/src/db/sqlite.ts +2 -5
  246. package/src/emdash-runtime.ts +84 -159
  247. package/src/index.ts +9 -0
  248. package/src/loader.ts +24 -1
  249. package/src/mcp/server.ts +103 -36
  250. package/src/page/site-identity.ts +58 -0
  251. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  252. package/src/plugins/context.ts +13 -10
  253. package/src/plugins/define-plugin.ts +40 -12
  254. package/src/plugins/hooks.ts +23 -19
  255. package/src/plugins/index.ts +9 -0
  256. package/src/plugins/manifest-schema.ts +37 -2
  257. package/src/plugins/types.ts +151 -11
  258. package/src/preview/urls.ts +23 -3
  259. package/src/query.ts +148 -5
  260. package/src/redirects/cache.ts +38 -18
  261. package/src/schema/registry.ts +56 -0
  262. package/src/schema/zod-generator.ts +27 -5
  263. package/src/seed/apply.ts +2 -0
  264. package/src/settings/index.ts +80 -6
  265. package/src/settings/types.ts +23 -1
  266. package/src/taxonomies/index.ts +11 -1
  267. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  268. package/dist/bylines-CRNsVG88.mjs +0 -157
  269. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  270. package/dist/cache-BkKBuIvS.mjs +0 -56
  271. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  272. package/dist/chunk-ClPoSABd.mjs +0 -21
  273. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  274. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  275. package/dist/loader-CndGj8kM.mjs.map +0 -1
  276. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  277. package/dist/query-fqEdLFms.mjs.map +0 -1
  278. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  279. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  280. package/dist/runner-OURCaApa.d.mts +0 -34
  281. package/dist/runner-OURCaApa.d.mts.map +0 -1
  282. package/dist/search-BoZYFuUk.mjs.map +0 -1
  283. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  284. package/dist/types-i36XcA_X.d.mts.map +0 -1
  285. package/dist/version-Bbq8TCrz.mjs +0 -7
  286. package/dist/zod-generator-CpwccCIv.mjs.map +0 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Resolution and validation of multi-origin passkey verification.
3
+ *
4
+ * `allowedOrigins` lets one EmDash deployment accept passkey assertions from
5
+ * several hostnames sharing the same `rpId` (e.g. apex + preview/staging
6
+ * subdomains under one registrable parent). Origins come from two sources:
7
+ *
8
+ * - `EmDashConfig.allowedOrigins` (declared in `astro.config.mjs`)
9
+ * - `EMDASH_ALLOWED_ORIGINS` (comma-separated runtime env var)
10
+ *
11
+ * Sources are merged (union of permissions, deduplicated). Each entry is
12
+ * validated against `siteUrl` to fail loud on dead config the browser would
13
+ * never honor.
14
+ */
15
+
16
+ import { getEnvAllowedOrigins } from "../api/public-url.js";
17
+ import type { EmDashConfig } from "../astro/integration/runtime.js";
18
+
19
+ export type AllowedOriginSource = "config.allowedOrigins" | "EMDASH_ALLOWED_ORIGINS";
20
+
21
+ export interface TaggedOrigin {
22
+ /** Raw entry as declared by the operator. */
23
+ origin: string;
24
+ /** Where the entry came from (used for source-attributed errors). */
25
+ source: AllowedOriginSource;
26
+ }
27
+
28
+ /**
29
+ * Collect raw allowedOrigins from config and env, source-tagged.
30
+ *
31
+ * Returns raw values — the caller is expected to pass the result through
32
+ * `validateAllowedOrigins()` before use in passkey verification.
33
+ */
34
+ export function getConfiguredAllowedOrigins(config?: EmDashConfig): TaggedOrigin[] {
35
+ const tagged: TaggedOrigin[] = [];
36
+ if (config?.allowedOrigins) {
37
+ for (const origin of config.allowedOrigins) {
38
+ if (origin) tagged.push({ origin, source: "config.allowedOrigins" });
39
+ }
40
+ }
41
+ for (const origin of getEnvAllowedOrigins()) {
42
+ tagged.push({ origin, source: "EMDASH_ALLOWED_ORIGINS" });
43
+ }
44
+ return tagged;
45
+ }
46
+
47
+ /**
48
+ * Validate per-entry shape rules (no `siteUrl` needed):
49
+ * - parses as `URL`
50
+ * - protocol is `http:` or `https:`
51
+ * - hostname has no trailing dot (`example.com.` rejected)
52
+ * - hostname has no empty labels (`foo..example.com` rejected)
53
+ *
54
+ * Returns the deduplicated, normalized origin form (`URL.origin`) of every
55
+ * input, in input order. Throws on the first violation with a source-tagged
56
+ * error message.
57
+ */
58
+ export function validateOriginShape(tagged: TaggedOrigin[]): string[] {
59
+ const normalized: string[] = [];
60
+ const seen = new Set<string>();
61
+ for (const { origin, source } of tagged) {
62
+ let parsed: URL;
63
+ try {
64
+ parsed = new URL(origin);
65
+ } catch (e) {
66
+ throw configError(source, `invalid URL: "${origin}"`, e);
67
+ }
68
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
69
+ throw configError(
70
+ source,
71
+ `origin must be http or https: "${origin}" (got ${parsed.protocol})`,
72
+ );
73
+ }
74
+ if (parsed.hostname.endsWith(".")) {
75
+ throw configError(
76
+ source,
77
+ `hostname has a trailing dot: "${origin}". Remove the trailing dot — assertion origins from the browser do not include it.`,
78
+ );
79
+ }
80
+ if (parsed.hostname.split(".").includes("")) {
81
+ throw configError(source, `hostname has empty labels: "${origin}"`);
82
+ }
83
+ if (!seen.has(parsed.origin)) {
84
+ seen.add(parsed.origin);
85
+ normalized.push(parsed.origin);
86
+ }
87
+ }
88
+ return normalized;
89
+ }
90
+
91
+ /**
92
+ * Validate the effective merged allowedOrigins set against `siteUrl`.
93
+ *
94
+ * Performs `validateOriginShape()` plus the siteUrl-dependent rules:
95
+ * - Rule A: non-empty origins ⇒ `siteUrl` is set
96
+ * - `siteUrl` hostname is not an IP literal (multi-origin requires a domain)
97
+ * - `siteUrl` hostname has no trailing dot (cannot match assertion origins)
98
+ * - Rule B: each origin's hostname is `siteHost` exactly or a subdomain
99
+ *
100
+ * Throws on first violation. Returns the deduplicated normalized origins.
101
+ *
102
+ * Use this at the runtime chokepoint (where config + env are merged into the
103
+ * effective set). At Astro integration init, prefer `validateOriginShape()`
104
+ * for shape-only checks on `config.allowedOrigins`, since `siteUrl` may be
105
+ * supplied at runtime via `EMDASH_SITE_URL`.
106
+ */
107
+ export function validateAllowedOrigins(
108
+ siteUrl: string | undefined,
109
+ tagged: TaggedOrigin[],
110
+ ): string[] {
111
+ const normalized = validateOriginShape(tagged);
112
+ if (normalized.length === 0) return normalized;
113
+
114
+ if (!siteUrl) {
115
+ throw new Error(
116
+ `EmDash config error: allowedOrigins is set (${normalized.length} ${
117
+ normalized.length === 1 ? "entry" : "entries"
118
+ }) but siteUrl is not. Without a canonical siteUrl, rpId is derived from the request hostname, defeating multi-origin passkeys. Set siteUrl in astro.config.mjs or via EMDASH_SITE_URL.`,
119
+ );
120
+ }
121
+
122
+ let siteHost: string;
123
+ try {
124
+ siteHost = new URL(siteUrl).hostname;
125
+ } catch (e) {
126
+ throw new Error(`EmDash config error: siteUrl is not a valid URL: "${siteUrl}"`, {
127
+ cause: e,
128
+ });
129
+ }
130
+
131
+ if (siteHost.endsWith(".")) {
132
+ throw new Error(
133
+ `EmDash config error: siteUrl "${siteUrl}" has a trailing-dot hostname, which cannot match assertion origins. Remove the trailing dot when using allowedOrigins.`,
134
+ );
135
+ }
136
+ if (isIPLiteralHostname(siteHost)) {
137
+ throw new Error(
138
+ `EmDash config error: siteUrl "${siteUrl}" uses an IP-literal hostname. Multi-origin passkeys require a domain-based siteUrl — IP addresses cannot have valid subdomains for WebAuthn rpId.`,
139
+ );
140
+ }
141
+
142
+ for (const { origin, source } of tagged) {
143
+ const h = new URL(origin).hostname;
144
+ if (h !== siteHost && !h.endsWith("." + siteHost)) {
145
+ throw configError(
146
+ source,
147
+ `"${origin}" is not a subdomain of siteUrl "${siteUrl}". Allowed origins must be the same hostname as siteUrl or a subdomain of it.`,
148
+ );
149
+ }
150
+ }
151
+
152
+ return normalized;
153
+ }
154
+
155
+ function configError(source: AllowedOriginSource, detail: string, cause?: unknown): Error {
156
+ const err = new Error(`EmDash config error in ${source}: ${detail}`);
157
+ if (cause !== undefined) (err as Error & { cause?: unknown }).cause = cause;
158
+ return err;
159
+ }
160
+
161
+ const IPV4_DOTTED_DECIMAL_RE = /^\d+(\.\d+){3}$/;
162
+
163
+ function isIPLiteralHostname(h: string): boolean {
164
+ // IPv6 hostnames are bracketed by URL.hostname, e.g. "[::1]"
165
+ if (h.startsWith("[")) return true;
166
+ // IPv4 dotted-decimal
167
+ return IPV4_DOTTED_DECIMAL_RE.test(h);
168
+ }
@@ -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");
@@ -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,