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.
- package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
package/src/api/public-url.ts
CHANGED
|
@@ -29,9 +29,10 @@ interface SiteUrlConfig {
|
|
|
29
29
|
*/
|
|
30
30
|
let _envSiteUrl: string | undefined | null = null;
|
|
31
31
|
|
|
32
|
-
/** @internal Reset cached env
|
|
33
|
-
export function
|
|
32
|
+
/** @internal Reset cached env values — test-only. */
|
|
33
|
+
export function _resetEnvCache(): void {
|
|
34
34
|
_envSiteUrl = null;
|
|
35
|
+
_envAllowedOrigins = null;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function getEnvSiteUrl(): string | undefined {
|
|
@@ -74,6 +75,51 @@ export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
|
|
|
74
75
|
return config?.siteUrl || getEnvSiteUrl() || url.origin;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Resolve additional accepted passkey origins from runtime environment.
|
|
80
|
+
*
|
|
81
|
+
* Reads `EMDASH_ALLOWED_ORIGINS` (comma-separated list of origins) for
|
|
82
|
+
* multi-origin deployments where the same RP is reachable under several
|
|
83
|
+
* hostnames sharing the registrable parent domain (e.g. apex + preview).
|
|
84
|
+
*
|
|
85
|
+
* Each entry is parsed via `new URL()` and reduced to its `origin`. Unlike
|
|
86
|
+
* `getEnvSiteUrl` (which silently falls back to `url.origin` on bad input),
|
|
87
|
+
* this throws on any unparseable or non-http(s) entry — `EMDASH_ALLOWED_ORIGINS`
|
|
88
|
+
* is an allowlist for passkey verification, so silently dropping a typo would
|
|
89
|
+
* surface as "I can't authenticate on this origin" with no diagnostic. Fail
|
|
90
|
+
* loud at first read.
|
|
91
|
+
*
|
|
92
|
+
* Uses `process.env` (Vite leaves it untouched at runtime). Result is cached
|
|
93
|
+
* on success.
|
|
94
|
+
*/
|
|
95
|
+
let _envAllowedOrigins: string[] | null = null;
|
|
96
|
+
|
|
97
|
+
export function getEnvAllowedOrigins(): string[] {
|
|
98
|
+
if (_envAllowedOrigins !== null) return _envAllowedOrigins;
|
|
99
|
+
const raw = typeof process !== "undefined" ? process.env?.EMDASH_ALLOWED_ORIGINS || "" : "";
|
|
100
|
+
const parsed: string[] = [];
|
|
101
|
+
for (const entry of raw.split(",")) {
|
|
102
|
+
const trimmed = entry.trim();
|
|
103
|
+
if (!trimmed) continue;
|
|
104
|
+
let u: URL;
|
|
105
|
+
try {
|
|
106
|
+
u = new URL(trimmed);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(`EmDash config error in EMDASH_ALLOWED_ORIGINS: invalid URL: "${trimmed}"`, {
|
|
109
|
+
cause: e,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`EmDash config error in EMDASH_ALLOWED_ORIGINS: origin must be http or https: "${trimmed}" (got ${u.protocol})`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
parsed.push(u.origin);
|
|
118
|
+
}
|
|
119
|
+
_envAllowedOrigins = parsed;
|
|
120
|
+
return parsed;
|
|
121
|
+
}
|
|
122
|
+
|
|
77
123
|
/**
|
|
78
124
|
* Build a full public URL by appending a path to the public origin.
|
|
79
125
|
*
|
|
@@ -33,8 +33,8 @@ export const commentListQuery = z
|
|
|
33
33
|
status: z.enum(["pending", "approved", "spam", "trash"]).optional(),
|
|
34
34
|
collection: z.string().optional(),
|
|
35
35
|
search: z.string().optional(),
|
|
36
|
-
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
37
|
-
cursor: z.string().optional(),
|
|
36
|
+
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
37
|
+
cursor: z.string().max(2048).optional(),
|
|
38
38
|
})
|
|
39
39
|
.meta({ id: "CommentListQuery" });
|
|
40
40
|
|
|
@@ -59,6 +59,13 @@ export const localeCode = z
|
|
|
59
59
|
.regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code")
|
|
60
60
|
.transform((v) => v.toLowerCase());
|
|
61
61
|
|
|
62
|
+
/** Shared `?locale=xx` query shape for endpoints that filter by locale. */
|
|
63
|
+
export const localeFilterQuery = z
|
|
64
|
+
.object({
|
|
65
|
+
locale: z.string().min(1).optional(),
|
|
66
|
+
})
|
|
67
|
+
.meta({ id: "LocaleFilterQuery" });
|
|
68
|
+
|
|
62
69
|
// ---------------------------------------------------------------------------
|
|
63
70
|
// OpenAPI: Shared response schemas
|
|
64
71
|
// ---------------------------------------------------------------------------
|
|
@@ -72,6 +72,23 @@ export const contentScheduleBody = z
|
|
|
72
72
|
})
|
|
73
73
|
.meta({ id: "ContentScheduleBody" });
|
|
74
74
|
|
|
75
|
+
export const contentPublishBody = z
|
|
76
|
+
.object({
|
|
77
|
+
// .optional() rather than .nullish(): publishing has no semantic
|
|
78
|
+
// meaning for `null` (you can't "clear" a publish timestamp by
|
|
79
|
+
// publishing). Tightening the schema here means callers either
|
|
80
|
+
// pass a valid datetime or omit the field, and the route doesn't
|
|
81
|
+
// have to silently drop a null that snuck through.
|
|
82
|
+
publishedAt: z.iso
|
|
83
|
+
.datetime({ offset: true, message: "must be an ISO 8601 datetime" })
|
|
84
|
+
.optional()
|
|
85
|
+
.meta({
|
|
86
|
+
description:
|
|
87
|
+
"Optional ISO 8601 datetime to backdate the publish (e.g. when migrating content). Requires content:publish_any permission. Without this, existing published_at is preserved on re-publish.",
|
|
88
|
+
}),
|
|
89
|
+
})
|
|
90
|
+
.meta({ id: "ContentPublishBody" });
|
|
91
|
+
|
|
75
92
|
export const contentPreviewUrlBody = z
|
|
76
93
|
.object({
|
|
77
94
|
expiresIn: z.union([z.string(), z.number()]).optional(),
|
package/src/api/schemas/menus.ts
CHANGED
|
@@ -20,6 +20,10 @@ export const createMenuBody = z
|
|
|
20
20
|
.object({
|
|
21
21
|
name: z.string().min(1),
|
|
22
22
|
label: z.string().min(1),
|
|
23
|
+
locale: z.string().min(1).optional(),
|
|
24
|
+
/** When set, clones the items from the source menu. The new menu joins
|
|
25
|
+
* the source's translation_group. */
|
|
26
|
+
translationOf: z.string().min(1).optional(),
|
|
23
27
|
})
|
|
24
28
|
.meta({ id: "CreateMenuBody" });
|
|
25
29
|
|
|
@@ -87,6 +91,8 @@ export const menuSchema = z
|
|
|
87
91
|
label: z.string(),
|
|
88
92
|
created_at: z.string(),
|
|
89
93
|
updated_at: z.string(),
|
|
94
|
+
locale: z.string(),
|
|
95
|
+
translation_group: z.string().nullable(),
|
|
90
96
|
})
|
|
91
97
|
.meta({ id: "Menu" });
|
|
92
98
|
|
|
@@ -105,9 +111,26 @@ export const menuItemSchema = z
|
|
|
105
111
|
target: z.string().nullable(),
|
|
106
112
|
css_classes: z.string().nullable(),
|
|
107
113
|
created_at: z.string(),
|
|
114
|
+
locale: z.string(),
|
|
115
|
+
translation_group: z.string().nullable(),
|
|
108
116
|
})
|
|
109
117
|
.meta({ id: "MenuItem" });
|
|
110
118
|
|
|
119
|
+
export const menuTranslationsSchema = z
|
|
120
|
+
.object({
|
|
121
|
+
translationGroup: z.string().nullable(),
|
|
122
|
+
translations: z.array(
|
|
123
|
+
z.object({
|
|
124
|
+
id: z.string(),
|
|
125
|
+
name: z.string(),
|
|
126
|
+
label: z.string(),
|
|
127
|
+
locale: z.string(),
|
|
128
|
+
updatedAt: z.string(),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
})
|
|
132
|
+
.meta({ id: "MenuTranslations" });
|
|
133
|
+
|
|
111
134
|
export const menuListItemSchema = menuSchema
|
|
112
135
|
.extend({
|
|
113
136
|
itemCount: z.number().int(),
|
|
@@ -10,8 +10,8 @@ export const sectionsListQuery = z
|
|
|
10
10
|
.object({
|
|
11
11
|
source: sectionSource.optional(),
|
|
12
12
|
search: z.string().optional(),
|
|
13
|
-
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
14
|
-
cursor: z.string().optional(),
|
|
13
|
+
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
14
|
+
cursor: z.string().max(2048).optional(),
|
|
15
15
|
})
|
|
16
16
|
.meta({ id: "SectionsListQuery" });
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ export const createSectionBody = z
|
|
|
23
23
|
keywords: z.array(z.string()).optional(),
|
|
24
24
|
content: z.array(z.record(z.string(), z.unknown())),
|
|
25
25
|
previewMediaId: z.string().optional(),
|
|
26
|
-
source:
|
|
26
|
+
source: z.enum(["user", "import"]).optional(),
|
|
27
27
|
themeId: z.string().optional(),
|
|
28
28
|
})
|
|
29
29
|
.meta({ id: "CreateSectionBody" });
|
|
@@ -15,6 +15,7 @@ export const createTaxonomyDefBody = z
|
|
|
15
15
|
.max(63)
|
|
16
16
|
.regex(/^[a-z][a-z0-9_]*$/, "Name must be lowercase alphanumeric with underscores"),
|
|
17
17
|
label: z.string().min(1).max(200),
|
|
18
|
+
labelSingular: z.string().min(1).max(200).optional(),
|
|
18
19
|
hierarchical: z.boolean().optional().default(false),
|
|
19
20
|
collections: z
|
|
20
21
|
.array(
|
|
@@ -23,6 +24,8 @@ export const createTaxonomyDefBody = z
|
|
|
23
24
|
.max(100)
|
|
24
25
|
.optional()
|
|
25
26
|
.default([]),
|
|
27
|
+
locale: z.string().min(1).optional(),
|
|
28
|
+
translationOf: z.string().min(1).optional(),
|
|
26
29
|
})
|
|
27
30
|
.meta({ id: "CreateTaxonomyDefBody" });
|
|
28
31
|
|
|
@@ -36,6 +39,8 @@ export const createTermBody = z
|
|
|
36
39
|
label: z.string().min(1),
|
|
37
40
|
parentId: z.string().nullish(),
|
|
38
41
|
description: z.string().optional(),
|
|
42
|
+
locale: z.string().min(1).optional(),
|
|
43
|
+
translationOf: z.string().min(1).optional(),
|
|
39
44
|
})
|
|
40
45
|
.meta({ id: "CreateTermBody" });
|
|
41
46
|
|
|
@@ -60,9 +65,25 @@ export const taxonomyDefSchema = z
|
|
|
60
65
|
labelSingular: z.string().optional(),
|
|
61
66
|
hierarchical: z.boolean(),
|
|
62
67
|
collections: z.array(z.string()),
|
|
68
|
+
locale: z.string(),
|
|
69
|
+
translationGroup: z.string().nullable(),
|
|
63
70
|
})
|
|
64
71
|
.meta({ id: "TaxonomyDef" });
|
|
65
72
|
|
|
73
|
+
export const taxonomyDefTranslationsSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
translationGroup: z.string().nullable(),
|
|
76
|
+
translations: z.array(
|
|
77
|
+
z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
name: z.string(),
|
|
80
|
+
label: z.string(),
|
|
81
|
+
locale: z.string(),
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
})
|
|
85
|
+
.meta({ id: "TaxonomyDefTranslations" });
|
|
86
|
+
|
|
66
87
|
export const taxonomyListResponseSchema = z
|
|
67
88
|
.object({ taxonomies: z.array(taxonomyDefSchema) })
|
|
68
89
|
.meta({ id: "TaxonomyListResponse" });
|
|
@@ -75,9 +96,25 @@ export const termSchema = z
|
|
|
75
96
|
label: z.string(),
|
|
76
97
|
parentId: z.string().nullable(),
|
|
77
98
|
description: z.string().optional(),
|
|
99
|
+
locale: z.string(),
|
|
100
|
+
translationGroup: z.string().nullable(),
|
|
78
101
|
})
|
|
79
102
|
.meta({ id: "Term" });
|
|
80
103
|
|
|
104
|
+
export const termTranslationsSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
translationGroup: z.string().nullable(),
|
|
107
|
+
translations: z.array(
|
|
108
|
+
z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
slug: z.string(),
|
|
111
|
+
label: z.string(),
|
|
112
|
+
locale: z.string(),
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
})
|
|
116
|
+
.meta({ id: "TermTranslations" });
|
|
117
|
+
|
|
81
118
|
export const termWithCountSchema: z.ZodType = z
|
|
82
119
|
.object({
|
|
83
120
|
id: z.string(),
|
|
@@ -88,6 +125,8 @@ export const termWithCountSchema: z.ZodType = z
|
|
|
88
125
|
description: z.string().optional(),
|
|
89
126
|
count: z.number().int(),
|
|
90
127
|
children: z.array(z.lazy(() => termWithCountSchema)),
|
|
128
|
+
locale: z.string(),
|
|
129
|
+
translationGroup: z.string().nullable(),
|
|
91
130
|
})
|
|
92
131
|
.meta({ id: "TermWithCount" });
|
|
93
132
|
|
package/src/api/schemas/users.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const usersListQuery = z
|
|
|
10
10
|
.object({
|
|
11
11
|
search: z.string().optional(),
|
|
12
12
|
role: z.string().optional(),
|
|
13
|
-
cursor: z.string().optional(),
|
|
13
|
+
cursor: z.string().max(2048).optional(),
|
|
14
14
|
limit: z.coerce.number().int().min(1).max(100).optional().default(50),
|
|
15
15
|
})
|
|
16
16
|
.meta({ id: "UsersListQuery" });
|
package/src/api/types.ts
CHANGED
|
@@ -51,7 +51,11 @@ export interface FieldDescriptor {
|
|
|
51
51
|
kind: string;
|
|
52
52
|
label?: string;
|
|
53
53
|
required?: boolean;
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* For `select` / `multiSelect`: the list of enum choices.
|
|
56
|
+
* For `json` fields driven by a plugin `widget`: arbitrary widget config.
|
|
57
|
+
*/
|
|
58
|
+
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { AstroIntegration, AstroIntegrationLogger } from "astro";
|
|
14
14
|
|
|
15
|
+
import { validateAllowedOrigins, validateOriginShape } from "../../auth/allowed-origins.js";
|
|
15
16
|
import type { ResolvedPlugin } from "../../plugins/types.js";
|
|
16
17
|
import { local } from "../storage/adapters.js";
|
|
17
18
|
import { notoSans } from "./font-provider.js";
|
|
@@ -117,6 +118,22 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
// Validate config.allowedOrigins shape at startup (per-entry rules: parseable,
|
|
122
|
+
// http(s), no trailing dots, no empty labels). The siteUrl-dependent rules
|
|
123
|
+
// (Rule A: requires siteUrl; Rule B: must be a subdomain of siteUrl) are
|
|
124
|
+
// deferred to runtime when config.siteUrl is absent — EMDASH_SITE_URL may
|
|
125
|
+
// supply it post-build, just like the env-var fallback for siteUrl above.
|
|
126
|
+
// When config.siteUrl IS present, run the full validator here for fail-fast.
|
|
127
|
+
if (resolvedConfig.allowedOrigins?.length) {
|
|
128
|
+
const tagged = resolvedConfig.allowedOrigins.map((origin) => ({
|
|
129
|
+
origin,
|
|
130
|
+
source: "config.allowedOrigins" as const,
|
|
131
|
+
}));
|
|
132
|
+
resolvedConfig.allowedOrigins = resolvedConfig.siteUrl
|
|
133
|
+
? validateAllowedOrigins(resolvedConfig.siteUrl, tagged)
|
|
134
|
+
: validateOriginShape(tagged);
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
// Plugin descriptors from config
|
|
121
138
|
const pluginDescriptors = resolvedConfig.plugins ?? [];
|
|
122
139
|
const sandboxedDescriptors = resolvedConfig.sandboxed ?? [];
|
|
@@ -313,6 +313,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
313
313
|
entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug].ts"),
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
injectRoute({
|
|
317
|
+
pattern: "/_emdash/api/taxonomies/[name]/terms/[slug]/translations",
|
|
318
|
+
entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug]/translations.ts"),
|
|
319
|
+
});
|
|
320
|
+
|
|
316
321
|
injectRoute({
|
|
317
322
|
pattern: "/_emdash/api/content/[collection]/[id]/terms/[taxonomy]",
|
|
318
323
|
entrypoint: resolveRoute("api/content/[collection]/[id]/terms/[taxonomy].ts"),
|
|
@@ -555,6 +560,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
555
560
|
entrypoint: resolveRoute("api/menus/[name]/reorder.ts"),
|
|
556
561
|
});
|
|
557
562
|
|
|
563
|
+
injectRoute({
|
|
564
|
+
pattern: "/_emdash/api/menus/[name]/translations",
|
|
565
|
+
entrypoint: resolveRoute("api/menus/[name]/translations.ts"),
|
|
566
|
+
});
|
|
567
|
+
|
|
558
568
|
// Widget area routes
|
|
559
569
|
injectRoute({
|
|
560
570
|
pattern: "/_emdash/api/widget-areas",
|
|
@@ -303,6 +303,36 @@ export interface EmDashConfig {
|
|
|
303
303
|
siteUrl?: string;
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
|
+
* Additional origins accepted by passkey verification.
|
|
307
|
+
*
|
|
308
|
+
* When the same EmDash deployment is reachable under several hostnames sharing
|
|
309
|
+
* a registrable parent (e.g. `https://example.com` plus
|
|
310
|
+
* `https://preview.example.com`), the canonical `siteUrl` defines the `rpId`
|
|
311
|
+
* and the entries here are the *additional* origins from which assertions
|
|
312
|
+
* are accepted. Each entry must be the same hostname as `siteUrl` or a
|
|
313
|
+
* subdomain of it — WebAuthn requires `rpId` to be a registrable suffix of
|
|
314
|
+
* every origin.
|
|
315
|
+
*
|
|
316
|
+
* Merged at runtime with the `EMDASH_ALLOWED_ORIGINS` env var (comma-separated).
|
|
317
|
+
* Validation:
|
|
318
|
+
* - Config-declared entries are shape-checked at Astro startup.
|
|
319
|
+
* - Subdomain relationship to `siteUrl` is checked at startup when
|
|
320
|
+
* `siteUrl` is also config-declared, otherwise at first passkey
|
|
321
|
+
* verification (since `siteUrl` may come from `EMDASH_SITE_URL`).
|
|
322
|
+
*
|
|
323
|
+
* Mismatches throw with a source-attributed message naming
|
|
324
|
+
* `config.allowedOrigins` or `EMDASH_ALLOWED_ORIGINS`.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* emdash({
|
|
329
|
+
* siteUrl: "https://example.com",
|
|
330
|
+
* allowedOrigins: ["https://preview.example.com"],
|
|
331
|
+
* })
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
allowedOrigins?: string[];
|
|
335
|
+
/*
|
|
306
336
|
* Headers to trust for client IP resolution when running behind a reverse
|
|
307
337
|
* proxy. The first header in this list that is present on the request
|
|
308
338
|
* wins. Applies to rate limiting for auth endpoints and comment
|
|
@@ -401,9 +401,20 @@ export function generateWaitUntilModule(adapterName: string | undefined): string
|
|
|
401
401
|
* Reads the user's seed file at build time (in Node context) and embeds it,
|
|
402
402
|
* so the runtime doesn't need filesystem access (required for workerd).
|
|
403
403
|
*
|
|
404
|
+
* Search order:
|
|
405
|
+
* 1. `.emdash/seed.json`
|
|
406
|
+
* 2. `package.json` → `emdash.seed` reference
|
|
407
|
+
* 3. `seed/seed.json` (conventional template path)
|
|
408
|
+
*
|
|
404
409
|
* Exports `userSeed` (user's seed or null) and `seed` (user's seed or default).
|
|
410
|
+
*
|
|
411
|
+
* When no user seed is found, falls back to the built-in default seed and
|
|
412
|
+
* (if `warnOnFallback` is true) logs a warning so misconfiguration is visible
|
|
413
|
+
* during `astro dev`. Build/preview/sync stay silent so sites that
|
|
414
|
+
* intentionally use the default seed (e.g. the blank template) don't
|
|
415
|
+
* generate noisy logs.
|
|
405
416
|
*/
|
|
406
|
-
export function generateSeedModule(projectRoot: string): string {
|
|
417
|
+
export function generateSeedModule(projectRoot: string, warnOnFallback = false): string {
|
|
407
418
|
let userSeedJson: string | null = null;
|
|
408
419
|
|
|
409
420
|
// Try .emdash/seed.json
|
|
@@ -434,11 +445,30 @@ export function generateSeedModule(projectRoot: string): string {
|
|
|
434
445
|
}
|
|
435
446
|
}
|
|
436
447
|
|
|
448
|
+
// Try conventional seed/seed.json fallback
|
|
449
|
+
if (!userSeedJson) {
|
|
450
|
+
try {
|
|
451
|
+
const seedPath = resolve(projectRoot, "seed", "seed.json");
|
|
452
|
+
const content = readFileSync(seedPath, "utf-8");
|
|
453
|
+
JSON.parse(content); // validate
|
|
454
|
+
userSeedJson = content;
|
|
455
|
+
} catch {
|
|
456
|
+
// Not found
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
437
460
|
if (userSeedJson) {
|
|
438
461
|
return [`export const userSeed = ${userSeedJson};`, `export const seed = userSeed;`].join("\n");
|
|
439
462
|
}
|
|
440
463
|
|
|
441
|
-
// No user seed — inline the default
|
|
464
|
+
// No user seed — inline the default. Caller (the Vite plugin) gates this
|
|
465
|
+
// to dev-only so production builds stay quiet for sites that intentionally
|
|
466
|
+
// rely on the default seed.
|
|
467
|
+
if (warnOnFallback) {
|
|
468
|
+
console.warn(
|
|
469
|
+
"[emdash] No user seed found at .emdash/seed.json, package.json#emdash.seed, or seed/seed.json. Falling back to the built-in default seed; the setup wizard will not offer demo content for this site.",
|
|
470
|
+
);
|
|
471
|
+
}
|
|
442
472
|
return [
|
|
443
473
|
`export const userSeed = null;`,
|
|
444
474
|
`export const seed = ${JSON.stringify(defaultSeed)};`,
|
|
@@ -156,8 +156,13 @@ export interface VitePluginOptions {
|
|
|
156
156
|
export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
157
157
|
const { serializableConfig, resolvedConfig, pluginDescriptors, astroConfig } = options;
|
|
158
158
|
|
|
159
|
+
let viteCommand: "build" | "serve" | undefined;
|
|
160
|
+
|
|
159
161
|
return {
|
|
160
162
|
name: "emdash-virtual-modules",
|
|
163
|
+
configResolved(config) {
|
|
164
|
+
viteCommand = config.command;
|
|
165
|
+
},
|
|
161
166
|
resolveId(id: string) {
|
|
162
167
|
if (id === VIRTUAL_CONFIG_ID) {
|
|
163
168
|
return RESOLVED_VIRTUAL_CONFIG_ID;
|
|
@@ -259,7 +264,7 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
259
264
|
// Generate seed module — embeds user seed or default at build time
|
|
260
265
|
if (id === RESOLVED_VIRTUAL_SEED_ID) {
|
|
261
266
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
262
|
-
return generateSeedModule(projectRoot);
|
|
267
|
+
return generateSeedModule(projectRoot, viteCommand === "serve");
|
|
263
268
|
}
|
|
264
269
|
// Generate wait-until module — re-exports cloudflare:workers'
|
|
265
270
|
// waitUntil under the Cloudflare adapter, undefined otherwise.
|
|
@@ -32,7 +32,7 @@ import { resolveApiToken, resolveOAuthToken } from "../../api/handlers/api-token
|
|
|
32
32
|
import { hasScope } from "../../auth/api-tokens.js";
|
|
33
33
|
import { getAuthMode, type ExternalAuthMode } from "../../auth/mode.js";
|
|
34
34
|
import type { ExternalAuthConfig } from "../../auth/types.js";
|
|
35
|
-
import type { EmDashHandlers
|
|
35
|
+
import type { EmDashHandlers } from "../types.js";
|
|
36
36
|
import { buildEmDashCsp } from "./csp.js";
|
|
37
37
|
|
|
38
38
|
declare global {
|
|
@@ -42,7 +42,6 @@ declare global {
|
|
|
42
42
|
/** Token scopes when authenticated via API token or OAuth token. Undefined for session auth. */
|
|
43
43
|
tokenScopes?: string[];
|
|
44
44
|
emdash?: EmDashHandlers;
|
|
45
|
-
emdashManifest?: EmDashManifest;
|
|
46
45
|
}
|
|
47
46
|
interface SessionData {
|
|
48
47
|
user: { id: string };
|
|
@@ -723,10 +722,14 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
|
|
|
723
722
|
["/_emdash/api/schema", "WRITE", "schema:write"],
|
|
724
723
|
|
|
725
724
|
// Taxonomy, menu, section, widget, revision — all content domain
|
|
725
|
+
// GET uses content:read (implicit from taxonomies:read / menus:read via role).
|
|
726
|
+
// WRITE uses the granular scope so tokens with only taxonomies:manage or
|
|
727
|
+
// menus:manage are not rejected. content:write implicitly grants these via
|
|
728
|
+
// IMPLICIT_SCOPE_GRANTS in @emdash-cms/auth.
|
|
726
729
|
["/_emdash/api/taxonomies", "GET", "content:read"],
|
|
727
|
-
["/_emdash/api/taxonomies", "WRITE", "
|
|
730
|
+
["/_emdash/api/taxonomies", "WRITE", "taxonomies:manage"],
|
|
728
731
|
["/_emdash/api/menus", "GET", "content:read"],
|
|
729
|
-
["/_emdash/api/menus", "WRITE", "
|
|
732
|
+
["/_emdash/api/menus", "WRITE", "menus:manage"],
|
|
730
733
|
["/_emdash/api/sections", "GET", "content:read"],
|
|
731
734
|
["/_emdash/api/sections", "WRITE", "content:write"],
|
|
732
735
|
["/_emdash/api/widget-areas", "GET", "content:read"],
|
|
@@ -738,12 +741,16 @@ const SCOPE_RULES: Array<[prefix: string, method: string, scope: string]> = [
|
|
|
738
741
|
["/_emdash/api/search", "GET", "content:read"],
|
|
739
742
|
["/_emdash/api/search", "WRITE", "admin"],
|
|
740
743
|
|
|
741
|
-
// Import, admin,
|
|
744
|
+
// Import, admin, plugins — all require admin scope
|
|
742
745
|
["/_emdash/api/import", "*", "admin"],
|
|
743
746
|
["/_emdash/api/admin", "*", "admin"],
|
|
744
|
-
["/_emdash/api/settings", "*", "admin"],
|
|
745
747
|
["/_emdash/api/plugins", "*", "admin"],
|
|
746
748
|
|
|
749
|
+
// Settings — use granular scopes so tokens with settings:read or
|
|
750
|
+
// settings:manage are not rejected at the middleware level.
|
|
751
|
+
["/_emdash/api/settings", "GET", "settings:read"],
|
|
752
|
+
["/_emdash/api/settings", "WRITE", "settings:manage"],
|
|
753
|
+
|
|
747
754
|
// MCP endpoint — scopes enforced per-tool inside mcp/server.ts
|
|
748
755
|
["/_emdash/api/mcp", "*", "content:read"],
|
|
749
756
|
];
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
import { defineMiddleware } from "astro:middleware";
|
|
18
18
|
|
|
19
19
|
import { RedirectRepository } from "../../database/repositories/redirect.js";
|
|
20
|
+
import { getDb } from "../../loader.js";
|
|
20
21
|
import {
|
|
21
|
-
|
|
22
|
+
getCachedRedirects,
|
|
22
23
|
matchCachedPatterns,
|
|
23
|
-
|
|
24
|
+
setCachedRedirects,
|
|
24
25
|
} from "../../redirects/cache.js";
|
|
25
26
|
|
|
26
27
|
/** Paths that should never be intercepted by redirects */
|
|
@@ -46,16 +47,34 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
46
47
|
return next();
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Public visitors hit the runtime's anonymous fast path, which intentionally
|
|
51
|
+
// omits `db` from `locals.emdash` to keep the public render boundary minimal
|
|
52
|
+
// (issue #808). Fall back to `getDb()`, which transparently returns the
|
|
53
|
+
// per-request scoped db (set in ALS by the runtime middleware) or the
|
|
54
|
+
// singleton — same path the loader and template helpers use.
|
|
55
|
+
let db = context.locals.emdash?.db;
|
|
56
|
+
if (!db) {
|
|
57
|
+
try {
|
|
58
|
+
db = await getDb();
|
|
59
|
+
} catch {
|
|
60
|
+
return next();
|
|
61
|
+
}
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
try {
|
|
55
|
-
const repo = new RedirectRepository(
|
|
65
|
+
const repo = new RedirectRepository(db);
|
|
66
|
+
|
|
67
|
+
// One query loads both exact and pattern rules into the cache; warm
|
|
68
|
+
// requests issue zero queries. Empty-redirect sites cache an empty
|
|
69
|
+
// Map + array, so the next request returns immediately without probing.
|
|
70
|
+
let cached = getCachedRedirects();
|
|
71
|
+
if (!cached) {
|
|
72
|
+
const all = await repo.findAllEnabled();
|
|
73
|
+
cached = setCachedRedirects(all);
|
|
74
|
+
}
|
|
56
75
|
|
|
57
|
-
// 1. Exact match (
|
|
58
|
-
const exact =
|
|
76
|
+
// 1. Exact match (O(1) Map lookup)
|
|
77
|
+
const exact = cached.exact.get(pathname);
|
|
59
78
|
if (exact) {
|
|
60
79
|
const dest = exact.destination;
|
|
61
80
|
if (dest.startsWith("//") || dest.startsWith("/\\")) return next();
|
|
@@ -64,14 +83,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
64
83
|
return context.redirect(dest, code);
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
// 2. Pattern match (
|
|
68
|
-
|
|
69
|
-
if (!rules) {
|
|
70
|
-
const patterns = await repo.findEnabledPatternRules();
|
|
71
|
-
rules = setCachedPatternRules(patterns);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const patternMatch = matchCachedPatterns(rules, pathname);
|
|
86
|
+
// 2. Pattern match (compile once, match every request)
|
|
87
|
+
const patternMatch = matchCachedPatterns(cached.patterns, pathname);
|
|
75
88
|
if (patternMatch) {
|
|
76
89
|
const { redirect, destination } = patternMatch;
|
|
77
90
|
if (destination.startsWith("//") || destination.startsWith("/\\")) return next();
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { defineMiddleware } from "astro:middleware";
|
|
14
14
|
|
|
15
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
16
|
+
|
|
15
17
|
import { verifyPreviewToken, parseContentId } from "../../preview/tokens.js";
|
|
16
18
|
import { getRequestContext, runWithContext } from "../../request-context.js";
|
|
17
19
|
import { renderToolbar } from "../../visual-editing/toolbar.js";
|
|
@@ -79,17 +81,25 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
79
81
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Astro context includes currentLocale when i18n is configured
|
|
80
82
|
const locale = (context as { currentLocale?: string }).currentLocale;
|
|
81
83
|
|
|
82
|
-
// Verify preview token if present
|
|
84
|
+
// Verify preview token if present.
|
|
85
|
+
// The preview secret is resolved via `resolveSecretsCached`: env wins,
|
|
86
|
+
// otherwise a DB-stored value is read (or generated on first need).
|
|
87
|
+
// `emdash.db` is set by the runtime middleware which runs first; the
|
|
88
|
+
// only path where it's missing is a runtime-init failure.
|
|
83
89
|
let preview: { collection: string; id: string } | undefined;
|
|
84
90
|
if (hasPreviewToken) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const result = await verifyPreviewToken({ url, secret });
|
|
91
|
+
const db = context.locals.emdash?.db;
|
|
92
|
+
if (db) {
|
|
93
|
+
const { previewSecret } = await resolveSecretsCached(db);
|
|
94
|
+
const result = await verifyPreviewToken({ url, secret: previewSecret });
|
|
89
95
|
if (result.valid) {
|
|
90
96
|
const { collection, id } = parseContentId(result.payload.cid);
|
|
91
97
|
preview = { collection, id };
|
|
92
98
|
}
|
|
99
|
+
} else {
|
|
100
|
+
console.warn(
|
|
101
|
+
"[emdash] Preview token present but EmDash runtime not initialized; preview disabled.",
|
|
102
|
+
);
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
|