emdash 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
  4. package/dist/apply-UsrFuO7l.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.d.mts.map +1 -1
  7. package/dist/astro/index.mjs +118 -4
  8. package/dist/astro/index.mjs.map +1 -1
  9. package/dist/astro/middleware/auth.d.mts +6 -7
  10. package/dist/astro/middleware/auth.d.mts.map +1 -1
  11. package/dist/astro/middleware/auth.mjs +14 -57
  12. package/dist/astro/middleware/auth.mjs.map +1 -1
  13. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  14. package/dist/astro/middleware/redirect.mjs +15 -10
  15. package/dist/astro/middleware/redirect.mjs.map +1 -1
  16. package/dist/astro/middleware/request-context.d.mts.map +1 -1
  17. package/dist/astro/middleware/request-context.mjs +8 -5
  18. package/dist/astro/middleware/request-context.mjs.map +1 -1
  19. package/dist/astro/middleware/setup.mjs +1 -1
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +70 -121
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/types.d.mts +25 -10
  24. package/dist/astro/types.d.mts.map +1 -1
  25. package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
  26. package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
  27. package/dist/bylines-esI7ioa9.mjs +113 -0
  28. package/dist/bylines-esI7ioa9.mjs.map +1 -0
  29. package/dist/cache-fTzxgMFJ.mjs +65 -0
  30. package/dist/cache-fTzxgMFJ.mjs.map +1 -0
  31. package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
  32. package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
  33. package/dist/cli/index.mjs +456 -90
  34. package/dist/cli/index.mjs.map +1 -1
  35. package/dist/client/cf-access.d.mts +1 -1
  36. package/dist/client/index.d.mts +1 -1
  37. package/dist/client/index.mjs +3 -3
  38. package/dist/client/index.mjs.map +1 -1
  39. package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
  40. package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
  41. package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
  42. package/dist/content-C7G4QXkK.mjs.map +1 -0
  43. package/dist/db/index.d.mts +3 -3
  44. package/dist/db/index.mjs +2 -2
  45. package/dist/db/libsql.d.mts +1 -1
  46. package/dist/db/libsql.d.mts.map +1 -1
  47. package/dist/db/libsql.mjs +7 -2
  48. package/dist/db/libsql.mjs.map +1 -1
  49. package/dist/db/postgres.d.mts +1 -1
  50. package/dist/db/sqlite.d.mts +1 -1
  51. package/dist/db/sqlite.d.mts.map +1 -1
  52. package/dist/db/sqlite.mjs +8 -3
  53. package/dist/db/sqlite.mjs.map +1 -1
  54. package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  55. package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  56. package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
  57. package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  58. package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
  59. package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
  60. package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
  61. package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  62. package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
  63. package/dist/index-DjPMOfO0.d.mts.map +1 -0
  64. package/dist/index.d.mts +11 -11
  65. package/dist/index.mjs +27 -24
  66. package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
  67. package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
  68. package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
  69. package/dist/loader-Bx2_9-5e.mjs.map +1 -0
  70. package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
  71. package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
  72. package/dist/media/index.d.mts +1 -1
  73. package/dist/media/index.mjs +1 -1
  74. package/dist/media/local-runtime.d.mts +7 -7
  75. package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
  76. package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  77. package/dist/options-nPxWnrya.mjs +117 -0
  78. package/dist/options-nPxWnrya.mjs.map +1 -0
  79. package/dist/page/index.d.mts +2 -2
  80. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  81. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  82. package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  83. package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  84. package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  85. package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  86. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  87. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  88. package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
  89. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  90. package/dist/public-url-B1AxbbbQ.mjs +51 -0
  91. package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
  92. package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
  93. package/dist/query-Bo-msrmu.mjs.map +1 -0
  94. package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
  95. package/dist/redirect-C5H7VGIX.mjs.map +1 -0
  96. package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
  97. package/dist/registry-Beb7wxFc.mjs.map +1 -0
  98. package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
  99. package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
  100. package/dist/runner-Clwe4Mme.d.mts +44 -0
  101. package/dist/runner-Clwe4Mme.d.mts.map +1 -0
  102. package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
  103. package/dist/runner-DMnlIkh4.mjs.map +1 -0
  104. package/dist/runtime.d.mts +6 -6
  105. package/dist/runtime.mjs +2 -2
  106. package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
  107. package/dist/search-DkN-BqsS.mjs.map +1 -0
  108. package/dist/secrets-CZ8rxLX3.mjs +314 -0
  109. package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
  110. package/dist/seed/index.d.mts +2 -2
  111. package/dist/seed/index.mjs +13 -11
  112. package/dist/seo/index.d.mts +1 -1
  113. package/dist/storage/local.d.mts +1 -1
  114. package/dist/storage/local.mjs +1 -1
  115. package/dist/storage/s3.d.mts +1 -1
  116. package/dist/storage/s3.mjs +1 -1
  117. package/dist/taxonomies-CTtewrSQ.mjs +407 -0
  118. package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
  119. package/dist/taxonomy-DSxx2K2L.mjs +218 -0
  120. package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
  121. package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
  122. package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  123. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  124. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  125. package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  126. package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  127. package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
  128. package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  129. package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
  130. package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  131. package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
  132. package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  133. package/dist/types-CoO6mpV3.mjs +68 -0
  134. package/dist/types-CoO6mpV3.mjs.map +1 -0
  135. package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
  136. package/dist/types-D19uBYWn.d.mts.map +1 -0
  137. package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
  138. package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
  139. package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
  140. package/dist/types-Dtx1mSMX.d.mts.map +1 -0
  141. package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
  142. package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
  143. package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
  144. package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  145. package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
  146. package/dist/validate-CBIbxM3L.mjs.map +1 -0
  147. package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
  148. package/dist/validate-DHGwADqO.d.mts.map +1 -0
  149. package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
  150. package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
  151. package/dist/version-CMD42IRC.mjs +7 -0
  152. package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
  153. package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
  154. package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
  155. package/locals.d.ts +1 -6
  156. package/package.json +9 -8
  157. package/src/api/handlers/comments.ts +6 -4
  158. package/src/api/handlers/content.ts +40 -1
  159. package/src/api/handlers/dashboard.ts +29 -36
  160. package/src/api/handlers/device-flow.ts +5 -0
  161. package/src/api/handlers/marketplace.ts +11 -4
  162. package/src/api/handlers/menus.ts +256 -75
  163. package/src/api/handlers/oauth-authorization.ts +72 -33
  164. package/src/api/handlers/revision.ts +23 -14
  165. package/src/api/handlers/taxonomies.ts +273 -100
  166. package/src/api/public-url.ts +48 -2
  167. package/src/api/schemas/comments.ts +2 -2
  168. package/src/api/schemas/common.ts +7 -0
  169. package/src/api/schemas/content.ts +17 -0
  170. package/src/api/schemas/menus.ts +23 -0
  171. package/src/api/schemas/sections.ts +3 -3
  172. package/src/api/schemas/taxonomies.ts +39 -0
  173. package/src/api/schemas/users.ts +1 -1
  174. package/src/api/types.ts +5 -1
  175. package/src/astro/integration/index.ts +17 -0
  176. package/src/astro/integration/routes.ts +10 -0
  177. package/src/astro/integration/runtime.ts +30 -0
  178. package/src/astro/integration/virtual-modules.ts +32 -2
  179. package/src/astro/integration/vite-config.ts +6 -1
  180. package/src/astro/middleware/auth.ts +13 -6
  181. package/src/astro/middleware/redirect.ts +29 -16
  182. package/src/astro/middleware/request-context.ts +15 -5
  183. package/src/astro/middleware.ts +23 -9
  184. package/src/astro/routes/api/auth/invite/complete.ts +6 -1
  185. package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
  186. package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
  187. package/src/astro/routes/api/auth/signup/complete.ts +6 -1
  188. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
  189. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  190. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  191. package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
  192. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
  193. package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
  194. package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
  195. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
  196. package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
  197. package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
  198. package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
  199. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  200. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  201. package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
  202. package/src/astro/routes/api/manifest.ts +62 -45
  203. package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
  204. package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
  205. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  206. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  207. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  208. package/src/astro/routes/api/menus/[name].ts +19 -10
  209. package/src/astro/routes/api/menus/index.ts +9 -6
  210. package/src/astro/routes/api/openapi.json.ts +27 -10
  211. package/src/astro/routes/api/redirects/404s/index.ts +10 -4
  212. package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
  213. package/src/astro/routes/api/redirects/[id].ts +10 -4
  214. package/src/astro/routes/api/redirects/index.ts +7 -3
  215. package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
  216. package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
  217. package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
  218. package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
  219. package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
  220. package/src/astro/routes/api/schema/collections/index.ts +1 -1
  221. package/src/astro/routes/api/search/index.ts +10 -2
  222. package/src/astro/routes/api/sections/[slug].ts +10 -4
  223. package/src/astro/routes/api/sections/index.ts +7 -3
  224. package/src/astro/routes/api/setup/admin-verify.ts +6 -1
  225. package/src/astro/routes/api/snapshot.ts +44 -18
  226. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  227. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  228. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  229. package/src/astro/routes/api/taxonomies/index.ts +9 -7
  230. package/src/astro/routes/api/themes/preview.ts +11 -5
  231. package/src/astro/types.ts +23 -3
  232. package/src/auth/allowed-origins.ts +168 -0
  233. package/src/auth/passkey-config.ts +35 -13
  234. package/src/bylines/index.ts +37 -88
  235. package/src/cli/commands/auth.ts +28 -6
  236. package/src/cli/commands/bundle-utils.ts +11 -2
  237. package/src/cli/commands/bundle.ts +28 -8
  238. package/src/cli/commands/content.ts +13 -0
  239. package/src/cli/commands/export-seed.ts +82 -21
  240. package/src/cli/commands/login.ts +8 -1
  241. package/src/cli/commands/plugin-init.ts +216 -90
  242. package/src/cli/commands/publish.ts +24 -0
  243. package/src/cli/commands/secrets.ts +183 -0
  244. package/src/cli/credentials.ts +1 -1
  245. package/src/cli/index.ts +5 -1
  246. package/src/client/index.ts +4 -4
  247. package/src/client/transport.ts +17 -7
  248. package/src/components/Break.astro +2 -2
  249. package/src/components/EmDashHead.astro +18 -13
  250. package/src/components/Embed.astro +1 -1
  251. package/src/components/Gallery.astro +1 -1
  252. package/src/components/Image.astro +1 -1
  253. package/src/components/InlinePortableTextEditor.tsx +104 -18
  254. package/src/config/secrets.ts +528 -0
  255. package/src/database/dialect-helpers.ts +50 -0
  256. package/src/database/migrations/034_published_at_index.ts +1 -1
  257. package/src/database/migrations/035_bounded_404_log.ts +56 -39
  258. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  259. package/src/database/migrations/runner.ts +158 -23
  260. package/src/database/repositories/content.ts +47 -12
  261. package/src/database/repositories/redirect.ts +14 -3
  262. package/src/database/repositories/taxonomy.ts +212 -82
  263. package/src/database/types.ts +10 -2
  264. package/src/db/libsql.ts +1 -3
  265. package/src/db/sqlite.ts +2 -5
  266. package/src/emdash-runtime.ts +84 -159
  267. package/src/i18n/resolve.ts +37 -0
  268. package/src/index.ts +9 -0
  269. package/src/loader.ts +73 -3
  270. package/src/mcp/server.ts +180 -54
  271. package/src/menus/index.ts +143 -124
  272. package/src/menus/types.ts +15 -1
  273. package/src/page/site-identity.ts +58 -0
  274. package/src/plugins/adapt-sandbox-entry.ts +22 -10
  275. package/src/plugins/context.ts +13 -10
  276. package/src/plugins/define-plugin.ts +40 -12
  277. package/src/plugins/hooks.ts +23 -19
  278. package/src/plugins/index.ts +9 -0
  279. package/src/plugins/manifest-schema.ts +37 -2
  280. package/src/plugins/types.ts +151 -11
  281. package/src/preview/urls.ts +23 -3
  282. package/src/query.ts +148 -5
  283. package/src/redirects/cache.ts +38 -18
  284. package/src/schema/registry.ts +56 -0
  285. package/src/schema/zod-generator.ts +39 -7
  286. package/src/seed/apply.ts +142 -54
  287. package/src/seed/types.ts +14 -1
  288. package/src/seed/validate.ts +27 -13
  289. package/src/settings/index.ts +80 -6
  290. package/src/settings/types.ts +23 -1
  291. package/src/taxonomies/index.ts +237 -210
  292. package/src/taxonomies/types.ts +10 -0
  293. package/dist/apply-x0eMK1lX.mjs.map +0 -1
  294. package/dist/bylines-CRNsVG88.mjs +0 -157
  295. package/dist/bylines-CRNsVG88.mjs.map +0 -1
  296. package/dist/cache-BkKBuIvS.mjs +0 -56
  297. package/dist/cache-BkKBuIvS.mjs.map +0 -1
  298. package/dist/chunk-ClPoSABd.mjs +0 -21
  299. package/dist/content-BcQPYxdV.mjs.map +0 -1
  300. package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
  301. package/dist/index-DIb-CzNx.d.mts.map +0 -1
  302. package/dist/loader-CndGj8kM.mjs.map +0 -1
  303. package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
  304. package/dist/query-fqEdLFms.mjs.map +0 -1
  305. package/dist/redirect-D_pshWdf.mjs.map +0 -1
  306. package/dist/registry-C3Mr0ODu.mjs.map +0 -1
  307. package/dist/runner-OURCaApa.d.mts +0 -34
  308. package/dist/runner-OURCaApa.d.mts.map +0 -1
  309. package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
  310. package/dist/search-BoZYFuUk.mjs.map +0 -1
  311. package/dist/taxonomies-B4IAshV8.mjs +0 -308
  312. package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
  313. package/dist/types-CS8FIX7L.d.mts.map +0 -1
  314. package/dist/types-i36XcA_X.d.mts.map +0 -1
  315. package/dist/validate-CxVsLehf.mjs.map +0 -1
  316. package/dist/validate-DHxmpFJt.d.mts.map +0 -1
  317. package/dist/version-Bbq8TCrz.mjs +0 -7
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * emdash plugin init
3
3
  *
4
- * Scaffold a new EmDash plugin. Generates the standard-format boilerplate:
4
+ * Scaffold a new EmDash plugin. Generates the sandboxed-format boilerplate:
5
5
  * src/index.ts -- descriptor factory
6
6
  * src/sandbox-entry.ts -- definePlugin({ hooks, routes })
7
7
  * package.json
8
8
  * tsconfig.json
9
9
  *
10
- * Use --native to generate native-format boilerplate instead (createPlugin + React admin).
10
+ * Use --format=native (or --native) to generate native-format boilerplate
11
+ * instead (createPlugin + React admin). When neither is passed and stdout
12
+ * is a TTY, the user is prompted to choose.
11
13
  *
12
14
  */
13
15
 
@@ -22,6 +24,8 @@ import { fileExists } from "./bundle-utils.js";
22
24
  const SLUG_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
23
25
  const SCOPE_RE = /^@[^/]+\//;
24
26
 
27
+ type PluginFormat = "standard" | "native";
28
+
25
29
  export const pluginInitCommand = defineCommand({
26
30
  meta: {
27
31
  name: "init",
@@ -37,15 +41,27 @@ export const pluginInitCommand = defineCommand({
37
41
  type: "string",
38
42
  description: "Plugin name/id (e.g. my-plugin or @org/my-plugin)",
39
43
  },
44
+ format: {
45
+ type: "string",
46
+ description:
47
+ "Plugin format: sandboxed or native. Prompts when running interactively if not set.",
48
+ valueHint: "sandboxed|native",
49
+ },
40
50
  native: {
41
51
  type: "boolean",
42
- description: "Generate native-format plugin (createPlugin + React admin)",
52
+ description: "Shortcut for --format=native",
43
53
  default: false,
44
54
  },
45
55
  },
46
56
  async run({ args }) {
47
57
  const targetDir = resolve(args.dir);
48
- const isNative = args.native;
58
+
59
+ const format = await resolveFormat(args.format, args.native);
60
+ if (!format) {
61
+ consola.info("Cancelled");
62
+ return;
63
+ }
64
+ const isNative = format === "native";
49
65
 
50
66
  // Derive plugin name from --name or directory name
51
67
  let pluginName = args.name || basename(targetDir);
@@ -71,7 +87,7 @@ export const pluginInitCommand = defineCommand({
71
87
  process.exit(1);
72
88
  }
73
89
 
74
- consola.start(`Scaffolding ${isNative ? "native" : "standard"} plugin: ${pluginName}`);
90
+ consola.start(`Scaffolding ${isNative ? "native" : "sandboxed"} plugin: ${pluginName}`);
75
91
 
76
92
  await mkdir(srcDir, { recursive: true });
77
93
 
@@ -83,22 +99,99 @@ export const pluginInitCommand = defineCommand({
83
99
 
84
100
  consola.success(`Plugin scaffolded in ${targetDir}`);
85
101
  consola.info("Next steps:");
86
- if (args.dir !== ".") {
87
- consola.info(` 1. cd ${args.dir}`);
88
- }
89
- consola.info(` ${args.dir !== "." ? "2" : "1"}. pnpm install`);
90
- if (isNative) {
91
- consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/index.ts to add hooks and routes`);
102
+ const steps: string[] = [];
103
+ if (args.dir !== ".") steps.push(`cd ${args.dir}`);
104
+ steps.push("pnpm install");
105
+ steps.push(
106
+ isNative
107
+ ? "Edit src/index.ts to add hooks and routes"
108
+ : "Edit src/sandbox-entry.ts to add hooks and routes",
109
+ );
110
+ steps.push("pnpm build");
111
+ if (!isNative) steps.push("emdash plugin validate --dir .");
112
+ steps.forEach((step, i) => consola.info(` ${i + 1}. ${step}`));
113
+ },
114
+ });
115
+
116
+ async function resolveFormat(
117
+ formatArg: string | undefined,
118
+ nativeFlag: boolean,
119
+ ): Promise<PluginFormat | null> {
120
+ if (formatArg) {
121
+ const normalized = formatArg.toLowerCase();
122
+ let parsed: PluginFormat;
123
+ if (normalized === "native") {
124
+ parsed = "native";
125
+ } else if (normalized === "sandboxed" || normalized === "standard") {
126
+ parsed = "standard";
92
127
  } else {
93
- consola.info(
94
- ` ${args.dir !== "." ? "3" : "2"}. Edit src/sandbox-entry.ts to add hooks and routes`,
95
- );
128
+ consola.error(`Invalid --format "${formatArg}". Use "sandboxed" or "native".`);
129
+ process.exit(1);
130
+ }
131
+ if (nativeFlag && parsed !== "native") {
132
+ consola.error(`Conflicting flags: --native and --format=${formatArg}. Pass only one.`);
133
+ process.exit(1);
96
134
  }
97
- consola.info(` ${args.dir !== "." ? "4" : "3"}. emdash plugin validate --dir .`);
135
+ return parsed;
136
+ }
137
+ if (nativeFlag) return "native";
138
+
139
+ if (!process.stdout.isTTY) return "standard";
140
+
141
+ const choice = await consola.prompt("Which plugin format?", {
142
+ type: "select",
143
+ initial: "standard",
144
+ options: [
145
+ {
146
+ label: "Sandboxed",
147
+ value: "standard",
148
+ hint: "runs in an isolated sandbox; safe to install from the marketplace",
149
+ },
150
+ {
151
+ label: "Native",
152
+ value: "native",
153
+ hint: "full runtime access; install from npm",
154
+ },
155
+ ],
156
+ cancel: "null",
157
+ });
158
+ if (choice === null) return null;
159
+ return choice as PluginFormat;
160
+ }
161
+
162
+ function camelCase(slug: string): string {
163
+ return slug
164
+ .split("-")
165
+ .map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
166
+ .join("");
167
+ }
168
+
169
+ function pascalCase(slug: string): string {
170
+ return slug
171
+ .split("-")
172
+ .map((s) => s[0].toUpperCase() + s.slice(1))
173
+ .join("");
174
+ }
175
+
176
+ const TSCONFIG = {
177
+ compilerOptions: {
178
+ target: "ES2022",
179
+ module: "preserve",
180
+ moduleResolution: "bundler",
181
+ strict: true,
182
+ esModuleInterop: true,
183
+ declaration: true,
184
+ outDir: "./dist",
185
+ rootDir: "./src",
98
186
  },
99
- });
187
+ include: ["src/**/*"],
188
+ exclude: ["node_modules", "dist"],
189
+ } as const;
190
+
191
+ const TSDOWN_VERSION = "^0.20.0";
192
+ const TYPESCRIPT_VERSION = "^5.9.0";
100
193
 
101
- // ── Standard format scaffolding ──────────────────────────────────
194
+ // ── Sandboxed format scaffolding ─────────────────────────────────
102
195
 
103
196
  async function scaffoldStandard(
104
197
  targetDir: string,
@@ -106,13 +199,8 @@ async function scaffoldStandard(
106
199
  pluginName: string,
107
200
  slug: string,
108
201
  ): Promise<void> {
109
- // Derive the camelCase function name from slug
110
- const fnName = slug
111
- .split("-")
112
- .map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
113
- .join("");
202
+ const fnName = camelCase(slug);
114
203
 
115
- // package.json
116
204
  await writeFile(
117
205
  join(targetDir, "package.json"),
118
206
  JSON.stringify(
@@ -120,74 +208,98 @@ async function scaffoldStandard(
120
208
  name: pluginName,
121
209
  version: "0.1.0",
122
210
  type: "module",
211
+ main: "./dist/index.mjs",
123
212
  exports: {
124
- ".": "./src/index.ts",
125
- "./sandbox": "./src/sandbox-entry.ts",
213
+ ".": {
214
+ types: "./dist/index.d.mts",
215
+ import: "./dist/index.mjs",
216
+ },
217
+ "./sandbox": {
218
+ types: "./dist/sandbox-entry.d.mts",
219
+ import: "./dist/sandbox-entry.mjs",
220
+ },
126
221
  },
127
- files: ["src"],
222
+ files: ["dist"],
223
+ scripts: {
224
+ build: "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean",
225
+ dev: "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --watch",
226
+ typecheck: "tsc --noEmit",
227
+ },
228
+ keywords: ["emdash", "emdash-plugin"],
229
+ license: "MIT",
128
230
  peerDependencies: {
129
231
  emdash: "*",
130
232
  },
131
- },
132
- null,
133
- "\t",
134
- ) + "\n",
135
- );
136
-
137
- // tsconfig.json
138
- await writeFile(
139
- join(targetDir, "tsconfig.json"),
140
- JSON.stringify(
141
- {
142
- compilerOptions: {
143
- target: "ES2022",
144
- module: "preserve",
145
- moduleResolution: "bundler",
146
- strict: true,
147
- esModuleInterop: true,
148
- declaration: true,
149
- outDir: "./dist",
150
- rootDir: "./src",
233
+ devDependencies: {
234
+ emdash: "*",
235
+ tsdown: TSDOWN_VERSION,
236
+ typescript: TYPESCRIPT_VERSION,
151
237
  },
152
- include: ["src/**/*"],
153
- exclude: ["node_modules", "dist"],
154
238
  },
155
239
  null,
156
240
  "\t",
157
241
  ) + "\n",
158
242
  );
159
243
 
160
- // src/index.ts -- descriptor factory
244
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify(TSCONFIG, null, "\t") + "\n");
245
+
161
246
  await writeFile(
162
247
  join(srcDir, "index.ts"),
163
248
  `import type { PluginDescriptor } from "emdash";
164
249
 
165
250
  export function ${fnName}Plugin(): PluginDescriptor {
166
251
  \treturn {
167
- \t\tid: "${pluginName}",
252
+ \t\tid: "${slug}",
168
253
  \t\tversion: "0.1.0",
169
254
  \t\tformat: "standard",
170
255
  \t\tentrypoint: "${pluginName}/sandbox",
171
- \t\tcapabilities: [],
256
+
257
+ \t\tcapabilities: ["content:read"],
258
+ \t\tstorage: {
259
+ \t\t\tevents: { indexes: ["timestamp"] },
260
+ \t\t},
172
261
  \t};
173
262
  }
174
263
  `,
175
264
  );
176
265
 
177
- // src/sandbox-entry.ts -- plugin definition
178
266
  await writeFile(
179
267
  join(srcDir, "sandbox-entry.ts"),
180
268
  `import { definePlugin } from "emdash";
181
269
  import type { PluginContext } from "emdash";
182
270
 
271
+ interface ContentSaveEvent {
272
+ \tcollection: string;
273
+ \tcontent: { id: string };
274
+ \tisNew: boolean;
275
+ }
276
+
183
277
  export default definePlugin({
184
278
  \thooks: {
185
279
  \t\t"content:afterSave": {
186
- \t\t\thandler: async (event: any, ctx: PluginContext) => {
280
+ \t\t\thandler: async (event: ContentSaveEvent, ctx: PluginContext) => {
187
281
  \t\t\t\tctx.log.info("Content saved", {
188
282
  \t\t\t\t\tcollection: event.collection,
189
283
  \t\t\t\t\tid: event.content.id,
190
284
  \t\t\t\t});
285
+
286
+ \t\t\t\tawait ctx.storage.events.put(\`save-\${Date.now()}\`, {
287
+ \t\t\t\t\ttimestamp: new Date().toISOString(),
288
+ \t\t\t\t\tcollection: event.collection,
289
+ \t\t\t\t\tcontentId: event.content.id,
290
+ \t\t\t\t});
291
+ \t\t\t},
292
+ \t\t},
293
+ \t},
294
+
295
+ \troutes: {
296
+ \t\trecent: {
297
+ \t\t\thandler: async (_routeCtx, ctx: PluginContext) => {
298
+ \t\t\t\tconst result = await ctx.storage.events.query({
299
+ \t\t\t\t\torderBy: { timestamp: "desc" },
300
+ \t\t\t\t\tlimit: 10,
301
+ \t\t\t\t});
302
+ \t\t\t\treturn { events: result.items };
191
303
  \t\t\t},
192
304
  \t\t},
193
305
  \t},
@@ -204,12 +316,9 @@ async function scaffoldNative(
204
316
  pluginName: string,
205
317
  slug: string,
206
318
  ): Promise<void> {
207
- const fnName = slug
208
- .split("-")
209
- .map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
210
- .join("");
319
+ const fnName = camelCase(slug);
320
+ const typeName = pascalCase(slug);
211
321
 
212
- // package.json
213
322
  await writeFile(
214
323
  join(targetDir, "package.json"),
215
324
  JSON.stringify(
@@ -217,71 +326,88 @@ async function scaffoldNative(
217
326
  name: pluginName,
218
327
  version: "0.1.0",
219
328
  type: "module",
329
+ main: "./dist/index.mjs",
220
330
  exports: {
221
- ".": "./src/index.ts",
331
+ ".": {
332
+ types: "./dist/index.d.mts",
333
+ import: "./dist/index.mjs",
334
+ },
335
+ },
336
+ files: ["dist"],
337
+ scripts: {
338
+ build: "tsdown src/index.ts --format esm --dts --clean",
339
+ dev: "tsdown src/index.ts --format esm --dts --watch",
340
+ typecheck: "tsc --noEmit",
222
341
  },
223
- files: ["src"],
342
+ keywords: ["emdash", "emdash-plugin"],
343
+ license: "MIT",
224
344
  peerDependencies: {
225
345
  emdash: "*",
226
346
  },
227
- },
228
- null,
229
- "\t",
230
- ) + "\n",
231
- );
232
-
233
- // tsconfig.json
234
- await writeFile(
235
- join(targetDir, "tsconfig.json"),
236
- JSON.stringify(
237
- {
238
- compilerOptions: {
239
- target: "ES2022",
240
- module: "preserve",
241
- moduleResolution: "bundler",
242
- strict: true,
243
- esModuleInterop: true,
244
- declaration: true,
245
- outDir: "./dist",
246
- rootDir: "./src",
347
+ devDependencies: {
348
+ emdash: "*",
349
+ tsdown: TSDOWN_VERSION,
350
+ typescript: TYPESCRIPT_VERSION,
247
351
  },
248
- include: ["src/**/*"],
249
- exclude: ["node_modules", "dist"],
250
352
  },
251
353
  null,
252
354
  "\t",
253
355
  ) + "\n",
254
356
  );
255
357
 
256
- // src/index.ts -- descriptor + createPlugin
358
+ await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify(TSCONFIG, null, "\t") + "\n");
359
+
257
360
  await writeFile(
258
361
  join(srcDir, "index.ts"),
259
362
  `import { definePlugin } from "emdash";
260
363
  import type { PluginDescriptor } from "emdash";
261
364
 
262
- export function ${fnName}Plugin(): PluginDescriptor {
365
+ export interface ${typeName}Options {
366
+ \tenabled?: boolean;
367
+ }
368
+
369
+ export function ${fnName}Plugin(options: ${typeName}Options = {}): PluginDescriptor<${typeName}Options> {
263
370
  \treturn {
264
- \t\tid: "${pluginName}",
371
+ \t\tid: "${slug}",
265
372
  \t\tversion: "0.1.0",
266
373
  \t\tformat: "native",
267
374
  \t\tentrypoint: "${pluginName}",
268
- \t\toptions: {},
375
+ \t\toptions,
269
376
  \t};
270
377
  }
271
378
 
272
- export function createPlugin() {
379
+ export function createPlugin(options: ${typeName}Options = {}) {
273
380
  \treturn definePlugin({
274
- \t\tid: "${pluginName}",
381
+ \t\tid: "${slug}",
275
382
  \t\tversion: "0.1.0",
276
383
 
384
+ \t\tcapabilities: ["content:read"],
385
+ \t\tstorage: {
386
+ \t\t\tevents: { indexes: ["createdAt"] },
387
+ \t\t},
388
+
277
389
  \t\thooks: {
278
390
  \t\t\t"content:afterSave": async (event, ctx) => {
279
- \t\t\t\tctx.log.info("Content saved", {
391
+ \t\t\t\tif (options.enabled === false) return;
392
+ \t\t\t\tawait ctx.storage.events.put(\`evt_\${Date.now()}\`, {
280
393
  \t\t\t\t\tcollection: event.collection,
281
- \t\t\t\t\tid: event.content.id,
394
+ \t\t\t\t\tcontentId: event.content.id,
395
+ \t\t\t\t\tcreatedAt: new Date().toISOString(),
282
396
  \t\t\t\t});
283
397
  \t\t\t},
284
398
  \t\t},
399
+
400
+ \t\troutes: {
401
+ \t\t\trecent: {
402
+ \t\t\t\thandler: async (ctx) => {
403
+ \t\t\t\t\tconst result = await ctx.storage.events.query({
404
+ \t\t\t\t\t\torderBy: { createdAt: "desc" },
405
+ \t\t\t\t\t\tlimit: 10,
406
+ \t\t\t\t\t});
407
+ \t\t\t\t\treturn { events: result.items };
408
+ \t\t\t\t},
409
+ \t\t\t},
410
+ \t\t},
285
411
  \t});
286
412
  }
287
413
 
@@ -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"), {