mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
152 lines
6.0 KiB
TypeScript
152 lines
6.0 KiB
TypeScript
/**
|
|
* Small pure helpers of the plugin module (#plugins): permission recognition and
|
|
* the code/data path resolution (both the env-override and default branches).
|
|
*/
|
|
import {
|
|
pluginsCodeRoot,
|
|
pluginsDataRoot,
|
|
pluginCodeDir,
|
|
pluginDataDir,
|
|
pluginDbFile,
|
|
resolveChildEntry,
|
|
serverCodeRoot,
|
|
pluginPermissionArgs,
|
|
pluginRealCodeDir,
|
|
ensurePluginModuleType,
|
|
} from '../../../src/nest/plugins/paths';
|
|
import {
|
|
isKnownPermission,
|
|
METHOD_PERMISSION,
|
|
KNOWN_METHODS,
|
|
HOOK_PERMISSION,
|
|
} from '../../../src/nest/plugins/protocol/envelope';
|
|
|
|
import path from 'node:path';
|
|
import { describe, it, expect, afterEach } from 'vitest';
|
|
|
|
afterEach(() => {
|
|
delete process.env.TREK_PLUGINS_DIR;
|
|
delete process.env.TREK_PLUGINS_DATA_DIR;
|
|
});
|
|
|
|
describe('envelope helpers', () => {
|
|
it('recognises known permissions, http:outbound:<host>, and rejects unknown', () => {
|
|
expect(isKnownPermission('db:own')).toBe(true);
|
|
expect(isKnownPermission('http:outbound')).toBe(true);
|
|
expect(isKnownPermission('http:outbound:api.example.com')).toBe(true);
|
|
expect(isKnownPermission('fs:read')).toBe(false);
|
|
expect(isKnownPermission('')).toBe(false);
|
|
});
|
|
|
|
// The compile side of this is `as const satisfies Record<KnownMethod, KnownPermission>`
|
|
// on METHOD_PERMISSION. These are the runtime mirror, and they also cover what the type
|
|
// system cannot: isKnownPermission's http:outbound: prefix branch.
|
|
it('every known method maps to a permission the host actually knows', () => {
|
|
for (const m of KNOWN_METHODS) {
|
|
const perm = METHOD_PERMISSION[m];
|
|
expect(isKnownPermission(perm), `${m} -> ${perm}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('every hook permission is a known permission', () => {
|
|
for (const [hook, perm] of Object.entries(HOOK_PERMISSION)) {
|
|
expect(isKnownPermission(perm), `${hook} -> ${perm}`).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('paths', () => {
|
|
it('uses the env override when set', () => {
|
|
process.env.TREK_PLUGINS_DIR = '/custom/code';
|
|
process.env.TREK_PLUGINS_DATA_DIR = '/custom/data';
|
|
expect(pluginsCodeRoot()).toBe('/custom/code');
|
|
expect(pluginsDataRoot()).toBe('/custom/data');
|
|
expect(pluginCodeDir('x')).toBe(path.join('/custom/code', 'x'));
|
|
expect(path.basename(pluginDbFile('x'))).toBe('plugin.db');
|
|
});
|
|
|
|
it('falls back to the data-dir default when unset', () => {
|
|
expect(pluginsCodeRoot()).toContain('plugins');
|
|
expect(pluginsDataRoot()).toContain('plugins-data');
|
|
});
|
|
|
|
it('resolves a child entry with fork args', () => {
|
|
const r = resolveChildEntry();
|
|
expect(r.entry).toMatch(/plugin-host-entry\.(js|ts)$/);
|
|
expect(Array.isArray(r.execArgv)).toBe(true);
|
|
expect(typeof r.jsMode).toBe('boolean');
|
|
});
|
|
|
|
it('builds scoped OS-permission flags for a plugin child (default on)', () => {
|
|
delete process.env.TREK_PLUGIN_PERMISSIONS;
|
|
const args = pluginPermissionArgs('flight-tracker');
|
|
expect(args).toContain('--permission');
|
|
// read is scoped to the compiled server dir + this plugin's own code dir…
|
|
expect(args.some((a) => a === `--allow-fs-read=${serverCodeRoot()}`)).toBe(true);
|
|
expect(args.some((a) => a === `--allow-fs-read=${pluginCodeDir('flight-tracker')}`)).toBe(true);
|
|
// …and never grants fs-write / child_process / the data root.
|
|
expect(args.some((a) => a.startsWith('--allow-fs-write'))).toBe(false);
|
|
expect(args.some((a) => a.startsWith('--allow-child-process'))).toBe(false);
|
|
expect(serverCodeRoot()).not.toContain(`${require('node:path').sep}data`);
|
|
});
|
|
|
|
it('lets an operator opt out of the permission model', () => {
|
|
process.env.TREK_PLUGIN_PERMISSIONS = 'off';
|
|
expect(pluginPermissionArgs('flight-tracker')).toEqual([]);
|
|
delete process.env.TREK_PLUGIN_PERMISSIONS;
|
|
});
|
|
|
|
it('pluginRealCodeDir falls back to the lexical path when the dir is absent', () => {
|
|
// no such plugin installed -> realpathSync throws -> lexical path returned
|
|
expect(pluginRealCodeDir('does-not-exist')).toBe(pluginCodeDir('does-not-exist'));
|
|
});
|
|
|
|
it('ensurePluginModuleType writes a commonjs package.json only when absent', () => {
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'plug-mod-'));
|
|
try {
|
|
ensurePluginModuleType(dir);
|
|
expect(JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))).toEqual({ type: 'commonjs' });
|
|
// an author-provided package.json is left untouched
|
|
fs.writeFileSync(path.join(dir, 'package.json'), '{"type":"module"}');
|
|
ensurePluginModuleType(dir);
|
|
expect(JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))).toEqual({ type: 'module' });
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
/**
|
|
* The id these helpers get comes off a route parameter — POST
|
|
* /api/admin/plugins/:id/uninstall walks it into pluginCodeDir and hands the result
|
|
* to a recursive remove. Express decodes %2F only AFTER routing, so a traversal
|
|
* still matches the route and arrives here decoded.
|
|
*/
|
|
describe('plugin id containment', () => {
|
|
const traversals = ['../../uploads', '../..', '..', '.', 'a/b', 'a\b', 'C:\Windows', '.hidden', ''];
|
|
|
|
it('refuses any id that could point outside its parent', () => {
|
|
for (const id of traversals) {
|
|
expect(() => pluginCodeDir(id), id).toThrow(/invalid plugin id/);
|
|
expect(() => pluginDataDir(id), id).toThrow(/invalid plugin id/);
|
|
}
|
|
});
|
|
|
|
it('still accepts the ids plugins actually use', () => {
|
|
for (const id of ['x', 'a1', '001', 'flight-tracker', 'Bad-Id', 'trip.todos', 'koffi_and_friends']) {
|
|
expect(path.basename(pluginCodeDir(id))).toBe(id);
|
|
expect(path.basename(pluginDataDir(id))).toBe(id);
|
|
}
|
|
});
|
|
|
|
it('keeps every accepted id inside the plugin roots', () => {
|
|
for (const id of ['x', 'flight-tracker', 'a.b', 'a-b_c']) {
|
|
expect(pluginCodeDir(id).startsWith(pluginsCodeRoot() + path.sep)).toBe(true);
|
|
expect(pluginDataDir(id).startsWith(pluginsDataRoot() + path.sep)).toBe(true);
|
|
}
|
|
});
|
|
});
|