mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Make the plugin permission facts live in one place
The hook->permission map existed verbatim twice (the host supervisor and the plugin SDK) and the permission list four times (envelope, SDK manifest, and both admin panels in the client). All of them were in sync when measured - 63 entries each, 15 hook entries each - so this changes no behaviour. It removes a quiet outage: a hook whose permission is added on only one side fails silently in production, with the plugin installing, activating and then simply never being called. protocol/envelope.ts is the single source now. METHOD_PERMISSION was typed Record<KnownMethod, string>, so its value side was unconstrained - 'db:read:trps' compiled and made the method unreachable forever. It is `as const satisfies Record<KnownMethod, KnownPermission>` now, which catches a typo'd value, a missing key and an extra key. The derived KnownPermission and MethodPermission<M> are what the @PluginMethod decorator will check its argument against. Deliberately no explicit annotation on METHOD_PERMISSION: with one, typeof is that Record, MethodPermission<M> widens to the whole union, and the check silently evaporates. server/scripts/gen-plugin-facts.ts imports envelope.ts and writes the SDK and shared copies. It does not scrape. The guard it replaces pulled the block out of plugin-supervisor.ts with a regex that goes null on a reformat, and it ran only from prepublishOnly - drift would have surfaced as a broken npm release. The generated files are checked in because publish-plugin-sdk builds from a standalone checkout with no server present. The SDK's re-exported types stay wide. plugin-sdk compiles under strict, and grantGaps indexes HOOK_PERMISSION with a plain string, so an `as const` there is TS7053 and would break the publish. Strictness belongs on the host side. --check also fails on a permission with no admin.plugins.perm.* translation and on one the CLI has no hint for. Both are silent today. plugin-sdk/** was in no workflow path filter, so nothing in CI ever ran its tests. It is in the filter now, alongside a job that runs the drift check.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* The SDK hand-mirrors two things the server owns: the hook→permission map, and the
|
||||
* The hook->permission map is no longer hand-mirrored: gen-plugin-facts.ts generates
|
||||
* src/generated/host-facts.ts from the server's protocol/envelope.ts, and that
|
||||
* generator's --check mode is a CI gate. What is still hand-vendored here is the
|
||||
* pure egress-policy helpers. trek-plugin-sdk ships standalone and cannot import across
|
||||
* the package boundary, so the copies are real copies — and a silent drift here is the
|
||||
* worst possible bug in this module: `dev` would confidently green-light a plugin that
|
||||
@@ -16,23 +18,24 @@ import { HOOK_PERMISSION } from '../src/permissions.js';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const serverPlugins = path.resolve(here, '../../server/src/nest/plugins');
|
||||
const supervisor = path.join(serverPlugins, 'supervisor/plugin-supervisor.ts');
|
||||
const envelopeFile = path.join(serverPlugins, 'protocol/envelope.ts');
|
||||
const serverEgress = path.join(serverPlugins, 'runtime/egress-policy.ts');
|
||||
const inMonorepo = fs.existsSync(supervisor) && fs.existsSync(serverEgress);
|
||||
const inMonorepo = fs.existsSync(envelopeFile) && fs.existsSync(serverEgress);
|
||||
|
||||
describe.skipIf(!inMonorepo)('parity with the host', () => {
|
||||
it('HOOK_PERMISSION matches the supervisor\'s map exactly', () => {
|
||||
const src = fs.readFileSync(supervisor, 'utf8');
|
||||
const block = src.match(/const HOOK_PERMISSION[^{]*\{([\s\S]*?)\n\};/);
|
||||
expect(block, 'could not find HOOK_PERMISSION in plugin-supervisor.ts').toBeTruthy();
|
||||
it('the generated host facts match the server's envelope.ts', () => {
|
||||
// Not a regex scrape any more: server/scripts/gen-plugin-facts.ts imports envelope.ts
|
||||
// and writes src/generated/host-facts.ts, and its --check mode is a CI gate. This
|
||||
// asserts the checked-in artefact is the one the host would produce, so a standalone
|
||||
// publish (which has no server present) still ships the right list.
|
||||
const generated = fs.readFileSync(path.resolve(here, '../src/generated/host-facts.ts'), 'utf8');
|
||||
const envelope = fs.readFileSync(envelopeFile, 'utf8');
|
||||
|
||||
const host: Record<string, string> = {};
|
||||
for (const [, key, perm] of block![1].matchAll(/^\s*(\w+):\s*'([^']+)'/gm)) host[key] = perm;
|
||||
|
||||
// Both directions: a hook the host gates that we don't know about means dev fires it
|
||||
// when TREK never would; one we gate that the host doesn't means dev refuses for nothing.
|
||||
expect(Object.keys(host).length).toBeGreaterThan(0);
|
||||
expect(HOOK_PERMISSION).toEqual(host);
|
||||
for (const [hook, perm] of Object.entries(HOOK_PERMISSION)) {
|
||||
expect(generated, `${hook} missing from the generated facts`).toContain(`${hook}: '${perm}'`);
|
||||
expect(envelope, `${hook} missing from envelope.ts`).toContain(`${hook}: '${perm}'`);
|
||||
}
|
||||
expect(Object.keys(HOOK_PERMISSION).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('the vendored egress-policy helpers are byte-identical to the server\'s', () => {
|
||||
|
||||
Reference in New Issue
Block a user