feat(plugins): host-served plugin redirects (api.cms.redirects) - #581
Draft
Mariomarquezt wants to merge 1 commit into
Draft
Mariomarquezt wants to merge 1 commit into
Mariomarquezt wants to merge 1 commit into
Conversation
Closes CoreBunch#383. Plugins can now answer URLs that have no content with a redirect served by the host. A plugin with the new `redirects.manage` permission manages its own exact-path rules through `api.cms.redirects` (list / set / delete / replaceAll); the public dispatcher consults them in one new step placed immediately before the designed 404 page, so a rule never shadows a live page, data row, baked artefact or row-rename redirect. - Migration 031 `plugin_redirects` (SQLite + Postgres), FK to installed_plugins with on delete cascade: uninstalling a plugin removes its rules. - Statuses 301 / 302 / 307 / 308 / 410. 410 reuses the notFound template body; 302/307 are no-store; the request query is carried over when the target has none. - Validation lives in the repository: from = printable-ASCII absolute path (no //, ?, #, reserved /admin, /_instatic, /uploads, <= 2048); to = same-origin path (no //, /\) or absolute http(s) URL, no control characters, != from; 5000 rules per plugin. Errors reach the plugin as "<field>: <message>". The RPC schemas are only a safety ceiling. - Two plugins owning the same path: the oldest rule wins, then plugin id. - One indexed query per otherwise-unmatched GET/HEAD; none for matched routes. - Docs: plugin-system.md (API + permission), server.md and publisher.md (route order). Tests: repository, route, sandbox/RPC/host dispatch, and an end-to-end test that installs real plugin zips, calls the API from the sandbox and checks the dispatcher responses (including uninstall and a published page winning over a rule).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #383.
Opened as a draft so the direction can be checked before review, as asked on #383. Happy to rename, reshape or split any of it.
Summary
Plugins can now answer URLs that have no content with a redirect served by Instatic itself. Today a plugin cannot take part in routing at all, so an SEO plugin can only export nginx / Caddy / Cloudflare rules for the operator to apply by hand.
redirects.managemanifest permission (risk: high). Without it the API fails closed, like thecms.content.*surfaces.api.cms.redirects.list / set / delete / replaceAll. Each plugin sees and changes only its own rules.031_plugin_redirects(SQLite + Postgres), FK toinstalled_pluginswithon delete cascade, so uninstalling a plugin removes its rules.tryServePluginRedirect, placed immediately before the designed 404 page. A rule never shadows a live page, data row, baked artefact or row-rename redirect. It costs one indexed query per otherwise-unmatched GET/HEAD and nothing for matched routes.no-store. The request query string is carried over when the target has none."<field>: <message>":from: printable-ASCII absolute path; no//,?,#; not under/admin,/_instaticor/uploads; at most 2048 characters.to: a same-origin path (no//or/\) or an absolute http(s) URL; no control characters; not equal tofrom.docs/features/plugin-system.md(API + permission),docs/server.mdanddocs/features/publisher.md(route order).Differences from the proposal in #383
These are narrower on purpose, to keep the first version small. Each is easy to extend later.
from_pathcms.redirectspermissionredirects.managepermissionlist / create / update / deletelist / set / delete / replaceAll(setupserts,replaceAllsupports sync-style plugins)Verification
bun run buildbun test: 7078 pass, 2 fail. Neither failure is in code this PR touches:Step-up auth > successful step-up clears the per-IP limiter…also fails on an untouchedmaincheckout on the same machine.Circular dependencies > keeps the tsconfig-aware source graph cycle-freehits its 15 s timeout on a slow laptop during the full run, and passes when run alone (14.9 s).bun run lint,tsc, andbootstrap:check(generated bootstrap is fresh)New tests:
pluginRedirects.test.ts: repository and validation.pluginRedirectRoute.test.ts: dispatcher responses.pluginRedirectsApi.test.ts: sandbox, RPC and host dispatch against a real test DB.pluginRedirectsEndToEnd.test.ts: installs real plugin zips, calls the API from the sandbox, and checks the dispatcher. It covers uninstall, and a published page beating a rule.Checklist
pluginBootstrap.ts, regenerated withbun run bootstrap:sync.🤖 Generated with Claude Code