mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
7d0e90eb47
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.
58 lines
3.3 KiB
TypeScript
58 lines
3.3 KiB
TypeScript
/**
|
|
* 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
|
|
* the host then refuses (or refuse one the host would allow).
|
|
*
|
|
* These tests read the server's source directly. They only run inside the TREK monorepo;
|
|
* in a published/standalone checkout the server isn't there and they skip.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
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 envelopeFile = path.join(serverPlugins, 'protocol/envelope.ts');
|
|
const serverEgress = path.join(serverPlugins, 'runtime/egress-policy.ts');
|
|
const inMonorepo = fs.existsSync(envelopeFile) && fs.existsSync(serverEgress);
|
|
|
|
describe.skipIf(!inMonorepo)('parity with the host', () => {
|
|
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');
|
|
|
|
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', () => {
|
|
const ours = fs.readFileSync(path.resolve(here, '../src/egress-policy.ts'), 'utf8');
|
|
const theirs = fs.readFileSync(serverEgress, 'utf8');
|
|
|
|
// Compare the pure helpers only — our file additionally carries installEgressGuard
|
|
// (the server's lives in plugin-host-entry.ts, where it is coupled to the child).
|
|
const fns = ['isBlockedIp', 'expandV6', 'makeHostAllow', 'dgramSendTarget', 'dgramConnectTarget', 'unwrapConnectArgs', 'classifyConnect'];
|
|
const body = (src: string, name: string): string => {
|
|
const start = src.indexOf(`function ${name}(`);
|
|
expect(start, `${name} not found`).toBeGreaterThan(-1);
|
|
// From the signature to the closing brace at column 0 — these are all top-level fns.
|
|
const end = src.indexOf('\n}', start);
|
|
return src.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
};
|
|
for (const fn of fns) expect(body(ours, fn), `${fn} has drifted from the server`).toBe(body(theirs, fn));
|
|
});
|
|
});
|