c0ce915c55
* feat(registry-cli): extend manifest schema with identity + trust contract
First phase of the sandboxed plugin redesign (#1028b). Adds the
manifest fields that make `src/index.ts` and the in-code descriptor
factory redundant. The trust contract is now hand-authored in the
manifest, where a security reviewer can find it without grep.
New required fields:
- `slug`: ASCII letter then letters/digits/hyphens/underscores, max 64
chars. Matches the registry lexicon's rkey grammar via the shared
PLUGIN_SLUG_RE in @emdash-cms/plugin-types.
- `version`: semver 2.0 subset, no build-metadata (atproto rkeys can't
contain `+`). Validated via PLUGIN_VERSION_RE.
- `publisher`: now required (was optional in #1028a). The runtime
cannot compute the plugin's AT URI without it; making it optional
meant the plugin couldn't load locally before first publish.
New optional fields with sensible defaults:
- `capabilities`: array of capability strings. Defaults to []. Each
entry validated against the current vocabulary; deprecated names are
hard-rejected with a hint at the replacement (no deprecation window
for new authoring).
- `allowedHosts`: array of host patterns. Defaults to []. Required
non-empty when `network:request` is declared without
`:unrestricted`. Forbidden when `:unrestricted` is declared.
- `storage`: map of collection name -> { indexes, uniqueIndexes? }.
Defaults to {}.
The cross-field rule for network:request / allowedHosts mirrors the
release-extension lexicon's networkRequestConstraints behaviour, so
authors hit the schema error here rather than a PDS validation error
at publish time.
Schema regenerated. 33 new tests; 204 total passing.
Part of #1028b. The bundle rewrite, init command, plugin migrations,
and `localPlugin` dev helper land in subsequent commits.
* feat(registry-cli): bundle reads identity + trust contract from manifest
Second phase of the sandboxed plugin redesign (#1028b). Bundle no longer
imports src/index.ts for a descriptor factory; the manifest is the
source of truth for identity (slug, version) and the trust contract
(capabilities, allowedHosts, storage). Bundle still probes the runtime
code for the hook/route surface — that's a syntactic property that
needs the code to exist.
Changes to bundle:
- Drop the main-entry build and descriptor extraction. No more
src/index.ts probing, no more `createPlugin` / default-factory /
default-object format detection.
- Replace `resolveEntries`: just locates emdash-plugin.jsonc (loaded
through the same loader the CLI's validate uses) and confirms
src/plugin.ts exists. No more package.json `exports` parsing.
- Replace `extractResolvedPlugin` with `assembleResolvedPlugin`: builds
the ResolvedPlugin shape from the manifest, then probes
src/plugin.ts for hook/route names.
- Probe (renamed from `augmentWithSandboxProbe` to `probePluginSurface`)
now reads src/plugin.ts. Hard-fails if the default export isn't a
definePlugin result.
- New error codes: MISSING_MANIFEST, MISSING_PLUGIN_ENTRY,
MANIFEST_INVALID. Old MISSING_PACKAGE_JSON / MISSING_ENTRYPOINT /
MAIN_BUILD_FAILED gone.
- Admin entry handling (admin.js, adminPages, adminWidgets) deferred
to a follow-up issue. The redesign hasn't touched admin yet; that
surface stays as-is and is gated on the descriptor's `admin` field
which no longer exists. When admin lands again it'll be a manifest
field with its own probe.
Changes to translate.ts:
- `NormalisedManifest` gains slug, version, publisher (required),
capabilities, allowedHosts, storage. Publisher is no longer
Optional — the schema enforces it.
Fixtures:
- `minimal-plugin/`: src/index.ts gone, sandbox-entry.ts renamed to
plugin.ts, new emdash-plugin.jsonc with identity + trust contract.
- `bad-plugin/`: stripped to manifest-only (no src/), exercises
MISSING_PLUGIN_ENTRY. Old "declares hooks but no sandbox entry"
case isn't possible anymore — there's no descriptor declaring
anything.
Net diff: -228 lines.
* feat(registry-cli): init command scaffolds a sandboxed plugin
Third phase of the redesign (#1028b). Adds `emdash-registry init [name]`
which produces the three-file plugin layout introduced by the previous
commits: emdash-plugin.jsonc, src/plugin.ts, package.json, plus a
tsconfig, README, .gitignore, and a passing test.
Modes:
- Interactive (default on a TTY): clack prompts for each unset field
with sensible defaults. ESC / Ctrl+C cancels cleanly.
- `--yes` / `-y` (non-interactive): no prompts; unset fields become
TODO placeholders in the manifest. The author fixes them before
first use.
- Non-TTY (CI, pipes): same as `--yes`; prompting into a non-
interactive stdin would hang.
Pre-fills:
- Publisher: the active session's handle from FileCredentialStore.
Resolved through @atcute/identity-resolver to a DID before write
so the runtime never sees a mutable handle. The handle is emitted
as a `// <handle>` line comment next to the pinned DID for `git
diff` readability — same convention as the post-publish write-back.
- Author name / email: `git config user.name` / `user.email`.
- Repo: `git remote get-url origin`, normalised from SSH to https
(`git@github.com:foo/bar.git` → `https://github.com/foo/bar`).
Falls back to `package.json#repository.url` if no git remote.
- License, description: `package.json` in the target dir if one
exists (for the "scaffold into existing repo skeleton" case).
Slug defaults to the positional `name`, `basename(--dir)`, or
basename(cwd) in that order. Every flag is optional in every mode.
Exported `resolveHandleToDid` from manifest/publisher.ts so init
can use the same resolver the post-publish write-back does.
Tests: 44 new (template renderers, scaffold filesystem behaviour,
environment probe). 249 total in the package.
* feat(plugins): migrate in-tree sandboxed plugins to the new layout
Fourth phase of the redesign (#1028b). Moves the 5 in-tree sandboxed
plugins to the manifest + src/plugin.ts shape so they become the
canonical references a plugin author looks at.
Each plugin's layout changes from:
src/index.ts (descriptor factory, ~50 lines)
src/sandbox-entry.ts (runtime code via definePlugin)
package.json (main / exports / files / build scripts)
to:
emdash-plugin.jsonc (identity + trust contract + admin surface)
src/plugin.ts (runtime code, unchanged)
package.json (private, typecheck script only)
Plugins migrated:
- atproto
- audit-log
- marketplace-test
- sandboxed-test
- webhook-notifier
Schema gains `admin` (pages + widgets) since four of the five plugins
declare admin surface. Mirrors PluginAdminPage / PluginDashboardWidget
in core. Atproto's plugin.test.ts rewritten to assert against the
manifest instead of the deleted descriptor factory.
KNOWN BREAKAGE: demos that import the old factories
(`auditLogPlugin()`, `webhookNotifierPlugin()`) from
astro.config.mjs are broken until the next commit ships
`@emdash-cms/registry-cli/dev`'s `localPlugin(dir)` helper and
updates the demos.
All published plugins still work — the bundled manifest.json shape
is unchanged. Only authoring changed.
* feat(registry-cli): add localPlugin(dir) dev helper + wire demos
Final piece of the sandboxed-plugin redesign (#1028b). Closes the gap
the plugin migrations opened — demos that previously imported
`auditLogPlugin()` / `webhookNotifierPlugin()` factories now consume
the plugins through their source directories.
New subpath `@emdash-cms/registry-cli/dev` exports `localPlugin(dir)`,
which:
- Reads `<dir>/emdash-plugin.jsonc` via the same loader the CLI uses.
- Confirms `<dir>/src/plugin.ts` exists.
- Resolves the manifest's publisher (handle → DID) so the descriptor
is in canonical form.
- Returns a PluginDescriptor-shaped object with `entrypoint` set to
the absolute `file://` URL of `src/plugin.ts`. Vite resolves the
URL through its standard fs path resolver — no build step needed.
The descriptor carries id, version, capabilities, allowedHosts,
storage, and (when declared) adminPages + adminWidgets from the
manifest. Plugins that don't expose admin surface pass through
without the optional fields, keeping the descriptor tidy.
Demos updated:
- demos/simple: auditLogPlugin() → localPlugin("../../packages/plugins/audit-log")
- demos/plugins-demo: auditLog + webhookNotifier the same way
- demos/cloudflare: webhookNotifier via localPlugin
- infra/cache-demo, infra/blog-demo: same
Trusted plugins (formsPlugin, embedsPlugin, apiTestPlugin) keep their
factory-based imports — they're not on the new shape and aren't part
of this redesign's scope.
Errors surface as a structured LocalPluginError with codes:
- MANIFEST_INVALID
- PLUGIN_ENTRY_MISSING
- PUBLISHER_UNRESOLVED
Tests: 10 new (descriptor shape, error paths, admin pass-through).
259 total in the package.
* feat(plugin-cli): rework sandboxed plugin authoring, build, and CLI
Renames @emdash-cms/registry-cli to @emdash-cms/plugin-cli and the
binary emdash-registry to emdash-plugin. Adds build + dev commands,
consolidates the build pipeline so bundle is a thin packaging step on
top of build. Introduces a strict author-facing SandboxedPlugin type
via the new emdash/plugin type-only subpath; sandboxed plugins now
default-export a bare { hooks?, routes? } object with satisfies
SandboxedPlugin and have no runtime emdash import. Drops definePlugin
and the build shim for sandboxed plugins (definePlugin is native-only
now). Migrates the five in-tree sandboxed plugins to the new shape.
Manifest version is optional and reconciled with package.json#version.
* fix(plugin-cli): adversarial review fixes
- init scaffold emits the new `satisfies SandboxedPlugin` shape and
npm-shape package.json (build/dev scripts, ./sandbox export, plugin-cli
devDep) instead of the broken `definePlugin` template
- publish reads package.json#version and reconciles via normaliseManifest
so the new "version in package.json only" pattern actually publishes;
malformed package.json surfaces a CliError, not a misleading
VERSION_MISSING further down
- dev watcher serialises rebuilds (queue collapsed to one follow-up),
closes the watcher before draining pending on Ctrl-C, short-circuits
scheduleRebuild during shutdown, handles Windows path separators in
the outDir ignore glob, clears pending+queuedTrigger in finally so an
IIFE rejection can't deadlock the session, and removes SIGINT handlers
on shutdown
- adapter normalises ctx.request to SandboxedRequest shape in-process
so handlers see the same { url, method, headers: Record } promised by
the strict type; null/array/non-object default exports rejected with
a plugin-id-bearing message
- build's readPackageMeta rejects empty/non-string version with the
same strictness as publish, killing the build-pass/publish-fail
asymmetry
- pipeline probe rejects invalid hook config (errorPolicy, priority,
timeout) so untyped JS authors get a build error rather than a
silently-wrong runtime contract
- versionless minimal-plugin fixture so bundle/publish/build integration
tests exercise the package.json-as-source-of-truth path
- definePlugin error wording softened for native-plugin authors whose
id field has a typo
- pipeline error messages and stale comments updated for the no-shim,
no-definePlugin authoring shape
- removed dead EMDASH_SHIM from the Cloudflare sandbox runner
- changesets retargeted to @emdash-cms/plugin-cli; scaffold/atproto/core
comments scrubbed for stale registry-cli references
* style: format
* docs(changesets): switch plugin migration examples to diff fences
* style: format
* Fix changeset ordering
* fix(ci): plugin build uses node-direct path; sweep stale registry-cli refs
In-workspace plugins use `node node_modules/@emdash-cms/plugin-cli/dist/index.mjs build`
because pnpm doesn't create the bin shim for a workspace package whose
bin target doesn't exist at install time. Plugin authors outside the
workspace get a published bin with a real dist, so `emdash-plugin build`
works for them via the natural scaffold.
Also fixes stale registry-cli references the rename pass missed:
- .oxfmtrc.json: schema ignore path
- .oxlintrc.json: 7 type-aware-cost allowlist entries
- .github/workflows/ci.yml: build filter includes plugin-cli for test:unit
- package.json: test:unit script
- packages/plugin-types/package.json: description
The schema file is regenerated to match what gen-schema produces. The
previously committed version had been hand-reformatted post-regen and
disagreed with the generator's output.
* fix(ci): remove legacy marketplace bundle path; address review findings
- Delete `packages/marketplace/tests/publish-e2e.test.ts` — invoked the
legacy `emdash plugin bundle` from core CLI against the new
manifest-driven plugin layout, which it doesn't understand.
- Remove the validate-plugins CI job — it used the same legacy CLI
command. Plugin validation is now covered by `pnpm build`, which
runs the new `emdash-plugin build` probe + manifest checks against
every in-tree sandboxed plugin.
- Fix `no-base-to-string` lint errors in audit-log/plugin.ts. The
canonical ContentHookEvent types `event.content.id` as unknown;
`String(unknown)` lands on '[object Object]' for record IDs. Added
a small `stringifyId` helper that returns '' for non-string/number
inputs so the caller's existence check skips bad rows.
- pipeline.ts now hard-errors when the probed module has no `default`
export, instead of silently falling through to an empty plugin
(build had been writing dist/ artifacts with empty hooks/routes for
any source that used `export const plugin = ...`).
- Scaffold README camelCases hyphenated slugs for the import binding.
Slugs like `my-plugin` were producing `import my-plugin from ...`
which is a syntax error. Test added with a hyphenated fixture.
Both bot review comments addressed.
* style: format
* fix(plugin-cli): bump test timeout to 30s for bundle tests on slow CI
bundle.test.ts > 'produces a tarball + manifest for a minimal valid
plugin' timed out at the 5s default on the GitHub-hosted runner.
The test runs the full build pipeline (tsdown probe + transpile +
tarball pack), which is fast locally (<2s) but cold-starts at 5-8s
on CI. Bump to 30s globally for the plugin-cli vitest config.
* chore: update lockfile
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ascorbic <213306+ascorbic@users.noreply.github.com>
Co-authored-by: ask-bonk[bot] <ask-bonk[bot]@users.noreply.github.com>