localeCompare is locale/ICU dependent and fed the signed digest, so a plugin signed on one machine could false-reject on another (live even in ASCII: README.md vs codeburn-plugin.json order flips between locale-aware and codepoint comparison). All four sort sites now use plain codepoint order, commented as a canonicalization requirement. The sign script now throws on unreadable files instead of signing an incomplete list; the loader keeps skipping (its omission causes a digest mismatch and rejection, the fail-closed direction). New test drives the real sign script over a tree with mixed-case and non-ASCII names, so test and production canonicalization cannot diverge again.
Closes security hole: plugin commands (executable .mjs files under commands/) are now part of the signed digest, preventing tampering. getPluginFilesList and sign-plugin.mjs getFilesList now walk directories recursively with relative POSIX paths, hashing all regular files while excluding codeburn-plugin.sig (the signature itself) and sections/ (runtime-mutable plugin output). plugin add copies the full tree recursively, preserving directory structure including commands/ and commands/subdir/*.mjs. Installed plugins verify correctly without CODEBURN_PLUGIN_DEV. checkForSymlinks now recurses to catch symlinks anywhere in the tree. Sections may change at runtime without breaking verification because they are excluded from the canonical digest.
Fixes critical bug where keygen() used random keyId but sign() derived it from sha256 of public key PEM. This caused embedded release key (f40248d0) to not match actual derived id (499923ae), breaking plugin verification.
Now both keygen and sign derive keyId consistently from sha256(publicKeyPem).slice(0,4).hex.
Added tests:
- keygen and sign derive identical keyIds from same ephemeral key
- RELEASE_PUBLIC_KEYS map keys match their derived keyIds from embedded PEM values
End-to-end verified: signing with private key and verifying with embedded public key now works without CODEBURN_PLUGIN_DEV.
Ships ed25519 signature verification for plugins with two new components:
- src/plugins/keys.ts: RELEASE_PUBLIC_KEYS map with one release keypair
- scripts/sign-plugin.mjs: keygen and sign commands for plugin developers
- verifyPlugin() in loader.ts: validates ed25519 signatures, rejects unsigned plugins unless CODEBURN_PLUGIN_DEV=1
- plugin add <path>: installs signed plugins to ~/.config/codeburn/plugins/
- plugin remove <name> --confirm: removes installed plugins
All three gates pass: tsc clean, vitest zero new failures, smoke test complete.
Design decision: public keys stored as base64-encoded PEM format rather than raw 32-byte keys. Node.js crypto.verify requires PEM/DER format or KeyObject for ed25519; raw bytes alone fail. PEM is standard and portable.
Private key written to: /tmp/codeburn-signing/codeburn-signing-key.pem