Commit Graph

74 Commits

Author SHA1 Message Date
emdashbot[bot] 4c3edadeec ci: release (#2419)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-18 13:26:49 +01:00
Mason James c91d56bb30 fix(forms): make standalone forms importable as documented (#2173)
* fix(forms): make standalone forms importable as documented

* test(forms): drop the redundant UI export check
2026-08-12 12:37:58 +01:00
emdashbot[bot] a891d73232 ci: release (#2350)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-11 15:49:52 +01:00
emdashbot[bot] 4255b04355 ci: release (#2244)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-06 00:24:20 +01:00
emdashbot[bot] 53dbf22555 ci: release (#2226) 2026-07-24 15:37:09 +01:00
emdashbot[bot] 09e5b5ca5b ci: release (#2176)
Auto Extract / Extract (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Version Check (push) Has been cancelled
CI / Changeset Validation (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Smoke Tests (push) Has been cancelled
CI / Integration Tests (push) Has been cancelled
CI / Browser Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / E2E tests (1/8) (push) Has been cancelled
CI / E2E tests (2/8) (push) Has been cancelled
CI / E2E tests (3/8) (push) Has been cancelled
CI / E2E tests (4/8) (push) Has been cancelled
CI / E2E tests (5/8) (push) Has been cancelled
CI / E2E tests (6/8) (push) Has been cancelled
CI / E2E tests (7/8) (push) Has been cancelled
CI / E2E tests (8/8) (push) Has been cancelled
CI / E2E Cloudflare (1/8) (push) Has been cancelled
CI / E2E Cloudflare (2/8) (push) Has been cancelled
CI / E2E Cloudflare (3/8) (push) Has been cancelled
CI / E2E Cloudflare (4/8) (push) Has been cancelled
CI / E2E Cloudflare (5/8) (push) Has been cancelled
CI / E2E Cloudflare (6/8) (push) Has been cancelled
CI / E2E Cloudflare (7/8) (push) Has been cancelled
CI / E2E Cloudflare (8/8) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Format / Format (push) Has been cancelled
Preview Releases / Publish Preview (push) Has been cancelled
Release / Release (push) Has been cancelled
Release / Sync Templates (push) Has been cancelled
zizmor / Run zizmor (push) Has been cancelled
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-22 11:47:04 +00:00
emdashbot[bot] 3c555590ab ci: release (#1930)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-21 11:26:37 +01:00
jc 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
2026-07-21 07:02:20 +01:00
emdashbot[bot] baf4d8394f ci: release (#1871)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-10 17:29:37 +01:00
Matt Kane 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.
2026-07-10 15:19:41 +00:00
emdashbot[bot] c67e177b13 ci: release (#1856)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 19:01:37 +00:00
emdashbot[bot] 5f9a1ba769 ci: release (#1734)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-07 06:27:29 +01:00
emdashbot[bot] b291f1cc67 ci: release (#1700)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-02 07:32:03 +01:00
emdashbot[bot] 71a148db9f ci: release (#1677)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-01 17:08:52 +01:00
emdashbot[bot] bc9f1b6111 ci: release (#1668)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-30 15:30:39 +01:00
emdashbot[bot] e7552e0c44 ci: release (#1660)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-30 14:36:20 +01:00
emdashbot[bot] 761164105d ci: release (#1653)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 12:42:00 +00:00
emdashbot[bot] 647cfe4da0 ci: release (#1629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 12:43:55 +01:00
emdashbot[bot] b66f697aa3 ci: release (#1592)
* ci: release

* format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
2026-06-25 19:35:58 +00:00
emdashbot[bot] 6b036c43d0 ci: release (#1534)
* ci: release

* format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
2026-06-22 20:23:36 +01:00
emdashbot[bot] 8b57af671c ci: release (#1516)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-17 20:42:46 +01:00
Matt Kane 997d7eea8f fix: close remaining isolate-poisoning hangs after #1489 (#1530)
#1489 migrated only getSiteSettings and ensureSearchHealthy off the
never-settling-promise pattern. Four more caches still cached an in-flight
promise on a long-lived singleton, so a request cancelled mid-fetch left a
promise that never settled and wedged every later request on the isolate
(524 at the 100s wall). Fix all four and rename the helper to be platform
neutral:

- Rename IsolateCache/isolateCachedAsync to SingleFlightCache/singleFlightCached
  (internal util in core, which also runs on Node; not Cloudflare-specific).
- byline field-defs cache: cache the value behind a reclaimable lock, preserving
  the cross-isolate persisted-version gate.
- config/secrets resolveSecretsCached: per-db SingleFlightCache.
- x402 enforcer: cache the ready server, drop the shared _initPromise.
- atproto plugin: inline poison-immune lock-and-poll for token refresh (KV is
  the source of truth; waiters never await the owner's promise).

Adds regression tests reproducing the stranded-owner hang for byline and atproto.

Follow-up to #1489.
2026-06-17 19:56:08 +01:00
emdashbot[bot] 7f143184d9 ci: release (#1449)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-16 17:57:16 +01:00
emdashbot[bot] 0df001ea9d ci: release (#1418)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-12 20:43:46 +01:00
jc 298895de11 Fix forms submit response parsing (#1395)
* Fix forms submit response parsing

* style: format

* Add forms submit response changeset

* Fix forms validation response envelope

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-06-12 16:38:41 +01:00
emdashbot[bot] 849628ff26 ci: release (#1360)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-11 20:46:32 +01:00
emdashbot[bot] 4fb27752e5 ci: release (#1354)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 12:14:08 +01:00
emdashbot[bot] a6e8a9185f ci: release (#1337)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 20:43:37 +01:00
emdashbot[bot] 73b5cf486c ci: release (#1283)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 23:41:53 +01:00
emdashbot[bot] d43a3808fb ci: release (#1280)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 23:06:13 +01:00
emdashbot[bot] 204b2f1931 ci: release (#1209)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 20:46:38 +01:00
ppppangu 5456514320 fix(forms): render public form embeds via SSR plugin routes (#985)
* fix(forms): render public form embeds via SSR plugin routes

* Address public form SSR review feedback

* style: format

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-05-29 10:46:05 +01:00
emdashbot[bot] 139a4d5c96 ci: release (#1123)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-28 12:01:24 +01:00
Matt Kane 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.
2026-05-22 11:26:08 +00:00
emdashbot[bot] 102535f308 ci: release (#1103)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-20 15:40:59 +01:00
emdashbot[bot] 361f0e27b4 ci: release (#1096)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-19 18:08:47 +01:00
Matt Kane 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>
2026-05-18 15:01:00 +01:00
Matt Kane dbaea9ccae feat(admin): install sandboxed plugins from the experimental registry (#1011)
* fix(deps): catalog-pin zod so trusted plugins typecheck

Astro bundles its own Zod and re-exports it as 'astro/zod'. Trusted
plugins like @emdash-cms/plugin-forms import their route schemas via
'astro/zod', then pass those schemas to definePlugin() in core. With
emdash's 'zod: ^4.3.5' resolving independently of Astro's caret,
pnpm kept two Zod 4 patches in the tree (e.g. 4.3.6 alongside 4.4.1).

Zod 4 embeds its semver in the type system, so two patches of Zod 4
are not assignable to each other. The forms plugin's route schemas
(ZodObject<..., $strip>) were rejected by PluginRoute<TInput>['input']
(ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>) with
'Type "3" is not assignable to type "4"' on the internal version
field. The native definePlugin overload silently failed, TS fell
through to the StandardPluginDefinition overload, and reported a
misleading 'id does not exist' error -- masking 8 cascading errors.

Catalog-pinning Zod forces a single workspace-wide instance and
restores normal overload resolution. No code changes needed in core
or plugins/forms.

Also adds a pnpm-workspace.yaml comment explaining the gotcha so the
next person doesn't bump emdash's pin past Astro's range.

* feat(registry): experimental decentralized plugin registry

Adds opt-in support for installing sandboxed plugins from the
decentralized plugin registry described in RFC #694. Enabled via
`experimental.registry.aggregatorUrl` in the EmDash integration
options; when set, the admin UI replaces marketplace browse/install
with the registry path.

Server: new install handler (RFC verification chain), endpoint at
POST /_emdash/api/admin/plugins/registry/install, migration 038 adds
`source = 'registry'` plus `registry_publisher_did` /
`registry_slug` columns on `_plugin_state`, runtime sync split into
shared marketplace + registry tiers via a normalized opaque
`r_<hash>` plugin id.

Browser: aggregator XRPC calls go direct from the admin UI via
@emdash-cms/registry-client. Install POST runs through the server.
Includes a minimum-release-age policy with a per-publisher exclude
allowlist, enforced both client-side (UX) and server-side (gate).

Hardening (5 rounds of adversarial review): bundle id rewritten to
the derived pluginId before storage, aggregator identity
cross-checked, artifact and aggregator URLs validated for SSRF
(https-only in prod, IPv6 brackets handled), per-request and total
budgets on every outbound call, decompressed bundle capped at 256
KiB to match the RFC publish-time limit, migration 038 idempotent
on both SQLite and Postgres.

Known gaps tracked for follow-up: full MST signature verification
against the publisher's PDS, multibase multihash decoding (hex SHA-256
is accepted today), registry plugin update + uninstall handlers.

* fix(registry-lexicons): drop codegen from build script

The generated lexicon types are committed to git so consumers don't
need the codegen toolchain. Running lex-cli generate as part of the
default build pipeline broke Cloudflare Pages builds for sites that
pull registry-lexicons in transitively, because lex-cli imports
lex.config.ts directly and Node in the CF Pages build environment
can't load .ts natively.

Codegen moves to a separate `regen` script (`pnpm regen` runs
codegen + full build). Maintainers run it when they edit the
lexicons; consumers just consume the committed output.

* fix(registry): copilot review fixes

- Drift check normalizes capabilities (filter strings, dedupe, sort) on
  both browser and server so reorderings or junk entries can't trigger
  spurious rejection. Adds a shared normalizeCapabilities helper in
  registry/config.ts and a mirror in admin/lib/api/registry.ts.
- RegistryPluginDetail no longer trusts the aggregator-supplied
  ext?.capabilities as already-validated string[]; runs it through
  normalizeCapabilities before display and before send.
- Fix stale '32 MiB' docstring on extractBundle (cap is actually
  MAX_DECOMPRESSED_BUNDLE_BYTES = 256 KiB).
- Fix plugin-id.ts JSDoc: validatePluginIdentifier regex is
  /^[a-z][a-z0-9_-]*$/ (allows hyphens); the prior 'cannot collide
  with marketplace ids' claim was too strong and is now framed as
  'syntactically distinct, plus an explicit pre-existing-row check
  in the install handler.'

* fix(registry): address review findings + CI failures

CI fixes:
- Rename normalizeCapabilities -> canonicalCapabilitiesForDriftCheck
  to avoid namespace clash with the existing capability normalizer
  exported from @emdash-cms/plugin-types via core's index. The old
  name shadowed plugin-types' helper at the top level of core's dist,
  which made the definePlugin() overload set look ambiguous to TS in
  plugins/forms and caused a typecheck cascade there.
- [...seen].toSorted() instead of [...seen].sort() to clear the
  e18e/prefer-spread-syntax + unicorn/no-array-sort lint errors.

Review findings (ask-bonk[bot]):
- HIGH: drift check tripped on every install when the release record's
  extension was empty. The browser now omits acknowledgedDeclaredAccess
  when capabilities is empty, opting out of the server-side drift gate
  for the (currently common) case where publishers haven't filled in
  the extension block. The bundle's real capabilities are still bound
  to the checksum-verified bytes.
- HIGH: DID-only publishers (no resolvable handle) could be linked from
  the browse grid but never installed because the server rejects
  handles without a '.'. Cards now render as non-interactive with a
  'Publisher handle unresolved' badge; the detail page surfaces a
  matching warning and disables Install.
- MEDIUM: registry-enabled sites were unconditionally routing existing
  marketplace plugin detail URLs to RegistryPluginDetail, breaking deep
  links. Detail-route selection now discriminates by param shape
  (pluginId.includes('/')) rather than the manifest flag.
- MEDIUM: state-row write failure after storeBundleInR2 left orphan
  bundles. Best-effort cleanup in the catch via deleteBundleFromR2.
- LOW: parseDurationSeconds runs on the user-supplied integration
  option per install (not the already-normalized manifest shape). Wrap
  in try/catch and surface as REGISTRY_POLICY_INVALID rather than
  letting it bubble to a generic INSTALL_FAILED.
- LOW: validator-pattern doc drift in plugin-id.ts (already fixed in
  the prior commit).

* fix(registry): move registry config types to their own module

The new RegistryConfig + ExperimentalConfig interfaces lived alongside
definePlugin's overloads in astro/integration/runtime.ts. tsdown +
rolldown's chunking decided to inline a bigger subset of plugin-related
types into the entry chunk as a result, which broke definePlugin()
overload resolution for trusted plugins building against core's dist
on CI (plugins/forms failed with 'id does not exist in type
StandardPluginDefinition').

Move both types to packages/core/src/registry/types.ts (still re-exported
from runtime.ts for backwards compatibility) so the chunking matches
main's layout and definePlugin's overloads resolve as before.

* fix(registry): wire up real-world install + display polish

Aggregator (apps/aggregator):
- Add CORS to /xrpc/* so the admin UI can call it from any origin
  (preflight 204, response headers on every method). Aggregator is a
  public read-only service; * is correct here.

Core (packages/core):
- Implement multibase-multihash checksum verification by re-encoding
  our SHA-256 digest in the same 'b<base32>' shape the registry CLI
  produces, rather than decoding the publisher's checksum. Same trust
  contract, no base32 decoder needed. Bare hex SHA-256 still accepted
  as a convenience fallback.
- Switch install handler to take 'did' (not handle) so packages whose
  handle the aggregator couldn't resolve are still installable. The
  browser resolves handle→DID via the aggregator before posting and
  sends DID directly; the server skips resolvePackage and goes
  straight to getPackage.
- Coerce 'experimental.registry' bare-string shorthand into the full
  RegistryConfig object via 'coerceRegistryConfig'. 'registry:
  "..."' is now equivalent to 'registry: { aggregatorUrl: "..." }'.
- Plumb 'experimental' through the integration's serializableConfig
  so the manifest endpoint actually sees the user's registry block.
  Previously it was being stripped, so the admin UI never branched to
  the registry path.
- Split RegistryConfig + ExperimentalConfig types into their own
  module (registry/types.ts) so they don't get bundled into the
  astro/integration/runtime.ts dist chunk -- the wider inlining was
  breaking definePlugin overload resolution for trusted plugins
  building against core's dist.

Admin (packages/admin):
- New <PublisherHandle> component + usePublisherHandle hook with
  tri-state result ('ok' / 'invalid' / 'missing'). Renders @handle,
  'Unverified publisher' (red), or DID respectively. Uses
  @atcute/identity-resolver's LocalActorResolver for bidirectional
  handle verification, localStorage-cached for 24h.
- Detail page disables install on 'invalid' status (publisher claims
  a handle that doesn't round-trip back to its DID -- impersonation
  risk). Surfaces 'We couldn't verify this publisher's identity'
  alert in plain language.
- Detail page reads installed state from fetchPlugins() and swaps
  the Install button to 'Installed' (disabled) when the package
  already has a 'source = "registry"' row matching its DID + slug.
  React Query's existing ['plugins'] invalidation handles the
  post-install UI update.
- Browse cards reuse <PublisherHandle> (variant='card') and link by
  handle when available, DID otherwise. Detail page parses either
  form from the URL.
- Browser sends 'did' (not handle) in the install POST.

Workspace:
- '@cloudflare/kumo' moved to the pnpm catalog and bumped to ^1.16.0
  workspace-wide. Older 1.10.0 was missing Sidebar export and being
  hoisted into the admin via packages/blocks's transitive dep.
- Add '@atcute/multibase' to core (for checksum encoding) and
  '@atcute/identity-resolver' to admin (for DID->handle resolution).
- Update DEFAULT_AGGREGATOR_URL + DiscoveryClient doc example from
  'experimental-registry.emdashcms.com' to 'registry.emdashcms.com'
  (the actual production host).

* fix(registry): adversarial review round 6 findings

Addresses 7 findings from the round-6 adversarial review and
documents the eighth.

#1 (high) Capability consent bypass [registry.ts, RegistryPluginDetail.tsx]
The drift check was gated on the client sending acknowledgedDeclaredAccess.
If the publisher's release record had no extension, the admin saw an
empty permission dialog, omitted the acknowledgement, and the server
skipped the check entirely -- letting a bundle whose manifest declares
real capabilities slip through behind an empty consent UI. Server now
extracts capabilities from the bundle manifest after download and
refuses with DECLARED_ACCESS_REQUIRED if the bundle declares any
capabilities and no acknowledgement was sent. Client always sends the
list (empty when no extension) so the new server check is always armed.

#2 (high) Concurrent install bundle deletion [registry.ts]
Two parallel installs of the same (did, slug, version) both passed the
pre-existing-row check, both uploaded to the same deterministic R2
prefix, and one then won the state-row PK race. The loser's catch block
deleted the R2 bundle the winner had just written. On state-write
failure we now re-query the state row: if a winner exists, we lost the
race and must not touch the R2 bundle. Cleanup runs only when the
failure is a real DB error, not a lost concurrent install.

#3 (high) SSRF via DNS-resolving public hostnames [registry.ts, ssrf.ts moved]
Literal-IP blocklist alone left a DNS-rebinding gap: any public DNS
service resolving an attacker-chosen hostname to loopback / RFC1918 /
169.254.169.254 passed the URL check. The import pipeline already
shipped resolveAndValidateExternalUrl which does Cloudflare DoH
resolution and rejects on any forbidden resolved address; reuse it
for artifact downloads. Move src/import/ssrf.ts to src/security/ssrf.ts
to reflect that it's not import-specific. Leave a re-export shim at
the old path so 13 existing callers keep working unchanged. Add
#security/* path alias.

#5 (high) Aggregator-supplied handles treated as verified [PublisherHandle.tsx]
usePublisherHandle returned status: 'ok' with the aggregator-supplied
handle whenever one was present, skipping local DID->handle round-trip.
A compromised aggregator could label an attacker DID as e.g.
'stripe.com' and the UI would render it as verified. Always run
LocalActorResolver via resolveDidToHandle; use the aggregator handle
only for a cross-check. If the aggregator's claim differs from the
verified handle, mark the publisher invalid.

#6 (medium) Postgres migration 038 schema-qualification [038_registry_plugin_state.ts]
The columns probe queried information_schema.columns without filtering
by table_schema. A _plugin_state table in another schema (multi-tenant
Postgres, per-test schemas) could make the migration skip the column
adds. Filter by table_schema = current_schema().

#7 (medium) Install errors leak full artifact URLs [registry.ts]
fetchArtifact recorded each full URL in the joined error message that
bubbled up to the admin client. Artifacts hosted on storage backends
often carry presigned tokens in the query string; failed installs were
leaking those into HTTP responses and logs. Strip query and fragment
when building client-visible errors (origin + path only); log the full
URL server-side for debugging.

#8 (medium) Credentialed aggregator URLs accepted [config.ts]
validateAggregatorUrl accepted https://user:pass@example.com.
The normalized URL ends up in the admin manifest and is shipped to
every admin browser; browser fetch() also rejects credentialed URLs
outright. Reject them at config-validation time.

#4 (high, documented not fixed) Aggregator-trust-root scope [types.ts]
Full MST proof / publisher signature verification is not in this PR;
the server still trusts the aggregator-supplied (did, slug, checksum,
artifact URL). Expand the JSDoc on EmDashConfig.experimental.registry
to spell out exactly what the v1 trust contract is, what EmDash does
verify independently (checksum, manifest id/version/capabilities), and
what it doesn't (release-record signatures, replay). Recommendation:
point aggregatorUrl only at an aggregator you operate or trust at
centralized-source level until signature verification lands.

* fix(registry): adversarial review round 7 followups

Two LOW findings from the round-7 review (PR #1011 comment).

NSID exact-match in RegistryPluginDetail.tsx
  Round-6 left a startsWith() match on the release-extension key.
  RFC 0001 fixes the NSID for the release extension; accepting prefix
  variants (...releaseExtensionV2, ...releaseExtension.deprecated)
  would let a publisher render a different capability list than the
  canonical key would. Use exact-equality keyed lookup.

Registry plugin uninstall affordance in PluginManager.tsx
  Registry-installed plugins appear in PluginManager but the Uninstall
  button is gated on isMarketplace. Admins see a permanent-looking
  install with no way to remove it short of editing the DB and R2 by
  hand. Add an inline note for source === 'registry' rows that says
  uninstall isn't available yet and points the admin at the disable
  toggle. Full uninstall handler lands in a follow-up PR.
2026-05-14 06:11:11 +00:00
emdashbot[bot] 29eeee7c2a ci: release (#999)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-12 17:05:44 +01:00
emdashbot[bot] 356c52e0e2 ci: release (#992)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 15:17:29 +00:00
emdashbot[bot] b86ce9f73b ci: release (#925)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 10:06:15 +01:00
ppppangu 06d5f3f016 fix(forms): unwrap public definition responses (#983) 2026-05-11 06:16:48 +00:00
Matt Kane 908787a469 feat(aggregator): scaffold plugin-registry aggregator (#971)
* feat(plugin-marketplace-test): add default export for registry-cli bundler

The registry-cli bundler resolves the plugin descriptor by looking for a
default export (function or pre-resolved descriptor) or a named
`createPlugin` export. The named-only `marketplaceTestPlugin` export wasn't
discoverable, so `emdash-registry bundle --dir packages/plugins/marketplace-test`
failed with INVALID_PLUGIN_FORMAT.

Add `export default marketplaceTestPlugin` to match the convention used by
other plugins (e.g. `forms`, `color`).

* feat(aggregator): scaffold plugin-registry aggregator (Slice 1 PR 1a)

Lands the apps/aggregator project skeleton plus the shared
@emdash-cms/atproto-test-utils package skeleton. No business logic yet —
this PR establishes the structure so subsequent PRs each touch one
component.

apps/aggregator
- Wrangler config: D1, Records Queue (+ DLQ), Records DO, 6h reconciliation
  Cron, vars (JETSTREAM_URL, CONSTELLATION_URL, WANTED_COLLECTIONS).
- Cloudflare Vite plugin for dev/build (`vite dev` / `vite build`),
  `wrangler deploy` after build for ship.
- Initial D1 migration `0001_init.sql` lands every v1 table at once
  (packages, releases + FK + idx, release_duplicate_attempts,
  mirrored_artifacts, labels, label_state + partial enforce idx,
  labellers, packages_fts + triggers, ingest_state, known_publishers).
  Slices 2 and 3 read these tables but don't add new ones, so this is
  the only DDL we expect to ship before NSID stabilisation.
- Worker entrypoint exports RecordsJetstreamDO + a no-op default with
  fetch/queue/scheduled stubs; subsequent PRs fill them in.
- Test rig uses @cloudflare/vitest-pool-workers v0.16's cloudflareTest()
  plugin. Migrations are read at config time and piped into the worker
  isolate via the TEST_MIGRATIONS binding; tests apply them in beforeAll.
  Smoke test proves migrations apply, INSERT round-trips, FTS5 trigger
  fires, and the FK rejects orphan releases.

packages/atproto-test-utils (skeleton)
- Private workspace package consumed by both registry-cli (later) and
  apps/aggregator. PR 1b lands the real-crypto MockPds + MockJetstream
  + MockDidResolver + createFakePublisher helper.

Workspace
- apps/* added to pnpm-workspace.yaml.
- Catalog entries added for @atcute/{car,cbor,cid,crypto,firehose,
  jetstream,mst,repo,xrpc-server,xrpc-server-cloudflare},
  @cloudflare/{vite-plugin,vitest-pool-workers}, vite.
- vitest bumped to ^4.1.5 — required by vitest-pool-workers v0.16.

* feat(atproto-test-utils): real-crypto MockPds + mocks (Slice 1 PR 1b)

Lands the in-memory atproto fixtures so the aggregator's verification path
exercises the same code in tests as in production.

The load-bearing claim: a record signed by a FakeRepo, fetched via
MockPds.handle as a sync.getRecord CAR, and fed into @atcute/repo's
verifyRecord round-trips cleanly. Mocks that skip signing would let
verification regressions slip through; this rules that out.

Components:
- FakeRepo wraps @atproto/repo's Repo + MemoryBlockstore for one DID. Real
  P-256 keypair, real signed commits, real MST construction. getRecordCar
  uses @atproto/repo's getRecords provider (same path the cirrus PDS
  reference uses).
- MockPds is multi-tenant (mounts many FakeRepos), implements
  FetchHandlerObject for @atcute/client, and serves both publish-side
  endpoints (applyWrites, putRecord, repo.getRecord) and aggregator-side
  endpoints (sync.getRecord-as-CAR with application/vnd.ipld.car,
  listRecords). Response shapes mirror cirrus.
- MockJetstream is a driveable async iterable; tests emit commit events,
  subscribers receive filtered events, history replays on reconnect with
  cursor.
- MockDidResolver maps DIDs to DID documents with the publisher's signing
  multikey + PDS endpoint.
- createFakePublisherFixture wires all four together: createPublisher
  registers a new keypair-backed repo with the PDS and resolver in one call.

Tests (11): record round-trips for profile + release records, signature
mismatch rejection, exclusion-proof CAR for missing records, JSON
listRecords shape, Jetstream filtering + history replay, DID resolution
with PDS endpoint extraction.

Workspace: adds @atproto/repo + @atproto/crypto to catalog as test-only
deps. Production aggregator still uses @atcute/repo for verification — the
heavyweight @atproto package is private to the test fixture package.

* chore: align @vitest/browser-playwright + @vitest/ui with vitest 4.1.5

PR 1a bumped vitest in the catalog from 4.0.18 to 4.1.5 because
`@cloudflare/vitest-pool-workers@0.16` requires it. The two related
packages had separate version pins outside the catalog and stayed at
4.0.x, producing "Running mixed versions is not supported" warnings
on every admin/core test run.

Bump both to 4.1.5 to match.

The bump to @vitest/browser-playwright 4.1.5 also tightens playwright's
strict-mode role inference: <input type="file" aria-label="Upload file">
is now treated as having an accessible name that matches a `/Upload/`
regex, which collides with the actual Upload button in
MediaPickerModal. Anchor the test's regex (`/^Upload$/`) so it only
matches the button's exact accessible name.

* fix(aggregator): address PR feedback (lint, wrangler peer, copilot)

- Bump catalog wrangler from ^4.80.0 to ^4.83.0. @cloudflare/vite-plugin
  (pulled in transitively via @astrojs/cloudflare) requires this; the
  outdated catalog version broke demos/cloudflare typecheck and the
  Cloudflare-template smoke tests on CI. infra/cache-demo had already
  pinned ^4.83.0, so this brings the catalog in line with the highest
  in-tree pin.

- Drop redundant `| undefined` from FakeRepo.getRecordValue's return type;
  `unknown` already includes undefined (typescript-eslint:no-redundant-type-constituents).

- Replace deep import `@atproto/repo/dist/sync/provider.js` with the
  package's public `getRecords` export. Same function, no exports-map
  brittleness on upgrade. (Per Copilot review.)

- Validate did:plc:/did:web: prefix in MockPds parseDid instead of
  accepting any did:* and casting. Matches the AtprotoDid type's runtime
  guarantee — invalid methods (e.g. did🔑) now reject at the boundary
  instead of slipping through tests. (Per Copilot review.)

- Drop rootDir from apps/aggregator and packages/atproto-test-utils
  tsconfigs; both `include` test directories that sit outside `./src`,
  which trips TS6059 in some tooling. Without rootDir, TS infers from
  include and tests live cleanly alongside source. (Per Copilot review.)

* fix(atproto-test-utils): address adversarial review findings

Six bugs/gaps found in adversarial review of the mock infrastructure.
Each fix lands with a regression test so the same bug can't slip back.

1. MockJetstream cursor was off-by-one. The replay filter used
   `event.time_us < cursor`, which redelivered the cursor event itself.
   Real Jetstream treats the cursor as "last seen, don't redeliver" —
   replay must be strict-after. Fixed to `<=` and added a test that emits
   two events, subscribes with cursor = first event's time_us, and
   asserts only the second is delivered.

2. MockJetstreamSubscription.cursor returned the global last event time,
   not the per-subscriber position. A subscriber that had consumed only
   event 3 of 10 read sub.cursor → time of event 10; reconnecting with
   that value would silently lose events 4-9. Now tracks
   lastDeliveredTimeUs per subscriber and exposes it through the cursor
   getter. Added a test that emits two events, consumes one, and asserts
   sub.cursor reflects the consumed event.

3. MockJetstreamSubscription.next() now throws when called concurrently
   from two consumers (previous behaviour silently orphaned the first
   resolver). Concurrent next() is legal for AsyncIterator; this mock
   doesn't support it, so failing loudly beats hanging.

4. MockPds dispatched on URL pathname only, ignoring HTTP method —
   `GET applyWrites` returned 200. Now switches on `(method, pathname)`
   and returns 405 MethodNotAllowed for known endpoints used with the
   wrong verb. Added a test asserting GET on applyWrites returns 405.

5. MockPds.repoApplyWrites only handled #create. PR 3's tests will need
   update + delete flows to model profile updates and tombstoning. Added
   FakeRepo.updateRecord + deleteRecord that drive @atproto/repo's
   applyWrites with the right WriteOpAction, and dispatched on $type in
   MockPds. Added a round-trip test that publishes a profile, updates
   its license, then deletes it.

6. FakePublisher.publishProfile defaulted authors to [] and let security
   end up empty if neither securityEmail nor securityUrl was passed —
   both fields require minLength: 1 in the lexicon. Now throws when no
   security contact is provided, and defaults authors to a single entry
   derived from the publisher's handle. Added two tests: one asserts the
   throw, the other asserts the default authors entry uses the handle.

Also reverted a misguided `cursor: undefined` addition to listRecords —
JSON.stringify drops undefined keys, which IS the cirrus end-of-stream
shape (the `cursor` field is optional). The reviewer's concern there was
misread; updated the comment to spell out the contract.

* fix(aggregator): drop placeholder database_id so D1 auto-provisions

A literal placeholder ID makes wrangler think the binding is already
configured and skip provisioning on first deploy, then fail when trying
to use a non-existent database. Omit the field entirely so auto-provision
fires.

* fix(aggregator): use wrangler-generated Env instead of hand-rolled type

Per the project's CLAUDE.md and wrangler's own guidance, the Env type
should come from `worker-configuration.d.ts` (generated by
`wrangler types`) rather than being maintained by hand. The hand-rolled
shape would drift from the actual bindings any time wrangler.jsonc
changed.

- Add `worker-configuration.d.ts` (generated, committed alongside the
  similar files in packages/marketplace, infra/perf-monitor, the
  cloudflare templates, etc.).
- Drop the manual `Env` interface from src/env.ts; keep only the
  project-specific `RecordsJob` type.
- Drop `@cloudflare/workers-types` from devDependencies + tsconfig
  `types` array. Wrangler now ships the runtime types in the generated
  d.ts; the workers-types package is superseded.
- Drop `test/env.d.ts` — its single triple-slash for vitest-pool-workers
  is now covered by the tsconfig's `types` entry.
- Update records-do.ts and index.ts to consume the global `Env`.

Re-run `wrangler types` after editing wrangler.jsonc to keep the
generated file in sync.

* chore(aggregator): WANTED_COLLECTIONS as code constant + drop slice/PR refs

WANTED_COLLECTIONS is part of the protocol contract, not a per-deployment
tunable. Move to apps/aggregator/src/constants.ts where the type system
can keep it honest, and drop it from wrangler.jsonc `vars`. Production,
staging, dev, and self-hosted instances all subscribe to the same NSIDs.

Also strip slice/PR scaffolding language from comments — those refs
mean nothing to anyone reading the code outside the immediate dev
context. Replace with descriptions of what the placeholder will become
(e.g. "PDS-verified ingest will land here") rather than which sub-PR
will land it.

Regenerated worker-configuration.d.ts after the wrangler.jsonc change.
2026-05-09 14:41:39 +01:00
emdashbot[bot] 71ba0fbe25 ci: release (#892)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-06 11:44:22 +01:00
Matt Kane 1e0cb76899 fix(plugins): rename deprecated capabilities and read version from package.json (#918)
* fix(plugins): rename deprecated capabilities and read version from package.json

Renames the deprecated capability aliases (`read:content`, `write:content`,
`read:media`, `write:media`, `network:fetch`, `network:fetch:any`) used by the
built-in plugins to their current names. Plugin descriptors now read `version`
from each package's `package.json` instead of carrying a stale hard-coded
literal.

* fix(marketplace): accept current capability names in publish manifest validation

The publish endpoint's allow-list still only contained the deprecated capability
aliases, so manifests using the current names (e.g. `content:read`) were
rejected with 400. Adds the current names alongside the deprecated ones during
the transition window. The publish e2e test now reads the audit-log version
from `package.json` instead of hard-coding it.
2026-05-05 08:34:17 +01:00
emdashbot[bot] be5f05b08d ci: release (#806)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 16:58:59 +01:00
Arash a838000686 feat: rtl style improvements - direction visuals improvements and usage updates (#814)
* rtl style improvements across packages

* add updated arrow and caret icon wrappers compatible with rtl

* update arrow and caret usage across packages

* format

* tiny update

* fix

* add changeset for update
2026-04-28 20:20:00 +01:00
Filip Ilić 0ee372a7f3 feat(plugins): add @emdash-cms/plugin-field-kit (#702)
Composable field widgets for `json` fields. Four widgets configured
entirely through seed `options` — no React required from site builders:

  - object-form — inline form for flat JSON objects
  - list        — ordered array editor with add/remove/reorder
  - grid        — rows × columns matrix (toggle / text / number / select cells)
  - tags        — free-form tag/chip input for string arrays

Widgets use Kumo components and semantic design tokens to match the
admin's visual language. Stored data is clean JSON that survives
removing the plugin — no shape mutation, no new columns, no migration.

Ships with 30 unit tests covering render, onChange shapes, grid
legacy-array format normalization, and tags dedupe/max/transform.

Also widens `FieldDescriptor.options` from
`Array<{ value: string; label: string }>` to
`Array<{ value: string; label: string }> | Record<string, unknown>`
so plugin widgets can accept arbitrary widget config (not only enum
choices). The array shape for `select` / `multiSelect` continues to
work unchanged — `ContentEditor` narrows at the usage site.

Signed-off-by: Filip Ilic <ilic.filip@gmail.com>
2026-04-28 12:58:08 +00:00
emdashbot[bot] 5c0195ac05 ci: release (#797)
* ci: release

* chore: restore original PR/author attribution in CHANGELOGs

* style: format

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <m@mk.gg>
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-04-27 21:21:37 +01:00
Matt Kane ba12ca5ee3 revert: undo accidental major and prevent recurrence (#796)
* revert: undo accidental 1.0.0 release; prevent recurrence

Reverts 46f3acb (ci: release #759), which bumped all 8 publishable
packages to 1.0.0. Restores 0.7.x versions and pending changesets so
the next release lands at 0.8.0.

Root cause: peer-dep cycle between `emdash` and `@emdash-cms/auth-atproto`,
both using `workspace:*`. Changesets resolves `workspace:*` to the exact
current version for semver checks, so a minor bump on either side falls
outside the range and escalates the cycle's other side to major. Other
plugins use `workspace:>=X.Y.Z` and don't trip this — switching both
ends of the cycle to the same form fixes the escalation.

The existing workflow guard greps `pnpm changeset status` for "bumped
at major", which only runs at PR-creation time. Once the version PR is
merged, no changesets are pending and the grep returns nothing — so
the publish step proceeded with already-bumped 1.0.0 versions.

Replaces it with a package.json scan that fails on any non-private 1.x
version. Wired into both `release.yml` (publish-time) and `ci.yml`
(every PR, including the auto-generated Version Packages PR).

Also adds .claude/* to .gitignore (mirroring the .opencode pattern)
so local agent state doesn't accidentally land in commits.

* style: format

* chore: update lockfile for workspace range changes

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
2026-04-27 21:00:59 +01:00