emdash 0.8.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-BKSf3T9R.d.mts → adapters-DoNJiveC.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-DoNJiveC.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-BzltprvY.mjs} +85 -135
- 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 +110 -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 +16 -59
- 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 +72 -124
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +26 -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-Chbr2GoP.mjs → byline-BSaNL1w7.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-BSaNL1w7.mjs.map} +1 -1
- 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 +224 -30
- 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-BcQPYxdV.mjs → content-8lOYF0pr.mjs} +32 -15
- package/dist/{content-BcQPYxdV.mjs.map → content-8lOYF0pr.mjs.map} +1 -1
- 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-WRezodiz.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-WRezodiz.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-D8ksjWhO.mjs} +1 -1
- package/dist/{default-DCVqE5ib.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-zG5T1UGA.mjs → error-D_-tqP-I.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-D_-tqP-I.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-BFRaVcD6.d.mts} +94 -34
- package/dist/index-BFRaVcD6.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -27
- package/dist/{load-CyEoextb.mjs → load-DDqMMvZL.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-DDqMMvZL.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-CKLbBnhK.mjs} +27 -7
- package/dist/loader-CKLbBnhK.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-DqWNC3lM.mjs} +33 -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-D8FbNsl0.mjs → media-BW32b4gi.mjs} +2 -2
- package/dist/{media-D8FbNsl0.mjs.map → media-BW32b4gi.mjs.map} +1 -1
- package/dist/{mode-BnAOqItE.mjs → mode-ier8jbBk.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-ier8jbBk.mjs.map} +1 -1
- 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-D29tWZ7o.d.mts → placeholder-BE4o_2dc.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.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-C-fk5hYI.mjs.map → placeholder-CIJejMlK.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-DByxYjUw.mjs +51 -0
- package/dist/public-url-DByxYjUw.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Cg9ZKRQ0.mjs} +114 -16
- package/dist/query-Cg9ZKRQ0.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-BhUBKRc1.mjs} +11 -6
- package/dist/redirect-BhUBKRc1.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Dw70ChxB.mjs} +38 -4
- package/dist/registry-Dw70ChxB.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-B-bmkipQ.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-B-bmkipQ.mjs.map} +1 -1
- 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-BoZYFuUk.mjs → search-dOGEccMa.mjs} +129 -83
- 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.mjs +1 -1
- package/dist/{taxonomies-B4IAshV8.mjs → taxonomies-ZlRtD6AG.mjs} +14 -7
- package/dist/taxonomies-ZlRtD6AG.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-D7zMmWi2.mjs} +2 -2
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-D7zMmWi2.mjs.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-BeMCmin1.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-BeMCmin1.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DNEfeMaU.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.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-BmPPSUEx.d.mts → types-BSyXeCFW.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-BSyXeCFW.d.mts.map} +1 -1
- package/dist/{types-i36XcA_X.d.mts → types-BuBIptGk.d.mts} +65 -134
- package/dist/types-BuBIptGk.d.mts.map +1 -0
- package/dist/{types-CgqmmMJB.mjs → types-CDbKp7ND.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-CDbKp7ND.mjs.map} +1 -1
- package/dist/{types-Bm1dn-q3.mjs → types-CIOg5AR8.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-CIOg5AR8.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-CJsYGpco.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-CJsYGpco.d.mts.map} +1 -1
- package/dist/{types-BIgulNsW.mjs → types-CRxNbK-Z.mjs} +2 -2
- package/dist/{types-BIgulNsW.mjs.map → types-CRxNbK-Z.mjs.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-CrtWgIvl.d.mts} +1 -1
- package/dist/{types-CS8FIX7L.d.mts.map → types-CrtWgIvl.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-M78DQ1lx.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-M78DQ1lx.d.mts.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-Baqf0slj.mjs} +3 -3
- package/dist/{validate-CxVsLehf.mjs.map → validate-Baqf0slj.mjs.map} +1 -1
- package/dist/{validate-DHxmpFJt.d.mts → validate-BfQh_C_y.d.mts} +4 -4
- package/dist/{validate-DHxmpFJt.d.mts.map → validate-BfQh_C_y.d.mts.map} +1 -1
- package/dist/{validation-C-ZpN2GI.mjs → validation-BfEI7tNe.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-BfEI7tNe.mjs.map} +1 -1
- package/dist/version-DoxrVdYf.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-DoxrVdYf.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-CC0xNe_K.mjs} +4 -4
- package/dist/zod-generator-CC0xNe_K.mjs.map +1 -0
- 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 +29 -1
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +3 -6
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/sections.ts +3 -3
- 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/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]/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-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/setup/admin-verify.ts +6 -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/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/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/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/runner.ts +156 -23
- package/src/database/repositories/content.ts +36 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +26 -0
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/index.ts +9 -0
- package/src/loader.ts +24 -1
- package/src/mcp/server.ts +103 -36
- 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 +27 -5
- package/src/seed/apply.ts +2 -0
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +11 -1
- 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/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/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
- package/dist/zod-generator-CpwccCIv.mjs.map +0 -1
package/src/mcp/server.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { canActOnOwn, hasPermission, Role } from "@emdash-cms/auth";
|
|
|
14
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
|
+
import { contentBylineInputSchema, contentSeoInput } from "#api/schemas.js";
|
|
18
|
+
|
|
17
19
|
import type { EmDashHandlers } from "../astro/types.js";
|
|
18
20
|
import { hasScope } from "../auth/api-tokens.js";
|
|
19
21
|
|
|
@@ -623,7 +625,10 @@ export function createMcpServer(): McpServer {
|
|
|
623
625
|
"Update an existing content item. Only include fields you want to change " +
|
|
624
626
|
"in the 'data' object — unspecified fields are left unchanged. Pass the " +
|
|
625
627
|
"_rev token from content_get to enable optimistic concurrency checking " +
|
|
626
|
-
"(the update fails if the item was modified since you read it)."
|
|
628
|
+
"(the update fails if the item was modified since you read it). " +
|
|
629
|
+
"`seo` and `bylines` are persisted alongside the field updates in a " +
|
|
630
|
+
"single transaction. `publishedAt` requires the content:publish_any " +
|
|
631
|
+
"permission and is useful for migrations or correcting historical dates.",
|
|
627
632
|
inputSchema: z.object({
|
|
628
633
|
collection: z.string().describe("Collection slug"),
|
|
629
634
|
id: z.string().describe("Content item ID or slug"),
|
|
@@ -638,6 +643,28 @@ export function createMcpServer(): McpServer {
|
|
|
638
643
|
.describe(
|
|
639
644
|
"New status. Setting to 'published' requires publish permission. Setting to 'draft' unpublishes the item and also requires publish permission.",
|
|
640
645
|
),
|
|
646
|
+
// Reuse the REST schema rather than redefining inline. The REST schema's
|
|
647
|
+
// `canonical` field is gated through `httpUrl` (validates the URL parses
|
|
648
|
+
// AND has an http(s) scheme) which rejects javascript:/data: URIs that
|
|
649
|
+
// would otherwise become stored XSS in the rendered <link rel="canonical">.
|
|
650
|
+
// Inlining a looser shape here would let MCP callers bypass that.
|
|
651
|
+
seo: contentSeoInput
|
|
652
|
+
.optional()
|
|
653
|
+
.describe(
|
|
654
|
+
"Per-content SEO metadata. Only valid for collections with SEO enabled (see schema_get_collection.hasSeo). Fields not included are left unchanged; pass null to clear.",
|
|
655
|
+
),
|
|
656
|
+
bylines: z
|
|
657
|
+
.array(contentBylineInputSchema)
|
|
658
|
+
.optional()
|
|
659
|
+
.describe(
|
|
660
|
+
"Replace the byline list for this item. The first entry becomes the primary byline. Pass an empty array to clear all bylines.",
|
|
661
|
+
),
|
|
662
|
+
publishedAt: z.iso
|
|
663
|
+
.datetime({ offset: true, message: "must be an ISO 8601 datetime" })
|
|
664
|
+
.nullish()
|
|
665
|
+
.describe(
|
|
666
|
+
"Override the publication timestamp (ISO 8601). Requires content:publish_any permission. Pass null to clear. Useful for content migrations.",
|
|
667
|
+
),
|
|
641
668
|
_rev: z
|
|
642
669
|
.string()
|
|
643
670
|
.optional()
|
|
@@ -647,35 +674,48 @@ export function createMcpServer(): McpServer {
|
|
|
647
674
|
async (args, extra) => {
|
|
648
675
|
requireScope(extra, "content:write");
|
|
649
676
|
requireRole(extra, Role.AUTHOR);
|
|
650
|
-
const { emdash, userId } = getExtra(extra);
|
|
677
|
+
const { emdash, userId, userRole } = getExtra(extra);
|
|
651
678
|
|
|
652
679
|
// Fetch item to check ownership
|
|
653
680
|
const existing = await emdash.handleContentGet(args.collection, args.id);
|
|
654
681
|
if (!existing.success) {
|
|
655
682
|
return unwrap(existing);
|
|
656
683
|
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
684
|
+
const ownerId = extractContentAuthorId(existing.data);
|
|
685
|
+
requireOwnership(extra, ownerId, "content:edit_own", "content:edit_any");
|
|
686
|
+
|
|
687
|
+
// Writing publishedAt directly (incl. clearing to null) overwrites
|
|
688
|
+
// historical record — gate behind publish_any, mirroring the REST PUT
|
|
689
|
+
// route. Status-driven publishes are gated separately below.
|
|
690
|
+
if (args.publishedAt !== undefined) {
|
|
691
|
+
const user = { id: userId, role: userRole };
|
|
692
|
+
if (!hasPermission(user, "content:publish_any" as Permission)) {
|
|
693
|
+
throw new EmDashAuthError(
|
|
694
|
+
"Setting publishedAt requires content:publish_any permission",
|
|
695
|
+
"INSUFFICIENT_PERMISSIONS",
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
663
699
|
|
|
664
700
|
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
665
701
|
|
|
666
702
|
// Status transitions route through dedicated handlers for proper revision management
|
|
667
703
|
if (args.status === "published") {
|
|
668
|
-
requireOwnership(
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
704
|
+
requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
|
|
705
|
+
if (
|
|
706
|
+
args.data ||
|
|
707
|
+
args.slug ||
|
|
708
|
+
args.seo !== undefined ||
|
|
709
|
+
args.bylines !== undefined ||
|
|
710
|
+
args.publishedAt !== undefined
|
|
711
|
+
) {
|
|
675
712
|
const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
676
713
|
data: args.data,
|
|
677
714
|
slug: args.slug,
|
|
678
715
|
authorId: userId,
|
|
716
|
+
seo: args.seo,
|
|
717
|
+
bylines: args.bylines,
|
|
718
|
+
publishedAt: args.publishedAt,
|
|
679
719
|
_rev: args._rev,
|
|
680
720
|
});
|
|
681
721
|
if (!updateResult.success) return unwrap(updateResult);
|
|
@@ -684,17 +724,21 @@ export function createMcpServer(): McpServer {
|
|
|
684
724
|
}
|
|
685
725
|
|
|
686
726
|
if (args.status === "draft") {
|
|
687
|
-
requireOwnership(
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
727
|
+
requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
|
|
728
|
+
if (
|
|
729
|
+
args.data ||
|
|
730
|
+
args.slug ||
|
|
731
|
+
args.seo !== undefined ||
|
|
732
|
+
args.bylines !== undefined ||
|
|
733
|
+
args.publishedAt !== undefined
|
|
734
|
+
) {
|
|
694
735
|
const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
695
736
|
data: args.data,
|
|
696
737
|
slug: args.slug,
|
|
697
738
|
authorId: userId,
|
|
739
|
+
seo: args.seo,
|
|
740
|
+
bylines: args.bylines,
|
|
741
|
+
publishedAt: args.publishedAt,
|
|
698
742
|
_rev: args._rev,
|
|
699
743
|
});
|
|
700
744
|
if (!updateResult.success) return unwrap(updateResult);
|
|
@@ -707,6 +751,9 @@ export function createMcpServer(): McpServer {
|
|
|
707
751
|
data: args.data,
|
|
708
752
|
slug: args.slug,
|
|
709
753
|
authorId: userId,
|
|
754
|
+
seo: args.seo,
|
|
755
|
+
bylines: args.bylines,
|
|
756
|
+
publishedAt: args.publishedAt,
|
|
710
757
|
_rev: args._rev,
|
|
711
758
|
}),
|
|
712
759
|
);
|
|
@@ -809,31 +856,53 @@ export function createMcpServer(): McpServer {
|
|
|
809
856
|
description:
|
|
810
857
|
"Publish a content item, making it live on the site. Creates a published " +
|
|
811
858
|
"revision from the current draft. Further edits create a new draft without " +
|
|
812
|
-
"affecting the live version until re-published."
|
|
859
|
+
"affecting the live version until re-published. Pass `publishedAt` to " +
|
|
860
|
+
"backdate (e.g. when migrating content from another CMS) — this requires " +
|
|
861
|
+
"the content:publish_any permission. Without `publishedAt`, the existing " +
|
|
862
|
+
"`published_at` is preserved on re-publish (idempotent) and falls back to " +
|
|
863
|
+
"the current time on first publish.",
|
|
813
864
|
inputSchema: z.object({
|
|
814
865
|
collection: z.string().describe("Collection slug"),
|
|
815
866
|
id: z.string().describe("Content item ID or slug"),
|
|
867
|
+
publishedAt: z.iso
|
|
868
|
+
.datetime({ offset: true, message: "must be an ISO 8601 datetime" })
|
|
869
|
+
.optional()
|
|
870
|
+
.describe(
|
|
871
|
+
"Override publication timestamp (ISO 8601). Requires content:publish_any permission. Useful when importing content with original publish dates.",
|
|
872
|
+
),
|
|
816
873
|
}),
|
|
817
874
|
},
|
|
818
875
|
async (args, extra) => {
|
|
819
876
|
requireScope(extra, "content:write");
|
|
820
877
|
requireRole(extra, Role.AUTHOR);
|
|
821
|
-
const
|
|
878
|
+
const { emdash, userId, userRole } = getExtra(extra);
|
|
822
879
|
|
|
823
880
|
// Fetch item to check ownership
|
|
824
|
-
const existing = await
|
|
881
|
+
const existing = await emdash.handleContentGet(args.collection, args.id);
|
|
825
882
|
if (!existing.success) {
|
|
826
883
|
return unwrap(existing);
|
|
827
884
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
)
|
|
885
|
+
const ownerId = extractContentAuthorId(existing.data);
|
|
886
|
+
requireOwnership(extra, ownerId, "content:publish_own", "content:publish_any");
|
|
887
|
+
|
|
888
|
+
// Backdating overwrites historical record — gate behind publish_any
|
|
889
|
+
// regardless of ownership (mirrors the REST PUT route's publishedAt gate).
|
|
890
|
+
if (args.publishedAt !== undefined) {
|
|
891
|
+
const user = { id: userId, role: userRole };
|
|
892
|
+
if (!hasPermission(user, "content:publish_any" as Permission)) {
|
|
893
|
+
throw new EmDashAuthError(
|
|
894
|
+
"Setting publishedAt requires content:publish_any permission",
|
|
895
|
+
"INSUFFICIENT_PERMISSIONS",
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
834
899
|
|
|
835
900
|
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
836
|
-
return unwrap(
|
|
901
|
+
return unwrap(
|
|
902
|
+
await emdash.handleContentPublish(args.collection, resolvedId, {
|
|
903
|
+
publishedAt: args.publishedAt,
|
|
904
|
+
}),
|
|
905
|
+
);
|
|
837
906
|
},
|
|
838
907
|
);
|
|
839
908
|
|
|
@@ -1195,7 +1264,7 @@ export function createMcpServer(): McpServer {
|
|
|
1195
1264
|
// ['drafts', 'revisions'] when undefined; pass through verbatim.
|
|
1196
1265
|
supports: args.supports,
|
|
1197
1266
|
});
|
|
1198
|
-
ec.
|
|
1267
|
+
ec.invalidateUrlPatternCache();
|
|
1199
1268
|
return jsonResult(collection);
|
|
1200
1269
|
} catch (error) {
|
|
1201
1270
|
return respondHandlerError(error, "SCHEMA_CREATE_ERROR");
|
|
@@ -1227,7 +1296,7 @@ export function createMcpServer(): McpServer {
|
|
|
1227
1296
|
const { SchemaRegistry } = await import("../schema/index.js");
|
|
1228
1297
|
const registry = new SchemaRegistry(ec.db);
|
|
1229
1298
|
await registry.deleteCollection(args.slug, { force: args.force });
|
|
1230
|
-
ec.
|
|
1299
|
+
ec.invalidateUrlPatternCache();
|
|
1231
1300
|
return jsonResult({ deleted: args.slug });
|
|
1232
1301
|
} catch (error) {
|
|
1233
1302
|
return respondHandlerError(error, "SCHEMA_DELETE_ERROR");
|
|
@@ -1331,7 +1400,6 @@ export function createMcpServer(): McpServer {
|
|
|
1331
1400
|
searchable: args.searchable,
|
|
1332
1401
|
translatable: args.translatable,
|
|
1333
1402
|
});
|
|
1334
|
-
ec.invalidateManifest();
|
|
1335
1403
|
return jsonResult(field);
|
|
1336
1404
|
} catch (error) {
|
|
1337
1405
|
return respondHandlerError(error, "FIELD_CREATE_ERROR");
|
|
@@ -1360,7 +1428,6 @@ export function createMcpServer(): McpServer {
|
|
|
1360
1428
|
const { SchemaRegistry } = await import("../schema/index.js");
|
|
1361
1429
|
const registry = new SchemaRegistry(ec.db);
|
|
1362
1430
|
await registry.deleteField(args.collection, args.fieldSlug);
|
|
1363
|
-
ec.invalidateManifest();
|
|
1364
1431
|
return jsonResult({ deleted: args.fieldSlug, collection: args.collection });
|
|
1365
1432
|
} catch (error) {
|
|
1366
1433
|
return respondHandlerError(error, "FIELD_DELETE_ERROR");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site identity head injection.
|
|
3
|
+
*
|
|
4
|
+
* Emits first-party `<head>` tags sourced from the user-configured Site
|
|
5
|
+
* Identity. These are rendered alongside, but separate from, the plugin
|
|
6
|
+
* contribution pipeline (`page/metadata.ts`) because:
|
|
7
|
+
*
|
|
8
|
+
* - Site identity is first-party, not plugin-supplied. The contribution
|
|
9
|
+
* pipeline's `isSafeHref` allowlist rejects same-origin paths like
|
|
10
|
+
* `/_emdash/api/media/file/...` (which is correct for sandboxed plugin
|
|
11
|
+
* contributions, but blocks our own favicon URLs).
|
|
12
|
+
* - The data shape is fixed and small. Routing it through a generic
|
|
13
|
+
* deduper buys nothing.
|
|
14
|
+
*
|
|
15
|
+
* Currently emits only `<link rel="icon">`. Other site-identity tags
|
|
16
|
+
* (`apple-touch-icon`, `theme-color`, `application-name`) need their own
|
|
17
|
+
* configurable fields in `SiteSettings` before they ship; emitting them
|
|
18
|
+
* automatically from the favicon would produce broken icons on iOS for
|
|
19
|
+
* SVG favicons or blurry home-screen icons when the favicon is a small
|
|
20
|
+
* PNG. Tracked separately.
|
|
21
|
+
*
|
|
22
|
+
* Templates that previously emitted their own `<link rel="icon">` are
|
|
23
|
+
* getting their lines dropped in the same change that introduced this
|
|
24
|
+
* helper.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { MediaReference } from "../settings/types.js";
|
|
28
|
+
import { escapeHtmlAttr } from "./metadata.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Subset of site settings consumed by `renderSiteIdentity`. Kept narrow
|
|
32
|
+
* so callers don't have to fetch fields they don't use.
|
|
33
|
+
*/
|
|
34
|
+
export interface SiteIdentityInput {
|
|
35
|
+
favicon?: MediaReference;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build the `<head>` HTML for site identity tags. Returns an empty string
|
|
40
|
+
* when no identity fields are configured.
|
|
41
|
+
*/
|
|
42
|
+
export function renderSiteIdentity(input: SiteIdentityInput | undefined): string {
|
|
43
|
+
if (!input) return "";
|
|
44
|
+
|
|
45
|
+
const parts: string[] = [];
|
|
46
|
+
|
|
47
|
+
const favicon = input.favicon;
|
|
48
|
+
if (favicon?.url) {
|
|
49
|
+
let tag = `<link rel="icon" href="${escapeHtmlAttr(favicon.url)}"`;
|
|
50
|
+
if (favicon.contentType) {
|
|
51
|
+
tag += ` type="${escapeHtmlAttr(favicon.contentType)}"`;
|
|
52
|
+
}
|
|
53
|
+
tag += ">";
|
|
54
|
+
parts.push(tag);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parts.join("\n");
|
|
58
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { PluginDescriptor } from "../astro/integration/runtime.js";
|
|
14
14
|
import { PLUGIN_CAPABILITIES, HOOK_NAMES } from "./manifest-schema.js";
|
|
15
|
+
import { normalizeCapabilities } from "./types.js";
|
|
15
16
|
import type {
|
|
16
17
|
StandardPluginDefinition,
|
|
17
18
|
StandardHookEntry,
|
|
@@ -147,7 +148,10 @@ export function adaptSandboxEntry(
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
// Build capabilities from descriptor.
|
|
150
|
-
// Validate against the known set (same as defineNativePlugin).
|
|
151
|
+
// Validate against the known set (same as defineNativePlugin). Both
|
|
152
|
+
// current and deprecated names are accepted; deprecated names are
|
|
153
|
+
// silently normalized to current names below so the runtime only ever
|
|
154
|
+
// sees the canonical form.
|
|
151
155
|
const rawCapabilities = descriptor.capabilities ?? [];
|
|
152
156
|
for (const cap of rawCapabilities) {
|
|
153
157
|
if (!VALID_CAPABILITIES_SET.has(cap)) {
|
|
@@ -157,20 +161,28 @@ export function adaptSandboxEntry(
|
|
|
157
161
|
);
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
|
|
165
|
+
// Silent normalization: rewrite deprecated names to current names.
|
|
166
|
+
// Safe assertion — `normalizeCapabilities` only emits validated input
|
|
167
|
+
// plus current names from the rename map, all of which are in the union.
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above; normalizeCapabilities only returns capabilities from the union
|
|
169
|
+
const capabilities = normalizeCapabilities(rawCapabilities) as PluginCapability[];
|
|
162
170
|
const allowedHosts = descriptor.allowedHosts ?? [];
|
|
163
171
|
|
|
164
172
|
// Capability implications: broader capabilities imply narrower ones
|
|
165
|
-
// (mirrors the normalization in define-plugin.ts for native format)
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
// (mirrors the normalization in define-plugin.ts for native format).
|
|
174
|
+
// Operates on canonical names only.
|
|
175
|
+
if (capabilities.includes("content:write") && !capabilities.includes("content:read")) {
|
|
176
|
+
capabilities.push("content:read");
|
|
168
177
|
}
|
|
169
|
-
if (capabilities.includes("write
|
|
170
|
-
capabilities.push("read
|
|
178
|
+
if (capabilities.includes("media:write") && !capabilities.includes("media:read")) {
|
|
179
|
+
capabilities.push("media:read");
|
|
171
180
|
}
|
|
172
|
-
if (
|
|
173
|
-
capabilities.
|
|
181
|
+
if (
|
|
182
|
+
capabilities.includes("network:request:unrestricted") &&
|
|
183
|
+
!capabilities.includes("network:request")
|
|
184
|
+
) {
|
|
185
|
+
capabilities.push("network:request");
|
|
174
186
|
}
|
|
175
187
|
|
|
176
188
|
// Build storage config from descriptor.
|
package/src/plugins/context.ts
CHANGED
|
@@ -647,14 +647,14 @@ export function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
/**
|
|
650
|
-
* Create blocked HTTP access (for plugins without network:
|
|
650
|
+
* Create blocked HTTP access (for plugins without network:request capability)
|
|
651
651
|
*/
|
|
652
652
|
export function createBlockedHttpAccess(pluginId: string): HttpAccess {
|
|
653
653
|
return {
|
|
654
654
|
async fetch(): Promise<never> {
|
|
655
655
|
throw new Error(
|
|
656
|
-
`Plugin "${pluginId}" does not have the "network:
|
|
657
|
-
`Add "network:
|
|
656
|
+
`Plugin "${pluginId}" does not have the "network:request" capability. ` +
|
|
657
|
+
`Add "network:request" to the plugin's capabilities to enable HTTP requests.`,
|
|
658
658
|
);
|
|
659
659
|
},
|
|
660
660
|
};
|
|
@@ -902,32 +902,35 @@ export class PluginContextFactory {
|
|
|
902
902
|
const storage = createStorageAccess(this.db, plugin.id, plugin.storage);
|
|
903
903
|
|
|
904
904
|
// Capability-gated: content
|
|
905
|
+
// Note: capabilities reach this point already normalized to the
|
|
906
|
+
// canonical names by definePlugin / adaptSandboxEntry. Deprecated
|
|
907
|
+
// names ("read:content", "write:content") never appear here.
|
|
905
908
|
let content: ContentAccess | ContentAccessWithWrite | undefined;
|
|
906
|
-
if (capabilities.has("write
|
|
909
|
+
if (capabilities.has("content:write")) {
|
|
907
910
|
content = createContentAccessWithWrite(this.db);
|
|
908
|
-
} else if (capabilities.has("read
|
|
911
|
+
} else if (capabilities.has("content:read")) {
|
|
909
912
|
content = createContentAccess(this.db);
|
|
910
913
|
}
|
|
911
914
|
|
|
912
915
|
// Capability-gated: media
|
|
913
916
|
let media: MediaAccess | MediaAccessWithWrite | undefined;
|
|
914
|
-
if (capabilities.has("write
|
|
917
|
+
if (capabilities.has("media:write") && this.getUploadUrl) {
|
|
915
918
|
media = createMediaAccessWithWrite(this.db, this.getUploadUrl, this.storage);
|
|
916
|
-
} else if (capabilities.has("read
|
|
919
|
+
} else if (capabilities.has("media:read")) {
|
|
917
920
|
media = createMediaAccess(this.db);
|
|
918
921
|
}
|
|
919
922
|
|
|
920
923
|
// Capability-gated: http
|
|
921
924
|
let http: HttpAccess | undefined;
|
|
922
|
-
if (capabilities.has("network:
|
|
925
|
+
if (capabilities.has("network:request:unrestricted")) {
|
|
923
926
|
http = createUnrestrictedHttpAccess(plugin.id);
|
|
924
|
-
} else if (capabilities.has("network:
|
|
927
|
+
} else if (capabilities.has("network:request")) {
|
|
925
928
|
http = createHttpAccess(plugin.id, plugin.allowedHosts);
|
|
926
929
|
}
|
|
927
930
|
|
|
928
931
|
// Capability-gated: users
|
|
929
932
|
let users: UserAccess | undefined;
|
|
930
|
-
if (capabilities.has("read
|
|
933
|
+
if (capabilities.has("users:read")) {
|
|
931
934
|
users = createUserAccess(this.db);
|
|
932
935
|
}
|
|
933
936
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { normalizeCapabilities } from "./types.js";
|
|
16
17
|
import type {
|
|
17
18
|
PluginDefinition,
|
|
18
19
|
ResolvedPlugin,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
ResolvedPluginHooks,
|
|
21
22
|
ResolvedHook,
|
|
22
23
|
HookConfig,
|
|
24
|
+
PluginCapability,
|
|
23
25
|
PluginStorageConfig,
|
|
24
26
|
StandardPluginDefinition,
|
|
25
27
|
} from "./types.js";
|
|
@@ -65,7 +67,7 @@ const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
|
|
|
65
67
|
* export default definePlugin({
|
|
66
68
|
* id: "my-plugin",
|
|
67
69
|
* version: "1.0.0",
|
|
68
|
-
* capabilities: ["read
|
|
70
|
+
* capabilities: ["content:read"],
|
|
69
71
|
* hooks: {
|
|
70
72
|
* "content:beforeSave": async (event, ctx) => {
|
|
71
73
|
* ctx.log.info("Saving content", { collection: event.collection });
|
|
@@ -143,8 +145,24 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
143
145
|
throw new Error(`Invalid plugin version "${version}". Must be semver format (e.g., "1.0.0").`);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
// Validate capabilities
|
|
147
|
-
|
|
148
|
+
// Validate capabilities. Both current names and deprecated aliases are
|
|
149
|
+
// accepted; aliases are silently rewritten to current names below so the
|
|
150
|
+
// runtime only ever sees the canonical form. Authors are warned at
|
|
151
|
+
// bundle/validate and hard-failed at publish.
|
|
152
|
+
const validCapabilities = new Set<string>([
|
|
153
|
+
// Current names
|
|
154
|
+
"network:request",
|
|
155
|
+
"network:request:unrestricted",
|
|
156
|
+
"content:read",
|
|
157
|
+
"content:write",
|
|
158
|
+
"media:read",
|
|
159
|
+
"media:write",
|
|
160
|
+
"users:read",
|
|
161
|
+
"email:send",
|
|
162
|
+
"hooks.email-transport:register",
|
|
163
|
+
"hooks.email-events:register",
|
|
164
|
+
"hooks.page-fragments:register",
|
|
165
|
+
// Deprecated aliases
|
|
148
166
|
"network:fetch",
|
|
149
167
|
"network:fetch:any",
|
|
150
168
|
"read:content",
|
|
@@ -152,7 +170,6 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
152
170
|
"read:media",
|
|
153
171
|
"write:media",
|
|
154
172
|
"read:users",
|
|
155
|
-
"email:send",
|
|
156
173
|
"email:provide",
|
|
157
174
|
"email:intercept",
|
|
158
175
|
"page:inject",
|
|
@@ -163,16 +180,27 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
// Silent normalization: rewrite deprecated names to current names. Done
|
|
184
|
+
// before the implication pass so implications work on canonical names.
|
|
185
|
+
// `as PluginCapability[]` is safe because `normalizeCapabilities` only
|
|
186
|
+
// returns strings from the validated input plus current names from the
|
|
187
|
+
// rename map, all of which are in the union.
|
|
188
|
+
const canonical = normalizeCapabilities(capabilities) as PluginCapability[];
|
|
189
|
+
|
|
190
|
+
// Capability implications: broader capabilities imply narrower ones.
|
|
191
|
+
// Operates on canonical names only.
|
|
192
|
+
const normalizedCapabilities: PluginCapability[] = [...canonical];
|
|
193
|
+
if (canonical.includes("content:write") && !canonical.includes("content:read")) {
|
|
194
|
+
normalizedCapabilities.push("content:read");
|
|
170
195
|
}
|
|
171
|
-
if (
|
|
172
|
-
normalizedCapabilities.push("read
|
|
196
|
+
if (canonical.includes("media:write") && !canonical.includes("media:read")) {
|
|
197
|
+
normalizedCapabilities.push("media:read");
|
|
173
198
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
199
|
+
if (
|
|
200
|
+
canonical.includes("network:request:unrestricted") &&
|
|
201
|
+
!canonical.includes("network:request")
|
|
202
|
+
) {
|
|
203
|
+
normalizedCapabilities.push("network:request");
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
// Normalize hooks
|
package/src/plugins/hooks.ts
CHANGED
|
@@ -248,28 +248,32 @@ export class HookPipeline {
|
|
|
248
248
|
* capability will have that hook silently skipped at registration time.
|
|
249
249
|
*/
|
|
250
250
|
private static readonly HOOK_REQUIRED_CAPABILITY: ReadonlyMap<string, string> = new Map([
|
|
251
|
-
// Email
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
["
|
|
258
|
-
["
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
["content:
|
|
262
|
-
["content:
|
|
251
|
+
// Email — registering email:beforeSend/afterSend/deliver requires the
|
|
252
|
+
// matching `hooks.email-*:register` capability. These are distinct
|
|
253
|
+
// from `email:send` (which gates ctx.email) so that "this plugin
|
|
254
|
+
// reads/writes email events" is visible separately from "this
|
|
255
|
+
// plugin can send email".
|
|
256
|
+
["email:beforeSend", "hooks.email-events:register"],
|
|
257
|
+
["email:afterSend", "hooks.email-events:register"],
|
|
258
|
+
["email:deliver", "hooks.email-transport:register"],
|
|
259
|
+
// Content — beforeSave can mutate content, so requires content:write.
|
|
260
|
+
// afterSave is read-only notification, so content:read suffices.
|
|
261
|
+
["content:beforeSave", "content:write"],
|
|
262
|
+
["content:afterSave", "content:read"],
|
|
263
|
+
["content:beforeDelete", "content:read"],
|
|
264
|
+
["content:afterDelete", "content:read"],
|
|
265
|
+
["content:afterPublish", "content:read"],
|
|
266
|
+
["content:afterUnpublish", "content:read"],
|
|
263
267
|
// Media
|
|
264
|
-
["media:beforeUpload", "write
|
|
265
|
-
["media:afterUpload", "read
|
|
268
|
+
["media:beforeUpload", "media:write"],
|
|
269
|
+
["media:afterUpload", "media:read"],
|
|
266
270
|
// Comments — hooks expose author email, IP hash, user agent
|
|
267
|
-
["comment:beforeCreate", "read
|
|
268
|
-
["comment:moderate", "read
|
|
269
|
-
["comment:afterCreate", "read
|
|
270
|
-
["comment:afterModerate", "read
|
|
271
|
+
["comment:beforeCreate", "users:read"],
|
|
272
|
+
["comment:moderate", "users:read"],
|
|
273
|
+
["comment:afterCreate", "users:read"],
|
|
274
|
+
["comment:afterModerate", "users:read"],
|
|
271
275
|
// Page fragments — can inject arbitrary scripts into every public page
|
|
272
|
-
["page:fragments", "page:
|
|
276
|
+
["page:fragments", "hooks.page-fragments:register"],
|
|
273
277
|
]);
|
|
274
278
|
|
|
275
279
|
/**
|
package/src/plugins/index.ts
CHANGED
|
@@ -192,3 +192,12 @@ export type {
|
|
|
192
192
|
StandardRouteEntry,
|
|
193
193
|
} from "./types.js";
|
|
194
194
|
export { isStandardPluginDefinition } from "./types.js";
|
|
195
|
+
|
|
196
|
+
// Capability normalization (legacy → canonical alias layer)
|
|
197
|
+
export {
|
|
198
|
+
CAPABILITY_RENAMES,
|
|
199
|
+
isDeprecatedCapability,
|
|
200
|
+
normalizeCapability,
|
|
201
|
+
normalizeCapabilities,
|
|
202
|
+
} from "./types.js";
|
|
203
|
+
export type { CurrentPluginCapability, DeprecatedPluginCapability } from "./types.js";
|
|
@@ -12,7 +12,31 @@ import { z } from "zod";
|
|
|
12
12
|
|
|
13
13
|
// ── Enum values (must stay in sync with types.ts) ───────────────
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Current capability names — the ones authors should use going forward.
|
|
17
|
+
* See `PluginCapability` in `types.ts` for documentation of each.
|
|
18
|
+
*/
|
|
19
|
+
export const CURRENT_PLUGIN_CAPABILITIES = [
|
|
20
|
+
"network:request",
|
|
21
|
+
"network:request:unrestricted",
|
|
22
|
+
"content:read",
|
|
23
|
+
"content:write",
|
|
24
|
+
"media:read",
|
|
25
|
+
"media:write",
|
|
26
|
+
"users:read",
|
|
27
|
+
"email:send",
|
|
28
|
+
"hooks.email-transport:register",
|
|
29
|
+
"hooks.email-events:register",
|
|
30
|
+
"hooks.page-fragments:register",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy capability names accepted during the deprecation window.
|
|
35
|
+
* Normalized to current names via `normalizeCapability()` in types.ts
|
|
36
|
+
* before reaching the runtime. Plugin authors are warned at bundle/validate
|
|
37
|
+
* and hard-failed at publish.
|
|
38
|
+
*/
|
|
39
|
+
export const DEPRECATED_PLUGIN_CAPABILITIES = [
|
|
16
40
|
"network:fetch",
|
|
17
41
|
"network:fetch:any",
|
|
18
42
|
"read:content",
|
|
@@ -20,12 +44,23 @@ export const PLUGIN_CAPABILITIES = [
|
|
|
20
44
|
"read:media",
|
|
21
45
|
"write:media",
|
|
22
46
|
"read:users",
|
|
23
|
-
"email:send",
|
|
24
47
|
"email:provide",
|
|
25
48
|
"email:intercept",
|
|
26
49
|
"page:inject",
|
|
27
50
|
] as const;
|
|
28
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Full set of accepted capability strings — current + deprecated.
|
|
54
|
+
*
|
|
55
|
+
* The manifest schema accepts both during the transition. The runtime only
|
|
56
|
+
* ever sees current names because `normalizeCapability()` rewrites legacy
|
|
57
|
+
* names at every external boundary (definePlugin, adaptSandboxEntry).
|
|
58
|
+
*/
|
|
59
|
+
export const PLUGIN_CAPABILITIES = [
|
|
60
|
+
...CURRENT_PLUGIN_CAPABILITIES,
|
|
61
|
+
...DEPRECATED_PLUGIN_CAPABILITIES,
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
29
64
|
/** Must stay in sync with FieldType in schema/types.ts */
|
|
30
65
|
const FIELD_TYPES = [
|
|
31
66
|
"string",
|