@emdash-cms/plugin-cli@0.8.1
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4c3edadeec |
ci: release (#2419)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
cd4268d1d1 |
fix(security): prevent image parser denial of service (#2504)
* fix(security): prevent image parser denial of service * test(security): cover all patched parser paths * chore(build): preserve patched dependency rationale |
||
|
|
5f0ef18d14 |
Revert "feat: cache purge API (object cache + native Workers Caching)" (#2281)
Reverts
|
||
|
|
e88655417d |
feat: cache purge API (object cache + native Workers Caching) (#2275)
* feat: add object-cache purge API and cache:purge plugin capability
Admins and sandboxed plugins can clear CMS object-cache namespaces
(KV/memory) via GET/POST /_emdash/api/admin/cache/object and
ctx.cache. Block Kit buttons gain optional disabled and title fields
for clearer troubleshooting UI.
* style: format
* feat: add Workers Cache purge API alongside object cache
Admins and plugins with cache:purge can clear edge-cached pages via
GET/POST /_emdash/api/admin/cache/workers and ctx.cache.purgeWorkersCache()
(Cloudflare purge_everything using CF_ZONE_ID + CF_CACHE_PURGE_TOKEN).
* feat: purge Workers Cache via native cache.purge()
Replace zone REST purge (CF_ZONE_ID + token) with cloudflare:workers
cache.purge({ purgeEverything: true }). Status is configured when the
native API is available — no secrets required.
* fix(core): resolve Workers Cache purge via virtual module
Dynamic import of cloudflare:workers from core failed under Vite.
Expose cache through virtual:emdash/workers-cache (same pattern as env
and waitUntil) so status/purge work on the Cloudflare adapter.
* feat: Workers Cache path-prefix purge
POST /admin/cache/workers and ctx.cache.purgeWorkersCache() accept
optional pathPrefixes (paths or full URLs, normalized). Empty input
still purges everything via cache.purge.
* fix: lint workers-cache handlers and marketplace capability list
Move URL regex to module scope, drop redundant unknown union, rename
shadowed Tooltip render prop, and include cache:purge in
CAPABILITY_LABELS contract test.
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
|
||
|
|
3c555590ab |
ci: release (#1930)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
e52dea9b72 |
feat(plugins): allow explicit MCP tool declarations (#2002)
* feat(plugins): add explicit MCP tool declarations * fix(plugins): harden MCP request and consent handling * fix(admin): send marketplace capability consent flag |
||
|
|
baf4d8394f |
ci: release (#1871)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
b530d4f1ea |
chore(deps): update oxlint 1.73 and oxlint-tsgolint 0.24 (#1919)
Bumps oxlint 1.71.0 to 1.73.0 and oxlint-tsgolint 0.23.0 to 0.24.0. The stricter no-unnecessary-type-assertion rule flagged 82 redundant assertions. Removed them via autofix, dropped the now-dangling no-unsafe-type-assertion disable comments, removed the type-only imports left unused, and added justified no-base-to-string suppressions at the few sites where a removed assertion had been narrowing an unknown scalar for String(). Compile-time only; emitted output is unchanged. |
||
|
|
7c5de08f63 |
feat(plugins): read-only taxonomy access via new taxonomies:read capability (#1719)
* feat(plugins): read-only taxonomy access via new taxonomies:read capability * test(cloudflare): cover PluginBridge taxonomy methods Review follow-up: capability enforcement, locale/taxonomy filter SQL wiring, and D1 row mapping (JSON parsing, int→bool, nullable columns) for taxonomyList/taxonomyTerms/taxonomyEntryTerms. * test/fix: review follow-ups for taxonomies:read Guard the in-process collections JSON parse like both bridges (an in-process plugin no longer crashes on malformed definition data), and extend the workerd conformance suite to taxonomy/terms and taxonomy/entryTerms: capability gating, locale filtering, data JSON parsing, and the pivot join on translation_group. |
||
|
|
d9d7cd545b |
docs: document required build step before plugin-cli typecheck (#1642)
* docs: document required build step before plugin-cli typecheck * chore: add empty changeset for docs-only build-step note (#1205) --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> |
||
|
|
7f143184d9 |
ci: release (#1449)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
b01aa9bbb4 |
fix(registry): declaredAccess as the plugin trust contract (#1461)
* feat(registry): declaredAccess vocabulary + total capability converters Adds the email.events/email.transport, page.fragments and users facets to the releaseExtension lexicon, and the canonical DeclaredAccess type plus the two total converters (capabilitiesToDeclaredAccess / declaredAccessToCapabilities) in plugin-types. A round-trip property test over all 1152 implication-closed capability states proves the two representations are isomorphic -- the guard whose absence caused the consent-drift bug. Adds an optional declaredAccess field to PluginManifest ahead of making it the authoritative wire contract. * feat(registry): emit declaredAccess in the bundle manifest; reconcile at parse The bundler (both extractManifest copies) now writes declaredAccess into manifest.json alongside the capability list. The manifest Zod schema accepts it, and reconcileManifestAccess -- applied at the two bundle-parse chokepoints (extractBundle, loadBundleFromR2) that both marketplace and registry installs flow through -- makes declaredAccess authoritative: capabilities/allowedHosts are re-derived from it so runtime enforcement always matches the trust contract. A pre-migration bundle without declaredAccess has it derived from the legacy list (back-compat). Runtime enforcement code is untouched. plugin-cli 391 + core plugins 596 tests green. * feat(registry): publish carries manifest.declaredAccess verbatim; drop buildDeclaredAccess The release record's trust extension now carries the bundle manifest's declaredAccess directly (falling back to deriving it from the legacy capability list for pre-migration tarballs), replacing the lossy buildDeclaredAccess/findUnmappedCapabilities that dropped hook-registration capabilities. Adds publish-side regression coverage for the email-transport+events+network plugin that caused DECLARED_ACCESS_DRIFT. * feat(registry): admin consent derives capabilities via the shared converter RegistryPluginDetail now builds the consent list from the record's declaredAccess using the same total declaredAccessToCapabilities from @emdash-cms/plugin-types that the bundler and runtime use, replacing a component-local flattener that produced category:operation strings and silently dropped hook-registration capabilities. The consent list now equals what the install handler enforces, so a plugin declaring email.transport/events installs cleanly. Adds @emdash-cms/plugin-types as an admin dependency to keep one source of truth rather than a third browser-side reimplementation. * chore: changeset for declaredAccess trust contract * fix(registry): close two declaredAccess permission holes found in review Finding 1 (network widening): an empty allowedHosts array decoded to network:request:unrestricted -- the most-restrictive spelling granted the most. declaredAccessToCapabilities now treats an absent allowedHosts as unrestricted and a present (even empty) list as host-restricted; the bundle schema also rejects an empty array to match the record lexicon. Finding 2 (consent bypass): the install consent gate compared capability strings, which discard host scope, so a record advertising one allowedHosts and a bundle enforcing another both reduced to network:request and passed. handleRegistryInstall now refuses any install whose bundle declaredAccess does not match the signed record's enforced access -- capabilities AND hosts. Round-trip and integrity regressions added. * style: format * fix(registry): apply the declaredAccess integrity gate on update too handleRegistryUpdate diffed capabilities for escalation but never checked the new bundle's declaredAccess against the new signed record's -- so an update changing only host scope (api.good.com -> evil.com) kept the capability set identical, passed the escalation diff, and installed a bundle enforcing a scope the record never advertised. Adds the same enforcedAccessEqual gate handleRegistryInstall uses. Caught by emdashbot review of #1461. * refactor(registry): localize the manifest cast; drop review-thread comments reconcileManifestAccess now returns PluginManifest, moving the as-unknown bridge (needed because Zod types Block Kit admin elements as unknown) into the helper so both parse sites read cleanly. Also rewrites test comments that referenced the review thread and bug history to state the invariant under test instead. --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com> |
||
|
|
73b5cf486c |
ci: release (#1283)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
204b2f1931 |
ci: release (#1209)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
d2f26792bc |
feat(registry): display long-form plugin info sections + SBOM/lastUpdated (#1253)
* refactor(admin): extract shared renderMarkdown into lib/markdown Lifts the audited marked+DOMPurify renderer out of MarketplacePluginDetail into a shared module so the registry plugin detail page (#1030 sections) can reuse one sanitizer. Adds an XSS regression suite covering script/raw-HTML/ javascript:/data: links, foreign-host images, and inline styles. No behavior change to the marketplace readme rendering. * feat(registry): profile sections (description/installation/faq/changelog/security) in CLI publish * feat(registry): admin sections, SBOM badge, lastUpdated, verified tooltip * feat(registry): make profile sections editable via update-package sections becomes a first-class editable profile field rather than publish-only/preserved: PackageUpdateInput carries it, the generic diff loop writes it (manifest-absent = preserve, like keywords), and the dry-run diff truncates long values so a sections change stays readable. * fix(registry): address PR review feedback on sections - validate now resolves section file-refs offline (reads files, enforces the byte/grapheme caps + path-escape guard), so it matches the documented 'fails locally' contract instead of deferring those checks to publish. - markdown renderer.link renders the link's inline tokens instead of the raw markdown source, so [**bold** text](url) formats correctly; DOMPurify still re-sanitizes the output. Fixes a bug inherited from the marketplace renderer. - drop the setState-during-render section reconcile; activeSection's fallback to the default already guarantees the Tabs value has a matching pane. |
||
|
|
245f8dc221 |
plugin-cli: fix emdash-plugin build ESM path resolution on Windows (#1247)
Closes #1236 Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> |
||
|
|
60c0b2eeab |
Registry: env constraints (env:emdash, env:astro) end-to-end with admin compatibility warning (#1238)
* feat(registry): release env requires + admin compatibility gate
Plugins published to the experimental registry can declare release-level
environment constraints. A manifest's `requires` block (e.g.
`{ "env:emdash": ">=1.0.0", "env:astro": ">=4.16" }`) is validated at
publish time and written into the release record.
- registry-client: new dependency-free `./env` module shared by the CLI,
server, and admin — `parseRequires` (guards the lexicon-`unknown`
value), `isValidVersionRange`, `satisfiesRange`, and
`checkEnvCompatibility` over a focused semver-range grammar
(comparators, caret, tilde, partial versions, wildcard, AND sets).
- plugin-cli: `RequiresSchema` on the manifest, threaded release-level
into `publishRelease` (never via the profile input); JSON Schema
regenerated.
- core: capture the host Astro version in `astro:config:setup` and
surface it (with EmDash VERSION) on the admin manifest. New
`assertEnvCompatible` gate refuses incompatible install AND update
with `ENV_INCOMPATIBLE` (409), placed after yank-check, before the
artifact fetch.
- admin: RegistryPluginDetail reads host versions, renders a localized
compatibility warning and disables Install when unsatisfied.
Closes #1031.
* fix(registry): apply adversarial review findings for env requires
Share the version->env:* host map between server and admin via a single
hostEnvFromVersions helper, dropping the admin's redundant /manifest fetch
and the duplicated dev-skip rule (admin now derives host env from the
manifest query the shell already runs).
Log skipped env constraints server-side (findSkippedEnvConstraints) so a
host version the gate can't evaluate is observable rather than a silent
bypass. Cover the gate end-to-end through handleRegistryUpdate with a
mocked DiscoveryClient, asserting ENV_INCOMPATIBLE aborts before any
artifact fetch. Document the accepted, more-permissive prerelease range
semantics relative to node-semver.
* refactor(registry): use node-semver for env requires range evaluation
Replace the hand-rolled range evaluator in registry-client's env module
with the semver package. Adds || union support and node-semver prerelease
gating; satisfiesRange passes includePrerelease so a prerelease host build
is evaluated by precedence rather than excluded from release-only ranges
(a prerelease host is not a definite mismatch). isValidVersionRange (shared
by the publish-time RequiresSchema) and the fail-open gate semantics are
unchanged.
* refactor(registry): drop redundant buildHostEnv wrapper
buildHostEnv was a pure pass-through to hostEnvFromVersions (the shared
registry-client/env helper). The install/update routes now call
hostEnvFromVersions directly, and its duplicate dev-skip/astro-omit test
block is removed (covered by registry-client's env tests).
* refactor(registry): nest requires under release.requires
Aligns env constraints with release.artifacts (merged in #1239) under a
single release block, matching the RFC. repo stays top-level (documented
legacy for backwards compatibility). Publish reads manifest.release.requires;
the normalised manifest keeps a flat requires field.
|
||
|
|
1a4918ff98 |
Registry: icon, screenshot, banner artifacts end-to-end (#1239)
* feat(registry): icon / screenshot / banner artifacts end-to-end
Wire release.artifacts.{icon,screenshot,banner} through publish and admin.
CLI:
- Manifest schema gains a `release.artifacts` block: icon/banner as single
`{ file }` refs, screenshot as an array. JSON Schema regenerated.
- `publish` resolves each ref relative to the manifest, measures dimensions
and content type via image-size, uploads to `--artifact-base-url`, and
embeds `{ url, checksum, contentType, width, height, lang? }` in the
release. The lexicon types `screenshot` as a single artifact, so the first
screenshot uses that slot and extras ride in `x-screenshot-N` custom keys.
- image-size catalog-pinned.
Server:
- New admin proxy `GET /registry/artifact?url=` for publisher-supplied image
URLs. Applies SSRF defences via assertSafeArtifactUrl (re-validating each
redirect hop), enforces an image content-type allowlist, caps the body,
and serves back with `private, no-store` plus attachment + sandbox CSP so
a navigated SVG can't execute in the admin origin.
Admin:
- RegistryPluginDetail renders the icon, banner, and a screenshot gallery
through the proxy. Every image URL goes through artifactProxyUrl (scheme
allow-list) before the proxy.
* fix(registry): collision-free artifact URLs and tighter publish guards
Prefix each uploaded artifact URL with its role/index slot
(icon-, banner-, screenshot-N-) so two refs that share a basename in
different source directories no longer collapse to one upload target.
Previously two screenshots named shot.png in light/ and dark/ both
mapped to <base>/<slug>/<version>/shot.png; the second PUT overwrote
the first and both records pointed at the same URL.
Also tighten the publish-side path-escape guard to reject the `..`
segment precisely (plus absolute paths) instead of any relative path
beginning with two dots, which false-positived filenames like
`..config.png`. Drop image/avif from the proxy allowlist so the
served content types match what the CLI can actually produce.
Add tests: same-basename screenshots/icon get distinct URLs, a
two-dot filename is accepted, and a streamed body with no
content-length that exceeds the cap is rejected with 413 via
readCapped.
* refactor(registry): first-class screenshots array, drop x-screenshot-N
The #1033 implementation stored extra screenshots under x-screenshot-N
custom artifact keys, which is not FAIR-aligned. FAIR's artifacts map
allows an artifact value to be a list of objects; model screenshots as
a first-class array end to end.
- lexicon: artifacts.screenshot (single ref) -> screenshots (array of
#artifact, maxLength 8); regenerate atcute types
- plugin-cli manifest schema: release.artifacts.screenshot -> screenshots;
regenerate JSON schema
- publish path: write artifacts.screenshots as an array, drop the
x-screenshot-N spillover; keep the collision-free slot-prefixed upload
URL scheme (screenshot-N-<filename>)
- admin: read the screenshots array directly, drop x-screenshot-N
collection and numeric ordering
* chore(registry): align bundle screenshot cap (5 -> 8) with the screenshots array cap
The bundle path's MAX_SCREENSHOTS governed a separate ingestion route
from the lexicon/manifest screenshots array (capped at 8). Align them so
all screenshot caps agree.
* refactor(registry): resolve artifact proxy URLs from the release record; forbid SVG, allow AVIF
The artifact proxy no longer accepts a caller-supplied URL. The client
addresses an artifact by coordinates (did, slug, version, kind, index); the
server resolves the declared URL from the validated release record, so the
proxy can only ever fetch a publisher-declared artifact. SSRF defences
remain as a second layer on the resolved URL (incl. every redirect hop).
SVG is dropped from both the proxy allowlist and the publish CLI (active
content); AVIF is added end-to-end.
|
||
|
|
139a4d5c96 |
ci: release (#1123)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
cf3c706a65 |
feat(plugin-cli): add update-package command (#1126)
* feat(plugin-cli): add update-profile command (#1032) Lets publishers edit an already-published profile (license, authors, security contacts, name/description/keywords) without cutting a new release. Dry-run by default; `--yes` writes via putRecord and bumps lastUpdated. Refuses to change the slug, and surfaces a clear rename warning when the manifest slug has drifted from the published one. Closes the gap from the publish flow's "Profile updates aren't supported yet" warning, which now points users at this command. * refactor(plugin-cli): rename update-profile → update-package "Profile" overloaded with the publisher's atproto profile (app.bsky.actor.profile / src/profile.ts). The command operates on com.emdashcms.experimental.package.profile — the per-package metadata record — so "package" describes the user-facing surface without the clash. Renames the command, its api module, exported types (PackageUpdateInput, UpdatePackageError, etc.), error codes (PACKAGE_NOT_FOUND, PACKAGE_INVALID), and updates the publish warning. The underlying record is still a profile record; docstrings keep that term where it refers to the lexicon shape. * fix(plugin-cli): address review on update-package Drop UpdatePackageOptions.did — derive from publisher.did so the two can't disagree (Copilot). POSSIBLE_RENAME now lists every other package the publisher owns rather than singling out an arbitrary one, so a publisher with multiple plugins doesn't see a misleading rename pointer at an unrelated slug (Copilot). Flip the optional-field policy: missing from manifest = preserve existing value, matching `publish` semantics. Removing `description` or `keywords` by accident no longer wipes the published value (ask-bonk). Add atproto swapRecord optimistic concurrency: PublishingClient gains swapRecord on putRecord/unsafePutRecord; update-package threads existing.cid through and surfaces concurrent edits as STALE_RECORD instead of silently overwriting (ask-bonk). Rethrow auth/permission errors from the sibling-scan path so an expired token surfaces as itself rather than a misleading PACKAGE_NOT_FOUND (ask-bonk). Reword LEXICON_VALIDATION_FAILED to name caller-input length/grapheme caps as the likely cause rather than blaming update-package (ask-bonk). Update --json help text to match the actual output shape (Copilot). Extract redirectConsolaToStderr into cli-output.ts, shared between publish and update-package (Copilot). |
||
|
|
463c7a2303 |
refactor(plugin-cli): validate probed plugin shape with Zod (#1145)
* refactor(plugin-cli): validate probed plugin shape with Zod
Replaces the hand-rolled `if`/`throw` chain in `probeAndAssemble`
with a single Zod schema (`ProbedDefaultSchema` in
`probe-schema.ts`) and translates Zod issues back into the same
`BuildPipelineError` format plugin authors already see. Removes the
last 9 `no-unsafe-type-assertion` warnings in `pipeline.ts` and
clears the file's lint debt.
Key design choices:
- `normaliseEntry` (preprocess) maps bare-function hooks/routes to
`{ handler }` so the schema only validates one shape per entry.
- Only plain objects (prototype is `Object.prototype` or null) reach
per-field validation; exotic objects (Date, RegExp, Promise, class
instances) are reduced to `null` so the entry-shape error fires
instead of a misleading 'missing handler' message.
- Non-record outer collections (`hooks: []`, `hooks: null`, etc.)
coerce to an empty record so the build doesn't fail on the kind of
shapes the original `Object.keys` iteration silently no-op'd on.
- A `safeStringify` helper survives BigInts, cycles, functions, and
symbols when embedding the offending value in the error message.
New tests in `probe-schema.test.ts` (32 cases) lock in the error-
message contract: code, exact message strings for the documented
field violations, and behavioural choices around exotic values.
* chore: add changeset
|
||
|
|
d4377338fc |
chore: clean up oxlint warnings across packages (#1140)
* chore: clean up oxlint warnings across packages Addresses 47 of 56 type-aware lint warnings, leaving only the plugin-cli pipeline.ts unsafe assertions which warrant a separate Zod-driven PR. Changes by category: - preserve-caught-error: add `cause` to thrown error in registry handler - no-unused-vars: drop unused `Link` import, prefix unused e2e vars - no-shadow: rename locals shadowing Lingui macros (`msg`, `plural`) - no-unnecessary-type-assertion: remove redundant casts and `!` where prior `typeof` checks or tsconfig settings already narrow the type - atproto-test-utils: add to test-override file glob (test factories legitimately produce branded types) - contentful-to-portable-text: add runtime type guards in types.ts (`isContentfulLinkPayload`, `isContentfulSysEnvelope`, `parseAssetFile`, `getStringField`, `getRecordField`) and use them throughout, replacing scattered `as` casts with validated narrowing - adapt-sandbox-entry: extract `normalizeRouteEntry` helper, simplify headers normalization (`ctx.request` is always a real Request in the in-process adapter), narrow remaining boundary casts to single disable comments with rationale - admin/api/registry: use the shared `parseApiResponse` helper for install endpoint; localStorage cache parse uses a type guard * test: drop unused byline IDs in bylines.spec.ts The `_firstBylineId` / `_secondBylineId` names read as if the values themselves are unused, but the test actually relies on the `createByline` side effects (the bylines need to exist before the combobox can find them by name). Drop the bindings and add a comment explaining the intent. |
||
|
|
102535f308 |
ci: release (#1103)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
3756168222 |
feat(registry): profile fields end-to-end (license, authors, security, keywords, repo) (#1112)
* feat(registry): profile fields end-to-end (license, authors, security, keywords, repo) Wires the emdash-plugin.jsonc profile block through the publish pipeline and renders it in the admin. CLI (@emdash-cms/plugin-cli): - New structured ProfileInput on publishRelease (multi-author, multi-security, name, description, keywords); release-level repo. - Flat ProfileBootstrap / --license / --author-* / --security-* kept working but deprecated; flags override the manifest and warn. - manifestToProfileInput translation. Admin (@emdash-cms/admin): - Detail page renders license (spdx-linked), keywords, authors, security contacts, repo link; browse card shows license. - Untrusted aggregator profile/release parsed defensively; external hrefs scheme-allowlisted; emails validated before mailto. Closes #1029 * refactor(registry): validate aggregator records at the client-lib trust boundary DiscoveryClient now lexicon-validates the embedded signed profile/release records (atcute safeParse), returning PackageProfile.Main | null / PackageRelease.Main | null instead of unknown. New Validated* view types. - Replaces the ad-hoc Array.isArray/typeof shape-parsing added for #1029 in the admin components with a single validated boundary. safeExternalHref / safeEmail are kept: the lexicon uri format permits javascript:, so validation is structural only and not an href-safety control. - Admin view types alias the registry-client Validated* types; wrapper as-casts removed. - Core install handler reads the typed release/profile; a non-conforming record (null) fails closed at the existing identity / artifact gates. - plugin-cli info/search drop now-redundant local re-validation. Validation is non-stripping (atcute is non-destructive; lexicon objects are open) and that contract is asserted in tests. Adversarial review (2 rounds) clean: no reintroduced XSS, core fails closed, typing sound. Refs #1029 * refactor(registry-client): validate response envelope via atcute client.call DiscoveryClient now routes every aggregator XRPC call through @atcute/client's schema-validating `.call()` instead of the NSID-string `.get()`, which did no runtime validation. atcute now validates request params and the response envelope against the aggregator method's output lexicon; a non-conforming envelope throws ClientValidationError. The existing per-record `validateProfile` / `validateRelease` step still runs because the aggregator lexicon intentionally types `profile` / `release` as `unknown` (verbatim pass-through of signed publisher records under a different lexicon namespace). Side effects of the envelope-validation layer: - A non-object `profile` / `release` from the aggregator now throws at the call layer (lexicon `unknown` is "open object", not "any value") instead of becoming `null` via the record validator. Strictly stricter, fail-closed earlier. - Request params (did/handle/slug/limit/cursor) are now validated before the request fires. Defense-in-depth; existing call sites already feed validated values. Test fixtures updated for the now-active envelope validation: real DASL CID, all required `packageView` / `releaseView` fields. No public type changes. Adversarial review clean (one deferred LOW: install handler surfaces ClientValidationError under generic INSTALL_FAILED code — pre-existing error-classification gap, belongs with #1036). Refs #1029 |
||
|
|
361f0e27b4 |
ci: release (#1096)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
67888292c8 |
fix(plugin-cli): rename --aggregator flag to --registry-url (#1092)
The discovery commands ('search', 'info') used '--aggregator <url>'
while the env var 'EMDASH_REGISTRY_URL', the default URL host
('registry.emdashcms.com'), and the user-facing concept all spoke of
the 'registry'. Three surface words for adjacent concepts hurt the
read-out-loud test.
Rename the flag to '--registry-url' to converge on a single user-facing
term. Internally the helper still calls into an aggregator service
(the registry is the broader system; an aggregator is one indexer of
it) — the rename only affects what users type.
Also fix the README's '--json' surface description, which mirrored the
out-of-date docs claim that 'all commands accept --json'.
|
||
|
|
6725e91431 |
fix(plugin-cli): normalise multi-word flags to kebab-case (#1091)
The 'publish' subcommand uses kebab-case for multi-word flags
('--author-name', '--no-manifest', ...) while 'build', 'dev', and
'bundle' use camelCase ('--outDir', '--validateOnly'). Standardise on
kebab-case across all subcommands.
- --outDir -> --out-dir
- --validateOnly -> --validate-only
The programmatic API options on bundlePlugin / buildPlugin remain
camelCase (they are JS function options, not CLI flags).
|
||
|
|
c0ce915c55 |
feat(plugin-cli): sandboxed plugin authoring CLI (#1057)
* 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>
|