Skip to content

feat(plugins): @own-created contentAccess marker for plugin-created tables - #335

Open
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:feat/plugin-own-created-tables
Open

feat(plugins): @own-created contentAccess marker for plugin-created tables#335
mostafasadeghidev wants to merge 1 commit into
CoreBunch:mainfrom
mostafasadeghidev:feat/plugin-own-created-tables

Conversation

@mostafasadeghidev

Copy link
Copy Markdown

What

Closes the design gap that made importer/migration plugins impossible to build on the sanctioned api.cms.content.* surface: cms.content.tables.create is allowed without a contentAccess[] 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.

  • New @own-created contentAccess marker (OWN_CREATED_TABLES_MARKER in the plugin SDK): a contentAccess[] entry whose table is @own-created covers every table the plugin itself created through cms.content.tables.create, with the entry's declared modes.
  • Durable creator record: migration 025_data_tables_created_by_plugin (additive, nullable, both dialects) adds data_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.
  • assertContentTableAccess now takes the resolved DataTable instead of a slug. Every handler resolves the table first (it always did anyway), so the check stays synchronous, adds no extra query, and can see createdByPluginId. Entries combine as a union: an operation is allowed when any matching entry declares the mode. tables.list / search filter through the same matcher (hasContentTableAccess), so own-created tables appear in listings.
  • Marker can never collide with a real table: marker entries never match by slug, the manifest slug pattern requires a leading letter (no static entry can be the literal), and the plugin-facing tables.create slug is now constrained to kebab-case, reserving the @ namespace.
  • Install-time coherence preserved: the marker satisfies the "contentAccess required when any 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

  • Importer/migration plugins (operator-chosen table names at runtime) can now be built entirely on the sandboxed api.cms.content.* surface instead of driving the admin HTTP API from unsandboxed admin-app code.
  • No change for existing plugins: slug entries grant exactly what they granted before; tables created by users/imports carry a null creator and are unreachable via the marker. One error-shape change from the resolve-then-assert ordering: probing a nonexistent table slug now yields "Content table not found" (or a null reply from tables.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.
  • Bundle export/import round-trips preserve plugin ownership (createdByPluginId rides DataTableSchema as an optional field, so pre-existing archives still validate).
  • Stale SDK doc claim fixed: the install consent dialog does not render 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 clean origin/main baseline worktree on the same (Windows) machine: baseline fails 302 tests (environmental — dominated by EBUSY temp-SQLite cleanup on Windows, plus the platform-sensitive plugin-bootstrap-fresh / bundle-budget gates), my branch failed 304. The 2-test delta was mine and is fixed in this PR: the dataCms list-shape expectation gained createdByPluginId: null, and a comment was tightened to keep src/core/plugins/manifest.ts under 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).

…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
mostafasadeghidev marked this pull request as ready for review August 3, 2026 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant