Skip to content

feat(plugins): unify 3 extension points into apiDispatch#1

Open
lauer3912 wants to merge 5 commits into
mainfrom
feat/plugin-extensions-unify
Open

feat(plugins): unify 3 extension points into apiDispatch#1
lauer3912 wants to merge 5 commits into
mainfrom
feat/plugin-extensions-unify

Conversation

@lauer3912

Copy link
Copy Markdown
Member

Summary

Wire the three plugin extension points (migrations, publicRoutes,
httpMiddleware) into the single apiDispatch handler table so plugin
code reaches them through the same permission-gated path as every
other api-call. Closes a 22-error TS drift that the working tree had
been carrying uncommitted, and adds a script for end-to-end
plugin-install verification.

Commits (6)

  1. 5385f818 feat: CI pipeline + fix admin UI endpoint paths + complete tier CRUD UI
  2. 50a3e5b2 refactor(router): hoist ServerRuntime + RouteHandler to break circular dep
  3. aa94c8ad feat(plugins): unify 3 extension points into apiDispatch + fix 22 TS errors
  4. d158fb98 chore(plugins): regenerate QuickJS bootstrap for 3 new schema targets
  5. f61ba262 test(plugins): add e2e-plugin-install script for plugin install/activate round-trip
  6. 07df63d5 fix(public-auth): generate exactly 10 alphanumeric chars in TOTP recovery codes

Why this change

The plugin system has three "extension points" — places where a
plugin's worker can register something with the host:

  • a DB migration to run at boot
  • a public HTTP path the plugin owns (OAuth callbacks, webhook
    receivers, etc.)
  • a request middleware that runs on every request

Until this PR, plugin code reached the host through a mix of paths:
two of the three went through the standard apiDispatch handler
table, but the third (public routes) had its own dedicated
host-side dispatcher (tryServePluginPublicRoute) that bypassed the
central permission gate. This refactor pulls all three into the
same code path so they share one permission map, one audit log, one
failure mode.

The refactor surfaced 22 pre-existing TS errors that had been
silently fixed in the working tree (most notably the
apiCallSchema.ts imports for the three new arg schemas) — this
PR also cleans them up.

User / developer impact

  • Plugin authors: nothing visible. The three extension points
    still register via the same SDK surface; the change is entirely
    host-internal.
  • Operators: the install dialog consent flow is unchanged.
  • Plugin admin UI: unchanged.

Verification

Run on the feat/plugin-extensions-unify branch:

Check Result
bunx tsc -p tsconfig.node.json --noEmit 0 errors
bunx tsc -p tsconfig.app.json --noEmit 0 errors
bunx tsc -p tests/e2e --noEmit 0 errors
bun run typecheck:plugins 0 errors across 8 plugins
bun run build:plugins 8/8 OK
bun run build (tsc -b + vite build) OK
bun test plugins/ all pass (was 1 fail — TOTP recovery codes — fixed in commit 6)
bun test src/__tests__/architecture/no-circular-dependencies pass
bun test src/__tests__/architecture/plugin-rpc-target-registry pass
bun test src/__tests__/architecture/plugin-sandbox-invariants pass
bun test src/__tests__/architecture/plugin-bootstrap-fresh pass
bun test src/__tests__/architecture/migration-parity pass
bunx eslint scripts/e2e-plugin-install.ts 0 errors

Known issues (pre-existing, out of scope)

  • 5 pre-existing architecture-test failures in
    src/__tests__/architecture/ (BTN-3 button primitive, CodeMirror
    lazy-load, dispatcher HTML pipeline, Error boundary coverage,
    Keybindings registry) — introduced by commits before this branch.
  • 96 pre-existing bun run lint errors under plugins/*/src/
    (unused vars, any casts) — same source.
  • bun test src/ (the src/-side unit tests) was not run end-to-end
    on Windows in this session — the 5-minute tool timeout is too
    short for the 262-test suite. CI on Linux runs the full suite as
    job 2 unit-tests.

A5050 added 5 commits July 13, 2026 17:39
…r dep

Move ServerRuntime and RouteHandler from server/router.ts into a new server/routerTypes.ts module so the plugin public-routes registry can type its handler without depending on the dispatcher:

  router.ts -> apiDispatch.ts -> extensions/publicRoutesProtocol.ts -> extensions/publicRoutes.ts -> router.ts

That cycle only closes now that RouteHandler is exported (it was declared local, so publicRoutes.ts's import previously failed). Hoisting both types to a neutral file breaks the cycle at the source.

Also lock the three new plugin extension-point targets (cms.migrations.register, cms.publicRoutes.register, cms.httpMiddleware.register) into the architecture-test contracts so the schema / handler / permission table can never silently desync.

Tests:

  - src/__tests__/architecture/plugin-rpc-target-registry.test.ts

  - src/__tests__/architecture/plugin-sandbox-invariants.test.ts

Verification: bun test src/__tests__/architecture/no-circular-dependencies.test.ts + plugin-rpc-target-registry.test.ts + plugin-sandbox-invariants.test.ts — all pass.
…errors

Wire the three plugin extension points (migrations registration,
public-route registration, HTTP middleware registration) into the
single apiDispatch handler table so plugin code reaches them through
the same permission-gated path as every other api-call — no
special-case host dispatchers, no ad-hoc host routes.

What changed:

  - server/plugins/host/apiDispatch.ts: register the three new
    handlers in the typed handler table (compile-enforced by
    `satisfies HostApiHandlerTable`).

  - server/plugins/extensions/{migrations,publicRoutes,httpMiddleware}Protocol.ts:
    add the missing `_entry: HostPluginRecord` parameter to match
    the three-arg HostApiHandler signature (msg, entry, db), and fix
    the import paths the three handlers had wrong
    (`../../protocol/...` was one level too deep; the public-route
    handler's unused `runtime` arg is now `_runtime`).

  - server/publish/publicRouter.ts: `renderNotFoundResponse` gains
    a `req?: Request` parameter so the Layer-B render can pass
    viewer context to the not-found template (router.ts already
    forwards the request). Also restore the trailing `}` the
    previous commit stripped, drop the unused `resolveViewerContext`
    import, and drop the now-unused `req?` from
    `renderPublicResolution` (its body never read it).

  - server/publish/publicRenderer.ts: import `resolveViewerContext`
    (used inside `buildViewerFrame`).

  - server/router.ts: drop the two now-unused plugin-route imports
    left behind when the dedicated `tryServePluginPublicRoute`
    handler was retired.

  - server/plugins/adminUi/apiKeysPage.ts: `db` -> `_db` (parameter
    was never read).

  - src/core/templates/tokenInterpolation.ts: delete the
    unreachable `case 'viewer':` — `DynamicPropBinding['source']`
    no longer includes 'viewer', so the case was dead.

  - server/plugins/protocol/apiCallSchema.ts: import the 3 new arg
    schemas (MigrationsRegisterArgSchema / PublicRoutesRegisterArgSchema
    / HttpMiddlewareRegisterArgSchema) that a previous commit
    registered but never imported — the typecheck has been relying
    on these imports living uncommitted in the working tree.

Verification:

  - bunx tsc -p tsconfig.node.json --noEmit   0 errors
  - bunx tsc -p tsconfig.app.json  --noEmit   0 errors
  - bunx tsc -p tests/e2e             --noEmit   0 errors
  - bun run typecheck:plugins                 0 errors across 8 plugins
  - bun run build:plugins                     8/8 OK
  - bun run build  (tsc -b && vite build)     OK
Re-run `bun run bootstrap:sync` after `apiCallSchema.ts` picked up
the three new `cms.migrations.register` / `cms.publicRoutes.register`
/ `cms.httpMiddleware.register` schemas, so the VM-side
`assertPermission` table and the worker-protocol allowlist match
the host dispatcher again.

Without this, the architecture gates
`src/__tests__/architecture/plugin-bootstrap-fresh.test.ts` and
`plugin-sandbox-invariants.test.ts` would fail on every CI run.
…ate round-trip

`scripts/e2e-plugin-install.ts` is an end-to-end smoke test for the
plugin install path. It boots the host (CMS only, no vite) on a
free port with a temporary SQLite database, creates the owner, runs
the step-up window, installs the requested plugin from a built
bundle as a .zip, activates it, and calls one of its endpoints to
verify the round trip works.

Usage:

  bun run scripts/e2e-plugin-install.ts
  bun run scripts/e2e-plugin-install.ts instatic.api-keys
  bun run scripts/e2e-plugin-install.ts instatic.public-auth

The script uses `fflate` for the zip writer, `spawn` to start the
host in a child process, and a small typed `Client` class to drive
the HTTP surface. Exit 0 on success, non-zero on failure.

`plugins/api-keys/package.json` and `plugins/public-auth/package.json`
are touched in lockstep so these two plugins' `instaticManifest`
metadata stays compatible with the e2e harness (network allowed
hosts, permissions, etc.).
…very codes

`generateRecoveryCodes` previously encoded 8 random bytes as
base64url (11 chars), uppercased, stripped the `-` / `_`
characters with `.replace(/[^A-Z0-9]/g, '')`, and `.slice(0, 10)`'d
the result. The strip step silently produced 8-10 char codes
depending on how many of the 11 base64url chars landed in the
index-62/63 range — the test asserted `/^[A-Z0-9]{10}$/` and would
fail on a run where three `-_` chars had been removed.

Generate exactly 10 alphanumeric chars instead: 10 random bytes
mapped directly to indices in a 36-char alphabet. Modulo bias
(256 % 36 = 4) is acceptable — recovery codes are rate-limited and
shown to the user once, not a bearer of long-lived access.

Discovered by `bun test plugins/` while verifying the plugin
extension-points refactor.
@lauer3912 lauer3912 marked this pull request as ready for review July 13, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant