emdash 0.7.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 (354) hide show
  1. package/dist/{adapters-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
  2. package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
  3. package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
  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 +194 -17
  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 +34 -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 +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 +301 -165
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +34 -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-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
  28. package/dist/byline-BSaNL1w7.mjs.map +1 -0
  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 +229 -31
  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-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
  44. package/dist/content-8lOYF0pr.mjs.map +1 -0
  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-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
  57. package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
  58. package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
  59. package/dist/{default-CME5YdZ3.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-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
  63. package/dist/error-D_-tqP-I.mjs.map +1 -0
  64. package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
  65. package/dist/index-BFRaVcD6.d.mts.map +1 -0
  66. package/dist/index.d.mts +11 -11
  67. package/dist/index.mjs +29 -25
  68. package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
  69. package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
  70. package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
  71. package/dist/loader-CKLbBnhK.mjs.map +1 -0
  72. package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -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-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
  79. package/dist/media-BW32b4gi.mjs.map +1 -0
  80. package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
  81. package/dist/mode-ier8jbBk.mjs.map +1 -0
  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-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
  86. package/dist/{placeholder-tzpqGWII.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-CIJejMlK.mjs.map +1 -0
  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-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
  96. package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
  97. package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
  98. package/dist/redirect-BhUBKRc1.mjs.map +1 -0
  99. package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
  100. package/dist/registry-Dw70ChxB.mjs.map +1 -0
  101. package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
  102. package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
  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-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
  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.d.mts.map +1 -1
  120. package/dist/storage/s3.mjs +4 -4
  121. package/dist/storage/s3.mjs.map +1 -1
  122. package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
  123. package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
  124. package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
  125. package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
  126. package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
  127. package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
  128. package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
  129. package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
  130. package/dist/types-4fVtCIm0.mjs +68 -0
  131. package/dist/types-4fVtCIm0.mjs.map +1 -0
  132. package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
  133. package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
  134. package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
  135. package/dist/types-BuBIptGk.d.mts.map +1 -0
  136. package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
  137. package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
  138. package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
  139. package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
  140. package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
  141. package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
  142. package/dist/types-CRxNbK-Z.mjs +68 -0
  143. package/dist/types-CRxNbK-Z.mjs.map +1 -0
  144. package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
  145. package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
  146. package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
  147. package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
  148. package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
  149. package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
  150. package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
  151. package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
  152. package/dist/validation-BfEI7tNe.mjs +144 -0
  153. package/dist/validation-BfEI7tNe.mjs.map +1 -0
  154. package/dist/version-DoxrVdYf.mjs +7 -0
  155. package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
  156. package/dist/zod-generator-CC0xNe_K.mjs +132 -0
  157. package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
  158. package/locals.d.ts +1 -6
  159. package/package.json +21 -7
  160. package/src/api/auth-storage.ts +37 -0
  161. package/src/api/error.ts +6 -0
  162. package/src/api/errors.ts +8 -0
  163. package/src/api/handlers/comments.ts +19 -4
  164. package/src/api/handlers/content.ts +151 -4
  165. package/src/api/handlers/device-flow.ts +5 -0
  166. package/src/api/handlers/index.ts +2 -0
  167. package/src/api/handlers/marketplace.ts +11 -4
  168. package/src/api/handlers/media.ts +8 -1
  169. package/src/api/handlers/menus.ts +160 -21
  170. package/src/api/handlers/oauth-authorization.ts +72 -33
  171. package/src/api/handlers/redirects.ts +16 -3
  172. package/src/api/handlers/revision.ts +23 -14
  173. package/src/api/handlers/sections.ts +8 -1
  174. package/src/api/handlers/taxonomies.ts +131 -22
  175. package/src/api/handlers/validation.ts +212 -0
  176. package/src/api/openapi/document.ts +4 -1
  177. package/src/api/public-url.ts +54 -5
  178. package/src/api/route-utils.ts +14 -0
  179. package/src/api/schemas/comments.ts +2 -2
  180. package/src/api/schemas/common.ts +1 -1
  181. package/src/api/schemas/content.ts +17 -0
  182. package/src/api/schemas/sections.ts +3 -3
  183. package/src/api/schemas/setup.ts +8 -0
  184. package/src/api/schemas/users.ts +1 -1
  185. package/src/api/schemas/widgets.ts +12 -10
  186. package/src/api/setup-complete.ts +40 -0
  187. package/src/api/types.ts +5 -1
  188. package/src/astro/integration/index.ts +30 -2
  189. package/src/astro/integration/routes.ts +28 -0
  190. package/src/astro/integration/runtime.ts +49 -1
  191. package/src/astro/integration/virtual-modules.ts +73 -2
  192. package/src/astro/integration/vite-config.ts +49 -13
  193. package/src/astro/middleware/auth.ts +34 -6
  194. package/src/astro/middleware/redirect.ts +29 -16
  195. package/src/astro/middleware/request-context.ts +15 -5
  196. package/src/astro/middleware.ts +41 -10
  197. package/src/astro/routes/PluginRegistry.tsx +10 -1
  198. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  199. package/src/astro/routes/api/auth/mode.ts +57 -0
  200. package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
  201. package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
  202. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  203. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  204. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  205. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  206. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  207. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  208. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  209. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  210. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  211. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  212. package/src/astro/routes/api/content/[collection]/[id]/translations.ts +1 -1
  213. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  214. package/src/astro/routes/api/content/[collection]/index.ts +1 -9
  215. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  216. package/src/astro/routes/api/import/wordpress/media.ts +2 -7
  217. package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
  218. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  219. package/src/astro/routes/api/manifest.ts +62 -45
  220. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  221. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  222. package/src/astro/routes/api/openapi.json.ts +27 -10
  223. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  224. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  225. package/src/astro/routes/api/redirects/[id].ts +10 -4
  226. package/src/astro/routes/api/redirects/index.ts +7 -3
  227. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  228. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  229. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  230. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  231. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  232. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  233. package/src/astro/routes/api/search/index.ts +10 -2
  234. package/src/astro/routes/api/sections/[slug].ts +10 -4
  235. package/src/astro/routes/api/sections/index.ts +7 -3
  236. package/src/astro/routes/api/settings/email.ts +4 -9
  237. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  238. package/src/astro/routes/api/setup/admin.ts +8 -2
  239. package/src/astro/routes/api/setup/index.ts +2 -2
  240. package/src/astro/routes/api/setup/status.ts +3 -1
  241. package/src/astro/routes/api/snapshot.ts +44 -18
  242. package/src/astro/routes/api/taxonomies/index.ts +0 -1
  243. package/src/astro/routes/api/themes/preview.ts +11 -5
  244. package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
  245. package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
  246. package/src/astro/routes/api/widget-areas/[name].ts +4 -1
  247. package/src/astro/routes/api/widget-areas/index.ts +4 -1
  248. package/src/astro/types.ts +32 -3
  249. package/src/auth/allowed-origins.ts +168 -0
  250. package/src/auth/mode.ts +15 -3
  251. package/src/auth/passkey-config.ts +35 -13
  252. package/src/auth/providers/github-admin.tsx +29 -0
  253. package/src/auth/providers/github.ts +31 -0
  254. package/src/auth/providers/google-admin.tsx +44 -0
  255. package/src/auth/providers/google.ts +31 -0
  256. package/src/auth/types.ts +114 -4
  257. package/src/bylines/index.ts +37 -88
  258. package/src/cli/commands/auth.ts +28 -6
  259. package/src/cli/commands/bundle-utils.ts +11 -2
  260. package/src/cli/commands/bundle.ts +31 -9
  261. package/src/cli/commands/content.ts +13 -0
  262. package/src/cli/commands/login.ts +8 -1
  263. package/src/cli/commands/publish.ts +24 -0
  264. package/src/cli/commands/secrets.ts +183 -0
  265. package/src/cli/credentials.ts +1 -1
  266. package/src/cli/index.ts +5 -1
  267. package/src/client/index.ts +4 -4
  268. package/src/client/transport.ts +17 -7
  269. package/src/components/Break.astro +2 -2
  270. package/src/components/EmDashHead.astro +18 -13
  271. package/src/components/EmDashImage.astro +7 -6
  272. package/src/components/Embed.astro +1 -1
  273. package/src/components/Gallery.astro +6 -4
  274. package/src/components/Image.astro +9 -4
  275. package/src/components/InlinePortableTextEditor.tsx +106 -19
  276. package/src/components/LiveSearch.astro +5 -14
  277. package/src/config/secrets.ts +528 -0
  278. package/src/database/dialect-helpers.ts +50 -0
  279. package/src/database/migrations/034_published_at_index.ts +1 -1
  280. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  281. package/src/database/migrations/runner.ts +156 -23
  282. package/src/database/repositories/audit.ts +6 -8
  283. package/src/database/repositories/byline.ts +6 -8
  284. package/src/database/repositories/comment.ts +12 -16
  285. package/src/database/repositories/content.ts +76 -52
  286. package/src/database/repositories/index.ts +1 -1
  287. package/src/database/repositories/media.ts +10 -13
  288. package/src/database/repositories/plugin-storage.ts +4 -6
  289. package/src/database/repositories/redirect.ts +26 -19
  290. package/src/database/repositories/taxonomy.ts +40 -3
  291. package/src/database/repositories/types.ts +57 -8
  292. package/src/database/repositories/user.ts +6 -8
  293. package/src/db/libsql.ts +1 -3
  294. package/src/db/sqlite.ts +2 -5
  295. package/src/emdash-runtime.ts +388 -247
  296. package/src/index.ts +14 -1
  297. package/src/loader.ts +30 -6
  298. package/src/mcp/server.ts +781 -141
  299. package/src/media/normalize.ts +1 -1
  300. package/src/media/url.ts +78 -0
  301. package/src/page/site-identity.ts +58 -0
  302. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  303. package/src/plugins/context.ts +13 -10
  304. package/src/plugins/define-plugin.ts +40 -12
  305. package/src/plugins/email-console.ts +10 -3
  306. package/src/plugins/hooks.ts +34 -19
  307. package/src/plugins/index.ts +9 -0
  308. package/src/plugins/manifest-schema.ts +49 -2
  309. package/src/plugins/types.ts +174 -13
  310. package/src/preview/urls.ts +23 -3
  311. package/src/query.ts +149 -6
  312. package/src/redirects/cache.ts +38 -18
  313. package/src/request-cache.ts +3 -0
  314. package/src/schema/registry.ts +97 -5
  315. package/src/schema/zod-generator.ts +27 -5
  316. package/src/search/fts-manager.ts +0 -2
  317. package/src/search/query.ts +111 -26
  318. package/src/search/types.ts +8 -1
  319. package/src/sections/index.ts +7 -9
  320. package/src/seed/apply.ts +2 -0
  321. package/src/settings/index.ts +80 -6
  322. package/src/settings/types.ts +23 -1
  323. package/src/storage/s3.ts +12 -6
  324. package/src/taxonomies/index.ts +11 -1
  325. package/src/virtual-modules.d.ts +21 -1
  326. package/src/widgets/index.ts +1 -1
  327. package/dist/apply-5uslYdUu.mjs.map +0 -1
  328. package/dist/byline-C4OVd8b3.mjs.map +0 -1
  329. package/dist/bylines-hPTW79hw.mjs +0 -157
  330. package/dist/bylines-hPTW79hw.mjs.map +0 -1
  331. package/dist/cache-BkKBuIvS.mjs +0 -56
  332. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  333. package/dist/chunk-ClPoSABd.mjs +0 -21
  334. package/dist/content-D7J5y73J.mjs.map +0 -1
  335. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  336. package/dist/error-CiYn9yDu.mjs.map +0 -1
  337. package/dist/index-De6_Xv3v.d.mts.map +0 -1
  338. package/dist/loader-DeiBJEMe.mjs.map +0 -1
  339. package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
  340. package/dist/media-DqHVh136.mjs.map +0 -1
  341. package/dist/mode-CpNnGkPz.mjs.map +0 -1
  342. package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
  343. package/dist/query-g4Ug-9j9.mjs.map +0 -1
  344. package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
  345. package/dist/registry-Ci3WxVAr.mjs.map +0 -1
  346. package/dist/request-cache-DiR961CV.mjs.map +0 -1
  347. package/dist/runner-BR2xKwhn.d.mts +0 -34
  348. package/dist/runner-BR2xKwhn.d.mts.map +0 -1
  349. package/dist/search-B0effn3j.mjs.map +0 -1
  350. package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
  351. package/dist/types-CMMN0pNg.mjs +0 -31
  352. package/dist/types-CMMN0pNg.mjs.map +0 -1
  353. package/dist/types-DgrIP0tF.d.mts.map +0 -1
  354. package/dist/version-BnTKdfam.mjs +0 -7
@@ -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,
@@ -38,7 +39,7 @@ import {
38
39
  ICON_SIZE,
39
40
  } from "./bundle-utils.js";
40
41
 
41
- const TS_EXT_RE = /\.tsx?$/;
42
+ const TS_EXT_RE = /\.(tsx?|[mc]?js)$/;
42
43
  const SLASH_RE = /\//g;
43
44
  const LEADING_AT_RE = /^@/;
44
45
  const emdash_SCOPE_RE = /^@emdash-cms\//;
@@ -163,6 +164,8 @@ export const bundleCommand = defineCommand({
163
164
  const tmpDir = join(pluginDir, ".emdash-bundle-tmp");
164
165
 
165
166
  try {
167
+ // Clean up any stale temp directory from a previous failed run
168
+ await rm(tmpDir, { recursive: true, force: true });
166
169
  await mkdir(tmpDir, { recursive: true });
167
170
 
168
171
  // Build main entry to extract manifest.
@@ -522,20 +525,39 @@ export const bundleCommand = defineCommand({
522
525
  }
523
526
  }
524
527
 
525
- // Check capabilities warnings
526
- 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) {
527
539
  consola.warn(
528
- "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",
529
541
  );
530
- } else if (
531
- manifest.capabilities.includes("network:fetch") &&
532
- manifest.allowedHosts.length === 0
533
- ) {
542
+ } else if (declaresHostRestricted && manifest.allowedHosts.length === 0) {
534
543
  consola.warn(
535
- "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",
536
545
  );
537
546
  }
538
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
+
539
561
  // Check for features that won't work in sandboxed mode
540
562
  if (
541
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,
@@ -21,6 +21,7 @@ import { createGzipDecoder, unpackTar } from "modern-tar";
21
21
  import pc from "picocolors";
22
22
 
23
23
  import { pluginManifestSchema } from "../../plugins/manifest-schema.js";
24
+ import { CAPABILITY_RENAMES, isDeprecatedCapability } from "../../plugins/types.js";
24
25
  import {
25
26
  getMarketplaceCredential,
26
27
  saveMarketplaceCredential,
@@ -440,6 +441,29 @@ export const publishCommand = defineCommand({
440
441
  }
441
442
  console.log();
442
443
 
444
+ // ── Step 2.5: Hard-fail on deprecated capability names ──
445
+ //
446
+ // Refusing to publish manifests that use deprecated capability names
447
+ // keeps the marketplace clean while the deprecation window is open.
448
+ // The fix is mechanical and entirely in the author's hands — they
449
+ // rename, re-bundle, and republish. Better to refuse 5 publishes
450
+ // than ship 500 deprecated manifests. We check before authentication
451
+ // so authors don't burn a device-flow login on a doomed publish.
452
+ const deprecatedCaps = manifest.capabilities.filter(isDeprecatedCapability);
453
+ if (deprecatedCaps.length > 0) {
454
+ consola.error(
455
+ "Plugin declares deprecated capability names. Rename them and re-bundle before publishing:",
456
+ );
457
+ for (const cap of deprecatedCaps) {
458
+ const replacement = CAPABILITY_RENAMES[cap];
459
+ consola.error(` ${cap} → ${replacement}`);
460
+ }
461
+ consola.error(
462
+ "See https://emdashcms.com/docs/plugins/overview#capabilities for the full rename table.",
463
+ );
464
+ process.exit(1);
465
+ }
466
+
443
467
  // ── Step 3: Authenticate ──
444
468
  //
445
469
  // Priority: EMDASH_MARKETPLACE_TOKEN env var > stored credential > interactive device flow.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Secrets CLI commands
3
+ *
4
+ * Pure (no-DB) commands for working with EmDash secrets:
5
+ *
6
+ * - `emdash secrets generate` — emits a fresh `EMDASH_ENCRYPTION_KEY`.
7
+ * Optionally writes it to `.dev.vars` (Workers) or `.env` (Node).
8
+ * - `emdash secrets fingerprint <key>` — prints the kid for a key,
9
+ * useful in CI for verifying what's been deployed without exposing
10
+ * the raw value.
11
+ *
12
+ * DB-touching commands (`status`, `migrate`, `rotate`) live elsewhere:
13
+ * the CLI process can't open the production D1/Postgres binding from
14
+ * the operator's machine, so those operations ship as admin HTTP
15
+ * endpoints in a later PR. A thin `--site <url>` wrapper for those
16
+ * endpoints can land alongside.
17
+ */
18
+
19
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { resolve } from "node:path";
21
+
22
+ import { defineCommand } from "citty";
23
+ import { consola } from "consola";
24
+ import pc from "picocolors";
25
+
26
+ import { EmDashSecretsError, fingerprintKey, generateEncryptionKey } from "../../config/secrets.js";
27
+
28
+ const KEY_VAR_NAME = "EMDASH_ENCRYPTION_KEY";
29
+ /** Matches a populated entry — `KEY=<at least one char>`. */
30
+ const POPULATED_KEY_LINE_PATTERN = /^EMDASH_ENCRYPTION_KEY=.+$/m;
31
+ /**
32
+ * Matches any line starting `KEY=` including `KEY=` with empty value.
33
+ * Used for in-place replacement when the entry exists but has no value.
34
+ */
35
+ const ANY_KEY_LINE_PATTERN = /^EMDASH_ENCRYPTION_KEY=.*$/m;
36
+
37
+ /**
38
+ * Append (or replace) `EMDASH_ENCRYPTION_KEY` in a dotenv-style file.
39
+ *
40
+ * Idempotent: if the entry exists with a populated value, leaves it alone
41
+ * (returns `"skipped"`) unless `force` is set. An entry with an empty
42
+ * value (`EMDASH_ENCRYPTION_KEY=`) is treated as "not set" and gets
43
+ * replaced — placeholder lines aren't a reason to refuse.
44
+ *
45
+ * Always ends the resulting file with a trailing newline. Doesn't touch
46
+ * other variables.
47
+ *
48
+ * Exported for tests.
49
+ */
50
+ export function writeEncryptionKeyToFile(
51
+ targetPath: string,
52
+ value: string,
53
+ force: boolean,
54
+ ): "wrote" | "skipped" {
55
+ const exists = existsSync(targetPath);
56
+ const existing = exists ? readFileSync(targetPath, "utf-8") : "";
57
+
58
+ const hasPopulatedKey = POPULATED_KEY_LINE_PATTERN.test(existing);
59
+ if (hasPopulatedKey && !force) {
60
+ return "skipped";
61
+ }
62
+
63
+ const newLine = `${KEY_VAR_NAME}=${value}`;
64
+ let next: string;
65
+ if (ANY_KEY_LINE_PATTERN.test(existing)) {
66
+ // In-place replace handles both populated-and-forced and empty-value
67
+ // cases. Then ensure trailing newline.
68
+ next = existing.replace(ANY_KEY_LINE_PATTERN, newLine);
69
+ if (!next.endsWith("\n")) next += "\n";
70
+ } else {
71
+ // Append. Insert a separating newline only if the file has content
72
+ // not already ending in one.
73
+ const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "" : "\n";
74
+ next = `${existing}${sep}${newLine}\n`;
75
+ }
76
+
77
+ writeFileSync(targetPath, next);
78
+ return "wrote";
79
+ }
80
+
81
+ const generateCommand = defineCommand({
82
+ meta: {
83
+ name: "generate",
84
+ description: "Generate a new EmDash encryption key",
85
+ },
86
+ args: {
87
+ write: {
88
+ type: "string",
89
+ description:
90
+ "Optional path to write the key to (e.g. .dev.vars or .env). " +
91
+ "Won't overwrite an existing entry without --force.",
92
+ },
93
+ force: {
94
+ type: "boolean",
95
+ description: "When used with --write, overwrite an existing entry",
96
+ default: false,
97
+ },
98
+ },
99
+ run({ args }) {
100
+ const value = generateEncryptionKey();
101
+
102
+ if (args.write) {
103
+ const targetPath = resolve(process.cwd(), args.write);
104
+ const result = writeEncryptionKeyToFile(targetPath, value, args.force);
105
+ if (result === "skipped") {
106
+ // Idempotent no-op: entry already populated. Exit 0 so chained
107
+ // scripts (`emdash secrets generate --write && pnpm dev`) don't
108
+ // break. Pass --force to replace, with full awareness that
109
+ // existing encrypted secrets become unreadable.
110
+ consola.info(
111
+ `${KEY_VAR_NAME} already set in ${pc.cyan(args.write)}; leaving it alone. ` +
112
+ `Pass ${pc.bold("--force")} to replace it.`,
113
+ );
114
+ return;
115
+ }
116
+ consola.log("");
117
+ consola.log(`${pc.bold("Wrote")} ${pc.cyan(KEY_VAR_NAME)} to ${pc.cyan(args.write)}`);
118
+ consola.log("");
119
+ consola.log(
120
+ pc.yellow(
121
+ "Keep this file out of version control. Losing the key means losing every secret encrypted with it.",
122
+ ),
123
+ );
124
+ consola.log("");
125
+ return;
126
+ }
127
+
128
+ // Print the key to stdout (one line, no decoration) so it can be
129
+ // piped into env files or secret-management tools without scraping.
130
+ // Explanatory text goes to stderr so it doesn't pollute the pipe.
131
+ process.stdout.write(`${value}\n`);
132
+ const guidance = [
133
+ "",
134
+ pc.bold("EmDash encryption key generated."),
135
+ "",
136
+ `Set ${pc.cyan(KEY_VAR_NAME)} in your environment.`,
137
+ "For Cloudflare deployments, push it to your Worker's secrets.",
138
+ "For Node deployments, add it to your process environment or .env file.",
139
+ "",
140
+ pc.yellow("Keep this value secret. Losing it means losing every secret encrypted with it."),
141
+ "",
142
+ ].join("\n");
143
+ process.stderr.write(`${guidance}\n`);
144
+ },
145
+ });
146
+
147
+ const fingerprintCommand = defineCommand({
148
+ meta: {
149
+ name: "fingerprint",
150
+ description: "Print the kid (8-char fingerprint) for an encryption key",
151
+ },
152
+ args: {
153
+ key: {
154
+ type: "positional",
155
+ description: "The full key value (with the emdash_enc_v1_ prefix)",
156
+ required: true,
157
+ },
158
+ },
159
+ async run({ args }) {
160
+ try {
161
+ const kid = await fingerprintKey(args.key);
162
+ // Newline-only on stdout so it pipes cleanly into env/CI logs
163
+ // without leaking the raw key.
164
+ process.stdout.write(`${kid}\n`);
165
+ } catch (error) {
166
+ consola.error(
167
+ error instanceof EmDashSecretsError ? error.message : "Failed to fingerprint key",
168
+ );
169
+ process.exit(1);
170
+ }
171
+ },
172
+ });
173
+
174
+ export const secretsCommand = defineCommand({
175
+ meta: {
176
+ name: "secrets",
177
+ description: "Manage EmDash secrets (generate, inspect)",
178
+ },
179
+ subCommands: {
180
+ generate: generateCommand,
181
+ fingerprint: fingerprintCommand,
182
+ },
183
+ });
@@ -130,7 +130,7 @@ function readStore(): CredentialStore {
130
130
 
131
131
  function writeStore(store: CredentialStore): void {
132
132
  const dir = getConfigDir();
133
- mkdirSync(dir, { recursive: true });
133
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
134
134
 
135
135
  const path = getCredentialPath();
136
136
  writeFileSync(path, JSON.stringify(store, null, "\t"), {