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
@@ -39,7 +39,6 @@ import type {
39
39
  PageMetadataContribution,
40
40
  PageFragmentContribution,
41
41
  } from "./plugins/types.js";
42
- import { invalidateUrlPatternCache } from "./query.js";
43
42
  import type { FieldType } from "./schema/types.js";
44
43
  import { hashString } from "./utils/hash.js";
45
44
  import { COMMIT, VERSION } from "./version.js";
@@ -111,6 +110,7 @@ import {
111
110
  DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
112
111
  defaultCommentModerate,
113
112
  } from "./comments/moderator.js";
113
+ import { validateEncryptionKeyAtStartup } from "./config/secrets.js";
114
114
  import { OptionsRepository } from "./database/repositories/options.js";
115
115
  import {
116
116
  handleContentList,
@@ -161,6 +161,7 @@ import { NodeCronScheduler } from "./plugins/scheduler/node.js";
161
161
  import { PiggybackScheduler } from "./plugins/scheduler/piggyback.js";
162
162
  import type { CronScheduler } from "./plugins/scheduler/types.js";
163
163
  import { PluginStateRepository } from "./plugins/state.js";
164
+ import { requestCached } from "./request-cache.js";
164
165
  import { getRequestContext } from "./request-context.js";
165
166
  import { FTSManager } from "./search/fts-manager.js";
166
167
 
@@ -287,7 +288,6 @@ export interface EmDashRuntimeParts {
287
288
  };
288
289
  runtimeDeps: RuntimeDependencies;
289
290
  pipelineRef: { current: HookPipeline };
290
- manifestCacheKey: string;
291
291
  }
292
292
 
293
293
  /**
@@ -343,10 +343,6 @@ export class EmDashRuntime {
343
343
  private enabledPlugins: Set<string>;
344
344
  private pluginStates: Map<string, string>;
345
345
 
346
- private _cachedManifest: EmDashManifest | null = null;
347
- private _manifestPromise: Promise<EmDashManifest> | null = null;
348
- private readonly _manifestCacheKey: string;
349
-
350
346
  /**
351
347
  * Set to true after FTS indexes have been verified for this worker
352
348
  * lifetime so we don't re-scan on every admin request. See
@@ -410,7 +406,6 @@ export class EmDashRuntime {
410
406
  this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
411
407
  this.runtimeDeps = parts.runtimeDeps;
412
408
  this.pipelineRef = parts.pipelineRef;
413
- this._manifestCacheKey = parts.manifestCacheKey;
414
409
  }
415
410
 
416
411
  /**
@@ -460,7 +455,6 @@ export class EmDashRuntime {
460
455
  this.enabledPlugins.delete(pluginId);
461
456
  await this.rebuildHookPipeline();
462
457
  }
463
- this.invalidateManifest();
464
458
  }
465
459
 
466
460
  /**
@@ -634,6 +628,13 @@ export class EmDashRuntime {
634
628
  // Initialize database (connects, runs migrations if needed)
635
629
  const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
636
630
 
631
+ // Validate EMDASH_ENCRYPTION_KEY once here so a malformed value
632
+ // surfaces in startup logs instead of as request-time 500s. The key
633
+ // itself is not yet consumed (a follow-up PR adds plugin-secret
634
+ // encryption); validating early just guards against silent
635
+ // misconfiguration.
636
+ await phase("rt.secrets", "Validate encryption key", () => validateEncryptionKeyAtStartup());
637
+
637
638
  // FTS verify/repair is deferred off the cold-start hot path.
638
639
  // See EmDashRuntime.ensureSearchHealthy().
639
640
 
@@ -697,7 +698,7 @@ export class EmDashRuntime {
697
698
  const devConsolePlugin = definePlugin({
698
699
  id: DEV_CONSOLE_EMAIL_PLUGIN_ID,
699
700
  version: "0.0.0",
700
- capabilities: ["email:provide"],
701
+ capabilities: ["hooks.email-transport:register"],
701
702
  hooks: {
702
703
  "email:deliver": {
703
704
  exclusive: true,
@@ -720,7 +721,7 @@ export class EmDashRuntime {
720
721
  const defaultModeratorPlugin = definePlugin({
721
722
  id: DEFAULT_COMMENT_MODERATOR_PLUGIN_ID,
722
723
  version: "0.0.0",
723
- capabilities: ["read:users"],
724
+ capabilities: ["users:read"],
724
725
  hooks: {
725
726
  "comment:moderate": {
726
727
  exclusive: true,
@@ -871,22 +872,6 @@ export class EmDashRuntime {
871
872
  }
872
873
  });
873
874
 
874
- // SHA of emdash commit + user config that affects the manifest.
875
- // COMMIT captures emdash code changes; plugin IDs/versions and i18n
876
- // capture user astro.config changes (e.g. upgrading a plugin package).
877
- // DB-driven changes (collections, fields, plugin toggle) go through
878
- // invalidateManifest(). Sorted for stability across nondeterministic
879
- // plugin ordering.
880
- const manifestCacheKey = await hashString(
881
- [
882
- COMMIT,
883
- ...deps.plugins.map((p) => `${p.id}@${p.version ?? ""}`).toSorted(),
884
- ...deps.sandboxedPluginEntries.map((e) => `${e.id}@${e.version}`).toSorted(),
885
- virtualConfig?.i18n?.defaultLocale ?? "",
886
- (virtualConfig?.i18n?.locales ?? []).toSorted().join(","),
887
- ].join("|"),
888
- );
889
-
890
875
  return new EmDashRuntime({
891
876
  db,
892
877
  storage,
@@ -906,7 +891,6 @@ export class EmDashRuntime {
906
891
  pipelineFactoryOptions,
907
892
  runtimeDeps: deps,
908
893
  pipelineRef,
909
- manifestCacheKey,
910
894
  });
911
895
  }
912
896
 
@@ -983,18 +967,15 @@ export class EmDashRuntime {
983
967
  const dialect = deps.createDialect(dbConfig.config);
984
968
  const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
985
969
 
986
- const { applied } = await runMigrations(db);
970
+ await runMigrations(db);
987
971
 
988
- // If migrations were applied, the schema changed — clear the
989
- // DB-persisted manifest cache so getManifest() rebuilds it.
990
- if (applied.length > 0) {
991
- try {
992
- const options = new OptionsRepository(db);
993
- await options.delete("emdash:manifest_cache");
994
- } catch {
995
- // Non-fatal
996
- }
997
- }
972
+ // Note: legacy installs may carry a stray `emdash:manifest_cache`
973
+ // row in the options table from versions that persisted a JSON
974
+ // manifest. The runtime no longer reads or writes it. We do not
975
+ // proactively delete it: the row is a few hundred bytes of dead
976
+ // weight and is never on the read path, whereas a one-shot
977
+ // cleanup-flag check costs an extra `options.get()` on every
978
+ // isolate cold boot forever. Cheaper to leave it.
998
979
 
999
980
  // Auto-seed schema if no collections exist and setup hasn't run.
1000
981
  // This covers first-load on sites that skip the setup wizard.
@@ -1256,80 +1237,35 @@ export class EmDashRuntime {
1256
1237
  // =========================================================================
1257
1238
 
1258
1239
  /**
1259
- * Get the manifest, using an in-memory cache with a DB-persisted
1260
- * fallback for cold starts. Avoids N+1 schema registry queries
1261
- * on every request.
1240
+ * Build the admin manifest from the live database.
1262
1241
  *
1263
- * Cache is invalidated by invalidateManifest(), called from schema
1264
- * API routes, MCP server, plugin toggle, and taxonomy def changes.
1242
+ * Used by the admin UI (sidebar collections, content editor field
1243
+ * dispatch, manifest endpoint) and by WordPress import it's never
1244
+ * read on a public request, so this isn't on any anonymous hot path.
1245
+ *
1246
+ * No cross-request cache. The previous worker-isolate cache produced
1247
+ * a class of cross-isolate staleness bugs (#776, #873, #876, #877)
1248
+ * because Cloudflare Workers keeps multiple warm isolates per region
1249
+ * and there's no fan-out primitive to invalidate them in step. The
1250
+ * cache existed to amortize an N+1 schema query pattern; now that
1251
+ * `listCollectionsWithFields()` does the same work in two queries,
1252
+ * the rebuild is fast enough to pay on every admin request.
1253
+ *
1254
+ * Within a single request, `requestCached` deduplicates concurrent
1255
+ * callers (the manifest endpoint and an admin SSR template, say).
1265
1256
  */
1266
- async getManifest(): Promise<EmDashManifest> {
1267
- // When the DB is overridden by an isolated instance (playground /
1268
- // DO-preview sessions), bypass the module-scoped manifest cache —
1269
- // its schema may diverge from the configured DB. Plain D1 Sessions
1270
- // routing does NOT set `dbIsIsolated`, so the cache still applies.
1271
- if (getRequestContext()?.dbIsIsolated) {
1272
- return this._buildManifest();
1273
- }
1274
-
1275
- if (this._cachedManifest) return this._cachedManifest;
1276
-
1277
- // DB-persisted cache (1 query instead of N+1 rebuild on cold start).
1278
- // Keyed by SHA of commit + config to bust on deploys. DB-driven
1279
- // changes (collections, fields, plugins, taxonomies) go through
1280
- // invalidateManifest().
1281
- try {
1282
- const options = new OptionsRepository(this.db);
1283
- const cached = await options.get<{ key: string; manifest: EmDashManifest }>(
1284
- "emdash:manifest_cache",
1285
- );
1286
- if (cached && cached.key === this._manifestCacheKey && cached.manifest) {
1287
- this._cachedManifest = cached.manifest;
1288
- return cached.manifest;
1289
- }
1290
- } catch {
1291
- // Options table may not exist yet
1292
- }
1293
-
1294
- // Full rebuild, then persist. Track which promise is current so
1295
- // an invalidation during the build can't be overwritten.
1296
- if (!this._manifestPromise) {
1297
- let manifestPromise: Promise<EmDashManifest>;
1298
- const isCurrentLoad = () => this._manifestPromise === manifestPromise;
1299
- manifestPromise = this._loadManifest(isCurrentLoad);
1300
- this._manifestPromise = manifestPromise;
1301
- }
1302
- return this._manifestPromise;
1303
- }
1304
-
1305
- private async _loadManifest(isCurrentLoad: () => boolean): Promise<EmDashManifest> {
1306
- try {
1307
- const manifest = await this._buildManifest();
1308
-
1309
- if (isCurrentLoad()) {
1310
- this._cachedManifest = manifest;
1311
-
1312
- try {
1313
- const options = new OptionsRepository(this.db);
1314
- await options.set("emdash:manifest_cache", {
1315
- key: this._manifestCacheKey,
1316
- manifest,
1317
- });
1318
- } catch {
1319
- // Non-fatal — will just rebuild next time
1320
- }
1321
- }
1322
-
1323
- return manifest;
1324
- } finally {
1325
- if (isCurrentLoad()) {
1326
- this._manifestPromise = null;
1327
- }
1328
- }
1257
+ getManifest(): Promise<EmDashManifest> {
1258
+ return requestCached("emdash:manifest", () => this._buildManifest());
1329
1259
  }
1330
1260
 
1331
1261
  /**
1332
- * Build the manifest from database (N+1 collection queries).
1262
+ * Build the manifest from the database.
1263
+ *
1264
+ * Constant query shapes via `listCollectionsWithFields()` — one query
1265
+ * for collections, one batched query for fields (chunked at
1266
+ * `SQL_BATCH_SIZE` collection IDs to stay under D1's bound-parameter
1267
+ * limit). Typical sites stay well under the chunk threshold, so this
1268
+ * is two queries in practice; never N+1.
1333
1269
  */
1334
1270
  private async _buildManifest(): Promise<EmDashManifest> {
1335
1271
  // Build collections from database.
@@ -1338,9 +1274,8 @@ export class EmDashRuntime {
1338
1274
  const manifestCollections: Record<string, ManifestCollection> = {};
1339
1275
  try {
1340
1276
  const registry = new SchemaRegistry(this.db);
1341
- const dbCollections = await registry.listCollections();
1277
+ const dbCollections = await registry.listCollectionsWithFields();
1342
1278
  for (const collection of dbCollections) {
1343
- const collectionWithFields = await registry.getCollectionWithFields(collection.slug);
1344
1279
  const fields: Record<
1345
1280
  string,
1346
1281
  {
@@ -1355,34 +1290,32 @@ export class EmDashRuntime {
1355
1290
  }
1356
1291
  > = {};
1357
1292
 
1358
- if (collectionWithFields?.fields) {
1359
- for (const field of collectionWithFields.fields) {
1360
- const entry: (typeof fields)[string] = {
1361
- kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
1362
- label: field.label,
1363
- required: field.required,
1364
- };
1365
- if (field.widget) entry.widget = field.widget;
1366
- // Plugin field widgets read their per-field config from `field.options`,
1367
- // which the seed schema types as `Record<string, unknown>`. Pass it
1368
- // through to the manifest so plugin widgets in the admin SPA receive it.
1369
- if (field.options) {
1370
- entry.options = field.options;
1371
- }
1372
- // Legacy: select/multiSelect enum options live on `field.validation.options`.
1373
- // Wins over `field.options` to preserve existing behavior for enum widgets.
1374
- if (field.validation?.options) {
1375
- entry.options = field.validation.options.map((v) => ({
1376
- value: v,
1377
- label: v.charAt(0).toUpperCase() + v.slice(1),
1378
- }));
1379
- }
1380
- // Include full validation for repeater fields (subFields, minItems, maxItems)
1381
- if (field.type === "repeater" && field.validation) {
1382
- (entry as Record<string, unknown>).validation = field.validation;
1383
- }
1384
- fields[field.slug] = entry;
1293
+ for (const field of collection.fields) {
1294
+ const entry: (typeof fields)[string] = {
1295
+ kind: FIELD_TYPE_TO_KIND[field.type] ?? "string",
1296
+ label: field.label,
1297
+ required: field.required,
1298
+ };
1299
+ if (field.widget) entry.widget = field.widget;
1300
+ // Plugin field widgets read their per-field config from `field.options`,
1301
+ // which the seed schema types as `Record<string, unknown>`. Pass it
1302
+ // through to the manifest so plugin widgets in the admin SPA receive it.
1303
+ if (field.options) {
1304
+ entry.options = field.options;
1305
+ }
1306
+ // Legacy: select/multiSelect enum options live on `field.validation.options`.
1307
+ // Wins over `field.options` to preserve existing behavior for enum widgets.
1308
+ if (field.validation?.options) {
1309
+ entry.options = field.validation.options.map((v) => ({
1310
+ value: v,
1311
+ label: v.charAt(0).toUpperCase() + v.slice(1),
1312
+ }));
1313
+ }
1314
+ // Include full validation for repeater fields (subFields, minItems, maxItems)
1315
+ if (field.type === "repeater" && field.validation) {
1316
+ (entry as Record<string, unknown>).validation = field.validation;
1385
1317
  }
1318
+ fields[field.slug] = entry;
1386
1319
  }
1387
1320
 
1388
1321
  manifestCollections[collection.slug] = {
@@ -1419,6 +1352,7 @@ export class EmDashRuntime {
1419
1352
  description?: string;
1420
1353
  placeholder?: string;
1421
1354
  fields?: Element[];
1355
+ category?: string;
1422
1356
  }>;
1423
1357
  fieldWidgets?: Array<{
1424
1358
  name: string;
@@ -1555,27 +1489,6 @@ export class EmDashRuntime {
1555
1489
  };
1556
1490
  }
1557
1491
 
1558
- /**
1559
- * Invalidate cached data derived from the manifest/schema.
1560
- * Called when collections, fields, plugins, or taxonomy defs change.
1561
- */
1562
- invalidateManifest(): void {
1563
- this._cachedManifest = null;
1564
- this._manifestPromise = null;
1565
- invalidateUrlPatternCache();
1566
- // Delete DB-persisted cache so the next cold start rebuilds.
1567
- // Fire-and-forget: in-memory is already cleared for this worker,
1568
- // DB delete is best-effort for the next cold start.
1569
- try {
1570
- const options = new OptionsRepository(this.db);
1571
- options.delete("emdash:manifest_cache").catch((error) => {
1572
- console.error("Failed to delete persisted manifest cache", error);
1573
- });
1574
- } catch (error) {
1575
- console.error("Failed to initialize manifest cache invalidation", error);
1576
- }
1577
- }
1578
-
1579
1492
  /**
1580
1493
  * Verify and repair FTS indexes on demand. Runs at most once per worker
1581
1494
  * lifetime.
@@ -1779,6 +1692,14 @@ export class EmDashRuntime {
1779
1692
  status?: string;
1780
1693
  authorId?: string | null;
1781
1694
  bylines?: Array<{ bylineId: string; roleLabel?: string | null }>;
1695
+ seo?: {
1696
+ title?: string | null;
1697
+ description?: string | null;
1698
+ image?: string | null;
1699
+ canonical?: string | null;
1700
+ noIndex?: boolean;
1701
+ };
1702
+ publishedAt?: string | null;
1782
1703
  /** Skip revision creation (used by autosave) */
1783
1704
  skipRevision?: boolean;
1784
1705
  _rev?: string;
@@ -2005,8 +1926,12 @@ export class EmDashRuntime {
2005
1926
  // Publishing & Scheduling Handlers
2006
1927
  // =========================================================================
2007
1928
 
2008
- async handleContentPublish(collection: string, id: string) {
2009
- const result = await handleContentPublish(this.db, collection, id);
1929
+ async handleContentPublish(
1930
+ collection: string,
1931
+ id: string,
1932
+ options: { publishedAt?: string } = {},
1933
+ ) {
1934
+ const result = await handleContentPublish(this.db, collection, id, options);
2010
1935
 
2011
1936
  // Run afterPublish hooks (fire-and-forget)
2012
1937
  if (result.success && result.data) {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared locale-resolution helpers.
3
+ *
4
+ * Matches the pattern used by `query.ts` for content: an explicit locale wins,
5
+ * otherwise we fall back to the request-context locale, otherwise to
6
+ * `defaultLocale` when i18n is enabled, otherwise to `undefined` (meaning "do
7
+ * not filter by locale" — legacy single-locale behaviour).
8
+ */
9
+
10
+ import { getRequestContext } from "../request-context.js";
11
+ import { getFallbackChain, getI18nConfig, isI18nEnabled } from "./config.js";
12
+
13
+ /**
14
+ * Resolve the locale to use for a query given an optional explicit value.
15
+ * Returns `undefined` when no locale information is available; callers should
16
+ * treat that as "do not filter by locale".
17
+ */
18
+ export function resolveLocale(explicit?: string): string | undefined {
19
+ if (explicit !== undefined) return explicit;
20
+ const ctxLocale = getRequestContext()?.locale;
21
+ if (ctxLocale !== undefined) return ctxLocale;
22
+ const cfg = getI18nConfig();
23
+ if (cfg && isI18nEnabled()) return cfg.defaultLocale;
24
+ return undefined;
25
+ }
26
+
27
+ /**
28
+ * Fallback chain to try when looking up a single item. When i18n is disabled
29
+ * or the locale is unspecified, returns a single-element array (or empty when
30
+ * no locale resolves) so callers can iterate uniformly.
31
+ */
32
+ export function resolveLocaleChain(explicit?: string): string[] {
33
+ const locale = resolveLocale(explicit);
34
+ if (locale === undefined) return [];
35
+ if (!isI18nEnabled()) return [locale];
36
+ return getFallbackChain(locale);
37
+ }
package/src/index.ts CHANGED
@@ -262,6 +262,15 @@ export type {
262
262
  SerializedRequest,
263
263
  } from "./plugins/index.js";
264
264
 
265
+ // Capability normalization (legacy → canonical alias layer)
266
+ export {
267
+ CAPABILITY_RENAMES,
268
+ isDeprecatedCapability,
269
+ normalizeCapability,
270
+ normalizeCapabilities,
271
+ } from "./plugins/index.js";
272
+ export type { CurrentPluginCapability, DeprecatedPluginCapability } from "./plugins/index.js";
273
+
265
274
  // Plugin descriptor (for astro.config.mjs)
266
275
  export type { PluginDescriptor } from "./astro/integration/runtime.js";
267
276
 
package/src/loader.ts CHANGED
@@ -115,12 +115,69 @@ const INCLUDE_IN_DATA: Record<string, string> = {
115
115
  /** System date columns that should be converted to Date objects */
116
116
  const DATE_COLUMNS = new Set(["created_at", "updated_at", "published_at", "scheduled_at"]);
117
117
 
118
+ /**
119
+ * Hidden, symbol-keyed property on each mapped data record carrying the raw
120
+ * DB string for every date column. Lets cursor encoders downstream reproduce
121
+ * the loader's exact `nextCursor` format without round-tripping through
122
+ * `new Date()`, which loses precision for stored values that aren't already
123
+ * ISO-with-milliseconds (e.g. `2026-01-01T00:00:00Z` becomes
124
+ * `2026-01-01T00:00:00.000Z`).
125
+ */
126
+ export const CURSOR_RAW_VALUES: unique symbol = Symbol("emdash:cursorRawValues");
127
+
128
+ const LOCAL_MEDIA_FILE_PREFIX = "/_emdash/api/media/file/";
129
+ const URL_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
130
+
118
131
  /** Safely extract a string value from a record, returning fallback if not a string */
119
132
  function rowStr(row: Record<string, unknown>, key: string, fallback = ""): string {
120
133
  const val = row[key];
121
134
  return typeof val === "string" ? val : fallback;
122
135
  }
123
136
 
137
+ function isRecord(value: unknown): value is Record<string, unknown> {
138
+ return typeof value === "object" && value !== null && !Array.isArray(value);
139
+ }
140
+
141
+ function isBareMediaKey(src: string): boolean {
142
+ return !src.startsWith("/") && !URL_SCHEME_PATTERN.test(src);
143
+ }
144
+
145
+ function normalizeLocalMediaValue(value: unknown): unknown {
146
+ if (Array.isArray(value)) {
147
+ return value.map(normalizeLocalMediaValue);
148
+ }
149
+
150
+ if (!isRecord(value)) {
151
+ return value;
152
+ }
153
+
154
+ const normalized: Record<string, unknown> = {};
155
+ for (const [key, child] of Object.entries(value)) {
156
+ normalized[key] = normalizeLocalMediaValue(child);
157
+ }
158
+
159
+ if (
160
+ normalized.provider === "local" &&
161
+ typeof normalized.src === "string" &&
162
+ normalized.src.length > 0
163
+ ) {
164
+ const src = normalized.src;
165
+ if (src.startsWith(LOCAL_MEDIA_FILE_PREFIX)) {
166
+ const id = src.slice(LOCAL_MEDIA_FILE_PREFIX.length);
167
+ if (!normalized.id && id) {
168
+ normalized.id = id;
169
+ }
170
+ } else if (isBareMediaKey(src)) {
171
+ if (!normalized.id) {
172
+ normalized.id = src;
173
+ }
174
+ normalized.src = `${LOCAL_MEDIA_FILE_PREFIX}${src}`;
175
+ }
176
+ }
177
+
178
+ return normalized;
179
+ }
180
+
124
181
  /**
125
182
  * Map a database row to entry data
126
183
  * Extracts content fields (non-system columns) and parses JSON where needed.
@@ -128,13 +185,19 @@ function rowStr(row: Record<string, unknown>, key: string, fallback = ""): strin
128
185
  */
129
186
  function mapRowToData(row: Record<string, unknown>): Record<string, unknown> {
130
187
  const data: Record<string, unknown> = {};
188
+ const rawDateValues: Record<string, string> = {};
131
189
 
132
190
  for (const [key, value] of Object.entries(row)) {
133
191
  // Include certain system columns (mapped to camelCase where needed)
134
192
  if (key in INCLUDE_IN_DATA) {
135
193
  // Convert date columns from ISO strings to Date objects
136
194
  if (DATE_COLUMNS.has(key)) {
137
- data[INCLUDE_IN_DATA[key]] = typeof value === "string" ? new Date(value) : null;
195
+ if (typeof value === "string") {
196
+ rawDateValues[key] = value;
197
+ data[INCLUDE_IN_DATA[key]] = new Date(value);
198
+ } else {
199
+ data[INCLUDE_IN_DATA[key]] = null;
200
+ }
138
201
  } else {
139
202
  data[INCLUDE_IN_DATA[key]] = value;
140
203
  }
@@ -148,7 +211,7 @@ function mapRowToData(row: Record<string, unknown>): Record<string, unknown> {
148
211
  try {
149
212
  // Only parse if it looks like JSON (starts with { or [)
150
213
  if (value.startsWith("{") || value.startsWith("[")) {
151
- data[key] = JSON.parse(value);
214
+ data[key] = normalizeLocalMediaValue(JSON.parse(value));
152
215
  } else {
153
216
  data[key] = value;
154
217
  }
@@ -160,6 +223,13 @@ function mapRowToData(row: Record<string, unknown>): Record<string, unknown> {
160
223
  }
161
224
  }
162
225
 
226
+ Object.defineProperty(data, CURSOR_RAW_VALUES, {
227
+ value: rawDateValues,
228
+ enumerable: false,
229
+ configurable: false,
230
+ writable: false,
231
+ });
232
+
163
233
  return data;
164
234
  }
165
235
 
@@ -171,7 +241,7 @@ function mapRevisionData(data: Record<string, unknown>): Record<string, unknown>
171
241
  const result: Record<string, unknown> = {};
172
242
  for (const [key, value] of Object.entries(data)) {
173
243
  if (key.startsWith("_")) continue; // revision metadata
174
- result[key] = value;
244
+ result[key] = normalizeLocalMediaValue(value);
175
245
  }
176
246
  return result;
177
247
  }