feat(store): plugin store host module — signed catalogs, tiered trust, install jobs
The index is the verification gate: a catalog entry pins one exact version
plus that version's tarball integrity hash, so 'verified on every release'
is a property of the data rather than a promise about process. Nothing can
express 'track latest' for a catalogued plugin.
- store/index.rs signed index parse + ed25519 verify (ring), validate-and-drop
per entry, semver minHost/advisory ranges
- store/sources.rs built-in unom source (compiled-in URL + two key slots for
rotation) + operator sources in plugin-sources.json
- store/catalog.rs https fetch with size/timeout/redirect caps, signature before
parse, last-good disk cache (stale-but-usable when offline)
- store/jobs.rs single-flight install/uninstall: registry-integrity preflight
against the pin, spawn the runner CLI with live log capture,
post-install version check with rollback, provenance record,
runner restart (discovery is startup-only)
- store/manifest.rs install provenance; absence means CLI-installed
- mgmt/store.rs 12 routes under /api/v1/store, denied to the plugin token
(a plugin that can install plugins is an escalation primitive)
Also generalizes runner discovery and listInstalled from @punktfunk/plugin-*
to ANY scope's plugin-*: catalog entries must be scoped so the scope can map
to that entry's registry, so a third-party plugin necessarily arrives under
its own scope and would otherwise install but never run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,6 +136,55 @@ describe("listInstalled", () => {
|
||||
{ pkg: "punktfunk-plugin-broken", version: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
test("finds plugins in ANY scope, not just @punktfunk", () => {
|
||||
// Plugin-store catalog entries must be scoped so the scope can map to that entry's
|
||||
// registry, so a third-party plugin necessarily arrives as `@their-scope/plugin-*`.
|
||||
// Discovery limited to @punktfunk would let it install and then never run.
|
||||
const dir = tmp("list-foreign-scope");
|
||||
writePkg(dir, path.join("@retro-hub", "plugin-x"), "1.0.0");
|
||||
writePkg(dir, path.join("@retro-hub", "helper"), "1.0.0"); // scoped non-plugin — ignored
|
||||
expect(listInstalled(dir)).toEqual([{ pkg: "@retro-hub/plugin-x", version: "1.0.0" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureBunfig with extra scopes", () => {
|
||||
const read = (dir: string) => fs.readFileSync(path.join(dir, "bunfig.toml"), "utf8");
|
||||
|
||||
test("maps a third-party scope alongside @punktfunk", () => {
|
||||
const dir = tmp("bunfig-extra");
|
||||
ensureBunfig(dir, { "@retro-hub": "https://retro.example/npm/" });
|
||||
const out = read(dir);
|
||||
expect(out).toContain(`"@punktfunk" = "${REGISTRY}"`);
|
||||
expect(out).toContain('"@retro-hub" = "https://retro.example/npm/"');
|
||||
});
|
||||
|
||||
test("is idempotent and rewrites a scope whose registry changed", () => {
|
||||
const dir = tmp("bunfig-rewrite");
|
||||
ensureBunfig(dir, { "@retro-hub": "https://old.example/npm/" });
|
||||
ensureBunfig(dir, { "@retro-hub": "https://old.example/npm/" });
|
||||
expect(read(dir).match(/@retro-hub/g)?.length).toBe(1);
|
||||
|
||||
ensureBunfig(dir, { "@retro-hub": "https://new.example/npm/" });
|
||||
const out = read(dir);
|
||||
expect(out).toContain('"@retro-hub" = "https://new.example/npm/"');
|
||||
expect(out).not.toContain("old.example");
|
||||
// the first-party mapping survives an unrelated scope edit
|
||||
expect(out).toContain(`"@punktfunk" = "${REGISTRY}"`);
|
||||
});
|
||||
|
||||
test("preserves unrelated scopes already in the file", () => {
|
||||
const dir = tmp("bunfig-preserve");
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "bunfig.toml"),
|
||||
'[install.scopes]\n"@acme" = "https://acme.example/"\n',
|
||||
);
|
||||
ensureBunfig(dir, { "@retro-hub": "https://retro.example/npm/" });
|
||||
const out = read(dir);
|
||||
expect(out).toContain('"@acme" = "https://acme.example/"');
|
||||
expect(out).toContain('"@retro-hub" = "https://retro.example/npm/"');
|
||||
expect(out).toContain(`"@punktfunk" = "${REGISTRY}"`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensurePluginsDir", () => {
|
||||
|
||||
Reference in New Issue
Block a user