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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emdash",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Astro-native CMS with WordPress migration support",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -45,6 +45,12 @@
45
45
  "default": "./dist/cli/index.mjs"
46
46
  },
47
47
  "./routes/*": "./src/astro/routes/*",
48
+ "./api/route-utils": "./src/api/route-utils.ts",
49
+ "./api/schemas": "./src/api/schemas/index.ts",
50
+ "./auth/providers/github": "./src/auth/providers/github.ts",
51
+ "./auth/providers/github-admin": "./src/auth/providers/github-admin.tsx",
52
+ "./auth/providers/google": "./src/auth/providers/google.ts",
53
+ "./auth/providers/google-admin": "./src/auth/providers/google-admin.tsx",
48
54
  "./db": {
49
55
  "types": "./dist/db/index.d.mts",
50
56
  "default": "./dist/db/index.mjs"
@@ -128,6 +134,7 @@
128
134
  "imports": {
129
135
  "#api/schemas.js": "./src/api/schemas/index.js",
130
136
  "#api/*": "./src/api/*",
137
+ "#config/*": "./src/config/*",
131
138
  "#db/*": "./src/database/*",
132
139
  "#auth/*": "./src/auth/*",
133
140
  "#schema/*": "./src/schema/*",
@@ -153,6 +160,8 @@
153
160
  "dependencies": {
154
161
  "@floating-ui/react": "^0.27.16",
155
162
  "@modelcontextprotocol/sdk": "^1.26.0",
163
+ "@oslojs/crypto": "^1.0.1",
164
+ "@oslojs/encoding": "^1.1.0",
156
165
  "@portabletext/toolkit": "^5.0.1",
157
166
  "@tiptap/core": "^3.20.0",
158
167
  "@tiptap/extension-focus": "^3.20.0",
@@ -185,9 +194,9 @@
185
194
  "ulidx": "^2.4.1",
186
195
  "upng-js": "^2.1.0",
187
196
  "zod": "^4.3.5",
188
- "@emdash-cms/admin": "0.7.0",
189
- "@emdash-cms/auth": "0.7.0",
190
- "@emdash-cms/gutenberg-to-portable-text": "0.7.0"
197
+ "@emdash-cms/admin": "0.9.0",
198
+ "@emdash-cms/auth": "0.9.0",
199
+ "@emdash-cms/gutenberg-to-portable-text": "0.9.0"
191
200
  },
192
201
  "optionalDependencies": {
193
202
  "@libsql/kysely-libsql": "^0.4.0",
@@ -195,16 +204,21 @@
195
204
  },
196
205
  "peerDependencies": {
197
206
  "@astrojs/react": ">=5.0.0-beta.0",
198
- "@tanstack/react-query": ">=5.0.0",
199
- "@tanstack/react-router": ">=1.100.0",
207
+ "@emdash-cms/auth-atproto": ">=0.2.1",
200
208
  "astro": ">=6.0.0-beta.0",
201
209
  "react": ">=18.0.0",
202
210
  "react-dom": ">=18.0.0"
203
211
  },
212
+ "peerDependenciesMeta": {
213
+ "@emdash-cms/auth-atproto": {
214
+ "optional": true
215
+ }
216
+ },
204
217
  "devDependencies": {
205
218
  "@apidevtools/swagger-parser": "^12.1.0",
206
219
  "@arethetypeswrong/cli": "^0.18.2",
207
220
  "@types/better-sqlite3": "^7.6.12",
221
+ "@types/react": "19.2.14",
208
222
  "@types/pg": "^8.16.0",
209
223
  "@types/sanitize-html": "^2.16.0",
210
224
  "@types/sax": "^1.2.7",
@@ -215,7 +229,7 @@
215
229
  "vite": "^6.0.0",
216
230
  "vitest": "^4.0.18",
217
231
  "zod-openapi": "^5.4.6",
218
- "@emdash-cms/blocks": "0.7.0"
232
+ "@emdash-cms/blocks": "0.9.0"
219
233
  },
220
234
  "repository": {
221
235
  "type": "git",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Auth provider storage helper.
3
+ *
4
+ * Gives auth provider routes access to plugin-style storage collections
5
+ * namespaced under `auth:<providerId>`. Reuses the existing `_plugin_storage`
6
+ * table and `PluginStorageRepository` infrastructure.
7
+ */
8
+
9
+ import type { Kysely } from "kysely";
10
+
11
+ import type { Database } from "../database/types.js";
12
+ import { createStorageAccess } from "../plugins/context.js";
13
+ import type { StorageCollection, StorageCollectionConfig } from "../plugins/types.js";
14
+
15
+ /**
16
+ * Get storage collections for an auth provider.
17
+ *
18
+ * Returns a record of `StorageCollection` instances, one per declared
19
+ * collection in the provider's `storage` config. Data is stored in the
20
+ * shared `_plugin_storage` table under the namespace `auth:<providerId>`.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const storage = getAuthProviderStorage(emdash.db, "atproto", {
25
+ * states: { indexes: [] },
26
+ * sessions: { indexes: [] },
27
+ * });
28
+ * const session = await storage.sessions.get(sessionId);
29
+ * ```
30
+ */
31
+ export function getAuthProviderStorage(
32
+ db: Kysely<Database>,
33
+ providerId: string,
34
+ storageConfig: Record<string, StorageCollectionConfig>,
35
+ ): Record<string, StorageCollection> {
36
+ return createStorageAccess(db, `auth:${providerId}`, storageConfig);
37
+ }
package/src/api/error.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * `new Response(JSON.stringify({ error: ... }), ...)` patterns.
6
6
  */
7
7
 
8
+ import { InvalidCursorError } from "../database/repositories/types.js";
8
9
  import { mapErrorStatus } from "./errors.js";
9
10
  import type { ApiResult } from "./types.js";
10
11
 
@@ -54,6 +55,11 @@ export function handleError(
54
55
  fallbackMessage: string,
55
56
  fallbackCode: string,
56
57
  ): Response {
58
+ // Bubble malformed-cursor errors as a structured 400 instead of a
59
+ // generic 500.
60
+ if (error instanceof InvalidCursorError) {
61
+ return apiError("INVALID_CURSOR", error.message, 400);
62
+ }
57
63
  console.error(`[${fallbackCode}]`, error);
58
64
  return apiError(fallbackCode, fallbackMessage, 500);
59
65
  }
package/src/api/errors.ts CHANGED
@@ -12,7 +12,9 @@ export const ErrorCode = {
12
12
  VALIDATION_ERROR: "VALIDATION_ERROR",
13
13
  INVALID_INPUT: "INVALID_INPUT",
14
14
  INVALID_JSON: "INVALID_JSON",
15
+ INVALID_CURSOR: "INVALID_CURSOR",
15
16
  CONFLICT: "CONFLICT",
17
+ SLUG_CONFLICT: "SLUG_CONFLICT",
16
18
  NOT_CONFIGURED: "NOT_CONFIGURED",
17
19
  UNAUTHORIZED: "UNAUTHORIZED",
18
20
  FORBIDDEN: "FORBIDDEN",
@@ -152,6 +154,8 @@ export const ErrorCode = {
152
154
  INVALID_CODE: "INVALID_CODE",
153
155
  EXPIRED_CODE: "EXPIRED_CODE",
154
156
  INSUFFICIENT_ROLE: "INSUFFICIENT_ROLE",
157
+ INSUFFICIENT_SCOPE: "INSUFFICIENT_SCOPE",
158
+ INSUFFICIENT_PERMISSIONS: "INSUFFICIENT_PERMISSIONS",
155
159
  TOKEN_EXCHANGE_ERROR: "TOKEN_EXCHANGE_ERROR",
156
160
  TOKEN_REFRESH_ERROR: "TOKEN_REFRESH_ERROR",
157
161
  TOKEN_REVOKE_ERROR: "TOKEN_REVOKE_ERROR",
@@ -335,6 +339,7 @@ export function mapErrorStatus(code: string | undefined): number {
335
339
  case ErrorCode.VALIDATION_ERROR:
336
340
  case ErrorCode.INVALID_INPUT:
337
341
  case ErrorCode.INVALID_JSON:
342
+ case ErrorCode.INVALID_CURSOR:
338
343
  case ErrorCode.MISSING_PARAM:
339
344
  case ErrorCode.INVALID_REQUEST:
340
345
  case ErrorCode.NOT_SUPPORTED:
@@ -373,6 +378,8 @@ export function mapErrorStatus(code: string | undefined): number {
373
378
  case ErrorCode.COMMENT_REJECTED:
374
379
  case ErrorCode.DOMAIN_NOT_ALLOWED:
375
380
  case ErrorCode.INSUFFICIENT_ROLE:
381
+ case ErrorCode.INSUFFICIENT_SCOPE:
382
+ case ErrorCode.INSUFFICIENT_PERMISSIONS:
376
383
  case ErrorCode.CAPABILITY_ESCALATION:
377
384
  case ErrorCode.ROUTE_VISIBILITY_ESCALATION:
378
385
  case ErrorCode.AUDIT_FAILED:
@@ -388,6 +395,7 @@ export function mapErrorStatus(code: string | undefined): number {
388
395
 
389
396
  // 409 Conflict
390
397
  case ErrorCode.CONFLICT:
398
+ case ErrorCode.SLUG_CONFLICT:
391
399
  case ErrorCode.COLLECTION_EXISTS:
392
400
  case ErrorCode.FIELD_EXISTS:
393
401
  case ErrorCode.CREDENTIAL_EXISTS:
@@ -8,6 +8,7 @@ import type { Kysely } from "kysely";
8
8
 
9
9
  import { CommentRepository } from "../../database/repositories/comment.js";
10
10
  import type { Comment, CommentStatus, PublicComment } from "../../database/repositories/comment.js";
11
+ import { InvalidCursorError } from "../../database/repositories/types.js";
11
12
  import type { Database } from "../../database/types.js";
12
13
  import type { ApiResult } from "../types.js";
13
14
 
@@ -60,6 +61,12 @@ export async function handleCommentList(
60
61
  },
61
62
  };
62
63
  } catch (error) {
64
+ if (error instanceof InvalidCursorError) {
65
+ return {
66
+ success: false,
67
+ error: { code: "INVALID_CURSOR", message: error.message },
68
+ };
69
+ }
63
70
  console.error("Comment list error:", error);
64
71
  return {
65
72
  success: false,
@@ -104,6 +111,12 @@ export async function handleCommentInbox(
104
111
  },
105
112
  };
106
113
  } catch (error) {
114
+ if (error instanceof InvalidCursorError) {
115
+ return {
116
+ success: false,
117
+ error: { code: "INVALID_CURSOR", message: error.message },
118
+ };
119
+ }
107
120
  console.error("Comment inbox error:", error);
108
121
  return {
109
122
  success: false,
@@ -303,11 +316,13 @@ export async function checkRateLimit(
303
316
  /**
304
317
  * Hash an IP address for storage (never store cleartext IPs).
305
318
  *
306
- * Uses full SHA-256 with an application salt to prevent rainbow-table
307
- * recovery of IPs. The caller should pass a site-specific secret;
308
- * falls back to a static salt if none is provided.
319
+ * Uses full SHA-256 with a site-specific salt to prevent rainbow-table
320
+ * recovery of IPs. The salt must be provided by the caller — typically
321
+ * via `resolveSecretsCached(db).ipSalt` from `#config/secrets.js`. The
322
+ * salt is generated and persisted on first need so it's stable across
323
+ * requests within a deployment but unique per install.
309
324
  */
310
- export async function hashIp(ip: string, salt: string = "emdash-ip-salt"): Promise<string> {
325
+ export async function hashIp(ip: string, salt: string): Promise<string> {
311
326
  const data = `ip:${salt}:${ip}`;
312
327
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data));
313
328
  return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
@@ -14,6 +14,7 @@ import { RevisionRepository } from "../../database/repositories/revision.js";
14
14
  import { SeoRepository } from "../../database/repositories/seo.js";
15
15
  import {
16
16
  EmDashValidationError,
17
+ InvalidCursorError,
17
18
  type ContentItem,
18
19
  type ContentSeo,
19
20
  type ContentSeoInput,
@@ -23,9 +24,26 @@ import type { Database } from "../../database/types.js";
23
24
  import { validateIdentifier } from "../../database/validate.js";
24
25
  import { isI18nEnabled } from "../../i18n/config.js";
25
26
  import { invalidateRedirectCache } from "../../redirects/cache.js";
27
+ import { isMissingTableError } from "../../utils/db-errors.js";
26
28
  import { encodeRev, validateRev } from "../rev.js";
27
29
  import type { ApiResult, ContentListResponse, ContentResponse } from "../types.js";
28
30
 
31
+ /**
32
+ * Narrow a caught error to one carrying a structured `apiError` discriminant.
33
+ * Used by transaction callbacks that want to surface a specific error code
34
+ * through the standard Error throwing path.
35
+ */
36
+ function hasApiError(error: unknown): error is Error & { apiError: { code: string } } {
37
+ if (!(error instanceof Error) || !("apiError" in error)) return false;
38
+ const { apiError } = error;
39
+ return (
40
+ typeof apiError === "object" &&
41
+ apiError !== null &&
42
+ "code" in apiError &&
43
+ typeof apiError.code === "string"
44
+ );
45
+ }
46
+
29
47
  /**
30
48
  * Extract a slug source (title or name) from content data.
31
49
  * Returns null if no suitable string field is found.
@@ -267,6 +285,28 @@ export async function handleContentList(
267
285
  },
268
286
  };
269
287
  } catch (error) {
288
+ if (error instanceof InvalidCursorError) {
289
+ return {
290
+ success: false,
291
+ error: { code: "INVALID_CURSOR", message: error.message },
292
+ };
293
+ }
294
+ if (isMissingTableError(error)) {
295
+ return {
296
+ success: false,
297
+ error: {
298
+ code: "COLLECTION_NOT_FOUND",
299
+ message: `Collection '${collection}' not found`,
300
+ },
301
+ };
302
+ }
303
+ if (error instanceof EmDashValidationError) {
304
+ // e.g. invalid orderBy field
305
+ return {
306
+ success: false,
307
+ error: { code: "VALIDATION_ERROR", message: error.message },
308
+ };
309
+ }
270
310
  console.error("Content list error:", error);
271
311
  return {
272
312
  success: false,
@@ -453,6 +493,46 @@ export async function handleContentCreate(
453
493
  data: { item, _rev: encodeRev(item) },
454
494
  };
455
495
  } catch (error) {
496
+ if (isMissingTableError(error)) {
497
+ return {
498
+ success: false,
499
+ error: {
500
+ code: "COLLECTION_NOT_FOUND",
501
+ message: `Collection '${collection}' not found`,
502
+ },
503
+ };
504
+ }
505
+ if (error instanceof EmDashValidationError) {
506
+ return {
507
+ success: false,
508
+ error: { code: "VALIDATION_ERROR", message: error.message },
509
+ };
510
+ }
511
+ // SQLite UNIQUE constraint OR Postgres unique_violation — slug
512
+ // collisions and any other unique violations land here. Match
513
+ // specifically on "unique constraint failed" / "duplicate key" so we
514
+ // don't false-positive on NOT NULL or CHECK violations whose
515
+ // messages also contain "constraint failed".
516
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
517
+ if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
518
+ // Detect slug-specific collisions by message fingerprint
519
+ if (message.includes("slug")) {
520
+ return {
521
+ success: false,
522
+ error: {
523
+ code: "SLUG_CONFLICT",
524
+ message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`,
525
+ },
526
+ };
527
+ }
528
+ return {
529
+ success: false,
530
+ error: {
531
+ code: "CONFLICT",
532
+ message: "Unique constraint violation",
533
+ },
534
+ };
535
+ }
456
536
  console.error("Content create error:", error);
457
537
  return {
458
538
  success: false,
@@ -604,11 +684,44 @@ export async function handleContentUpdate(
604
684
  } catch (error) {
605
685
  // Handle structured errors thrown from inside the transaction
606
686
  // (rev check failures, not-found)
607
- if (error instanceof Error && "apiError" in error) {
608
- const { code } = (error as Error & { apiError: { code: string } }).apiError;
687
+ if (hasApiError(error)) {
609
688
  return {
610
689
  success: false,
611
- error: { code, message: error.message },
690
+ error: { code: error.apiError.code, message: error.message },
691
+ };
692
+ }
693
+ if (isMissingTableError(error)) {
694
+ return {
695
+ success: false,
696
+ error: {
697
+ code: "COLLECTION_NOT_FOUND",
698
+ message: `Collection '${collection}' not found`,
699
+ },
700
+ };
701
+ }
702
+ if (error instanceof EmDashValidationError) {
703
+ return {
704
+ success: false,
705
+ error: { code: "VALIDATION_ERROR", message: error.message },
706
+ };
707
+ }
708
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
709
+ if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
710
+ if (message.includes("slug")) {
711
+ return {
712
+ success: false,
713
+ error: {
714
+ code: "SLUG_CONFLICT",
715
+ message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`,
716
+ },
717
+ };
718
+ }
719
+ return {
720
+ success: false,
721
+ error: {
722
+ code: "CONFLICT",
723
+ message: "Unique constraint violation",
724
+ },
612
725
  };
613
726
  }
614
727
  console.error("Content update error:", error);
@@ -869,6 +982,12 @@ export async function handleContentListTrashed(
869
982
  },
870
983
  };
871
984
  } catch (error) {
985
+ if (error instanceof InvalidCursorError) {
986
+ return {
987
+ success: false,
988
+ error: { code: "INVALID_CURSOR", message: error.message },
989
+ };
990
+ }
872
991
  console.error("Content list trashed error:", error);
873
992
  return {
874
993
  success: false,
@@ -974,6 +1093,15 @@ export async function handleContentUnschedule(
974
1093
  data: { item },
975
1094
  };
976
1095
  } catch (error) {
1096
+ if (error instanceof EmDashValidationError) {
1097
+ return {
1098
+ success: false,
1099
+ error: {
1100
+ code: "VALIDATION_ERROR",
1101
+ message: error.message,
1102
+ },
1103
+ };
1104
+ }
977
1105
  console.error("Content unschedule error:", error);
978
1106
  return {
979
1107
  success: false,
@@ -996,12 +1124,13 @@ export async function handleContentPublish(
996
1124
  db: Kysely<Database>,
997
1125
  collection: string,
998
1126
  id: string,
1127
+ options: { publishedAt?: string } = {},
999
1128
  ): Promise<ApiResult<ContentResponse>> {
1000
1129
  try {
1001
1130
  const item = await withTransaction(db, async (trx) => {
1002
1131
  const repo = new ContentRepository(trx);
1003
1132
  const resolvedId = (await resolveId(repo, collection, id)) ?? id;
1004
- return repo.publish(collection, resolvedId);
1133
+ return repo.publish(collection, resolvedId, options.publishedAt);
1005
1134
  });
1006
1135
 
1007
1136
  const hasSeo = await collectionHasSeo(db, collection);
@@ -1012,6 +1141,15 @@ export async function handleContentPublish(
1012
1141
  data: { item },
1013
1142
  };
1014
1143
  } catch (error) {
1144
+ if (error instanceof EmDashValidationError) {
1145
+ return {
1146
+ success: false,
1147
+ error: {
1148
+ code: "VALIDATION_ERROR",
1149
+ message: error.message,
1150
+ },
1151
+ };
1152
+ }
1015
1153
  console.error("Content publish error:", error);
1016
1154
  return {
1017
1155
  success: false,
@@ -1049,6 +1187,15 @@ export async function handleContentUnpublish(
1049
1187
  data: { item },
1050
1188
  };
1051
1189
  } catch (error) {
1190
+ if (error instanceof EmDashValidationError) {
1191
+ return {
1192
+ success: false,
1193
+ error: {
1194
+ code: "VALIDATION_ERROR",
1195
+ message: error.message,
1196
+ },
1197
+ };
1198
+ }
1052
1199
  console.error("Content unpublish error:", error);
1053
1200
  return {
1054
1201
  success: false,
@@ -135,6 +135,11 @@ export async function handleDeviceCodeRequest(
135
135
  verificationUri: string,
136
136
  ): Promise<ApiResult<DeviceCodeResponse>> {
137
137
  try {
138
+ // Note: client_id is accepted but not validated against _emdash_oauth_clients
139
+ // because the CLI uses a well-known built-in client ID ("emdash-cli") that
140
+ // isn't stored in the DB. Full client_id validation + scope clamping for the
141
+ // device flow is tracked as a follow-up.
142
+
138
143
  // Parse and validate scopes
139
144
  const requestedScopes = input.scope ? input.scope.split(" ").filter(Boolean) : [];
140
145
  const scopes = normalizeScopes(requestedScopes);
@@ -113,11 +113,13 @@ export {
113
113
  handleMenuItemUpdate,
114
114
  handleMenuItemDelete,
115
115
  handleMenuItemReorder,
116
+ handleMenuSetItems,
116
117
  type MenuListItem,
117
118
  type MenuWithItems,
118
119
  type CreateMenuItemInput,
119
120
  type UpdateMenuItemInput,
120
121
  type ReorderItem,
122
+ type MenuSetItemsInput,
121
123
  } from "./menus.js";
122
124
 
123
125
  // Section handlers
@@ -24,6 +24,7 @@ import {
24
24
  } from "../../plugins/marketplace.js";
25
25
  import type { SandboxRunner } from "../../plugins/sandbox/types.js";
26
26
  import { PluginStateRepository } from "../../plugins/state.js";
27
+ import { normalizeCapabilities } from "../../plugins/types.js";
27
28
  import type { PluginManifest } from "../../plugins/types.js";
28
29
  import { EmDashStorageError } from "../../storage/types.js";
29
30
  import type { Storage } from "../../storage/types.js";
@@ -95,11 +96,17 @@ function diffCapabilities(
95
96
  oldCaps: string[],
96
97
  newCaps: string[],
97
98
  ): { added: string[]; removed: string[] } {
98
- const oldSet = new Set(oldCaps);
99
- const newSet = new Set(newCaps);
99
+ // Normalize both sides before diffing so that an installed v1 manifest
100
+ // declaring `read:content` and an upgrade v2 manifest declaring
101
+ // `content:read` produces an empty diff — users should not see a
102
+ // spurious "capability changed" prompt for a pure rename.
103
+ const oldNorm = normalizeCapabilities(oldCaps);
104
+ const newNorm = normalizeCapabilities(newCaps);
105
+ const oldSet = new Set(oldNorm);
106
+ const newSet = new Set(newNorm);
100
107
  return {
101
- added: newCaps.filter((c) => !oldSet.has(c)),
102
- removed: oldCaps.filter((c) => !newSet.has(c)),
108
+ added: newNorm.filter((c) => !oldSet.has(c)),
109
+ removed: oldNorm.filter((c) => !newSet.has(c)),
103
110
  };
104
111
  }
105
112
 
@@ -5,6 +5,7 @@
5
5
  import type { Kysely } from "kysely";
6
6
 
7
7
  import { MediaRepository, type MediaItem } from "../../database/repositories/media.js";
8
+ import { InvalidCursorError } from "../../database/repositories/types.js";
8
9
  import type { Database } from "../../database/types.js";
9
10
  import type { ApiResult } from "../types.js";
10
11
 
@@ -43,7 +44,13 @@ export async function handleMediaList(
43
44
  nextCursor: result.nextCursor,
44
45
  },
45
46
  };
46
- } catch {
47
+ } catch (error) {
48
+ if (error instanceof InvalidCursorError) {
49
+ return {
50
+ success: false,
51
+ error: { code: "INVALID_CURSOR", message: error.message },
52
+ };
53
+ }
47
54
  return {
48
55
  success: false,
49
56
  error: {