Skip to content

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

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

mostafasadeghidev wants to merge 6 commits into
CoreBunch:mainfrom
mostafasadeghidev:feat/plugin-own-created-tables

Conversation

@mostafasadeghidev

@mostafasadeghidev mostafasadeghidev commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

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 033_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.)

Updated for site branches

Resynced with main after #484. Three things changed:

  • The migration is now 033 — site branches took 027–030, feat(plugins): author, build and activate site plugins in a full-screen IDE #495 claims 031_installed_plugins_source, and feat(media): record what depends on an asset, starting with avatars #505 claims 032. An ALTER can only be renumbered while nothing has applied it; nothing upstream has, so it moves now rather than making whoever merges second do it.
  • Every handler takes MAIN_SCOPE, and in every one the access check stays after table resolution, against the table itself. Checked by count: 17 assertContentTableAccess calls on this branch before the merge, 17 upstream, 17 after — none dropped, none left on the slug form.
  • A fork no longer drops the creator. branches/entities/table.ts copies data_tables with an explicit column list, and created_by_plugin_id wasn't in it — so on every branch a plugin-created table stopped belonging to its plugin. It now sits beside created_by_user_id. It is ownership rather than content, so it stays out of the merge diff. The new test in tables.test.ts fails without the change (Received: null) and passes with it.

tables.test.ts, content.test.ts, registry.test.ts: 25 pass. Import round-trip tests: pass.

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).

@mostafasadeghidev
mostafasadeghidev marked this pull request as ready for review August 3, 2026 03:53
…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 force-pushed the feat/plugin-own-created-tables branch from ef03591 to 7795898 Compare August 7, 2026 00:03
mostafasadeghidev and others added 2 commits September 4, 2026 15:04
…tants

Three conflicts, and the migration one deserves the explanation.

Upstream took slot `025` for `025_remove_non_owner_role_management` while this
branch adds `025_data_tables_created_by_plugin`. Both entries are kept, shared
prefix and all, rather than renumbering this one to `026`.

That is deliberate, not laziness. `runMigrations` looks up applied migrations
by the FULL id string, so two ids sharing a numeric prefix both run exactly
once and neither masks the other — the prefix is a naming convention, not the
key. And the ALTER here carries no guard, because SQLite has no
`add column if not exists`; migration 010 already documents that the runner's
exactly-once gate is what makes a bare ALTER safe. An id that any installation
has already applied therefore cannot be renumbered: the new id re-runs the
ALTER against a column that exists and fails the boot. Renumber freely before
release, never after. Both dialect files list the pair in the same order,
which is what the parity gate asserts.

`contentSchemas.ts` is an adjacent-insert collision that shares one `/**`
opener, so neither side can simply win: upstream's
`CONTENT_ACCESS_MODE_PERMISSIONS` (which mode consumes which permission) and
this branch's `OWN_CREATED_TABLES_MARKER` (which tables an entry addresses)
are orthogonal, and both are kept with their own doc blocks.

Verified: tsc clean; migration parity, the plugin suites and the
content-access enforcement gate 183/183.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`026_plugin_media_sources` landed upstream while this sat open, and both
sides append to the same point in each dialect file.

Upstream's entry keeps its id. This branch's moves from `025_` — which it
shared with `025_remove_non_owner_role_management` — to `027`, so the
sequence reads cleanly again.

Renumbering is safe here for exactly the reason the comment on it states: no
installation has applied this migration under any id, because the PR is
unmerged. The ALTER carries no guard — SQLite has no `add column if not
exists`, as migration 010 documents — so once this ships, the id is fixed.

Both dialect files list the three in the same order, which is what the parity
gate asserts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 5, 2026
…red migration

Two resolutions worth recording.

`manifest.ts` and `plugin-system.md` collided on the same doc text in two
versions. The stack's is the fuller one — it also describes the consent
screen CoreBunch#336 adds, which the CoreBunch#335 branch alone knows nothing about — so the
stack's copy wins both.

The migrations needed care in both directions. CoreBunch#335 renumbers this fork's
`025_data_tables_created_by_plugin` to `027` for upstream, which is correct
there and wrong here: two live installations have already recorded the `025`
id, and the ALTER carries no guard, so a second id would re-run it against an
existing column and fail the boot. The merge left both entries; the `027`
duplicate is dropped and the fork keeps its own id.

Checking that turned up a real loss. `026_plugin_media_sources` arrived
upstream in e7a27ca and the commit IS in this branch's history, but the
entry was not in either dialect file — an earlier conflict resolution here
dropped it while keeping the code that reads the table
(`server/repositories/pluginMediaSources.ts` queries `plugin_media_sources`).
A fresh install would have created that table nowhere. Restored from upstream
verbatim, after our 025 so both dialects keep the same order.

Verified: tsc, build and lint clean; media + plugins + settings + parity
292/292; db and plugin-handler suites 40/40, which is what actually runs the
migrations; fork-stack-capabilities 12/12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
Site branches took 027–029 and the ISO timestamp rewrite took 030, so the
avatar backfill moves from 028 to 032. It skips 031, which CoreBunch#335 now claims.

Renumbering this one is safe in a way renumbering an ALTER is not: the
backfill inserts only where `not exists`, so an installation that recorded
it under 028 runs it again under 032 and inserts nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev and others added 2 commits September 13, 2026 10:05
Site branches took 027–029 and the timestamp rewrite took 030, so this
migration moves to 031. Nothing has applied 027 under this id upstream,
which is the only condition under which an ALTER may be renumbered.

Site branches also made every repository call take a `BranchScope`. Plugins
only ever act on the live site, so each handler gains `MAIN_SCOPE` — and in
every hunk the access check this branch introduced stays exactly where it
was: after the table is resolved, against the table itself, because the
`@own-created` marker needs the table's creator and a slug cannot carry it.

The count is the check on that: 17 `assertContentTableAccess` calls here,
17 on this branch before the merge, 17 upstream — none dropped, none left
on the slug form.

`tables.list` loses upstream's `allowedSlugs` set; it was the slug-only
filter this branch replaced with `hasContentTableAccess`. The tests that
auto-merged still called the pre-branch repository signatures, and are
scoped here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Forking a branch copies `data_tables` with an explicit column list, and
`created_by_plugin_id` was not in it. The branch's copy of a plugin-created
table came out with no creator — so the `@own-created` contentAccess marker
no longer matched it, and the table stopped belonging to the plugin that
made it. Nothing reported it; the column was simply null on the copy.

It is ownership, not content, so it does not belong in the merge diff — it
belongs in the copy, beside `created_by_user_id`, which was already there.

The test fails without the change (`Received: null`) and passes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
CoreBunch#498 landing

Twenty upstream commits, including two of this stack's own PRs (CoreBunch#359, CoreBunch#498)
merged exactly as submitted.

Resolved hunk by hunk, and each file checked against the resolution its PR
branch reached independently:

  - content.ts, import.ts — every handler gains `MAIN_SCOPE`; every
    `@own-created` access check stays on the resolved table. Identical to
    CoreBunch#335's branch.
  - ConfirmDeleteContext — upstream's `tone` beside this stack's `details`.
    Identical to CoreBunch#507's branch.
  - publishSite.ts — only an import collided; the file now matches upstream
    byte for byte, as it should once CoreBunch#359 has landed.

Migrations are the one place the stack deliberately differs from the PRs.
`025_data_tables_created_by_plugin` keeps its id: two installations recorded
it, and the ALTER cannot run twice. Its comment now says what to do when
CoreBunch#335 lands as `031_…`. The avatar backfill takes CoreBunch#505's `032` — safe to
renumber because it inserts only where `not exists`.

`contentUsage.ts` gets `MAIN_SCOPE` to type-check; reading every branch
arrives with CoreBunch#507's merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
…opy stays out

Brings CoreBunch#335's branch-copy fix (`created_by_plugin_id` joins the column list
a fork copies) and the tests its upstream merge re-scoped.

The migration conflict is the case the stack's 025 note describes: CoreBunch#335
carries the ALTER as `031_data_tables_created_by_plugin`, and this branch
already runs it as `025_…` on installations that recorded that id. Keeping
both would run the ALTER twice and stop those servers booting, so the
incoming 031 entry is dropped. Both migration files are byte-identical to
before this merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
…icated

`tables.test.ts` carried the `created_by_plugin_id` describe block twice:
an earlier upstream merge moved the route-base block between them and kept
both copies. The first was the stale one, still on the pre-branch repository
signatures, so it failed as soon as those signatures took a scope. The file
is now identical to CoreBunch#335's branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 13, 2026
… and refuse a doubled ALTER

README: CoreBunch#359 and CoreBunch#498 move to the landed table, the count drops to ten,
and CoreBunch#507's row says what it does now — every page, on every branch.

The fork gate's own rule is that a merged PR's row is deleted, since the
capability then arrives with main. Nine merged rows had never been removed;
they go. CoreBunch#336 was never pinned at all; it is now.

And one new check, for the one merge mistake that takes a server down on
boot: no column may be added by two different migrations. The stack runs
CoreBunch#335's ALTER as `025_…` while upstream will carry it as `031_…`; keeping
both when CoreBunch#335 lands makes every recorded installation run it twice.
Verified by planting that exact `031` entry — the test names the collision
and fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CoreBunch#495 claims `031_installed_plugins_source` and CoreBunch#505 claims `032`, so this
ALTER moves to `033` before either lands. The runner keys on the full id,
so two `031_…` entries would both run — but a shared number still makes
whoever merges second renumber, and an ALTER can only be renumbered while
no installation has applied it. That is true today, so it moves now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 16, 2026
The note still said CoreBunch#335 claims 031. It now claims 033, because CoreBunch#495
took 031 — the backfill stays at 032 between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 16, 2026
… and 033 (CoreBunch#335)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 16, 2026
… and 033 (CoreBunch#335)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mostafasadeghidev added a commit to mostafasadeghidev/Instatic that referenced this pull request Sep 16, 2026
CoreBunch#335 moved its ALTER from 031 to 033 because CoreBunch#495 claims 031. This branch
runs the same ALTER as `025_…`, recorded by both live installations, so
the incoming entry is dropped exactly as the 031 one was: both migration
files are unchanged by the merge itself. The notes that tell a future merge
what to delete now name 033, and so does the fork gate's comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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