From d11faf9251bd5030a7a475f052e959020fa7632b Mon Sep 17 00:00:00 2001 From: Aaron Sachs <898627+asachs01@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:16:26 +0000 Subject: [PATCH] fix(plugin-sdk): definePlugin() was silently dropping contentAccess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit definePlugin() built its returned manifest by copying known fields off config one by one, and contentAccess was never one of them — even though it's a real field on both DefinePluginConfig and PluginManifest, and the CLI's own `init --kind content-editor` scaffold writes it into the generated instatic-plugin.config.ts. Any content-editor-kind plugin declaring a cms.content.* permission therefore failed manifest validation ("contentAccess is required...") even with contentAccess correctly declared — the config field existed, the builder just never copied it through. Found building wyre.approvals (task_1788833699913_71019278): a plugin using content.publish to gate publishing behind an approval flow. Test added — confirmed it fails on the old code with the exact error above (git stash the fix, 2/3 tests fail), passes with it. Full src/__tests__/plugins/ suite (174 tests) still green. --- src/__tests__/plugins/definePlugin.test.ts | 55 ++++++++++++++++++++ src/core/plugin-sdk/builders/definePlugin.ts | 14 +++++ 2 files changed, 69 insertions(+) create mode 100644 src/__tests__/plugins/definePlugin.test.ts diff --git a/src/__tests__/plugins/definePlugin.test.ts b/src/__tests__/plugins/definePlugin.test.ts new file mode 100644 index 000000000..100135e4e --- /dev/null +++ b/src/__tests__/plugins/definePlugin.test.ts @@ -0,0 +1,55 @@ +/** + * Regression test for a real gap found building a content-editor-kind + * plugin (wyre.approvals, 2026-09-08): `definePlugin()` silently dropped + * `contentAccess` from the manifest it returns, even though it's a real + * field on `DefinePluginConfig`/`PluginManifest` and the CLI's own + * `init --kind content-editor` scaffold writes it into the generated + * `instatic-plugin.config.ts`. Any plugin declaring a `cms.content.*` + * permission then failed manifest validation with "contentAccess is + * required..." despite having declared it correctly — the config field + * existed, the builder just never copied it into its output. + */ +import { describe, expect, it } from 'bun:test' +import { definePlugin } from '@core/plugin-sdk' +import { parsePluginManifest } from '@core/plugins/manifest' + +describe('definePlugin — contentAccess', () => { + it('carries contentAccess through into the returned manifest', () => { + const { manifest } = definePlugin({ + id: 'acme.example', + name: 'Example', + version: '0.1.0', + permissions: ['cms.content.read', 'cms.content.publish'], + contentAccess: [{ table: 'pages', modes: ['read', 'publish'] }], + }) + + expect(manifest.contentAccess).toEqual([{ table: 'pages', modes: ['read', 'publish'] }]) + }) + + it('omits contentAccess entirely when not declared (matches every other optional field)', () => { + const { manifest } = definePlugin({ + id: 'acme.example', + name: 'Example', + version: '0.1.0', + permissions: ['cms.storage'], + }) + + expect(manifest.contentAccess).toBeUndefined() + }) + + it('the resulting manifest passes parsePluginManifest — the actual failure mode this fixes', () => { + const { manifest } = definePlugin({ + id: 'acme.example', + name: 'Example', + version: '0.1.0', + permissions: ['cms.content.read', 'cms.content.publish'], + contentAccess: [{ table: 'pages', modes: ['read', 'publish'] }], + }) + + // Before the fix this threw: "contentAccess is required when any + // cms.content.* permission is granted" — parsing the manifest built by + // definePlugin() itself failed even though the author had declared + // contentAccess correctly in instatic-plugin.config.ts. + expect(() => parsePluginManifest(manifest)).not.toThrow() + }) +}) diff --git a/src/core/plugin-sdk/builders/definePlugin.ts b/src/core/plugin-sdk/builders/definePlugin.ts index 369849fbb..93953a384 100644 --- a/src/core/plugin-sdk/builders/definePlugin.ts +++ b/src/core/plugin-sdk/builders/definePlugin.ts @@ -33,6 +33,7 @@ import type { PluginPermission, PluginResource, } from '../types' +import type { ContentAccessEntry } from '../contentSchemas' import type { PluginModuleDefinition } from '../modules' import type { PluginPackContents } from './definePack' import { @@ -83,6 +84,16 @@ export interface DefinePluginConfig { */ resources?: PluginResource[] + /** + * Per-table allowlist for the `cms.content.*` SDK surface. Required + * (non-empty) when `permissions` includes any of `cms.content.read` / + * `cms.content.write` / `cms.content.publish` / `cms.content.delete` — + * `parsePluginManifest` fails closed otherwise. Each entry's `modes[]` + * must be covered by the matching `cms.content.*` permission (a `write` + * mode with no `cms.content.write` permission is rejected at parse time). + */ + contentAccess?: ContentAccessEntry[] + /** * Admin pages registered by the plugin (markdown / map / resource / app). * Auto-deduped against the page id. @@ -188,6 +199,9 @@ export function definePlugin(config: DefinePluginConfig): PluginDefinition { ...(config.frontend !== undefined ? { frontend: { assets: config.frontend.assets.map((asset) => ({ ...asset })) } } : {}), + ...(config.contentAccess + ? { contentAccess: config.contentAccess.map((entry) => ({ ...entry, modes: [...entry.modes] })) } + : {}), } return { manifest,