/** * The permission families TREK enforces OUTSIDE the ctx object — and the single * PermissionDenied both the dev server and the mock host throw. * * ctx methods are gated at call time (an ungranted ctx.trips.getById throws). But a * hook, an event subscription and a job are gated BEFORE the plugin is ever reached: * the supervisor simply never selects an ungranted plugin as a provider, never * delivers it an event, and never schedules its jobs. That failure is SILENT in * production — the plugin installs, activates, and then does nothing at all — so dev * and the mock driver must be the place it becomes loud. */ /** Thrown by the dev server and the mock host for any ungranted capability. */ export class PermissionDenied extends Error {} /** * hooks. -> the permission that must ALSO be granted for TREK to ever call it, plus * the three capability gates that are not hooks.* keys. * * These are GENERATED from the host's single source, server/src/nest/plugins/protocol/ * envelope.ts, by server/scripts/gen-plugin-facts.ts. They used to be a hand-kept copy * guarded by a regex scrape of plugin-supervisor.ts in test/permissions-parity.test.ts - * which only ran from prepublishOnly, so drift would have surfaced as a broken release. * * The re-exported types stay WIDE (Readonly>), exactly as before: * grantGaps below indexes HOOK_PERMISSION with a plain string, and this package compiles * under "strict": true. */ export { HOOK_PERMISSION, USER_DATA_PERMISSION, EVENTS_PERMISSION, JOBS_PERMISSION, } from './generated/host-facts.js'; import { HOOK_PERMISSION, USER_DATA_PERMISSION, EVENTS_PERMISSION, JOBS_PERMISSION, HTTP_OUTBOUND_PREFIX as HTTP_OUTBOUND, } from './generated/host-facts.js'; /** The structural shape of a loaded plugin that grantGaps needs. */ export interface PluginEntryPoints { jobs?: unknown[]; scheduled?: unknown; events?: unknown[]; deleteUserData?: unknown; exportUserData?: unknown; hooks?: Record; } /** An entry point the plugin implements but has no permission to actually run. */ export interface GrantGap { /** What the plugin implements, e.g. `hooks.warningProvider` or `jobs`. */ entryPoint: string; /** The permission the manifest is missing. */ permission: string; /** What TREK does instead — always a silent no-op, hence the warning. */ consequence: string; } /** * Every entry point the plugin implements without the permission that makes TREK * call it. Pure, so both the dev banner and the tests can use it. * * The consequence is worth spelling out to the author: production does NOT throw * here, it just never invokes them — "my plugin does nothing and logs nothing" is * exactly the bug this catches. */ export function grantGaps(plugin: PluginEntryPoints, grants: ReadonlySet): GrantGap[] { const gaps: GrantGap[] = []; const gap = (entryPoint: string, permission: string, noun: string) => { if (!grants.has(permission)) gaps.push({ entryPoint, permission, consequence: `TREK will never ${noun}` }); }; for (const key of Object.keys(plugin.hooks ?? {})) { const perm = HOOK_PERMISSION[key]; // An unknown hooks.* key is the plugin's business (the host ignores it), not a gap. if (perm && plugin.hooks?.[key]) gap(`hooks.${key}`, perm, `call this hook`); } if (plugin.jobs?.length) gap('jobs', JOBS_PERMISSION, 'schedule your jobs'); if (typeof plugin.scheduled === 'function') gap('scheduled', JOBS_PERMISSION, 'let you arm a timer (ctx.scheduler is denied)'); if (plugin.events?.length) gap('events', EVENTS_PERMISSION, 'deliver you any event'); if (typeof plugin.deleteUserData === 'function') gap('deleteUserData', USER_DATA_PERMISSION, 'call your GDPR erasure handler'); if (typeof plugin.exportUserData === 'function') gap('exportUserData', USER_DATA_PERMISSION, 'call your GDPR export handler'); return gaps; } /** * The hosts a plugin may actually reach. TREK builds its runtime network allowlist * from the `http:outbound:` PERMISSIONS — the manifest's `egress[]` array is * the consent-screen declaration and is never read at runtime. A bare `http:outbound` * with no host suffix therefore reaches nothing. */ export function grantedHosts(grants: Iterable): string[] { return [...grants] .filter((p) => p.startsWith(HTTP_OUTBOUND)) .map((p) => p.slice(HTTP_OUTBOUND.length)) .filter(Boolean); // a bare `http:outbound:` names no host }