emdash 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-Di31kZ28.d.mts → adapters-DoNJiveC.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
- package/dist/{apply-5uslYdUu.mjs → apply-BzltprvY.mjs} +90 -139
- package/dist/apply-BzltprvY.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 +194 -17
- 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 +34 -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 +17 -12
- 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 +9 -6
- 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 +301 -165
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +34 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-MBPo9ozB.mjs → base64-BRICGH2l.mjs} +1 -1
- package/dist/{base64-MBPo9ozB.mjs.map → base64-BRICGH2l.mjs.map} +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-BSaNL1w7.mjs} +5 -5
- package/dist/byline-BSaNL1w7.mjs.map +1 -0
- package/dist/bylines-CvJ3PYz2.mjs +113 -0
- package/dist/bylines-CvJ3PYz2.mjs.map +1 -0
- package/dist/cache-C6N_hhN7.mjs +65 -0
- package/dist/cache-C6N_hhN7.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-NBQVDOci.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-NBQVDOci.mjs.map} +1 -1
- package/dist/cli/index.mjs +229 -31
- 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-BI0V3ICQ.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-BI0V3ICQ.mjs.map} +1 -1
- package/dist/{content-D7J5y73J.mjs → content-8lOYF0pr.mjs} +43 -28
- package/dist/content-8lOYF0pr.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-D0UT85nC.mjs → db-errors-WRezodiz.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-D8ksjWhO.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-D8ksjWhO.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-CiYn9yDu.mjs → error-D_-tqP-I.mjs} +1 -1
- package/dist/error-D_-tqP-I.mjs.map +1 -0
- package/dist/{index-De6_Xv3v.d.mts → index-BFRaVcD6.d.mts} +243 -40
- package/dist/index-BFRaVcD6.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -25
- package/dist/{load-CBcmDIot.mjs → load-DDqMMvZL.mjs} +2 -2
- package/dist/{load-CBcmDIot.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CKLbBnhK.mjs} +32 -10
- package/dist/loader-CKLbBnhK.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DqWNC3lM.mjs} +45 -3
- package/dist/manifest-schema-DqWNC3lM.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/media/local-runtime.mjs +3 -3
- package/dist/{media-DqHVh136.mjs → media-BW32b4gi.mjs} +4 -7
- package/dist/media-BW32b4gi.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-ier8jbBk.mjs} +1 -1
- package/dist/mode-ier8jbBk.mjs.map +1 -0
- package/dist/options-BVp3UsTS.mjs +117 -0
- package/dist/options-BVp3UsTS.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-BE4o_2dc.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-CIJejMlK.mjs} +1 -1
- package/dist/placeholder-CIJejMlK.mjs.map +1 -0
- 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-DByxYjUw.mjs +51 -0
- package/dist/public-url-DByxYjUw.mjs.map +1 -0
- package/dist/{query-g4Ug-9j9.mjs → query-Cg9ZKRQ0.mjs} +114 -16
- package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-BhUBKRc1.mjs} +13 -8
- package/dist/redirect-BhUBKRc1.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-Dw70ChxB.mjs} +69 -11
- package/dist/registry-Dw70ChxB.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-B-bmkipQ.mjs} +1 -1
- package/dist/request-cache-B-bmkipQ.mjs.map +1 -0
- package/dist/runner-Bnoj7vjK.d.mts +44 -0
- package/dist/runner-Bnoj7vjK.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-C7ADox5q.mjs} +185 -55
- package/dist/{runner-tQ7BJ4T7.mjs.map → runner-C7ADox5q.mjs.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +4 -4
- package/dist/{search-B0effn3j.mjs → search-dOGEccMa.mjs} +341 -152
- package/dist/search-dOGEccMa.mjs.map +1 -0
- package/dist/secrets-CW3reAnU.mjs +314 -0
- package/dist/secrets-CW3reAnU.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- 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.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-K2z0Uhnj.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
- package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D7zMmWi2.mjs} +2 -2
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-BeMCmin1.mjs} +6 -5
- package/dist/{transport-BykRfpyy.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-DNEfeMaU.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-DNEfeMaU.d.mts.map} +1 -1
- package/dist/types-4fVtCIm0.mjs +68 -0
- package/dist/types-4fVtCIm0.mjs.map +1 -0
- package/dist/{types-CnZYHyLW.d.mts → types-BSyXeCFW.d.mts} +24 -2
- package/dist/{types-CnZYHyLW.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-BuBIptGk.d.mts} +80 -106
- package/dist/types-BuBIptGk.d.mts.map +1 -0
- package/dist/{types-BH2L167P.mjs → types-CDbKp7ND.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
- package/dist/{types-DDS4MxsT.mjs → types-CIOg5AR8.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-CJsYGpco.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
- package/dist/types-CRxNbK-Z.mjs +68 -0
- package/dist/types-CRxNbK-Z.mjs.map +1 -0
- package/dist/{types-C2v0c34j.d.mts → types-CrtWgIvl.d.mts} +1 -1
- package/dist/{types-C2v0c34j.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-M78DQ1lx.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
- package/dist/{validate-CqsNItbt.mjs → validate-Baqf0slj.mjs} +3 -3
- package/dist/{validate-CqsNItbt.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
- package/dist/{validate-kM8Pjuf7.d.mts → validate-BfQh_C_y.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
- package/dist/validation-BfEI7tNe.mjs +144 -0
- package/dist/validation-BfEI7tNe.mjs.map +1 -0
- package/dist/version-DoxrVdYf.mjs +7 -0
- package/dist/{version-BnTKdfam.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
- package/dist/zod-generator-CC0xNe_K.mjs +132 -0
- package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
- package/locals.d.ts +1 -6
- package/package.json +21 -7
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +19 -4
- package/src/api/handlers/content.ts +151 -4
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +131 -22
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +54 -5
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +30 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +49 -1
- package/src/astro/integration/virtual-modules.ts +73 -2
- package/src/astro/integration/vite-config.ts +49 -13
- package/src/astro/middleware/auth.ts +34 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +41 -10
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- 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]/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]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +9 -0
- 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/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/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/index.ts +0 -1
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +32 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/passkey-config.ts +35 -13
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/types.ts +114 -4
- 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 +31 -9
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/login.ts +8 -1
- 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/EmDashImage.astro +7 -6
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +6 -4
- package/src/components/Image.astro +9 -4
- package/src/components/InlinePortableTextEditor.tsx +106 -19
- package/src/components/LiveSearch.astro +5 -14
- 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/runner.ts +156 -23
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +76 -52
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +26 -19
- package/src/database/repositories/taxonomy.ts +40 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +388 -247
- package/src/index.ts +14 -1
- package/src/loader.ts +30 -6
- package/src/mcp/server.ts +781 -141
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- 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/email-console.ts +10 -3
- package/src/plugins/hooks.ts +34 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +49 -2
- package/src/plugins/types.ts +174 -13
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +149 -6
- package/src/redirects/cache.ts +38 -18
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +97 -5
- package/src/schema/zod-generator.ts +27 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +2 -0
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/storage/s3.ts +12 -6
- package/src/taxonomies/index.ts +11 -1
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/bylines-hPTW79hw.mjs +0 -157
- package/dist/bylines-hPTW79hw.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-D7J5y73J.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/placeholder-C-fk5hYI.mjs.map +0 -1
- package/dist/query-g4Ug-9j9.mjs.map +0 -1
- package/dist/redirect-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-BR2xKwhn.d.mts +0 -34
- package/dist/runner-BR2xKwhn.d.mts.map +0 -1
- package/dist/search-B0effn3j.mjs.map +0 -1
- package/dist/taxonomies-K2z0Uhnj.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-BnTKdfam.mjs +0 -7
|
@@ -122,16 +122,27 @@ export async function handleTaxonomyList(
|
|
|
122
122
|
db: Kysely<Database>,
|
|
123
123
|
): Promise<ApiResult<TaxonomyListResponse>> {
|
|
124
124
|
try {
|
|
125
|
-
const rows = await
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const [rows, collectionRows] = await Promise.all([
|
|
126
|
+
db.selectFrom("_emdash_taxonomy_defs").selectAll().execute(),
|
|
127
|
+
db.selectFrom("_emdash_collections").select("slug").execute(),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Filter orphan collection references on read so the response stays
|
|
131
|
+
// consistent with `schema_list_collections`. Storage is untouched —
|
|
132
|
+
// re-creating the collection re-links automatically.
|
|
133
|
+
const realCollections = new Set(collectionRows.map((r) => r.slug));
|
|
134
|
+
|
|
135
|
+
const taxonomies: TaxonomyDef[] = rows.map((row) => {
|
|
136
|
+
const stored: string[] = row.collections ? JSON.parse(row.collections) : [];
|
|
137
|
+
return {
|
|
138
|
+
id: row.id,
|
|
139
|
+
name: row.name,
|
|
140
|
+
label: row.label,
|
|
141
|
+
labelSingular: row.label_singular ?? undefined,
|
|
142
|
+
hierarchical: row.hierarchical === 1,
|
|
143
|
+
collections: stored.filter((slug) => realCollections.has(slug)),
|
|
144
|
+
};
|
|
145
|
+
});
|
|
135
146
|
|
|
136
147
|
return { success: true, data: { taxonomies } };
|
|
137
148
|
} catch {
|
|
@@ -260,12 +271,9 @@ export async function handleTermList(
|
|
|
260
271
|
const repo = new TaxonomyRepository(db);
|
|
261
272
|
const terms = await repo.findByName(taxonomyName);
|
|
262
273
|
|
|
263
|
-
//
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const count = await repo.countEntriesWithTerm(term.id);
|
|
267
|
-
counts.set(term.id, count);
|
|
268
|
-
}
|
|
274
|
+
// Batch count entries per term in a single query (replaces N+1 pattern)
|
|
275
|
+
const termIds = terms.map((t) => t.id);
|
|
276
|
+
const counts = await repo.countEntriesForTerms(termIds);
|
|
269
277
|
|
|
270
278
|
const termData: TermWithCount[] = terms.map((term) => ({
|
|
271
279
|
id: term.id,
|
|
@@ -290,6 +298,84 @@ export async function handleTermList(
|
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Validate a parent term reference for create/update.
|
|
303
|
+
*
|
|
304
|
+
* Returns `null` on success or a structured error message that callers
|
|
305
|
+
* wrap in their own ApiResult.
|
|
306
|
+
*
|
|
307
|
+
* - `parentId === undefined` -> no-op (no parent change requested).
|
|
308
|
+
* - `parentId === null` -> caller intends to detach; no-op here.
|
|
309
|
+
* - parent must exist (FK exists -> term row not soft-deleted).
|
|
310
|
+
* - parent must live in the same taxonomy.
|
|
311
|
+
* - if `termId` is provided (update path), reject `parentId === termId`
|
|
312
|
+
* (self-parent) and walk up the parent chain to detect cycles.
|
|
313
|
+
*/
|
|
314
|
+
async function validateParentTerm(
|
|
315
|
+
repo: TaxonomyRepository,
|
|
316
|
+
taxonomyName: string,
|
|
317
|
+
termId: string | undefined,
|
|
318
|
+
parentId: string | null | undefined,
|
|
319
|
+
): Promise<{ code: "VALIDATION_ERROR"; message: string } | null> {
|
|
320
|
+
if (parentId === undefined || parentId === null) return null;
|
|
321
|
+
|
|
322
|
+
if (termId !== undefined && parentId === termId) {
|
|
323
|
+
return {
|
|
324
|
+
code: "VALIDATION_ERROR",
|
|
325
|
+
message: "A term cannot be its own parent",
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const parent = await repo.findById(parentId);
|
|
330
|
+
if (!parent) {
|
|
331
|
+
return {
|
|
332
|
+
code: "VALIDATION_ERROR",
|
|
333
|
+
message: `Parent term '${parentId}' not found`,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (parent.name !== taxonomyName) {
|
|
337
|
+
return {
|
|
338
|
+
code: "VALIDATION_ERROR",
|
|
339
|
+
message: `Parent term '${parentId}' belongs to taxonomy '${parent.name}', not '${taxonomyName}'`,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Walk up the parent chain. Two checks fold into one walk:
|
|
344
|
+
// - Cycle detection (only on update — a non-existent term-being-
|
|
345
|
+
// created can't be its own ancestor): if the walk revisits termId
|
|
346
|
+
// the proposed parent makes the term a descendant of itself.
|
|
347
|
+
// - Depth bound: refuse to extend a chain past MAX_DEPTH ancestors.
|
|
348
|
+
// Runs on both create and update so a malicious or buggy caller
|
|
349
|
+
// can't grow the tree without limit.
|
|
350
|
+
//
|
|
351
|
+
// The depth-exceeded error fires only when we hit the limit AND there
|
|
352
|
+
// was still chain to walk — a legitimate chain of exactly MAX_DEPTH
|
|
353
|
+
// ancestors exits with `cursor === null` and is accepted.
|
|
354
|
+
const MAX_DEPTH = 100;
|
|
355
|
+
let cursor: string | null = parent.parentId;
|
|
356
|
+
let steps = 0;
|
|
357
|
+
while (cursor !== null && steps < MAX_DEPTH) {
|
|
358
|
+
if (termId !== undefined && cursor === termId) {
|
|
359
|
+
return {
|
|
360
|
+
code: "VALIDATION_ERROR",
|
|
361
|
+
message: "Cycle detected: cannot make a descendant the parent",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const next = await repo.findById(cursor);
|
|
365
|
+
if (!next) break;
|
|
366
|
+
cursor = next.parentId;
|
|
367
|
+
steps++;
|
|
368
|
+
}
|
|
369
|
+
if (cursor !== null && steps >= MAX_DEPTH) {
|
|
370
|
+
return {
|
|
371
|
+
code: "VALIDATION_ERROR",
|
|
372
|
+
message: "Parent chain exceeds maximum depth",
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
293
379
|
/**
|
|
294
380
|
* Create a new term in a taxonomy
|
|
295
381
|
*/
|
|
@@ -304,6 +390,10 @@ export async function handleTermCreate(
|
|
|
304
390
|
|
|
305
391
|
const repo = new TaxonomyRepository(db);
|
|
306
392
|
|
|
393
|
+
// Coerce empty-string parentId to undefined (treat as "no parent").
|
|
394
|
+
const parentId =
|
|
395
|
+
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
396
|
+
|
|
307
397
|
// Check for slug conflict
|
|
308
398
|
const existing = await repo.findBySlug(taxonomyName, input.slug);
|
|
309
399
|
if (existing) {
|
|
@@ -316,11 +406,18 @@ export async function handleTermCreate(
|
|
|
316
406
|
};
|
|
317
407
|
}
|
|
318
408
|
|
|
409
|
+
// Validate parentId: must exist AND belong to the same taxonomy.
|
|
410
|
+
// (Cycle check is N/A on create — the term doesn't exist yet.)
|
|
411
|
+
const parentError = await validateParentTerm(repo, taxonomyName, undefined, parentId);
|
|
412
|
+
if (parentError) {
|
|
413
|
+
return { success: false, error: parentError };
|
|
414
|
+
}
|
|
415
|
+
|
|
319
416
|
const term = await repo.create({
|
|
320
417
|
name: taxonomyName,
|
|
321
418
|
slug: input.slug,
|
|
322
419
|
label: input.label,
|
|
323
|
-
parentId:
|
|
420
|
+
parentId: parentId ?? undefined,
|
|
324
421
|
data: input.description ? { description: input.description } : undefined,
|
|
325
422
|
});
|
|
326
423
|
|
|
@@ -426,24 +523,36 @@ export async function handleTermUpdate(
|
|
|
426
523
|
};
|
|
427
524
|
}
|
|
428
525
|
|
|
526
|
+
// Coerce empty-string slug/parentId to undefined (treat as "no change").
|
|
527
|
+
// `null` parentId is a valid request meaning "detach from parent".
|
|
528
|
+
const newSlug = input.slug === "" || input.slug === undefined ? undefined : input.slug;
|
|
529
|
+
const newParentId =
|
|
530
|
+
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
531
|
+
|
|
429
532
|
// Check if new slug conflicts
|
|
430
|
-
if (
|
|
431
|
-
const existing = await repo.findBySlug(taxonomyName,
|
|
533
|
+
if (newSlug !== undefined && newSlug !== termSlug) {
|
|
534
|
+
const existing = await repo.findBySlug(taxonomyName, newSlug);
|
|
432
535
|
if (existing && existing.id !== term.id) {
|
|
433
536
|
return {
|
|
434
537
|
success: false,
|
|
435
538
|
error: {
|
|
436
539
|
code: "CONFLICT",
|
|
437
|
-
message: `Term with slug '${
|
|
540
|
+
message: `Term with slug '${newSlug}' already exists in taxonomy '${taxonomyName}'`,
|
|
438
541
|
},
|
|
439
542
|
};
|
|
440
543
|
}
|
|
441
544
|
}
|
|
442
545
|
|
|
546
|
+
// Validate parentId: existence, same-taxonomy, no self-parent, no cycle.
|
|
547
|
+
const parentError = await validateParentTerm(repo, taxonomyName, term.id, newParentId);
|
|
548
|
+
if (parentError) {
|
|
549
|
+
return { success: false, error: parentError };
|
|
550
|
+
}
|
|
551
|
+
|
|
443
552
|
const updated = await repo.update(term.id, {
|
|
444
|
-
slug:
|
|
553
|
+
slug: newSlug,
|
|
445
554
|
label: input.label,
|
|
446
|
-
parentId:
|
|
555
|
+
parentId: newParentId,
|
|
447
556
|
data: input.description !== undefined ? { description: input.description } : undefined,
|
|
448
557
|
});
|
|
449
558
|
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-level validation for content create / update.
|
|
3
|
+
*
|
|
4
|
+
* Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)
|
|
5
|
+
* into the handler boundary so REST and MCP both get the same enforcement:
|
|
6
|
+
*
|
|
7
|
+
* - required fields must be present and non-empty
|
|
8
|
+
* - select / multiSelect values must match the configured options
|
|
9
|
+
* - reference fields must resolve to a real, non-trashed target
|
|
10
|
+
*
|
|
11
|
+
* Errors surface as `{ code: "VALIDATION_ERROR", message }` with all
|
|
12
|
+
* offending fields listed in one message so callers can fix everything in
|
|
13
|
+
* a single round trip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { sql, type Kysely } from "kysely";
|
|
17
|
+
|
|
18
|
+
import type { Database } from "../../database/types.js";
|
|
19
|
+
import { validateIdentifier } from "../../database/validate.js";
|
|
20
|
+
import { SchemaRegistry } from "../../schema/registry.js";
|
|
21
|
+
import type { Field } from "../../schema/types.js";
|
|
22
|
+
import { generateZodSchema } from "../../schema/zod-generator.js";
|
|
23
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
24
|
+
import { isMissingTableError } from "../../utils/db-errors.js";
|
|
25
|
+
|
|
26
|
+
type ValidationResult =
|
|
27
|
+
| { ok: true }
|
|
28
|
+
| { ok: false; error: { code: "VALIDATION_ERROR" | "COLLECTION_NOT_FOUND"; message: string } };
|
|
29
|
+
|
|
30
|
+
/** Treat `undefined`, `null`, and `""` as "not set". */
|
|
31
|
+
function isMissing(value: unknown): boolean {
|
|
32
|
+
return value === undefined || value === null || value === "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the target collection slug for a reference field.
|
|
37
|
+
*
|
|
38
|
+
* Schema-defined reference fields (the static `reference()` factory in
|
|
39
|
+
* `fields/reference.ts`) put the target in `options.collection`. The MCP
|
|
40
|
+
* `schema_create_field` tool also puts it there. Tests and some admin paths
|
|
41
|
+
* stash it inside `validation.collection` directly; we accept both.
|
|
42
|
+
*/
|
|
43
|
+
function getReferenceTargetCollection(field: Field): string | undefined {
|
|
44
|
+
const fromOptions = field.options?.collection;
|
|
45
|
+
if (typeof fromOptions === "string" && fromOptions.length > 0) return fromOptions;
|
|
46
|
+
const validation = field.validation;
|
|
47
|
+
if (validation && "collection" in validation) {
|
|
48
|
+
const fromValidation: unknown = (validation as { collection?: unknown }).collection;
|
|
49
|
+
if (typeof fromValidation === "string" && fromValidation.length > 0) return fromValidation;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format a Zod issue path into a human-readable field reference, e.g.
|
|
56
|
+
* `tags`, `tags.1`, `image.alt`.
|
|
57
|
+
*/
|
|
58
|
+
function formatIssuePath(path: ReadonlyArray<PropertyKey>): string {
|
|
59
|
+
if (path.length === 0) return "(root)";
|
|
60
|
+
return path.map((seg) => String(seg)).join(".");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate `data` against the collection's field definitions.
|
|
65
|
+
*
|
|
66
|
+
* `partial: true` switches Zod into partial mode so updates can include
|
|
67
|
+
* only the fields being changed without tripping required-field errors on
|
|
68
|
+
* fields the caller didn't touch. Required fields that ARE present in
|
|
69
|
+
* partial-mode data still get the empty-string check below.
|
|
70
|
+
*/
|
|
71
|
+
export async function validateContentData(
|
|
72
|
+
db: Kysely<Database>,
|
|
73
|
+
collection: string,
|
|
74
|
+
data: Record<string, unknown>,
|
|
75
|
+
options: { partial?: boolean } = {},
|
|
76
|
+
): Promise<ValidationResult> {
|
|
77
|
+
const registry = new SchemaRegistry(db);
|
|
78
|
+
const collectionWithFields = await registry.getCollectionWithFields(collection);
|
|
79
|
+
if (!collectionWithFields) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "COLLECTION_NOT_FOUND",
|
|
84
|
+
message: `Collection '${collection}' not found`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const issues: string[] = [];
|
|
90
|
+
|
|
91
|
+
// Detect unknown keys explicitly so callers get a useful error rather
|
|
92
|
+
// than silently dropped data. Leading-underscore keys (e.g. `_slug`,
|
|
93
|
+
// `_rev`) are reserved for internal handler/runtime use and aren't real
|
|
94
|
+
// fields; skip them.
|
|
95
|
+
const knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));
|
|
96
|
+
for (const key of Object.keys(data)) {
|
|
97
|
+
if (key.startsWith("_")) continue;
|
|
98
|
+
if (!knownFields.has(key)) {
|
|
99
|
+
issues.push(`${key}: unknown field on collection '${collection}'`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Zod handles type, enum, length and missing-required (in non-partial
|
|
104
|
+
// mode) checks. Empty-string handling for required string fields is
|
|
105
|
+
// done as a separate pass below since Zod's `z.string()` accepts "".
|
|
106
|
+
const baseSchema = generateZodSchema(collectionWithFields);
|
|
107
|
+
const schema = options.partial ? baseSchema.partial() : baseSchema;
|
|
108
|
+
const parsed = schema.safeParse(data);
|
|
109
|
+
if (!parsed.success) {
|
|
110
|
+
for (const issue of parsed.error.issues) {
|
|
111
|
+
issues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Empty-string-on-required check. In create mode (partial=false) Zod
|
|
116
|
+
// already catches missing/null for required fields, but `z.string()`
|
|
117
|
+
// happily accepts "". In update mode (partial=true) the field is only
|
|
118
|
+
// checked if it's present in `data`.
|
|
119
|
+
for (const field of collectionWithFields.fields) {
|
|
120
|
+
if (!field.required) continue;
|
|
121
|
+
const present = Object.hasOwn(data, field.slug);
|
|
122
|
+
if (options.partial && !present) continue;
|
|
123
|
+
if (data[field.slug] === "") {
|
|
124
|
+
issues.push(`${field.slug}: required (empty value not allowed)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Reference target existence. Only check fields that:
|
|
129
|
+
// - have a value (non-missing) in `data`
|
|
130
|
+
// - have a resolvable target collection
|
|
131
|
+
// - in partial mode: are present in `data`
|
|
132
|
+
// Batch one IN-query per target collection to keep round-trips low.
|
|
133
|
+
const refsByTarget = new Map<string, { field: string; id: string }[]>();
|
|
134
|
+
for (const field of collectionWithFields.fields) {
|
|
135
|
+
if (field.type !== "reference") continue;
|
|
136
|
+
if (options.partial && !Object.hasOwn(data, field.slug)) continue;
|
|
137
|
+
const value = data[field.slug];
|
|
138
|
+
if (isMissing(value)) continue;
|
|
139
|
+
if (typeof value !== "string") continue; // Zod will have flagged this already
|
|
140
|
+
const target = getReferenceTargetCollection(field);
|
|
141
|
+
if (!target) continue;
|
|
142
|
+
const list = refsByTarget.get(target) ?? [];
|
|
143
|
+
list.push({ field: field.slug, id: value });
|
|
144
|
+
refsByTarget.set(target, list);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const [target, refs] of refsByTarget) {
|
|
148
|
+
// Validate the target collection slug before interpolating into raw
|
|
149
|
+
// SQL — defense-in-depth even though slugs are already validated at
|
|
150
|
+
// schema-create time.
|
|
151
|
+
try {
|
|
152
|
+
validateIdentifier(target, "reference target collection");
|
|
153
|
+
} catch {
|
|
154
|
+
for (const ref of refs) {
|
|
155
|
+
issues.push(`${ref.field}: invalid reference target collection '${target}'`);
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const ids = [...new Set(refs.map((r) => r.id))];
|
|
161
|
+
const tableName = `ec_${target}`;
|
|
162
|
+
|
|
163
|
+
// Chunk the IN clause to stay below D1's bind-parameter limit. One
|
|
164
|
+
// reference per request is the common case today; chunking makes the
|
|
165
|
+
// helper safe if a future multiSelect-of-references is added.
|
|
166
|
+
const found = new Set<string>();
|
|
167
|
+
let targetTableMissing = false;
|
|
168
|
+
for (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {
|
|
169
|
+
try {
|
|
170
|
+
const rows = await sql<{ id: string }>`
|
|
171
|
+
SELECT id FROM ${sql.ref(tableName)}
|
|
172
|
+
WHERE id IN (${sql.join(idChunk)})
|
|
173
|
+
AND deleted_at IS NULL
|
|
174
|
+
`.execute(db);
|
|
175
|
+
for (const row of rows.rows) {
|
|
176
|
+
found.add(row.id);
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Missing table = the target collection table doesn't exist
|
|
180
|
+
// (orphan reference). Treat all those references as missing.
|
|
181
|
+
// Any other DB error (permissions, connection, syntax) must
|
|
182
|
+
// propagate — silently dropping data integrity errors as
|
|
183
|
+
// "not found" is exactly the bug F5 fixes.
|
|
184
|
+
if (isMissingTableError(error)) {
|
|
185
|
+
targetTableMissing = true;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (targetTableMissing) {
|
|
192
|
+
for (const ref of refs) {
|
|
193
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
194
|
+
}
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const ref of refs) {
|
|
198
|
+
if (!found.has(ref.id)) {
|
|
199
|
+
issues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (issues.length === 0) return { ok: true };
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: {
|
|
208
|
+
code: "VALIDATION_ERROR",
|
|
209
|
+
message: issues.join("; "),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
reorderWidgetsBody,
|
|
123
123
|
updateWidgetBody,
|
|
124
124
|
widgetAreaSchema,
|
|
125
|
+
widgetAreaWithWidgetsAndCountSchema,
|
|
125
126
|
widgetAreaWithWidgetsSchema,
|
|
126
127
|
widgetSchema,
|
|
127
128
|
} from "../schemas/widgets.js";
|
|
@@ -1581,7 +1582,9 @@ const widgetPaths = {
|
|
|
1581
1582
|
description: "Widget area list",
|
|
1582
1583
|
content: {
|
|
1583
1584
|
[JSON_CONTENT]: {
|
|
1584
|
-
schema: successEnvelope(
|
|
1585
|
+
schema: successEnvelope(
|
|
1586
|
+
z.object({ items: z.array(widgetAreaWithWidgetsAndCountSchema) }),
|
|
1587
|
+
),
|
|
1585
1588
|
},
|
|
1586
1589
|
},
|
|
1587
1590
|
},
|
package/src/api/public-url.ts
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* Workers-safe: no Node.js imports.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** Minimal config shape — avoids importing the full EmDashConfig type tree. */
|
|
13
|
+
interface SiteUrlConfig {
|
|
14
|
+
siteUrl?: string;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Resolve siteUrl from runtime environment variables.
|
|
@@ -26,9 +29,10 @@ import type { EmDashConfig } from "../astro/integration/runtime.js";
|
|
|
26
29
|
*/
|
|
27
30
|
let _envSiteUrl: string | undefined | null = null;
|
|
28
31
|
|
|
29
|
-
/** @internal Reset cached env
|
|
30
|
-
export function
|
|
32
|
+
/** @internal Reset cached env values — test-only. */
|
|
33
|
+
export function _resetEnvCache(): void {
|
|
31
34
|
_envSiteUrl = null;
|
|
35
|
+
_envAllowedOrigins = null;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function getEnvSiteUrl(): string | undefined {
|
|
@@ -67,10 +71,55 @@ function getEnvSiteUrl(): string | undefined {
|
|
|
67
71
|
* @param config The EmDash config (from `locals.emdash?.config`)
|
|
68
72
|
* @returns Origin string, e.g. `"https://mysite.example.com"`
|
|
69
73
|
*/
|
|
70
|
-
export function getPublicOrigin(url: URL, config?:
|
|
74
|
+
export function getPublicOrigin(url: URL, config?: SiteUrlConfig): string {
|
|
71
75
|
return config?.siteUrl || getEnvSiteUrl() || url.origin;
|
|
72
76
|
}
|
|
73
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
|
+
|
|
74
123
|
/**
|
|
75
124
|
* Build a full public URL by appending a path to the public origin.
|
|
76
125
|
*
|
|
@@ -79,6 +128,6 @@ export function getPublicOrigin(url: URL, config?: EmDashConfig): string {
|
|
|
79
128
|
* @param path Path to append (must start with `/`)
|
|
80
129
|
* @returns Full URL string, e.g. `"https://mysite.example.com/_emdash/admin/login"`
|
|
81
130
|
*/
|
|
82
|
-
export function getPublicUrl(url: URL, config:
|
|
131
|
+
export function getPublicUrl(url: URL, config: SiteUrlConfig | undefined, path: string): string {
|
|
83
132
|
return `${getPublicOrigin(url, config)}${path}`;
|
|
84
133
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API route utilities for auth provider routes.
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the utilities that auth provider route handlers
|
|
5
|
+
* need from core. Auth providers (plugins) import these via `emdash/api/route-utils`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { apiError, apiSuccess, handleError } from "./error.js";
|
|
9
|
+
export { parseBody, parseQuery, isParseError } from "./parse.js";
|
|
10
|
+
export type { ParseResult } from "./parse.js";
|
|
11
|
+
export { finalizeSetup } from "./setup-complete.js";
|
|
12
|
+
export { OptionsRepository } from "../database/repositories/options.js";
|
|
13
|
+
export { getAuthProviderStorage } from "./auth-storage.js";
|
|
14
|
+
export { getPublicOrigin } from "./public-url.js";
|
|
@@ -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
|
|
|
@@ -22,7 +22,7 @@ export const roleLevel = z.coerce
|
|
|
22
22
|
/** Pagination query params — cursor-based */
|
|
23
23
|
export const cursorPaginationQuery = z
|
|
24
24
|
.object({
|
|
25
|
-
cursor: z.string().optional().meta({ description: "Opaque cursor for pagination" }),
|
|
25
|
+
cursor: z.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }),
|
|
26
26
|
limit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({
|
|
27
27
|
description: "Maximum number of items to return (1-100, default 50)",
|
|
28
28
|
}),
|
|
@@ -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(),
|
|
@@ -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" });
|
package/src/api/schemas/setup.ts
CHANGED
|
@@ -35,3 +35,11 @@ export const setupAdminBody = z.object({
|
|
|
35
35
|
export const setupAdminVerifyBody = z.object({
|
|
36
36
|
credential: registrationCredential,
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
export const atprotoLoginBody = z.object({
|
|
40
|
+
handle: z.string().trim().min(1),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const setupAtprotoAdminBody = z.object({
|
|
44
|
+
handle: z.string().trim().min(1),
|
|
45
|
+
});
|
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" });
|
|
@@ -60,16 +60,12 @@ export const widgetAreaSchema = z
|
|
|
60
60
|
export const widgetSchema = z
|
|
61
61
|
.object({
|
|
62
62
|
id: z.string(),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
component_props: z.string().nullable(),
|
|
70
|
-
sort_order: z.number().int(),
|
|
71
|
-
created_at: z.string(),
|
|
72
|
-
updated_at: z.string(),
|
|
63
|
+
type: widgetType,
|
|
64
|
+
title: z.string().optional(),
|
|
65
|
+
content: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
66
|
+
menuName: z.string().optional(),
|
|
67
|
+
componentId: z.string().optional(),
|
|
68
|
+
componentProps: z.record(z.string(), z.unknown()).optional(),
|
|
73
69
|
})
|
|
74
70
|
.meta({ id: "Widget" });
|
|
75
71
|
|
|
@@ -78,3 +74,9 @@ export const widgetAreaWithWidgetsSchema = widgetAreaSchema
|
|
|
78
74
|
widgets: z.array(widgetSchema),
|
|
79
75
|
})
|
|
80
76
|
.meta({ id: "WidgetAreaWithWidgets" });
|
|
77
|
+
|
|
78
|
+
export const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema
|
|
79
|
+
.extend({
|
|
80
|
+
widgetCount: z.number().int(),
|
|
81
|
+
})
|
|
82
|
+
.meta({ id: "WidgetAreaWithWidgetsAndCount" });
|