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,7 +1,11 @@
1
1
  /**
2
- * Navigation menu runtime functions
2
+ * Navigation menu runtime functions.
3
3
  *
4
- * These are called from templates to query menus and resolve URLs.
4
+ * These are called from templates to query menus and resolve URLs. All queries
5
+ * are locale-aware: when a locale is configured (or passed explicitly) items
6
+ * are filtered to that locale, and menu item references resolve against the
7
+ * referenced content's translation_group so the URL points at the right
8
+ * per-locale row.
5
9
  */
6
10
 
7
11
  import type { Kysely } from "kysely";
@@ -9,50 +13,61 @@ import { sql } from "kysely";
9
13
 
10
14
  import type { Database } from "../database/types.js";
11
15
  import { validateIdentifier } from "../database/validate.js";
16
+ import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
12
17
  import { getDb } from "../loader.js";
13
18
  import { requestCached } from "../request-cache.js";
14
19
  import { sanitizeHref } from "../utils/url.js";
15
20
  import type { Menu, MenuItem, MenuItemRow } from "./types.js";
16
21
 
22
+ export interface MenuQueryOptions {
23
+ /** Override the locale used for the lookup. When omitted, the locale comes
24
+ * from the request context or the configured defaultLocale. */
25
+ locale?: string;
26
+ }
27
+
17
28
  /**
18
- * Get menu by name with resolved URLs
29
+ * Get a menu by name with resolved URLs.
19
30
  *
20
31
  * @example
21
32
  * ```ts
22
- * import { getMenu } from "emdash";
23
- *
24
33
  * const menu = await getMenu("primary");
25
- * if (menu) {
26
- * console.log(menu.items); // Array of MenuItem with resolved URLs
27
- * }
34
+ * const menuEs = await getMenu("primary", { locale: "es" });
28
35
  * ```
29
36
  */
30
- export function getMenu(name: string): Promise<Menu | null> {
31
- return requestCached(`menu:${name}`, async () => {
37
+ export function getMenu(name: string, options: MenuQueryOptions = {}): Promise<Menu | null> {
38
+ const locale = resolveLocale(options.locale);
39
+ return requestCached(`menu:${name}:${locale ?? "*"}`, async () => {
32
40
  const db = await getDb();
33
- return getMenuWithDb(name, db);
41
+ return getMenuWithDb(name, db, { locale });
34
42
  });
35
43
  }
36
44
 
37
45
  /**
38
- * Get menu by name with resolved URLs (with explicit db)
39
- *
40
- * @internal Use `getMenu()` in templates. This variant is for admin routes
41
- * that already have a database handle.
46
+ * Get menu by name with resolved URLs (with explicit db). Internal helper for
47
+ * admin routes that already have a database handle.
42
48
  */
43
- export async function getMenuWithDb(name: string, db: Kysely<Database>): Promise<Menu | null> {
44
- // Get menu
45
- const menuRow = await db
46
- .selectFrom("_emdash_menus")
47
- .selectAll()
48
- .where("name", "=", name)
49
- .executeTakeFirst();
50
-
51
- if (!menuRow) {
52
- return null;
49
+ export async function getMenuWithDb(
50
+ name: string,
51
+ db: Kysely<Database>,
52
+ options: MenuQueryOptions = {},
53
+ ): Promise<Menu | null> {
54
+ const chain = resolveLocaleChain(options.locale);
55
+
56
+ const selectMenu = () => db.selectFrom("_emdash_menus").selectAll().where("name", "=", name);
57
+
58
+ let menuRow: Awaited<ReturnType<ReturnType<typeof selectMenu>["executeTakeFirst"]>>;
59
+ if (chain.length === 0) {
60
+ menuRow = await selectMenu().orderBy("locale", "asc").executeTakeFirst();
61
+ } else {
62
+ menuRow = undefined;
63
+ for (const locale of chain) {
64
+ menuRow = await selectMenu().where("locale", "=", locale).executeTakeFirst();
65
+ if (menuRow) break;
66
+ }
53
67
  }
54
68
 
55
- // Get all menu items
69
+ if (!menuRow) return null;
70
+
56
71
  const itemRows = await db
57
72
  .selectFrom("_emdash_menu_items")
58
73
  .selectAll()
@@ -61,31 +76,27 @@ export async function getMenuWithDb(name: string, db: Kysely<Database>): Promise
61
76
  .orderBy("sort_order", "asc")
62
77
  .execute();
63
78
 
64
- // Resolve URLs and build tree
65
- const items = await buildMenuTree(itemRows, db);
79
+ const items = await buildMenuTree(itemRows, db, menuRow.locale);
66
80
 
67
81
  return {
68
82
  id: menuRow.id,
69
83
  name: menuRow.name,
70
84
  label: menuRow.label,
71
85
  items,
86
+ locale: menuRow.locale,
87
+ translationGroup: menuRow.translation_group,
72
88
  };
73
89
  }
74
90
 
75
91
  /**
76
- * Get all menus (without items - for admin list)
77
- *
78
- * @example
79
- * ```ts
80
- * import { getMenus } from "emdash";
81
- *
82
- * const menus = await getMenus();
83
- * console.log(menus); // [{ id, name, label }]
84
- * ```
92
+ * Get all menus (without items, locale-filtered for admin list / site nav
93
+ * summaries). When no locale is configured, returns menus across all locales.
85
94
  */
86
- export async function getMenus(): Promise<Array<{ id: string; name: string; label: string }>> {
95
+ export async function getMenus(
96
+ options: MenuQueryOptions = {},
97
+ ): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
87
98
  const db = await getDb();
88
- return getMenusWithDb(db);
99
+ return getMenusWithDb(db, options);
89
100
  }
90
101
 
91
102
  /**
@@ -96,26 +107,30 @@ export async function getMenus(): Promise<Array<{ id: string; name: string; labe
96
107
  */
97
108
  export async function getMenusWithDb(
98
109
  db: Kysely<Database>,
99
- ): Promise<Array<{ id: string; name: string; label: string }>> {
100
- const rows = await db
110
+ options: MenuQueryOptions = {},
111
+ ): Promise<Array<{ id: string; name: string; label: string; locale: string }>> {
112
+ const locale = resolveLocale(options.locale);
113
+ let query = db
101
114
  .selectFrom("_emdash_menus")
102
- .select(["id", "name", "label"])
103
- .orderBy("name", "asc")
104
- .execute();
105
-
106
- return rows;
115
+ .select(["id", "name", "label", "locale"])
116
+ .orderBy("name", "asc");
117
+ if (locale !== undefined) query = query.where("locale", "=", locale);
118
+ return query.execute();
107
119
  }
108
120
 
109
121
  /**
110
- * Build hierarchical menu tree from flat array of items
122
+ * Build a hierarchical menu tree from a flat list of items. Items are
123
+ * resolved against the given `locale` so references land on the right
124
+ * per-locale content rows.
111
125
  */
112
- async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promise<MenuItem[]> {
113
- // Pre-load URL patterns for all collections referenced in this menu
126
+ async function buildMenuTree(
127
+ items: MenuItemRow[],
128
+ db: Kysely<Database>,
129
+ locale: string,
130
+ ): Promise<MenuItem[]> {
114
131
  const collectionSlugs = new Set<string>();
115
132
  for (const item of items) {
116
- if (item.reference_collection) {
117
- collectionSlugs.add(item.reference_collection);
118
- }
133
+ if (item.reference_collection) collectionSlugs.add(item.reference_collection);
119
134
  if (item.type === "page" || item.type === "post") {
120
135
  collectionSlugs.add(item.reference_collection || `${item.type}s`);
121
136
  }
@@ -128,41 +143,28 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
128
143
  .select(["slug", "url_pattern"])
129
144
  .where("slug", "in", [...collectionSlugs])
130
145
  .execute();
131
- for (const row of rows) {
132
- urlPatterns.set(row.slug, row.url_pattern);
133
- }
146
+ for (const row of rows) urlPatterns.set(row.slug, row.url_pattern);
134
147
  }
135
148
 
136
- // Resolve all URLs first
137
149
  const resolvedItems = await Promise.all(
138
- items.map((item) => resolveMenuItem(item, db, urlPatterns)),
150
+ items.map((item) => resolveMenuItem(item, db, urlPatterns, locale)),
139
151
  );
152
+ const validItems = resolvedItems.filter((item): item is MenuItem => item !== null);
140
153
 
141
- // Filter out items that couldn't be resolved (e.g., deleted content)
142
- const validItems = resolvedItems.filter((item) => item !== null);
143
-
144
- // Build tree structure
145
154
  const itemMap = new Map<string, MenuItem & { children: MenuItem[] }>();
146
155
  const rootItems: MenuItem[] = [];
147
156
 
148
- // First pass: create all items
149
157
  for (const item of validItems) {
150
158
  itemMap.set(item.id, { ...item, children: [] });
151
159
  }
152
160
 
153
- // Second pass: build parent-child relationships
154
161
  for (const item of items) {
155
162
  const menuItem = itemMap.get(item.id);
156
163
  if (!menuItem) continue;
157
-
158
164
  if (item.parent_id) {
159
165
  const parent = itemMap.get(item.parent_id);
160
- if (parent) {
161
- parent.children.push(menuItem);
162
- } else {
163
- // Parent not found, treat as root
164
- rootItems.push(menuItem);
165
- }
166
+ if (parent) parent.children.push(menuItem);
167
+ else rootItems.push(menuItem);
166
168
  } else {
167
169
  rootItems.push(menuItem);
168
170
  }
@@ -172,14 +174,15 @@ async function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promis
172
174
  }
173
175
 
174
176
  /**
175
- * Resolve a single menu item's URL
176
- *
177
- * Returns null if the referenced content no longer exists (item should be skipped)
177
+ * Resolve a single menu item's URL. `reference_id` is a translation_group
178
+ * (migration 036 remapped all existing references); we join it against
179
+ * the per-locale ec_* row or per-locale taxonomy row.
178
180
  */
179
181
  async function resolveMenuItem(
180
182
  item: MenuItemRow,
181
183
  db: Kysely<Database>,
182
184
  urlPatterns: Map<string, string | null>,
185
+ locale: string,
183
186
  ): Promise<MenuItem | null> {
184
187
  let url: string | null;
185
188
 
@@ -192,24 +195,18 @@ async function resolveMenuItem(
192
195
  case "page":
193
196
  case "post":
194
197
  url = await resolveContentUrl(
195
- // Default to plural collection name (pages/posts) if not specified
196
198
  item.reference_collection || `${item.type}s`,
197
199
  item.reference_id,
198
200
  db,
199
201
  urlPatterns,
202
+ locale,
200
203
  );
201
- // Skip items where content no longer exists
202
- if (url === null) {
203
- return null;
204
- }
204
+ if (url === null) return null;
205
205
  break;
206
206
 
207
207
  case "taxonomy":
208
- url = await resolveTaxonomyUrl(item.reference_id, db);
209
- // Skip items where taxonomy no longer exists
210
- if (url === null) {
211
- return null;
212
- }
208
+ url = await resolveTaxonomyUrl(item.reference_id, db, locale);
209
+ if (url === null) return null;
213
210
  break;
214
211
 
215
212
  case "collection":
@@ -223,16 +220,14 @@ async function resolveMenuItem(
223
220
  item.reference_id,
224
221
  db,
225
222
  urlPatterns,
223
+ locale,
226
224
  );
227
- if (url === null) {
228
- return null;
229
- }
225
+ if (url === null) return null;
230
226
  } else {
231
227
  url = "#";
232
228
  }
233
229
  }
234
230
  } catch (error) {
235
- // If resolution fails, skip this item
236
231
  console.error(`Failed to resolve menu item ${item.id}:`, error);
237
232
  return null;
238
233
  }
@@ -244,7 +239,7 @@ async function resolveMenuItem(
244
239
  target: item.target || undefined,
245
240
  titleAttr: item.title_attr || undefined,
246
241
  cssClasses: item.css_classes || undefined,
247
- children: [], // Will be populated by buildMenuTree
242
+ children: [],
248
243
  };
249
244
  }
250
245
 
@@ -261,72 +256,96 @@ function interpolateUrlPattern(pattern: string, slug: string, id: string): strin
261
256
  }
262
257
 
263
258
  /**
264
- * Resolve URL for a content entry (page/post)
265
- *
266
- * Uses the collection's url_pattern if set, otherwise falls back to /{collection}/{slug}.
267
- * Returns null if content not found (item should be skipped).
259
+ * Resolve the URL for a content reference. `referenceGroup` is the content
260
+ * row's translation_group; we look up the row in the requested locale
261
+ * (falling back to the source if no translation exists so the menu link is
262
+ * still clickable).
268
263
  */
269
264
  async function resolveContentUrl(
270
265
  collection: string,
271
- entryId: string | null,
266
+ referenceGroup: string | null,
272
267
  db: Kysely<Database>,
273
268
  urlPatterns: Map<string, string | null>,
269
+ locale: string,
274
270
  ): Promise<string | null> {
275
- if (!entryId) {
276
- return null;
277
- }
271
+ if (!referenceGroup) return null;
278
272
 
279
273
  try {
280
- // Validate collection name before interpolating into table reference
281
274
  validateIdentifier(collection, "menu item collection");
282
275
 
283
- // Dynamic content tables (ec_*) aren't in the Database type, so use sql
284
- const result = await sql<{ slug: string }>`
285
- SELECT slug FROM ${sql.ref(`ec_${collection}`)} WHERE id = ${entryId} LIMIT 1
276
+ // Try the requested locale first, then any locale (deterministic).
277
+ let result = await sql<{ id: string; slug: string }>`
278
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
279
+ WHERE translation_group = ${referenceGroup} AND locale = ${locale}
280
+ LIMIT 1
286
281
  `.execute(db);
287
-
288
- const row = result.rows[0];
289
- if (row) {
290
- const pattern = urlPatterns.get(collection);
291
- if (pattern) {
292
- return interpolateUrlPattern(pattern, row.slug, entryId);
293
- }
294
- return `/${collection}/${row.slug}`;
282
+ let row = result.rows[0];
283
+ if (!row) {
284
+ result = await sql<{ id: string; slug: string }>`
285
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
286
+ WHERE translation_group = ${referenceGroup}
287
+ ORDER BY locale ASC LIMIT 1
288
+ `.execute(db);
289
+ row = result.rows[0];
295
290
  }
291
+ if (!row) {
292
+ // Legacy rows whose reference_id still points at an id directly
293
+ // (defensive — migration 036 normalised these, but a row inserted
294
+ // between migrations could predate the remap).
295
+ const legacy = await sql<{ id: string; slug: string }>`
296
+ SELECT id, slug FROM ${sql.ref(`ec_${collection}`)}
297
+ WHERE id = ${referenceGroup} LIMIT 1
298
+ `.execute(db);
299
+ row = legacy.rows[0];
300
+ }
301
+ if (!row) return null;
296
302
 
297
- // Content not found, skip item
298
- return null;
303
+ const pattern = urlPatterns.get(collection);
304
+ if (pattern) return interpolateUrlPattern(pattern, row.slug, row.id);
305
+ return `/${collection}/${row.slug}`;
299
306
  } catch (error) {
300
- // Table might not exist or query failed
301
- console.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);
307
+ console.error(`Failed to resolve content URL for ${collection}/${referenceGroup}:`, error);
302
308
  return null;
303
309
  }
304
310
  }
305
311
 
306
312
  /**
307
- * Resolve URL for a taxonomy term
308
- *
309
- * Returns null if taxonomy not found (item should be skipped)
313
+ * Resolve URL for a taxonomy term reference. `referenceGroup` is the term's
314
+ * translation_group; we pick the row in the active locale (or fall back).
310
315
  */
311
316
  async function resolveTaxonomyUrl(
312
- taxonomyId: string | null,
317
+ referenceGroup: string | null,
313
318
  db: Kysely<Database>,
319
+ locale: string,
314
320
  ): Promise<string | null> {
315
- if (!taxonomyId) {
316
- return null;
317
- }
321
+ if (!referenceGroup) return null;
318
322
 
319
- const taxonomy = await db
323
+ let taxonomy = await db
320
324
  .selectFrom("taxonomies")
321
325
  .select(["name", "slug"])
322
- .where("id", "=", taxonomyId)
326
+ .where("translation_group", "=", referenceGroup)
327
+ .where("locale", "=", locale)
323
328
  .executeTakeFirst();
324
329
 
325
330
  if (!taxonomy) {
326
- // Taxonomy not found, skip item
327
- return null;
331
+ taxonomy = await db
332
+ .selectFrom("taxonomies")
333
+ .select(["name", "slug"])
334
+ .where("translation_group", "=", referenceGroup)
335
+ .orderBy("locale", "asc")
336
+ .executeTakeFirst();
328
337
  }
329
338
 
330
- // Use taxonomy name as base (e.g., "categories" or "tags")
339
+ if (!taxonomy) {
340
+ // Legacy: id-based reference that predates the migration remap.
341
+ taxonomy = await db
342
+ .selectFrom("taxonomies")
343
+ .select(["name", "slug"])
344
+ .where("id", "=", referenceGroup)
345
+ .executeTakeFirst();
346
+ }
347
+
348
+ if (!taxonomy) return null;
349
+
331
350
  return `/${taxonomy.name}/${taxonomy.slug}`;
332
351
  }
@@ -24,6 +24,8 @@ export interface Menu {
24
24
  name: string;
25
25
  label: string;
26
26
  items: MenuItem[];
27
+ locale: string;
28
+ translationGroup: string | null;
27
29
  }
28
30
 
29
31
  /**
@@ -36,13 +38,15 @@ export interface MenuItemRow {
36
38
  sort_order: number;
37
39
  type: MenuItemType;
38
40
  reference_collection: string | null;
39
- reference_id: string | null;
41
+ reference_id: string | null; // translation_group of referenced content/term
40
42
  custom_url: string | null;
41
43
  label: string;
42
44
  title_attr: string | null;
43
45
  target: string | null;
44
46
  css_classes: string | null;
45
47
  created_at: string;
48
+ locale: string;
49
+ translation_group: string | null;
46
50
  }
47
51
 
48
52
  /**
@@ -54,6 +58,8 @@ export interface MenuRow {
54
58
  label: string;
55
59
  created_at: string;
56
60
  updated_at: string;
61
+ locale: string;
62
+ translation_group: string | null;
57
63
  }
58
64
 
59
65
  /**
@@ -62,6 +68,11 @@ export interface MenuRow {
62
68
  export interface CreateMenuItemInput {
63
69
  type: MenuItemType;
64
70
  label: string;
71
+ /**
72
+ * Identifier of the referenced entity. For `reference_collection` items it is
73
+ * the content's translation_group (locale-agnostic); for `taxonomy` items it
74
+ * is the term's translation_group.
75
+ */
65
76
  referenceCollection?: string;
66
77
  referenceId?: string;
67
78
  customUrl?: string;
@@ -91,6 +102,9 @@ export interface UpdateMenuItemInput {
91
102
  export interface CreateMenuInput {
92
103
  name: string;
93
104
  label: string;
105
+ locale?: string;
106
+ /** When set, links the new menu into an existing translation_group. */
107
+ translationOf?: string;
94
108
  }
95
109
 
96
110
  /**
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Site identity head injection.
3
+ *
4
+ * Emits first-party `<head>` tags sourced from the user-configured Site
5
+ * Identity. These are rendered alongside, but separate from, the plugin
6
+ * contribution pipeline (`page/metadata.ts`) because:
7
+ *
8
+ * - Site identity is first-party, not plugin-supplied. The contribution
9
+ * pipeline's `isSafeHref` allowlist rejects same-origin paths like
10
+ * `/_emdash/api/media/file/...` (which is correct for sandboxed plugin
11
+ * contributions, but blocks our own favicon URLs).
12
+ * - The data shape is fixed and small. Routing it through a generic
13
+ * deduper buys nothing.
14
+ *
15
+ * Currently emits only `<link rel="icon">`. Other site-identity tags
16
+ * (`apple-touch-icon`, `theme-color`, `application-name`) need their own
17
+ * configurable fields in `SiteSettings` before they ship; emitting them
18
+ * automatically from the favicon would produce broken icons on iOS for
19
+ * SVG favicons or blurry home-screen icons when the favicon is a small
20
+ * PNG. Tracked separately.
21
+ *
22
+ * Templates that previously emitted their own `<link rel="icon">` are
23
+ * getting their lines dropped in the same change that introduced this
24
+ * helper.
25
+ */
26
+
27
+ import type { MediaReference } from "../settings/types.js";
28
+ import { escapeHtmlAttr } from "./metadata.js";
29
+
30
+ /**
31
+ * Subset of site settings consumed by `renderSiteIdentity`. Kept narrow
32
+ * so callers don't have to fetch fields they don't use.
33
+ */
34
+ export interface SiteIdentityInput {
35
+ favicon?: MediaReference;
36
+ }
37
+
38
+ /**
39
+ * Build the `<head>` HTML for site identity tags. Returns an empty string
40
+ * when no identity fields are configured.
41
+ */
42
+ export function renderSiteIdentity(input: SiteIdentityInput | undefined): string {
43
+ if (!input) return "";
44
+
45
+ const parts: string[] = [];
46
+
47
+ const favicon = input.favicon;
48
+ if (favicon?.url) {
49
+ let tag = `<link rel="icon" href="${escapeHtmlAttr(favicon.url)}"`;
50
+ if (favicon.contentType) {
51
+ tag += ` type="${escapeHtmlAttr(favicon.contentType)}"`;
52
+ }
53
+ tag += ">";
54
+ parts.push(tag);
55
+ }
56
+
57
+ return parts.join("\n");
58
+ }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { PluginDescriptor } from "../astro/integration/runtime.js";
14
14
  import { PLUGIN_CAPABILITIES, HOOK_NAMES } from "./manifest-schema.js";
15
+ import { normalizeCapabilities } from "./types.js";
15
16
  import type {
16
17
  StandardPluginDefinition,
17
18
  StandardHookEntry,
@@ -147,7 +148,10 @@ export function adaptSandboxEntry(
147
148
  }
148
149
 
149
150
  // Build capabilities from descriptor.
150
- // Validate against the known set (same as defineNativePlugin).
151
+ // Validate against the known set (same as defineNativePlugin). Both
152
+ // current and deprecated names are accepted; deprecated names are
153
+ // silently normalized to current names below so the runtime only ever
154
+ // sees the canonical form.
151
155
  const rawCapabilities = descriptor.capabilities ?? [];
152
156
  for (const cap of rawCapabilities) {
153
157
  if (!VALID_CAPABILITIES_SET.has(cap)) {
@@ -157,20 +161,28 @@ export function adaptSandboxEntry(
157
161
  );
158
162
  }
159
163
  }
160
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated against VALID_CAPABILITIES_SET above; descriptor uses string[] for flexibility
161
- const capabilities = [...rawCapabilities] as PluginCapability[];
164
+
165
+ // Silent normalization: rewrite deprecated names to current names.
166
+ // Safe assertion — `normalizeCapabilities` only emits validated input
167
+ // plus current names from the rename map, all of which are in the union.
168
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above; normalizeCapabilities only returns capabilities from the union
169
+ const capabilities = normalizeCapabilities(rawCapabilities) as PluginCapability[];
162
170
  const allowedHosts = descriptor.allowedHosts ?? [];
163
171
 
164
172
  // Capability implications: broader capabilities imply narrower ones
165
- // (mirrors the normalization in define-plugin.ts for native format)
166
- if (capabilities.includes("write:content") && !capabilities.includes("read:content")) {
167
- capabilities.push("read:content");
173
+ // (mirrors the normalization in define-plugin.ts for native format).
174
+ // Operates on canonical names only.
175
+ if (capabilities.includes("content:write") && !capabilities.includes("content:read")) {
176
+ capabilities.push("content:read");
168
177
  }
169
- if (capabilities.includes("write:media") && !capabilities.includes("read:media")) {
170
- capabilities.push("read:media");
178
+ if (capabilities.includes("media:write") && !capabilities.includes("media:read")) {
179
+ capabilities.push("media:read");
171
180
  }
172
- if (capabilities.includes("network:fetch:any") && !capabilities.includes("network:fetch")) {
173
- capabilities.push("network:fetch");
181
+ if (
182
+ capabilities.includes("network:request:unrestricted") &&
183
+ !capabilities.includes("network:request")
184
+ ) {
185
+ capabilities.push("network:request");
174
186
  }
175
187
 
176
188
  // Build storage config from descriptor.
@@ -647,14 +647,14 @@ export function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {
647
647
  }
648
648
 
649
649
  /**
650
- * Create blocked HTTP access (for plugins without network:fetch capability)
650
+ * Create blocked HTTP access (for plugins without network:request capability)
651
651
  */
652
652
  export function createBlockedHttpAccess(pluginId: string): HttpAccess {
653
653
  return {
654
654
  async fetch(): Promise<never> {
655
655
  throw new Error(
656
- `Plugin "${pluginId}" does not have the "network:fetch" capability. ` +
657
- `Add "network:fetch" to the plugin's capabilities to enable HTTP requests.`,
656
+ `Plugin "${pluginId}" does not have the "network:request" capability. ` +
657
+ `Add "network:request" to the plugin's capabilities to enable HTTP requests.`,
658
658
  );
659
659
  },
660
660
  };
@@ -902,32 +902,35 @@ export class PluginContextFactory {
902
902
  const storage = createStorageAccess(this.db, plugin.id, plugin.storage);
903
903
 
904
904
  // Capability-gated: content
905
+ // Note: capabilities reach this point already normalized to the
906
+ // canonical names by definePlugin / adaptSandboxEntry. Deprecated
907
+ // names ("read:content", "write:content") never appear here.
905
908
  let content: ContentAccess | ContentAccessWithWrite | undefined;
906
- if (capabilities.has("write:content")) {
909
+ if (capabilities.has("content:write")) {
907
910
  content = createContentAccessWithWrite(this.db);
908
- } else if (capabilities.has("read:content")) {
911
+ } else if (capabilities.has("content:read")) {
909
912
  content = createContentAccess(this.db);
910
913
  }
911
914
 
912
915
  // Capability-gated: media
913
916
  let media: MediaAccess | MediaAccessWithWrite | undefined;
914
- if (capabilities.has("write:media") && this.getUploadUrl) {
917
+ if (capabilities.has("media:write") && this.getUploadUrl) {
915
918
  media = createMediaAccessWithWrite(this.db, this.getUploadUrl, this.storage);
916
- } else if (capabilities.has("read:media")) {
919
+ } else if (capabilities.has("media:read")) {
917
920
  media = createMediaAccess(this.db);
918
921
  }
919
922
 
920
923
  // Capability-gated: http
921
924
  let http: HttpAccess | undefined;
922
- if (capabilities.has("network:fetch:any")) {
925
+ if (capabilities.has("network:request:unrestricted")) {
923
926
  http = createUnrestrictedHttpAccess(plugin.id);
924
- } else if (capabilities.has("network:fetch")) {
927
+ } else if (capabilities.has("network:request")) {
925
928
  http = createHttpAccess(plugin.id, plugin.allowedHosts);
926
929
  }
927
930
 
928
931
  // Capability-gated: users
929
932
  let users: UserAccess | undefined;
930
- if (capabilities.has("read:users")) {
933
+ if (capabilities.has("users:read")) {
931
934
  users = createUserAccess(this.db);
932
935
  }
933
936