feat(plugins): @own-created contentAccess marker for plugin-created tables - #335
Open
mostafasadeghidev wants to merge 1 commit into
Open
Conversation
…ables Plugins could create tables via cms.content.tables.create but never read or write their entries: the static contentAccess[] allowlist matches by exact slug, and a runtime-chosen slug cannot be pre-declared. This made importer/migration plugins impossible on the sanctioned surface. - Migration 025 (both dialects, additive): data_tables.created_by_plugin_id, stamped by the create handler with the host-authenticated worker identity. - New @own-created contentAccess marker (OWN_CREATED_TABLES_MARKER): covers every table the declaring plugin created, resolved against the stored creator (never by slug), with the entry's declared modes; entries combine as a union. Marker modes go through the same install-time modes<->permission coherence check as slug entries. - assertContentTableAccess now takes the resolved DataTable (handlers resolve-then-assert; tree callbacks already had the table). tables.list / search filter through the same matcher, so own-created tables appear. - tables.create slug constrained to kebab-case, reserving the @ namespace; bundle export/import preserves createdByPluginId. - Docs, capability descriptions, SDK comments updated; stale "consent screen renders contentAccess verbatim" claim corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mostafasadeghidev
marked this pull request as ready for review
August 3, 2026 03:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes the design gap that made importer/migration plugins impossible to build on the sanctioned
api.cms.content.*surface:cms.content.tables.createis allowed without acontentAccess[]entry, but every subsequent entry read/write to the created table failed closed, because a runtime-chosen slug can never be pre-declared in the static manifest.@own-createdcontentAccess marker (OWN_CREATED_TABLES_MARKERin the plugin SDK): acontentAccess[]entry whosetableis@own-createdcovers every table the plugin itself created throughcms.content.tables.create, with the entry's declared modes.025_data_tables_created_by_plugin(additive, nullable, both dialects) addsdata_tables.created_by_plugin_id. The create handler stamps the host-authenticated worker identity (msg.pluginId) — never plugin-supplied input. The marker resolves against this column, so access survives server restarts and admin-side slug renames, and is correctly lost if the table is deleted and recreated by someone else.assertContentTableAccessnow takes the resolvedDataTableinstead of a slug. Every handler resolves the table first (it always did anyway), so the check stays synchronous, adds no extra query, and can seecreatedByPluginId. Entries combine as a union: an operation is allowed when any matching entry declares the mode.tables.list/searchfilter through the same matcher (hasContentTableAccess), so own-created tables appear in listings.tables.createslug is now constrained to kebab-case, reserving the@namespace.cms.content.*permission is declared" rule (the error message now points at it), and marker modes go through the same modes↔permissions check as slug entries.Why this design (vs. implicit creator access)
The manifest parser requires a non-empty
contentAccess[]whenever entry-level content permissions are declared. Implicit creator access would need a carve-out to that rule, making an importer's manifest claim it touches nothing while it reads/writes its own tables. The explicit marker keeps the manifest the single reviewable declaration surface, keeps mode semantics uniform, and lets a plugin self-narrow modes on its own tables. Host-side registry state (the alternative to the DB column) was rejected because it would not survive restarts.Impact
api.cms.content.*surface instead of driving the admin HTTP API from unsandboxed admin-app code.nullreply fromtables.get) instead of the contentAccess error — for existing tables the fail-closed access error is unchanged. Slug existence is not sensitive (any authorized admin UI shows it), and access to data still requires passing the assert.createdByPluginIdridesDataTableSchemaas an optional field, so pre-existing archives still validate).contentAccess[]verbatim; comments now say the manifest is the reviewable surface. (Rendering it in the consent UI is flagged as a follow-up task.)Verification
bun run build(tsc -b+vite build) — clean.bun run lint— clean.bun test— full suite compared against a cleanorigin/mainbaseline worktree on the same (Windows) machine: baseline fails 302 tests (environmental — dominated byEBUSYtemp-SQLite cleanup on Windows, plus the platform-sensitiveplugin-bootstrap-fresh/ bundle-budget gates), my branch failed 304. The 2-test delta was mine and is fixed in this PR: thedataCmslist-shape expectation gainedcreatedByPluginId: null, and a comment was tightened to keepsrc/core/plugins/manifest.tsunder the 700-line module ceiling. After the fixes the failure set is identical to baseline; every touched-area test file passes (tables.test.ts,registry.test.ts,content.test.ts,contentProjection.test.ts,pluginManifest.test.ts,plugin-content-access-enforced,plugin-cms-content-surface,migration-parity,db-postgres-isms,db-json-column-naming,boundary-validation,module-size-budgets,dataCms).