mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(plugin-sdk): dev server, preflight, auto-PR submit, signing, wizard
Round out the author experience so the loop is create -> dev -> release/submit without hand-work or a round-trip through registry review. - `dev`: run a plugin locally with a real request loop and hot reload — no full TREK. Injects a ctx that enforces the manifest's granted permissions (an ungranted call throws, so you catch a missing grant), backs db:own with a real SQLite file (node:sqlite), serves routes under /api and page/widget UI at /ui, and reloads on save. Dependency-free (node:http + built-ins). - `preflight`: run the registry CI checks locally over the network (tag->commit, manifest parity, artifact sha256/size, native scan, README quality gate) so a green run predicts a green CI. - `submit`: fork TREK-Plugins, branch off current main, write/merge the entry, push, and open the PR — the last manual publishing step, automated. - `keygen`/`sign` + `--sign` on entry/release/submit: dependency-free Ed25519 author signatures over the artifact bytes, verified 1:1 against the server's TOFU check. Fills authorPublicKey + signature and guards against a key change. - `create` gains an interactive wizard (id/type/author/permissions) and flags. - README + Development/Publishing/Permissions wikis document the new flow. 24 tests pass (sign round-trips through a server-shaped verifier; zip reader; scaffold options; entry signing + key-change guard).
This commit is contained in:
@@ -6,9 +6,10 @@ import zlib from 'node:zlib';
|
||||
import { definePlugin, PLUGIN_API_VERSION, validateManifest, createMockHost } from '../src/index.js';
|
||||
import { scaffold } from '../src/cli/create.js';
|
||||
import { validatePluginDir } from '../src/cli/validate.js';
|
||||
import { makeZip } from '../src/zip.js';
|
||||
import { makeZip, listZipNames } from '../src/zip.js';
|
||||
import { packPluginDir } from '../src/cli/pack.js';
|
||||
import { buildEntry } from '../src/cli/entry.js';
|
||||
import { generateKeypair, signArtifact, publicKeyBase64, verifyArtifact, loadPrivateKey } from '../src/cli/sign.js';
|
||||
|
||||
/** A central-directory zip reader mirroring the TREK server's, to prove round-trip. */
|
||||
function readZip(buf: Buffer): Record<string, Buffer> {
|
||||
@@ -104,6 +105,14 @@ describe('scaffold + validate CLIs', () => {
|
||||
expect(r.warnings.some((w) => /placeholder|screenshot/.test(w))).toBe(true); // README is the unfilled template
|
||||
});
|
||||
|
||||
it('applies author, description, and permissions from options', () => {
|
||||
scaffold('opt-plug', 'integration', tmp, { author: 'Jane', description: 'Does X', permissions: ['db:own', 'db:read:trips'] });
|
||||
const m = JSON.parse(fs.readFileSync(path.join(tmp, 'opt-plug', 'trek-plugin.json'), 'utf8'));
|
||||
expect(m.author).toBe('Jane');
|
||||
expect(m.description).toBe('Does X');
|
||||
expect(m.permissions).toEqual(['db:own', 'db:read:trips']);
|
||||
});
|
||||
|
||||
it('rejects an invalid plugin name', () => {
|
||||
expect(() => scaffold('Bad Name', 'widget', tmp)).toThrow(/invalid plugin id/);
|
||||
});
|
||||
@@ -208,3 +217,61 @@ describe('pack + entry (publishing automation)', () => {
|
||||
expect(merged.versions.map((v) => v.version)).toEqual(['1.0.0', '0.9.0']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listZipNames', () => {
|
||||
it('lists central-directory entries of a makeZip archive', () => {
|
||||
const zip = makeZip([{ name: 'a.js', data: Buffer.from('x') }, { name: 'server/b.js', data: Buffer.from('y'.repeat(100)) }]);
|
||||
expect(listZipNames(zip).sort()).toEqual(['a.js', 'server/b.js']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sign + keygen (author signatures, TOFU)', () => {
|
||||
const koffi = path.resolve(import.meta.dirname, '..', 'examples', 'koffi');
|
||||
let tmp: string;
|
||||
beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sign-')); });
|
||||
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
it('keygen writes a private key and returns a schema-length public key', () => {
|
||||
const keyPath = path.join(tmp, 'k.pem');
|
||||
const { publicKey } = generateKeypair(keyPath);
|
||||
expect(fs.existsSync(keyPath)).toBe(true);
|
||||
expect(publicKey.length).toBeGreaterThanOrEqual(40); // registry schema minLength
|
||||
expect(() => generateKeypair(keyPath)).toThrow(/already exists/); // never clobbers a key
|
||||
});
|
||||
|
||||
it('a signature round-trips through the server-shaped verifier', () => {
|
||||
const keyPath = path.join(tmp, 'k.pem');
|
||||
generateKeypair(keyPath);
|
||||
const key = loadPrivateKey(keyPath);
|
||||
const bytes = Buffer.from('the exact plugin.zip bytes ' + 'x'.repeat(60));
|
||||
const sig = signArtifact(bytes, key);
|
||||
expect(sig.length).toBeGreaterThanOrEqual(40);
|
||||
expect(verifyArtifact(bytes, sig, publicKeyBase64(key))).toBe(true);
|
||||
expect(verifyArtifact(Buffer.concat([bytes, Buffer.from('!')]), sig, publicKeyBase64(key))).toBe(false);
|
||||
});
|
||||
|
||||
it('entry --sign fills signature + authorPublicKey that verify against the artifact', () => {
|
||||
const out = path.join(tmp, 'plugin.zip');
|
||||
packPluginDir(koffi, out);
|
||||
const keyPath = path.join(tmp, 'k.pem');
|
||||
const { publicKey } = generateKeypair(keyPath);
|
||||
const entry = buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: keyPath, now: '2026-07-04T00:00:00.000Z' });
|
||||
expect(entry.authorPublicKey).toBe(publicKey);
|
||||
const sig = entry.versions[0].signature;
|
||||
expect(sig).toBeTruthy();
|
||||
expect(verifyArtifact(fs.readFileSync(out), sig!, entry.authorPublicKey!)).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses to sign an update with a different key than the one already published', () => {
|
||||
const out = path.join(tmp, 'plugin.zip');
|
||||
packPluginDir(koffi, out);
|
||||
const key1 = path.join(tmp, 'k1.pem');
|
||||
generateKeypair(key1);
|
||||
const existing = buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.0.0', zipPath: out, commit: 'a'.repeat(40), signKeyPath: key1, now: '2026-07-04T00:00:00.000Z' });
|
||||
const existingPath = path.join(tmp, 'koffi.json');
|
||||
fs.writeFileSync(existingPath, JSON.stringify({ ...existing, versions: existing.versions.map((v) => ({ ...v, version: '0.9.0', gitTag: 'v0.9.0' })) }));
|
||||
const key2 = path.join(tmp, 'k2.pem');
|
||||
generateKeypair(key2);
|
||||
expect(() => buildEntry({ dir: koffi, repo: 'mauriceboe/trek-plugin-koffi', tag: 'v1.1.0', zipPath: out, commit: 'a'.repeat(40), mergePath: existingPath, signKeyPath: key2, now: '2026-07-04T00:00:00.000Z' })).toThrow(/differs from the one already published/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user