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:
@@ -0,0 +1,318 @@
|
||||
// GENERATED — do not edit by hand.
|
||||
// Source: server/src/nest/plugins/protocol/envelope.ts
|
||||
// Regenerate: node --import tsx server/scripts/gen-plugin-facts.ts
|
||||
|
||||
export const HOOK_PERMISSION: Readonly<Record<string, string>> = {
|
||||
photoProvider: 'hook:photo-provider',
|
||||
calendarSource: 'hook:calendar-source',
|
||||
placeDetailProvider: 'hook:place-detail-provider',
|
||||
warningProvider: 'hook:trip-warning-provider',
|
||||
tableContributor: 'hook:table-contributor',
|
||||
mapMarkerProvider: 'hook:map-marker-provider',
|
||||
mapLayerProvider: 'hook:map-layer-provider',
|
||||
routeProvider: 'hook:route-provider',
|
||||
dayScheduleProvider: 'hook:day-schedule-provider',
|
||||
dayTintProvider: 'hook:day-tint-provider',
|
||||
pdfSectionProvider: 'hook:pdf-section-provider',
|
||||
atlasLayerProvider: 'hook:atlas-layer-provider',
|
||||
journalEntryProvider: 'hook:journal-entry-provider',
|
||||
tripCardProvider: 'hook:trip-card-provider',
|
||||
notificationChannel: 'hook:notification-channel',
|
||||
};
|
||||
|
||||
export const KNOWN_PERMISSIONS: string[] = [
|
||||
'db:own',
|
||||
'db:read:trips',
|
||||
'db:read:users',
|
||||
'db:read:costs',
|
||||
'db:read:packing',
|
||||
'db:write:packing',
|
||||
'db:read:files',
|
||||
'db:read:files:content',
|
||||
'db:write:files',
|
||||
'db:read:collab',
|
||||
'db:write:collab',
|
||||
'db:write:members',
|
||||
'db:create:trips',
|
||||
'db:read:journal',
|
||||
'db:read:atlas',
|
||||
'rates:read',
|
||||
'db:read:vacay',
|
||||
'db:read:daynotes',
|
||||
'db:read:collections',
|
||||
'db:write:collections',
|
||||
'db:write:atlas',
|
||||
'db:write:vacay',
|
||||
'db:write:journal',
|
||||
'db:read:categories',
|
||||
'db:read:tags',
|
||||
'db:write:tags',
|
||||
'db:read:todos',
|
||||
'db:write:todos',
|
||||
'weather:read',
|
||||
'db:write:daynotes',
|
||||
'db:write:costs',
|
||||
'db:write:places',
|
||||
'db:write:days',
|
||||
'db:write:itinerary',
|
||||
'db:write:trips',
|
||||
'db:write:reservations',
|
||||
'db:write:accommodations',
|
||||
'db:meta',
|
||||
'ws:broadcast:trip',
|
||||
'ws:broadcast:user',
|
||||
'hook:photo-provider',
|
||||
'hook:calendar-source',
|
||||
'hook:place-detail-provider',
|
||||
'hook:trip-warning-provider',
|
||||
'hook:table-contributor',
|
||||
'hook:map-marker-provider',
|
||||
'hook:map-layer-provider',
|
||||
'hook:route-provider',
|
||||
'hook:day-schedule-provider',
|
||||
'hook:day-tint-provider',
|
||||
'hook:pdf-section-provider',
|
||||
'hook:atlas-layer-provider',
|
||||
'hook:journal-entry-provider',
|
||||
'hook:trip-card-provider',
|
||||
'hook:notification-channel',
|
||||
'hook:user-data',
|
||||
'events:subscribe',
|
||||
'jobs:run',
|
||||
'http:outbound',
|
||||
'notify:send',
|
||||
'ai:invoke',
|
||||
'oauth:client',
|
||||
'geolocation:read',
|
||||
];
|
||||
|
||||
export const METHOD_PERMISSION: Readonly<Record<string, string>> = {
|
||||
'db.exec': 'db:own',
|
||||
'db.query': 'db:own',
|
||||
'db.migrate': 'db:own',
|
||||
'db.tx': 'db:own',
|
||||
'trips.getById': 'db:read:trips',
|
||||
'trips.getPlaces': 'db:read:trips',
|
||||
'trips.getReservations': 'db:read:trips',
|
||||
'trips.getDays': 'db:read:trips',
|
||||
'trips.getAccommodations': 'db:read:trips',
|
||||
'trips.listMine': 'db:read:trips',
|
||||
'reservations.listMine': 'db:read:trips',
|
||||
'reservations.create': 'db:write:reservations',
|
||||
'reservations.update': 'db:write:reservations',
|
||||
'reservations.delete': 'db:write:reservations',
|
||||
'accommodations.create': 'db:write:accommodations',
|
||||
'accommodations.update': 'db:write:accommodations',
|
||||
'accommodations.delete': 'db:write:accommodations',
|
||||
'packing.list': 'db:read:packing',
|
||||
'packing.create': 'db:write:packing',
|
||||
'packing.update': 'db:write:packing',
|
||||
'packing.delete': 'db:write:packing',
|
||||
'packing.listBags': 'db:write:packing',
|
||||
'packing.createBag': 'db:write:packing',
|
||||
'packing.updateBag': 'db:write:packing',
|
||||
'packing.deleteBag': 'db:write:packing',
|
||||
'packing.setBagMembers': 'db:write:packing',
|
||||
'files.list': 'db:read:files',
|
||||
'files.getContent': 'db:read:files:content',
|
||||
'files.create': 'db:write:files',
|
||||
'files.createLink': 'db:write:files',
|
||||
'files.update': 'db:write:files',
|
||||
'files.softDelete': 'db:write:files',
|
||||
'collab.listNotes': 'db:read:collab',
|
||||
'collab.listPolls': 'db:read:collab',
|
||||
'collab.listMessages': 'db:read:collab',
|
||||
'collab.createNote': 'db:write:collab',
|
||||
'collab.createPoll': 'db:write:collab',
|
||||
'collab.votePoll': 'db:write:collab',
|
||||
'collab.createMessage': 'db:write:collab',
|
||||
'trips.addMember': 'db:write:members',
|
||||
'trips.removeMember': 'db:write:members',
|
||||
'trips.create': 'db:create:trips',
|
||||
'journal.listMine': 'db:read:journal',
|
||||
'journal.getEntries': 'db:read:journal',
|
||||
'atlas.visited': 'db:read:atlas',
|
||||
'atlas.bucketList': 'db:read:atlas',
|
||||
'rates.get': 'rates:read',
|
||||
'vacay.mine': 'db:read:vacay',
|
||||
'daynotes.list': 'db:read:daynotes',
|
||||
'daynotes.create': 'db:write:daynotes',
|
||||
'daynotes.update': 'db:write:daynotes',
|
||||
'daynotes.delete': 'db:write:daynotes',
|
||||
'collections.listMine': 'db:read:collections',
|
||||
'collections.get': 'db:read:collections',
|
||||
'collections.create': 'db:write:collections',
|
||||
'collections.update': 'db:write:collections',
|
||||
'collections.savePlace': 'db:write:collections',
|
||||
'collections.copyToTrip': 'db:write:collections',
|
||||
'collections.deletePlace': 'db:write:collections',
|
||||
'atlas.markCountry': 'db:write:atlas',
|
||||
'atlas.unmarkCountry': 'db:write:atlas',
|
||||
'atlas.markRegion': 'db:write:atlas',
|
||||
'atlas.unmarkRegion': 'db:write:atlas',
|
||||
'atlas.createBucketItem': 'db:write:atlas',
|
||||
'atlas.deleteBucketItem': 'db:write:atlas',
|
||||
'vacay.toggleEntry': 'db:write:vacay',
|
||||
'vacay.toggleCompanyHoliday': 'db:write:vacay',
|
||||
'journal.createEntry': 'db:write:journal',
|
||||
'journal.updateEntry': 'db:write:journal',
|
||||
'journal.deleteEntry': 'db:write:journal',
|
||||
'journal.createJourney': 'db:write:journal',
|
||||
'journal.deleteJourney': 'db:write:journal',
|
||||
'weather.get': 'weather:read',
|
||||
'categories.list': 'db:read:categories',
|
||||
'tags.list': 'db:read:tags',
|
||||
'tags.create': 'db:write:tags',
|
||||
'tags.update': 'db:write:tags',
|
||||
'tags.delete': 'db:write:tags',
|
||||
'trips.members': 'db:read:trips',
|
||||
'todos.list': 'db:read:todos',
|
||||
'todos.create': 'db:write:todos',
|
||||
'todos.update': 'db:write:todos',
|
||||
'todos.delete': 'db:write:todos',
|
||||
'costs.getByTrip': 'db:read:costs',
|
||||
'costs.listMine': 'db:read:costs',
|
||||
'costs.create': 'db:write:costs',
|
||||
'costs.update': 'db:write:costs',
|
||||
'costs.delete': 'db:write:costs',
|
||||
'places.create': 'db:write:places',
|
||||
'places.update': 'db:write:places',
|
||||
'places.delete': 'db:write:places',
|
||||
'days.create': 'db:write:days',
|
||||
'days.update': 'db:write:days',
|
||||
'days.delete': 'db:write:days',
|
||||
'itinerary.assign': 'db:write:itinerary',
|
||||
'itinerary.unassign': 'db:write:itinerary',
|
||||
'trips.update': 'db:write:trips',
|
||||
'meta.get': 'db:meta',
|
||||
'meta.set': 'db:meta',
|
||||
'meta.list': 'db:meta',
|
||||
'meta.delete': 'db:meta',
|
||||
'users.getById': 'db:read:users',
|
||||
'ws.broadcastToTrip': 'ws:broadcast:trip',
|
||||
'ws.broadcastToUser': 'ws:broadcast:user',
|
||||
'notify.send': 'notify:send',
|
||||
'ai.complete': 'ai:invoke',
|
||||
'ai.extract': 'ai:invoke',
|
||||
'oauth.getToken': 'oauth:client',
|
||||
'scheduler.set': 'jobs:run',
|
||||
'scheduler.cancel': 'jobs:run',
|
||||
};
|
||||
|
||||
export const KNOWN_METHODS: string[] = [
|
||||
'db.exec',
|
||||
'db.query',
|
||||
'db.migrate',
|
||||
'db.tx',
|
||||
'trips.getById',
|
||||
'trips.getPlaces',
|
||||
'trips.getReservations',
|
||||
'trips.getDays',
|
||||
'trips.getAccommodations',
|
||||
'trips.listMine',
|
||||
'reservations.listMine',
|
||||
'reservations.create',
|
||||
'reservations.update',
|
||||
'reservations.delete',
|
||||
'accommodations.create',
|
||||
'accommodations.update',
|
||||
'accommodations.delete',
|
||||
'packing.list',
|
||||
'packing.create',
|
||||
'packing.update',
|
||||
'packing.delete',
|
||||
'packing.listBags',
|
||||
'packing.createBag',
|
||||
'packing.updateBag',
|
||||
'packing.deleteBag',
|
||||
'packing.setBagMembers',
|
||||
'files.list',
|
||||
'files.getContent',
|
||||
'files.create',
|
||||
'files.createLink',
|
||||
'files.update',
|
||||
'files.softDelete',
|
||||
'collab.listNotes',
|
||||
'collab.listPolls',
|
||||
'collab.listMessages',
|
||||
'collab.createNote',
|
||||
'collab.createPoll',
|
||||
'collab.votePoll',
|
||||
'collab.createMessage',
|
||||
'trips.addMember',
|
||||
'trips.removeMember',
|
||||
'trips.create',
|
||||
'journal.listMine',
|
||||
'journal.getEntries',
|
||||
'atlas.visited',
|
||||
'atlas.bucketList',
|
||||
'rates.get',
|
||||
'vacay.mine',
|
||||
'daynotes.list',
|
||||
'daynotes.create',
|
||||
'daynotes.update',
|
||||
'daynotes.delete',
|
||||
'collections.listMine',
|
||||
'collections.get',
|
||||
'collections.create',
|
||||
'collections.update',
|
||||
'collections.savePlace',
|
||||
'collections.copyToTrip',
|
||||
'collections.deletePlace',
|
||||
'atlas.markCountry',
|
||||
'atlas.unmarkCountry',
|
||||
'atlas.markRegion',
|
||||
'atlas.unmarkRegion',
|
||||
'atlas.createBucketItem',
|
||||
'atlas.deleteBucketItem',
|
||||
'vacay.toggleEntry',
|
||||
'vacay.toggleCompanyHoliday',
|
||||
'journal.createEntry',
|
||||
'journal.updateEntry',
|
||||
'journal.deleteEntry',
|
||||
'journal.createJourney',
|
||||
'journal.deleteJourney',
|
||||
'weather.get',
|
||||
'categories.list',
|
||||
'tags.list',
|
||||
'tags.create',
|
||||
'tags.update',
|
||||
'tags.delete',
|
||||
'trips.members',
|
||||
'todos.list',
|
||||
'todos.create',
|
||||
'todos.update',
|
||||
'todos.delete',
|
||||
'costs.getByTrip',
|
||||
'costs.listMine',
|
||||
'costs.create',
|
||||
'costs.update',
|
||||
'costs.delete',
|
||||
'places.create',
|
||||
'places.update',
|
||||
'places.delete',
|
||||
'days.create',
|
||||
'days.update',
|
||||
'days.delete',
|
||||
'itinerary.assign',
|
||||
'itinerary.unassign',
|
||||
'trips.update',
|
||||
'meta.get',
|
||||
'meta.set',
|
||||
'meta.list',
|
||||
'meta.delete',
|
||||
'users.getById',
|
||||
'ws.broadcastToTrip',
|
||||
'ws.broadcastToUser',
|
||||
'notify.send',
|
||||
'ai.complete',
|
||||
'ai.extract',
|
||||
'oauth.getToken',
|
||||
'scheduler.set',
|
||||
'scheduler.cancel',
|
||||
];
|
||||
|
||||
export const USER_DATA_PERMISSION = 'hook:user-data';
|
||||
export const EVENTS_PERMISSION = 'events:subscribe';
|
||||
export const JOBS_PERMISSION = 'jobs:run';
|
||||
export const HTTP_OUTBOUND_PREFIX = 'http:outbound:';
|
||||
@@ -75,26 +75,8 @@ export const CHANNEL_EVENTS = [
|
||||
// This is THE list. `create`'s permission picker is built from it (see PERMISSION_FAMILIES
|
||||
// in cli/ui.ts, which only supplies the grouping and hints), so a permission added here can
|
||||
// never again go missing from the scaffolder — test/cli.test.ts fails until it has an entry.
|
||||
export const KNOWN_PERMISSIONS = [
|
||||
'db:own',
|
||||
'db:read:trips', 'db:read:users', 'db:read:costs', 'db:read:packing', 'db:read:files',
|
||||
'db:read:files:content', 'db:read:collab',
|
||||
'db:read:journal', 'db:read:atlas', 'db:read:vacay', 'db:read:daynotes', 'db:read:collections',
|
||||
'db:read:categories', 'db:read:tags', 'db:read:todos',
|
||||
'db:write:costs', 'db:write:places', 'db:write:days', 'db:write:itinerary', 'db:write:trips',
|
||||
'db:write:reservations', 'db:write:accommodations', 'db:write:packing', 'db:write:files',
|
||||
'db:write:collab', 'db:write:members', 'db:write:collections', 'db:write:atlas', 'db:write:vacay',
|
||||
'db:write:journal', 'db:write:tags', 'db:write:todos', 'db:write:daynotes',
|
||||
'db:create:trips',
|
||||
'db:meta',
|
||||
'ws:broadcast:trip', 'ws:broadcast:user',
|
||||
'hook:photo-provider', 'hook:calendar-source', 'hook:place-detail-provider', 'hook:trip-warning-provider',
|
||||
'hook:table-contributor', 'hook:map-marker-provider', 'hook:map-layer-provider', 'hook:route-provider', 'hook:day-schedule-provider', 'hook:day-tint-provider', 'hook:pdf-section-provider', 'hook:atlas-layer-provider',
|
||||
'hook:journal-entry-provider', 'hook:trip-card-provider', 'hook:notification-channel', 'hook:user-data',
|
||||
'events:subscribe', 'jobs:run', 'http:outbound',
|
||||
'weather:read', 'rates:read', 'notify:send', 'ai:invoke', 'oauth:client',
|
||||
'geolocation:read',
|
||||
];
|
||||
export { KNOWN_PERMISSIONS } from './generated/host-facts.js';
|
||||
import { KNOWN_PERMISSIONS } from './generated/host-facts.js';
|
||||
|
||||
function isKnownPermission(p: string): boolean {
|
||||
return KNOWN_PERMISSIONS.includes(p) || p.startsWith('http:outbound:');
|
||||
|
||||
@@ -14,36 +14,28 @@
|
||||
export class PermissionDenied extends Error {}
|
||||
|
||||
/**
|
||||
* hooks.<key> → the permission that must ALSO be granted for TREK to ever call it.
|
||||
* Mirrors HOOK_PERMISSION in the host's supervisor (plugin-supervisor.ts): a plugin
|
||||
* provides a hook only if it BOTH implements it and holds this grant.
|
||||
* hooks.<key> -> 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<Record<string, string>>), exactly as before:
|
||||
* grantGaps below indexes HOOK_PERMISSION with a plain string, and this package compiles
|
||||
* under "strict": true.
|
||||
*/
|
||||
export const HOOK_PERMISSION: Readonly<Record<string, string>> = {
|
||||
photoProvider: 'hook:photo-provider',
|
||||
calendarSource: 'hook:calendar-source',
|
||||
placeDetailProvider: 'hook:place-detail-provider',
|
||||
warningProvider: 'hook:trip-warning-provider',
|
||||
tableContributor: 'hook:table-contributor',
|
||||
mapMarkerProvider: 'hook:map-marker-provider',
|
||||
mapLayerProvider: 'hook:map-layer-provider',
|
||||
routeProvider: 'hook:route-provider',
|
||||
dayScheduleProvider: 'hook:day-schedule-provider',
|
||||
dayTintProvider: 'hook:day-tint-provider',
|
||||
pdfSectionProvider: 'hook:pdf-section-provider',
|
||||
atlasLayerProvider: 'hook:atlas-layer-provider',
|
||||
journalEntryProvider: 'hook:journal-entry-provider',
|
||||
tripCardProvider: 'hook:trip-card-provider',
|
||||
notificationChannel: 'hook:notification-channel',
|
||||
};
|
||||
|
||||
/** Gates the GDPR handlers (deleteUserData / exportUserData). Not a hooks.* key. */
|
||||
export const USER_DATA_PERMISSION = 'hook:user-data';
|
||||
/** Gates event subscriptions — without it the host delivers the plugin nothing. */
|
||||
export const EVENTS_PERMISSION = 'events:subscribe';
|
||||
/** Gates jobs, and the ctx.scheduler timers that fire `scheduled`. */
|
||||
export const JOBS_PERMISSION = 'jobs:run';
|
||||
|
||||
const HTTP_OUTBOUND = 'http:outbound:';
|
||||
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 {
|
||||
|
||||
@@ -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