feat(plugins): add content discovery capabilities - #3171
Conversation
🦋 Changeset detectedLatest commit: 2771851 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Scope checkThis PR changes 1,605 lines across 55 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 2771851 | Sep 19 2026, 10:30 AM |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-test
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-loader
@emdash-cms/registry-moderation
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
This is a well-scoped slice of the Stack A sandboxed-plugin capability expansion. The approach is sound: schema:read, content:revisions:read, and the extended content:read surface (getTranslations, getPublicUrl) are capability-gated consistently across the manifest/lexicon trust contract, core PluginContextFactory, the Cloudflare PluginBridge, the Node.js/workerd bridge, and the plugin-test runtime. The shared resolveLocalizedContentRoutePath helper correctly unifies locale-aware public URL resolution between menus, sitemaps, hreflang, and the new plugin API. Revision snapshots correctly omit author identity and require the owning content row to be visible. Admin consent copies, CLI schema, registry lexicons, and docs are updated in step.
I checked:
- Manifest → declaredAccess → capability round-trip in
plugin-typesand core. - Core context creation (
createContentAccess,createSchemaAccess) and implication closure (definePlugin,adaptSandboxEntry). - Cloudflare bridge/wrapper and workerd bridge/wrapper parity for the new methods.
RevisionRepository.findVisibleBy*SQL safety and author stripping.resolveLocalizedContentRoutePathusage in sitemap, hreflang, and menus.- Admin marketplace consent labels (Lingui descriptors present).
- Documentation against implementation (
capabilities.mdx,installing.mdx,SKILL.md). - Changeset against
.changeset/README.md. - Query-count impact: the new methods are plugin-context only; EmDashHead/sitemap/menus already existed or only gained the existing
trailingSlashoption, with no new logged-out round-trips.
Headline: code is clean and the surface is consistent. The only gap I found is in the exhaustive round-trip test, which did not get updated for the new capabilities.
Findings
-
[suggestion]
packages/plugin-types/tests/capabilities.test.ts:176The exhaustive declaredAccess ↔ capabilities round-trip test was not updated for the new capabilities added in this PR.
contentChoicesshould includecontent:revisions:readstates (e.g.["content:read", "content:revisions:read"]and["content:read", "content:write", "content:revisions:read"]) andsingletonFacetsshould include"schema:read".The separate unit tests above do cover these mappings, but the comment here claims this enumeration is “the only states that can reach a published manifest,” so leaving them out makes the round-trip guard incomplete for the very surface this PR introduces. Update the choices and the expected count at line 222.
|
Addressed the exhaustive round-trip coverage gap in 95cfe6e: revision-read states and schema-read singleton combinations are now included, with the matrix updated to 9,600 implication-closed states. The focused test and format/lint checks pass. |
There was a problem hiding this comment.
Re-review of the Stack A sandboxed-plugin content discovery slice. The only finding from the previous review — the incomplete exhaustive declaredAccess ↔ capabilities round-trip enumeration in packages/plugin-types/tests/capabilities.test.ts — has been addressed. contentChoices now covers the content:revisions:read states and singletonFacets includes "schema:read", with the expected count updated to 9600.
I re-checked the full surface introduced by this PR:
- Trust contract:
schema:readandcontent:revisions:readare present inplugin-types, core, plugin-cli, admin consent labels, registry lexicons, and the release-action bundle. Implication closure (revisionsRead→read) is applied consistently indefinePlugin,adaptSandboxEntry, anddeclared-access.ts. - Runtime gating:
PluginContextFactorypopulatesctx.schema,ctx.content.getTranslations,ctx.content.getPublicUrl, and revision methods only when the corresponding capabilities are held. The CloudflarePluginBridgeand workerdbridge-handlerenforce the same capability checks before delegating. - Security boundaries:
RevisionRepository.findVisibleBy*uses Kysely's taggedsqltemplate with parameterized values andsql.ref()aftervalidateIdentifier, checks the owning content row is not trashed, and the context stripsauthorIdfrom revision snapshots.getPublicUrlonly resolves published, routable rows with valid slugs. - URL resolution parity: The new
resolveLocalizedContentRoutePathhelper is shared by menus, sitemaps, hreflang output, and the plugin API, and now receives the sitetrailingSlashsetting consistently. - Tests: New integration/unit tests cover schema discovery, translation discovery, public URL resolution, revision access, and the updated round-trip enumeration. The plugin-test fixture exercises the new methods end-to-end through the sandbox.
- Docs:
capabilities.mdx,installing.mdx,api-routes.mdx, andskills/creating-plugins/SKILL.mddescribe the new capabilities accurately and match the implementation. - Changeset: The changeset explains the user-facing capability additions and the separate consent required for revision history.
No remaining logic bugs, security gaps, or AGENTS.md convention violations were found.
95cfe6e to
4b5e49e
Compare
There was a problem hiding this comment.
Re-review of the Stack A plugin content-discovery slice. The prior review’s only finding — the incomplete contentChoices/singletonFacets enumeration in packages/plugin-types/tests/capabilities.test.ts — has been addressed: contentChoices now covers the content:revisions:read states, singletonFacets includes "schema:read", and the expected count was updated to 9600.
The change is the right one for the stated capability plan: it adds schema:read for collection/field introspection, extends content:read with safe identity fields, translation discovery, and public URL resolution, and adds a separately consented content:revisions:read for revision history. The trust-contract plumbing (implication closure, consent labels, manifest schemas, registry lexicons, and all three runtimes) is consistent. SQL is parameterized; identifier interpolation goes through validateIdentifier; revision reads are scoped to non-trashed owning content; and public URL resolution correctly requires published, a valid slug, and a routable collection.
I did find one remaining input-validation bug introduced by the new revision-list surface. createContentAccess.listRevisions and RevisionRepository.findVisibleByEntry both derive LIMIT values without coercing/validating the caller-provided limit. A non-numeric (or NaN) limit reaches Kysely parameterized SQL as LIMIT NaN, which will produce a runtime SQL error from a sandboxed plugin. The contentList bridge handler already has the correct safe pattern (Number(opts.limit) || 50); the new revision path should match it. A test exercising the malformed-limit boundary is also missing.
Everything else — docs accuracy for the new capabilities, changeset prose, test coverage of the happy paths, runtime gating, and i18n routing parity — looks clean.
4b5e49e to
2771851
Compare
There was a problem hiding this comment.
This re-review confirms that the Stack A plugin content-discovery slice is the right change for the stated capability plan and that the only prior finding has been addressed.
Approach. The PR adds schema:read for collection/field introspection, extends content:read with safe identity fields, translation discovery, and public URL resolution, and introduces a separately consented content:revisions:read for revision history. The trust-contract plumbing is consistent across native plugins, the Cloudflare Worker Loader bridge, and the Node.js workerd bridge: capability normalization adds the content:revisions:read → content:read implication, manifest schemas and registry lexicons include the new authorities, and wrappers generate the matching ctx.schema / ctx.content methods.
Prior finding — resolved. The previous review called out that createContentAccess.listRevisions and RevisionRepository.findVisibleByEntry passed unvalidated limit values through to Kysely. This is now fixed: normalizeRevisionLimit coerces non-numeric/NaN/infinite values and clamps the result to 1–100, and packages/core/tests/integration/plugins/capabilities.test.ts exercises the malformed-limit boundary for both the access surface and the repository.
What I checked. Capability implication closure in definePlugin, adaptSandboxEntry, PluginContextFactory, and @emdash-cms/plugin-types declared-access conversion; manifest schemas in packages/core and plugin-cli; the registry lexicon; SQL in revision reads (parameterized values, validateIdentifier for identifiers, deleted_at IS NULL scoping); public URL and menu/hreflang/sitemap resolution with the new shared resolveLocalizedContentRoutePath; per-runtime bridge gating in Cloudflare and workerd; wrapper code generation; docs accuracy against implementation; changeset prose per .changeset/README.md; and the new admin consent labels.
Conclusion. I found no blocking issues, regressions, or AGENTS.md violations. The diff is clean and the tests cover the new boundaries.
What does this PR do?
Adds the first Stack A capability slice for sandboxed plugin content discovery.
Plugins can declare
schema:readto inspect batched public collection and field definitions. Existingcontent:readaccess gains safe content identity fields, translation discovery, and public URL resolution shared with menus, sitemaps, and hreflang output. Retained revision snapshots require the separatecontent:revisions:readcapability, omit revision-author identity, and cannot read trashed entries.The contract and consent boundary are implemented across shared/core manifest validation, registry lexicons, plugin CLI generation, native execution, Cloudflare Worker Loader, Node.js workerd,
@emdash-cms/plugin-test, public docs, and canonical/generated authoring guidance.Closes: n/a
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.AI-generated code disclosure
Screenshots / test output
Not applicable. The admin change adds localized consent copy without changing layout.
Validated with:
pnpm buildpnpm typecheckpnpm lint:quickpnpm lint:json(zero diagnostics)pnpm --dir docs buildorigin/main, including a material-fix re-check