mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
14b348581a
Every plugin now runs in its own forked child process with a scrubbed env (no JWT_SECRET, no db path, nothing inherited). It talks to TREK only over a JSON-RPC channel, and the host's capability router registers ONLY the methods a plugin's granted permissions unlock — so an ungranted call is unreachable, not merely refused. The plugin's own data lives in a separate sqlite file it can never open directly; core reads (trips/users) go through membership-checked, column-projected host methods; ws broadcasts are force-namespaced. - protocol/envelope: the wire types + method→permission map (pure, shared by host and the isolated child) - host/rpc-host: the capability router = the enforcement point (dispatch, BAD_PARAMS / PERMISSION_DENIED / RESOURCE_FORBIDDEN / UNKNOWN_METHOD) - host/plugin-data: the per-plugin sqlite file (db:own), guarded against ATTACH/PRAGMA escape, idempotent migrations - host/create-rpc-host: wires the router to the real db/websocket (host-only) - runtime/plugin-sdk + plugin-host-entry: the child bootstrap + definePlugin ctx; turns each ctx call into an RPC, never imports a privileged module - supervisor: spawn on activate, heartbeat/reap, crash backoff + auto-disable, graceful shutdown — a plugin crash/OOM/hang can never reach the Nest loop - paths: code/data layout, dist-vs-tsx child entry resolution Nothing is wired into activation yet (that's the next slice); exercised by unit tests for the router/sdk/data and an integration test that forks a real child.
57 lines
2.4 KiB
TypeScript
57 lines
2.4 KiB
TypeScript
/**
|
|
* The per-plugin sqlite file (#plugins, M1, db:own). Proves migrations are
|
|
* idempotent, reads/writes work against the plugin's OWN file, and the guard
|
|
* blocks statements that would let a plugin escape its file (ATTACH/PRAGMA).
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { PluginDataDb, removePluginData } from '../../../src/nest/plugins/host/plugin-data.service';
|
|
|
|
let tmp: string;
|
|
beforeAll(() => {
|
|
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'trekplug-data-'));
|
|
process.env.TREK_PLUGINS_DATA_DIR = tmp;
|
|
});
|
|
afterAll(() => {
|
|
delete process.env.TREK_PLUGINS_DATA_DIR;
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('PluginDataDb', () => {
|
|
it('migrates once (idempotent by id), then reads and writes its own data', () => {
|
|
const db = new PluginDataDb('notes');
|
|
expect(db.migrate('001', 'CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)').applied).toBe(true);
|
|
expect(db.migrate('001', 'CREATE TABLE notes (x)').applied).toBe(false); // same id -> skipped
|
|
|
|
db.exec('INSERT INTO notes (body) VALUES (?)', ['hello']);
|
|
// exec without bound args runs the multi-statement path
|
|
db.exec("INSERT INTO notes (body) VALUES ('second')");
|
|
const rows = db.query('SELECT body FROM notes ORDER BY id') as Array<{ body: string }>;
|
|
expect(rows).toEqual([{ body: 'hello' }, { body: 'second' }]);
|
|
db.close();
|
|
|
|
// The data lives in its own file, not trek.db
|
|
expect(fs.existsSync(path.join(tmp, 'notes', 'plugin.db'))).toBe(true);
|
|
});
|
|
|
|
it('rejects statements that would escape the plugin file, and bad sql', () => {
|
|
const db = new PluginDataDb('guard');
|
|
expect(() => db.exec("ATTACH DATABASE 'trek.db' AS core")).toThrow(/not allowed/);
|
|
expect(() => db.query('PRAGMA table_info(x)')).toThrow(/not allowed/);
|
|
expect(() => db.exec(123 as unknown as string)).toThrow(/must be a string/);
|
|
expect(() => db.query('x'.repeat(100_001))).toThrow(/too long/);
|
|
db.close();
|
|
});
|
|
|
|
it('removePluginData deletes the whole data dir', () => {
|
|
const db = new PluginDataDb('temp');
|
|
db.migrate('001', 'CREATE TABLE t (id INTEGER)');
|
|
db.close();
|
|
expect(fs.existsSync(path.join(tmp, 'temp'))).toBe(true);
|
|
removePluginData('temp');
|
|
expect(fs.existsSync(path.join(tmp, 'temp'))).toBe(false);
|
|
});
|
|
});
|