mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
Add a decorator kit for the plugin RPC surface
The method-to-permission association exists twice today: declaratively in METHOD_PERMISSION, and imperatively as 44 if (has(...)) blocks in rpc-host.ts, with nothing asserting the two agree. protocol-paths.test.ts checks that each method has some permission, never that it is the same one the router gates on, and plugin-audit.ts inherits any drift silently. @PluginMethod binds the two together. Its permission argument is typed as MethodPermission<M>, so it resolves to the single literal the table assigns to that method and a mismatch does not compile. validate() re-checks the same argument against the same table at boot, because types evaporate under an as cast. @PluginHook does the same against HOOK_PERMISSION. UNCONDITIONAL_METHODS was missing from envelope.ts, so the three methods the router registers outside every grant check had no vocabulary of their own. Adding it here, with a compile-time proof that it stays disjoint from KNOWN_METHODS: giving those three a METHOD_PERMISSION row would change which methods isAuditable considers, and settings.get is deliberately the one unconditional method that is not audited. The kit is written to stay extractable into its own package, so an ESLint rule pins what it may import. Note the rule is a deny list on purpose: these patterns are gitignore-style, and a leading ** would exclude the parent directory of every allowed path, which makes the ! re-includes silently ineffective.
This commit is contained in:
@@ -160,6 +160,25 @@ export const KNOWN_METHODS = [
|
||||
] as const;
|
||||
export type KnownMethod = (typeof KNOWN_METHODS)[number];
|
||||
|
||||
/**
|
||||
* The three methods the router registers UNCONDITIONALLY (see rpc-host.ts, the block
|
||||
* after the last `if (has(...))`). `plugins.call` and `events.emit` are authorized by
|
||||
* the router's declared-dependency-edge check instead of by a grant, and `settings.get`
|
||||
* only ever returns THIS plugin's config for the acting user, so it needs none.
|
||||
*
|
||||
* They MUST stay disjoint from KNOWN_METHODS. Putting them there would give each one a
|
||||
* METHOD_PERMISSION row, and that table is what isAuditable (plugin-audit.ts) reads;
|
||||
* settings.get is deliberately the one unconditional method that is NOT audited. It
|
||||
* would also change the UNKNOWN_METHOD/PERMISSION_DENIED split documented above.
|
||||
*/
|
||||
export const UNCONDITIONAL_METHODS = ['plugins.call', 'events.emit', 'settings.get'] as const;
|
||||
export type UnconditionalMethod = (typeof UNCONDITIONAL_METHODS)[number];
|
||||
|
||||
/** `true` only when the two unions share no member; otherwise `never`, which fails to compile. */
|
||||
type AssertDisjoint<A extends string, B extends string> = [Extract<A, B>] extends [never] ? true : never;
|
||||
/** Compile-time proof of the invariant the comment above states. */
|
||||
export const UNCONDITIONAL_METHODS_ARE_DISJOINT: AssertDisjoint<UnconditionalMethod, KnownMethod> = true;
|
||||
|
||||
/** Which permission unlocks which method(s). The single source for the router. */
|
||||
export const METHOD_PERMISSION = {
|
||||
'db.exec': 'db:own',
|
||||
|
||||
Reference in New Issue
Block a user