mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
feat(plugins): lifecycle polish — uninstall, error log, egress guard, widget slot (M8)
- Uninstall with data disposition: POST /api/admin/plugins/:id/uninstall kills the plugin, removes its code + DB metadata, and (deleteData) drops its data dir, error log and per-user settings. - Error log: GET/DELETE /api/admin/plugins/:id/errors — the plugin's own crash / request-failure log, surfaced in the admin panel. - Egress guard: the isolated child wraps global fetch and refuses any outbound host not in the plugin's declared egress[]; with none declared, all outbound is blocked. Process-level defense in depth (the container runtime enforces it at the network layer in v2). - Admin → Plugins is now actionable: activate / deactivate / uninstall, a registry browser (install), and per-plugin error log. i18n across all locales. - Dashboard widget slot: active widget plugins render as sandboxed cards. The trek:request core-data bridge + photo/calendar hook consumers remain follow-ups.
This commit is contained in:
@@ -15,7 +15,8 @@ const { testDb } = vi.hoisted(() => {
|
||||
id TEXT PRIMARY KEY, status TEXT, permissions TEXT DEFAULT '[]', granted_permissions TEXT DEFAULT '[]',
|
||||
config TEXT DEFAULT '{}', last_error TEXT, updated_at TEXT);
|
||||
CREATE TABLE plugin_error_log (id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_id TEXT, level TEXT, message TEXT, ts TEXT);
|
||||
CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER);`);
|
||||
CREATE TABLE plugin_settings_fields (plugin_id TEXT, field_key TEXT, scope TEXT, secret INTEGER);
|
||||
CREATE TABLE settings (user_id INTEGER, key TEXT, value TEXT);`);
|
||||
return { testDb: db };
|
||||
});
|
||||
vi.mock('../../../src/db/database', () => ({ db: testDb, canAccessTrip: () => undefined }));
|
||||
@@ -150,6 +151,31 @@ describe('PluginRuntimeService (M2 end-to-end)', () => {
|
||||
expect(rt.outboundHostsOf('missing')).toEqual([]);
|
||||
});
|
||||
|
||||
it('uninstall removes the code, DB rows, settings and (with deleteData) data', async () => {
|
||||
fs.mkdirSync(path.join(codeRoot, 'gone', 'server'), { recursive: true });
|
||||
fs.writeFileSync(path.join(codeRoot, 'gone', 'server', 'index.js'), 'module.exports={}');
|
||||
testDb.prepare("INSERT INTO plugins (id, status, permissions, config) VALUES ('gone','inactive','[]','{}')").run();
|
||||
testDb.prepare("INSERT INTO plugin_settings_fields (plugin_id, field_key, scope, secret) VALUES ('gone','k','instance',0)").run();
|
||||
testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (1, 'plugin:gone:units', 'metric')").run();
|
||||
|
||||
await new PluginRuntimeService().uninstall('gone', true);
|
||||
|
||||
expect(fs.existsSync(path.join(codeRoot, 'gone'))).toBe(false);
|
||||
expect(testDb.prepare("SELECT COUNT(*) c FROM plugins WHERE id='gone'").get()).toMatchObject({ c: 0 });
|
||||
expect(testDb.prepare("SELECT COUNT(*) c FROM plugin_settings_fields WHERE plugin_id='gone'").get()).toMatchObject({ c: 0 });
|
||||
expect(testDb.prepare("SELECT COUNT(*) c FROM settings WHERE key LIKE 'plugin:gone:%'").get()).toMatchObject({ c: 0 });
|
||||
});
|
||||
|
||||
it('the egress guard blocks a fetch to an undeclared host', async () => {
|
||||
fs.mkdirSync(path.join(codeRoot, 'nettry', 'server'), { recursive: true });
|
||||
fs.writeFileSync(path.join(codeRoot, 'nettry', 'server', 'index.js'),
|
||||
"module.exports = { async onLoad() { await fetch('https://blocked.example/x'); } };");
|
||||
testDb.prepare("INSERT INTO plugins (id, status, permissions, config) VALUES ('nettry','inactive','[]','{}')").run();
|
||||
// no http:outbound granted -> egress guard blocks all outbound -> onLoad throws
|
||||
await expect(new PluginRuntimeService().activate('nettry')).rejects.toThrow(/egress/);
|
||||
await new PluginRuntimeService().deactivate('nettry').catch(() => {});
|
||||
});
|
||||
|
||||
it('onModuleDestroy tears down cleanly', async () => {
|
||||
await expect(new PluginRuntimeService().onModuleDestroy()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user