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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Excludes auth/user/session/token tables
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { User } from "@emdash-cms/auth";
|
|
10
11
|
import type { APIRoute } from "astro";
|
|
11
12
|
|
|
12
13
|
import { requirePerm } from "#api/authorize.js";
|
|
@@ -17,11 +18,31 @@ import {
|
|
|
17
18
|
verifyPreviewSignature,
|
|
18
19
|
} from "#api/handlers/snapshot.js";
|
|
19
20
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
20
22
|
|
|
21
23
|
export const prerender = false;
|
|
22
24
|
|
|
23
|
-
export const GET: APIRoute = async ({ request, locals, url }) => {
|
|
24
|
-
const { emdash
|
|
25
|
+
export const GET: APIRoute = async ({ request, locals, url, session }) => {
|
|
26
|
+
const { emdash } = locals;
|
|
27
|
+
// This route is in PUBLIC_API_EXACT (for preview-signature callers with no session),
|
|
28
|
+
// so auth middleware skips user resolution. Manually resolve the session user here
|
|
29
|
+
// to support session-authenticated admin users alongside preview-signature auth.
|
|
30
|
+
let user: User | undefined = (locals as { user?: User }).user;
|
|
31
|
+
if (!user && session && emdash?.db) {
|
|
32
|
+
try {
|
|
33
|
+
const { createKyselyAdapter } = await import("@emdash-cms/auth/adapters/kysely");
|
|
34
|
+
const sessionUser = await session.get("user");
|
|
35
|
+
if (sessionUser?.id) {
|
|
36
|
+
const adapter = createKyselyAdapter(emdash.db);
|
|
37
|
+
const resolved = await adapter.getUserById(sessionUser.id);
|
|
38
|
+
if (resolved && !resolved.disabled) {
|
|
39
|
+
user = resolved;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Session resolution failed, continue to preview-signature check
|
|
44
|
+
}
|
|
45
|
+
}
|
|
25
46
|
|
|
26
47
|
if (!emdash?.db) {
|
|
27
48
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -32,24 +53,29 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|
|
32
53
|
let authorized = false;
|
|
33
54
|
|
|
34
55
|
if (previewSig) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
// Resolves env override or DB-stored value. Always non-empty after
|
|
57
|
+
// resolution, so the signature path is never silently disabled.
|
|
58
|
+
// Note: a signing process without access to this database (e.g. a
|
|
59
|
+
// remote preview Worker) must set the same `EMDASH_PREVIEW_SECRET`
|
|
60
|
+
// env var on both sides.
|
|
61
|
+
const { previewSecret: secret, previewSecretSource } = await resolveSecretsCached(emdash.db);
|
|
62
|
+
const parsed = parsePreviewSignatureHeader(previewSig);
|
|
63
|
+
if (!parsed) {
|
|
64
|
+
console.warn("[snapshot] Failed to parse X-Preview-Signature header");
|
|
40
65
|
} else {
|
|
41
|
-
|
|
42
|
-
if (!
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
authorized = await verifyPreviewSignature(parsed.source, parsed.exp, parsed.sig, secret);
|
|
67
|
+
if (!authorized) {
|
|
68
|
+
const fields: Record<string, unknown> = {
|
|
69
|
+
source: parsed.source,
|
|
70
|
+
exp: parsed.exp,
|
|
71
|
+
expired: parsed.exp < Date.now() / 1000,
|
|
72
|
+
secretSource: previewSecretSource,
|
|
73
|
+
};
|
|
74
|
+
if (previewSecretSource === "db") {
|
|
75
|
+
fields.hint =
|
|
76
|
+
"Set EMDASH_PREVIEW_SECRET in both this process and the signing process to share secrets across deployments";
|
|
52
77
|
}
|
|
78
|
+
console.warn("[snapshot] Preview signature verification failed", fields);
|
|
53
79
|
}
|
|
54
80
|
}
|
|
55
81
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Term translation endpoints
|
|
3
|
+
*
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms/:slug/translations[?locale=xx]
|
|
5
|
+
* POST /_emdash/api/taxonomies/:name/terms/:slug/translations
|
|
6
|
+
* body: { locale, label?, slug? }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { APIRoute } from "astro";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
import { requirePerm } from "#api/authorize.js";
|
|
13
|
+
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
14
|
+
import {
|
|
15
|
+
handleTermCreate,
|
|
16
|
+
handleTermGet,
|
|
17
|
+
handleTermTranslations,
|
|
18
|
+
} from "#api/handlers/taxonomies.js";
|
|
19
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
20
|
+
import { localeFilterQuery } from "#api/schemas.js";
|
|
21
|
+
|
|
22
|
+
export const prerender = false;
|
|
23
|
+
|
|
24
|
+
const createTermTranslationBody = z
|
|
25
|
+
.object({
|
|
26
|
+
locale: z.string().min(1),
|
|
27
|
+
label: z.string().min(1).optional(),
|
|
28
|
+
slug: z.string().min(1).optional(),
|
|
29
|
+
})
|
|
30
|
+
.meta({ id: "CreateTermTranslationBody" });
|
|
31
|
+
|
|
32
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
33
|
+
const { emdash, user } = locals;
|
|
34
|
+
const { name, slug } = params;
|
|
35
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
36
|
+
|
|
37
|
+
const dbErr = requireDb(emdash?.db);
|
|
38
|
+
if (dbErr) return dbErr;
|
|
39
|
+
|
|
40
|
+
const denied = requirePerm(user, "taxonomies:read");
|
|
41
|
+
if (denied) return denied;
|
|
42
|
+
|
|
43
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
44
|
+
if (isParseError(query)) return query;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const anchor = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
48
|
+
if (!anchor.success) return unwrapResult(anchor);
|
|
49
|
+
const result = await handleTermTranslations(emdash.db, anchor.data.term.id);
|
|
50
|
+
return unwrapResult(result);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return handleError(error, "Failed to list term translations", "TERM_TRANSLATIONS_ERROR");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
57
|
+
const { emdash, user } = locals;
|
|
58
|
+
const { name, slug } = params;
|
|
59
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
60
|
+
|
|
61
|
+
const dbErr = requireDb(emdash?.db);
|
|
62
|
+
if (dbErr) return dbErr;
|
|
63
|
+
|
|
64
|
+
const denied = requirePerm(user, "taxonomies:manage");
|
|
65
|
+
if (denied) return denied;
|
|
66
|
+
|
|
67
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
68
|
+
if (isParseError(query)) return query;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const body = await parseBody(request, createTermTranslationBody);
|
|
72
|
+
if (isParseError(body)) return body;
|
|
73
|
+
|
|
74
|
+
const source = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
75
|
+
if (!source.success) return unwrapResult(source);
|
|
76
|
+
|
|
77
|
+
const result = await handleTermCreate(emdash.db, name, {
|
|
78
|
+
slug: body.slug ?? source.data.term.slug,
|
|
79
|
+
label: body.label ?? source.data.term.label,
|
|
80
|
+
parentId: source.data.term.parentId,
|
|
81
|
+
description: source.data.term.description,
|
|
82
|
+
locale: body.locale,
|
|
83
|
+
translationOf: source.data.term.id,
|
|
84
|
+
});
|
|
85
|
+
return unwrapResult(result, 201);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return handleError(error, "Failed to create term translation", "TERM_TRANSLATION_CREATE_ERROR");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single term endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
* PUT
|
|
6
|
-
* DELETE /_emdash/api/taxonomies/:name/terms/:slug
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
5
|
+
* PUT /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
6
|
+
* DELETE /_emdash/api/taxonomies/:name/terms/:slug[?locale=xx]
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { APIRoute } from "astro";
|
|
@@ -11,21 +11,18 @@ import type { APIRoute } from "astro";
|
|
|
11
11
|
import { requirePerm } from "#api/authorize.js";
|
|
12
12
|
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
13
13
|
import { handleTermDelete, handleTermGet, handleTermUpdate } from "#api/handlers/taxonomies.js";
|
|
14
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
15
|
-
import { updateTermBody } from "#api/schemas.js";
|
|
14
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
15
|
+
import { localeFilterQuery, updateTermBody } from "#api/schemas.js";
|
|
16
16
|
|
|
17
17
|
export const prerender = false;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Get a single term
|
|
21
21
|
*/
|
|
22
|
-
export const GET: APIRoute = async ({ params, locals }) => {
|
|
22
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
23
23
|
const { emdash, user } = locals;
|
|
24
24
|
const { name, slug } = params;
|
|
25
|
-
|
|
26
|
-
if (!name || !slug) {
|
|
27
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
28
|
-
}
|
|
25
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
29
26
|
|
|
30
27
|
const dbErr = requireDb(emdash?.db);
|
|
31
28
|
if (dbErr) return dbErr;
|
|
@@ -33,8 +30,11 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
33
30
|
const denied = requirePerm(user, "taxonomies:read");
|
|
34
31
|
if (denied) return denied;
|
|
35
32
|
|
|
33
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
34
|
+
if (isParseError(query)) return query;
|
|
35
|
+
|
|
36
36
|
try {
|
|
37
|
-
const result = await handleTermGet(emdash.db, name, slug);
|
|
37
|
+
const result = await handleTermGet(emdash.db, name, slug, { locale: query.locale });
|
|
38
38
|
return unwrapResult(result);
|
|
39
39
|
} catch (error) {
|
|
40
40
|
return handleError(error, "Failed to get term", "TERM_GET_ERROR");
|
|
@@ -47,10 +47,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
47
47
|
export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
48
48
|
const { emdash, user } = locals;
|
|
49
49
|
const { name, slug } = params;
|
|
50
|
-
|
|
51
|
-
if (!name || !slug) {
|
|
52
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
53
|
-
}
|
|
50
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
54
51
|
|
|
55
52
|
const dbErr = requireDb(emdash?.db);
|
|
56
53
|
if (dbErr) return dbErr;
|
|
@@ -58,11 +55,14 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
58
55
|
const denied = requirePerm(user, "taxonomies:manage");
|
|
59
56
|
if (denied) return denied;
|
|
60
57
|
|
|
58
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
59
|
+
if (isParseError(query)) return query;
|
|
60
|
+
|
|
61
61
|
try {
|
|
62
62
|
const body = await parseBody(request, updateTermBody);
|
|
63
63
|
if (isParseError(body)) return body;
|
|
64
64
|
|
|
65
|
-
const result = await handleTermUpdate(emdash.db, name, slug, body);
|
|
65
|
+
const result = await handleTermUpdate(emdash.db, name, slug, body, { locale: query.locale });
|
|
66
66
|
return unwrapResult(result);
|
|
67
67
|
} catch (error) {
|
|
68
68
|
return handleError(error, "Failed to update term", "TERM_UPDATE_ERROR");
|
|
@@ -72,13 +72,10 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
72
72
|
/**
|
|
73
73
|
* Delete a term
|
|
74
74
|
*/
|
|
75
|
-
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
75
|
+
export const DELETE: APIRoute = async ({ params, request, locals }) => {
|
|
76
76
|
const { emdash, user } = locals;
|
|
77
77
|
const { name, slug } = params;
|
|
78
|
-
|
|
79
|
-
if (!name || !slug) {
|
|
80
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
81
|
-
}
|
|
78
|
+
if (!name || !slug) return apiError("VALIDATION_ERROR", "Taxonomy name and slug required", 400);
|
|
82
79
|
|
|
83
80
|
const dbErr = requireDb(emdash?.db);
|
|
84
81
|
if (dbErr) return dbErr;
|
|
@@ -86,8 +83,11 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
|
86
83
|
const denied = requirePerm(user, "taxonomies:manage");
|
|
87
84
|
if (denied) return denied;
|
|
88
85
|
|
|
86
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
87
|
+
if (isParseError(query)) return query;
|
|
88
|
+
|
|
89
89
|
try {
|
|
90
|
-
const result = await handleTermDelete(emdash.db, name, slug);
|
|
90
|
+
const result = await handleTermDelete(emdash.db, name, slug, { locale: query.locale });
|
|
91
91
|
return unwrapResult(result);
|
|
92
92
|
} catch (error) {
|
|
93
93
|
return handleError(error, "Failed to delete term", "TERM_DELETE_ERROR");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Taxonomy terms list and create endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET
|
|
5
|
-
* POST /_emdash/api/taxonomies/:name/terms
|
|
4
|
+
* GET /_emdash/api/taxonomies/:name/terms[?locale=xx] - List terms (tree for hierarchical)
|
|
5
|
+
* POST /_emdash/api/taxonomies/:name/terms - Create a new term (body may include locale & translationOf)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
@@ -10,21 +10,18 @@ import type { APIRoute } from "astro";
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { apiError, handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { handleTermCreate, handleTermList } from "#api/handlers/taxonomies.js";
|
|
13
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
-
import { createTermBody } from "#api/schemas.js";
|
|
13
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
|
+
import { createTermBody, localeFilterQuery } from "#api/schemas.js";
|
|
15
15
|
|
|
16
16
|
export const prerender = false;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* List all terms for a taxonomy
|
|
20
20
|
*/
|
|
21
|
-
export const GET: APIRoute = async ({ params, locals }) => {
|
|
21
|
+
export const GET: APIRoute = async ({ params, request, locals }) => {
|
|
22
22
|
const { emdash, user } = locals;
|
|
23
23
|
const { name } = params;
|
|
24
|
-
|
|
25
|
-
if (!name) {
|
|
26
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
27
|
-
}
|
|
24
|
+
if (!name) return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
28
25
|
|
|
29
26
|
const dbErr = requireDb(emdash?.db);
|
|
30
27
|
if (dbErr) return dbErr;
|
|
@@ -32,8 +29,11 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
32
29
|
const denied = requirePerm(user, "taxonomies:read");
|
|
33
30
|
if (denied) return denied;
|
|
34
31
|
|
|
32
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
33
|
+
if (isParseError(query)) return query;
|
|
34
|
+
|
|
35
35
|
try {
|
|
36
|
-
const result = await handleTermList(emdash.db, name);
|
|
36
|
+
const result = await handleTermList(emdash.db, name, { locale: query.locale });
|
|
37
37
|
return unwrapResult(result);
|
|
38
38
|
} catch (error) {
|
|
39
39
|
return handleError(error, "Failed to list terms", "TERM_LIST_ERROR");
|
|
@@ -46,10 +46,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
46
46
|
export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
47
47
|
const { emdash, user } = locals;
|
|
48
48
|
const { name } = params;
|
|
49
|
-
|
|
50
|
-
if (!name) {
|
|
51
|
-
return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
52
|
-
}
|
|
49
|
+
if (!name) return apiError("VALIDATION_ERROR", "Taxonomy name required", 400);
|
|
53
50
|
|
|
54
51
|
const dbErr = requireDb(emdash?.db);
|
|
55
52
|
if (dbErr) return dbErr;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Taxonomy definitions endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET /_emdash/api/taxonomies - List
|
|
5
|
-
* POST /_emdash/api/taxonomies
|
|
4
|
+
* GET /_emdash/api/taxonomies[?locale=xx] - List taxonomy definitions
|
|
5
|
+
* POST /_emdash/api/taxonomies - Create a custom taxonomy definition
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
@@ -10,15 +10,15 @@ import type { APIRoute } from "astro";
|
|
|
10
10
|
import { requirePerm } from "#api/authorize.js";
|
|
11
11
|
import { handleError, requireDb, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { handleTaxonomyCreate, handleTaxonomyList } from "#api/handlers/taxonomies.js";
|
|
13
|
-
import { isParseError, parseBody } from "#api/parse.js";
|
|
14
|
-
import { createTaxonomyDefBody } from "#api/schemas.js";
|
|
13
|
+
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
|
+
import { createTaxonomyDefBody, localeFilterQuery } from "#api/schemas.js";
|
|
15
15
|
|
|
16
16
|
export const prerender = false;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* List taxonomy definitions
|
|
20
20
|
*/
|
|
21
|
-
export const GET: APIRoute = async ({ locals }) => {
|
|
21
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
22
22
|
const { emdash, user } = locals;
|
|
23
23
|
|
|
24
24
|
const dbErr = requireDb(emdash?.db);
|
|
@@ -27,8 +27,11 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
27
27
|
const denied = requirePerm(user, "taxonomies:read");
|
|
28
28
|
if (denied) return denied;
|
|
29
29
|
|
|
30
|
+
const query = parseQuery(new URL(request.url), localeFilterQuery);
|
|
31
|
+
if (isParseError(query)) return query;
|
|
32
|
+
|
|
30
33
|
try {
|
|
31
|
-
const result = await handleTaxonomyList(emdash.db);
|
|
34
|
+
const result = await handleTaxonomyList(emdash.db, { locale: query.locale });
|
|
32
35
|
return unwrapResult(result);
|
|
33
36
|
} catch (error) {
|
|
34
37
|
return handleError(error, "Failed to list taxonomies", "TAXONOMY_LIST_ERROR");
|
|
@@ -52,7 +55,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
52
55
|
if (isParseError(body)) return body;
|
|
53
56
|
|
|
54
57
|
const result = await handleTaxonomyCreate(emdash.db, body);
|
|
55
|
-
if (result.success) emdash.invalidateManifest();
|
|
56
58
|
return unwrapResult(result, 201);
|
|
57
59
|
} catch (error) {
|
|
58
60
|
return handleError(error, "Failed to create taxonomy", "TAXONOMY_CREATE_ERROR");
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* POST /_emdash/api/themes/preview
|
|
5
5
|
*
|
|
6
6
|
* Generates a signed preview URL for the "Try with my data" feature.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Uses the resolved preview secret: env override (`EMDASH_PREVIEW_SECRET`)
|
|
9
|
+
* wins, otherwise an auto-generated stable per-site value persisted in the
|
|
10
|
+
* options table is used. Processes that share the same database converge on
|
|
11
|
+
* the same auto-generated value; only set `EMDASH_PREVIEW_SECRET` in both
|
|
12
|
+
* processes when the verifying side runs without access to the EmDash DB
|
|
13
|
+
* (e.g. a remote preview Worker).
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
import type { APIRoute } from "astro";
|
|
@@ -12,6 +18,7 @@ import type { APIRoute } from "astro";
|
|
|
12
18
|
import { requirePerm } from "#api/authorize.js";
|
|
13
19
|
import { apiError, apiSuccess } from "#api/error.js";
|
|
14
20
|
import { getPublicOrigin } from "#api/public-url.js";
|
|
21
|
+
import { resolveSecretsCached } from "#config/secrets.js";
|
|
15
22
|
|
|
16
23
|
export const prerender = false;
|
|
17
24
|
|
|
@@ -25,10 +32,9 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
25
32
|
const denied = requirePerm(user, "plugins:read");
|
|
26
33
|
if (denied) return denied;
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
35
|
+
// Always non-empty after resolution; env override wins, otherwise a
|
|
36
|
+
// stable DB-stored value is used.
|
|
37
|
+
const { previewSecret: secret } = await resolveSecretsCached(emdash.db);
|
|
32
38
|
|
|
33
39
|
let body: { previewUrl: string };
|
|
34
40
|
try {
|
package/src/astro/types.ts
CHANGED
|
@@ -228,6 +228,15 @@ export interface EmDashHandlers {
|
|
|
228
228
|
slug?: string;
|
|
229
229
|
status?: string;
|
|
230
230
|
authorId?: string | null;
|
|
231
|
+
bylines?: Array<{ bylineId: string; roleLabel?: string | null }>;
|
|
232
|
+
seo?: {
|
|
233
|
+
title?: string | null;
|
|
234
|
+
description?: string | null;
|
|
235
|
+
image?: string | null;
|
|
236
|
+
canonical?: string | null;
|
|
237
|
+
noIndex?: boolean;
|
|
238
|
+
};
|
|
239
|
+
publishedAt?: string | null;
|
|
231
240
|
_rev?: string;
|
|
232
241
|
},
|
|
233
242
|
) => Promise<HandlerResponse>;
|
|
@@ -255,7 +264,11 @@ export interface EmDashHandlers {
|
|
|
255
264
|
) => Promise<HandlerResponse>;
|
|
256
265
|
|
|
257
266
|
// Publishing & Scheduling handlers
|
|
258
|
-
handleContentPublish: (
|
|
267
|
+
handleContentPublish: (
|
|
268
|
+
collection: string,
|
|
269
|
+
id: string,
|
|
270
|
+
options?: { publishedAt?: string },
|
|
271
|
+
) => Promise<HandlerResponse>;
|
|
259
272
|
|
|
260
273
|
handleContentUnpublish: (collection: string, id: string) => Promise<HandlerResponse>;
|
|
261
274
|
|
|
@@ -362,8 +375,15 @@ export interface EmDashHandlers {
|
|
|
362
375
|
// Configuration (for checking database type, auth mode, etc.)
|
|
363
376
|
config: import("./integration/runtime.js").EmDashConfig;
|
|
364
377
|
|
|
365
|
-
//
|
|
366
|
-
|
|
378
|
+
// Build the admin manifest from the live database. Only used by admin
|
|
379
|
+
// routes; logged-out requests don't need it. Per-request, deduplicated
|
|
380
|
+
// by `requestCached`.
|
|
381
|
+
getManifest: () => Promise<EmDashManifest>;
|
|
382
|
+
|
|
383
|
+
// Clear the cached URL patterns used by `resolveEmDashPath`. Call after
|
|
384
|
+
// any schema mutation that creates/updates/deletes a collection's
|
|
385
|
+
// `urlPattern` so public routing picks up the change immediately.
|
|
386
|
+
invalidateUrlPatternCache: () => void;
|
|
367
387
|
|
|
368
388
|
// Sandbox runner (for marketplace plugin install/update)
|
|
369
389
|
getSandboxRunner: () => import("../plugins/sandbox/types.js").SandboxRunner | null;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolution and validation of multi-origin passkey verification.
|
|
3
|
+
*
|
|
4
|
+
* `allowedOrigins` lets one EmDash deployment accept passkey assertions from
|
|
5
|
+
* several hostnames sharing the same `rpId` (e.g. apex + preview/staging
|
|
6
|
+
* subdomains under one registrable parent). Origins come from two sources:
|
|
7
|
+
*
|
|
8
|
+
* - `EmDashConfig.allowedOrigins` (declared in `astro.config.mjs`)
|
|
9
|
+
* - `EMDASH_ALLOWED_ORIGINS` (comma-separated runtime env var)
|
|
10
|
+
*
|
|
11
|
+
* Sources are merged (union of permissions, deduplicated). Each entry is
|
|
12
|
+
* validated against `siteUrl` to fail loud on dead config the browser would
|
|
13
|
+
* never honor.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getEnvAllowedOrigins } from "../api/public-url.js";
|
|
17
|
+
import type { EmDashConfig } from "../astro/integration/runtime.js";
|
|
18
|
+
|
|
19
|
+
export type AllowedOriginSource = "config.allowedOrigins" | "EMDASH_ALLOWED_ORIGINS";
|
|
20
|
+
|
|
21
|
+
export interface TaggedOrigin {
|
|
22
|
+
/** Raw entry as declared by the operator. */
|
|
23
|
+
origin: string;
|
|
24
|
+
/** Where the entry came from (used for source-attributed errors). */
|
|
25
|
+
source: AllowedOriginSource;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Collect raw allowedOrigins from config and env, source-tagged.
|
|
30
|
+
*
|
|
31
|
+
* Returns raw values — the caller is expected to pass the result through
|
|
32
|
+
* `validateAllowedOrigins()` before use in passkey verification.
|
|
33
|
+
*/
|
|
34
|
+
export function getConfiguredAllowedOrigins(config?: EmDashConfig): TaggedOrigin[] {
|
|
35
|
+
const tagged: TaggedOrigin[] = [];
|
|
36
|
+
if (config?.allowedOrigins) {
|
|
37
|
+
for (const origin of config.allowedOrigins) {
|
|
38
|
+
if (origin) tagged.push({ origin, source: "config.allowedOrigins" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const origin of getEnvAllowedOrigins()) {
|
|
42
|
+
tagged.push({ origin, source: "EMDASH_ALLOWED_ORIGINS" });
|
|
43
|
+
}
|
|
44
|
+
return tagged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate per-entry shape rules (no `siteUrl` needed):
|
|
49
|
+
* - parses as `URL`
|
|
50
|
+
* - protocol is `http:` or `https:`
|
|
51
|
+
* - hostname has no trailing dot (`example.com.` rejected)
|
|
52
|
+
* - hostname has no empty labels (`foo..example.com` rejected)
|
|
53
|
+
*
|
|
54
|
+
* Returns the deduplicated, normalized origin form (`URL.origin`) of every
|
|
55
|
+
* input, in input order. Throws on the first violation with a source-tagged
|
|
56
|
+
* error message.
|
|
57
|
+
*/
|
|
58
|
+
export function validateOriginShape(tagged: TaggedOrigin[]): string[] {
|
|
59
|
+
const normalized: string[] = [];
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
for (const { origin, source } of tagged) {
|
|
62
|
+
let parsed: URL;
|
|
63
|
+
try {
|
|
64
|
+
parsed = new URL(origin);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw configError(source, `invalid URL: "${origin}"`, e);
|
|
67
|
+
}
|
|
68
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
69
|
+
throw configError(
|
|
70
|
+
source,
|
|
71
|
+
`origin must be http or https: "${origin}" (got ${parsed.protocol})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (parsed.hostname.endsWith(".")) {
|
|
75
|
+
throw configError(
|
|
76
|
+
source,
|
|
77
|
+
`hostname has a trailing dot: "${origin}". Remove the trailing dot — assertion origins from the browser do not include it.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
if (parsed.hostname.split(".").includes("")) {
|
|
81
|
+
throw configError(source, `hostname has empty labels: "${origin}"`);
|
|
82
|
+
}
|
|
83
|
+
if (!seen.has(parsed.origin)) {
|
|
84
|
+
seen.add(parsed.origin);
|
|
85
|
+
normalized.push(parsed.origin);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validate the effective merged allowedOrigins set against `siteUrl`.
|
|
93
|
+
*
|
|
94
|
+
* Performs `validateOriginShape()` plus the siteUrl-dependent rules:
|
|
95
|
+
* - Rule A: non-empty origins ⇒ `siteUrl` is set
|
|
96
|
+
* - `siteUrl` hostname is not an IP literal (multi-origin requires a domain)
|
|
97
|
+
* - `siteUrl` hostname has no trailing dot (cannot match assertion origins)
|
|
98
|
+
* - Rule B: each origin's hostname is `siteHost` exactly or a subdomain
|
|
99
|
+
*
|
|
100
|
+
* Throws on first violation. Returns the deduplicated normalized origins.
|
|
101
|
+
*
|
|
102
|
+
* Use this at the runtime chokepoint (where config + env are merged into the
|
|
103
|
+
* effective set). At Astro integration init, prefer `validateOriginShape()`
|
|
104
|
+
* for shape-only checks on `config.allowedOrigins`, since `siteUrl` may be
|
|
105
|
+
* supplied at runtime via `EMDASH_SITE_URL`.
|
|
106
|
+
*/
|
|
107
|
+
export function validateAllowedOrigins(
|
|
108
|
+
siteUrl: string | undefined,
|
|
109
|
+
tagged: TaggedOrigin[],
|
|
110
|
+
): string[] {
|
|
111
|
+
const normalized = validateOriginShape(tagged);
|
|
112
|
+
if (normalized.length === 0) return normalized;
|
|
113
|
+
|
|
114
|
+
if (!siteUrl) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`EmDash config error: allowedOrigins is set (${normalized.length} ${
|
|
117
|
+
normalized.length === 1 ? "entry" : "entries"
|
|
118
|
+
}) but siteUrl is not. Without a canonical siteUrl, rpId is derived from the request hostname, defeating multi-origin passkeys. Set siteUrl in astro.config.mjs or via EMDASH_SITE_URL.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let siteHost: string;
|
|
123
|
+
try {
|
|
124
|
+
siteHost = new URL(siteUrl).hostname;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
throw new Error(`EmDash config error: siteUrl is not a valid URL: "${siteUrl}"`, {
|
|
127
|
+
cause: e,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (siteHost.endsWith(".")) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`EmDash config error: siteUrl "${siteUrl}" has a trailing-dot hostname, which cannot match assertion origins. Remove the trailing dot when using allowedOrigins.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (isIPLiteralHostname(siteHost)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`EmDash config error: siteUrl "${siteUrl}" uses an IP-literal hostname. Multi-origin passkeys require a domain-based siteUrl — IP addresses cannot have valid subdomains for WebAuthn rpId.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const { origin, source } of tagged) {
|
|
143
|
+
const h = new URL(origin).hostname;
|
|
144
|
+
if (h !== siteHost && !h.endsWith("." + siteHost)) {
|
|
145
|
+
throw configError(
|
|
146
|
+
source,
|
|
147
|
+
`"${origin}" is not a subdomain of siteUrl "${siteUrl}". Allowed origins must be the same hostname as siteUrl or a subdomain of it.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function configError(source: AllowedOriginSource, detail: string, cause?: unknown): Error {
|
|
156
|
+
const err = new Error(`EmDash config error in ${source}: ${detail}`);
|
|
157
|
+
if (cause !== undefined) (err as Error & { cause?: unknown }).cause = cause;
|
|
158
|
+
return err;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const IPV4_DOTTED_DECIMAL_RE = /^\d+(\.\d+){3}$/;
|
|
162
|
+
|
|
163
|
+
function isIPLiteralHostname(h: string): boolean {
|
|
164
|
+
// IPv6 hostnames are bracketed by URL.hostname, e.g. "[::1]"
|
|
165
|
+
if (h.startsWith("[")) return true;
|
|
166
|
+
// IPv4 dotted-decimal
|
|
167
|
+
return IPV4_DOTTED_DECIMAL_RE.test(h);
|
|
168
|
+
}
|