mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
d110b3bb6d
* fix(plugins): enforce manifest apiVersion at install and activation * test(plugins): exercise the absent-apiVersion default * feat(plugins): generate the core-event catalog into host-facts * docs(plugins): explain the packing.listBags write-permission binding * test(plugins): cover apiVersion upload refusal; tidy docblocks and generator header * fix(plugin-sdk): validate settings options and oauth like the host * fix(plugin-sdk): model the full manifest shape and stop dropping fields * fix(plugin-sdk): drop non-string routeProfiles icons like the host * docs(plugin-sdk): document host limits, oauth contract, and ignored asUserId * feat(plugin-sdk): enforce daily budgets and meta quotas in the mock host * feat(plugin-sdk): expose the generated core-event catalog * feat(plugin-sdk): scaffold trek range >=3.4.0 <5.0.0 ahead of TREK 4.0 * feat(plugin-sdk): scaffold the oauth broker settings for oauth:client plugins * chore(plugin-sdk): fix stale mirror docs and the colliding permissions export * docs(plugin-sdk): add the rate-limit string; restore trek docblock; match host meta sizing * feat(plugin-sdk): raise the scaffold trek floor to 4.0.0 * docs(wiki): bring the plugin pages in line with the current runtime * docs: bring the workspace CLAUDE.md guides up to date and make them committable * feat(plugin-sdk): roll back failed publishes, add unrelease, sync the fork, retro-sign updates * docs(wiki): document the publish rollback, unrelease, fork sync, and retro-signing * chore(plugin-sdk): 1.6.0
82 lines
3.7 KiB
TypeScript
82 lines
3.7 KiB
TypeScript
/**
|
|
* Retro-signing: the missing half of "you can start signing late".
|
|
*
|
|
* Server-side, adding a key at v1.4.0 breaks nobody — TREK pins the key only when a signed
|
|
* version installs. But the REGISTRY refuses a mixed entry (key present, older versions
|
|
* unsigned), and for good reason: once an instance pins your key, it refuses the unsigned
|
|
* older versions, and "install latest compatible" can legitimately resolve an older host to
|
|
* one of them. So every version must be signed the moment a key appears — which used to mean
|
|
* downloading each old artifact and signing it by hand.
|
|
*
|
|
* This does that tedious part: for each unsigned version it downloads the pinned artifact,
|
|
* verifies the bytes still hash to the entry's pinned sha256 (a mismatch means the release
|
|
* asset was changed — a far bigger problem than signing), and signs those exact bytes.
|
|
*/
|
|
import { createHash } from 'node:crypto';
|
|
import { loadPrivateKey, publicKeyBase64, signArtifact } from './sign.js';
|
|
|
|
export interface RetroSignableEntry {
|
|
id?: string;
|
|
authorPublicKey?: string;
|
|
versions: Array<{ version: string; downloadUrl: string; sha256: string; signature?: string }>;
|
|
}
|
|
|
|
/** True when the entry carries a key but not a signature on every version. */
|
|
export function needsRetroSign(entry: RetroSignableEntry): boolean {
|
|
return !!entry.authorPublicKey && entry.versions.some((v) => !v.signature);
|
|
}
|
|
|
|
/**
|
|
* Sign every unsigned version in place with the key at `keyPath`. Returns the versions it
|
|
* signed. Throws — without half-signing the entry — when the key doesn't match the entry's
|
|
* `authorPublicKey`, or when any artifact can't be fetched or no longer matches its pin.
|
|
*/
|
|
export async function retroSignVersions(
|
|
entry: RetroSignableEntry,
|
|
keyPath: string,
|
|
log: (line: string) => void = () => {},
|
|
): Promise<string[]> {
|
|
if (!needsRetroSign(entry)) return [];
|
|
|
|
const key = loadPrivateKey(keyPath);
|
|
if (publicKeyBase64(key) !== entry.authorPublicKey) {
|
|
throw new Error(
|
|
'this signing key differs from the entry\'s authorPublicKey — retro-signing older versions with it would produce signatures TREK rejects. Use the original key.',
|
|
);
|
|
}
|
|
|
|
const unsigned = entry.versions.filter((v) => !v.signature);
|
|
const signatures = new Map<string, string>();
|
|
const failures: string[] = [];
|
|
for (const v of unsigned) {
|
|
try {
|
|
const r = await fetch(v.downloadUrl, { headers: { 'User-Agent': 'trek-plugin' } });
|
|
if (!r.ok) { failures.push(`v${v.version}: HTTP ${r.status} fetching ${v.downloadUrl}`); continue; }
|
|
const bytes = Buffer.from(await r.arrayBuffer());
|
|
const hash = createHash('sha256').update(bytes).digest('hex');
|
|
if (hash !== v.sha256) {
|
|
failures.push(`v${v.version}: the release asset no longer hashes to its pinned sha256 (got ${hash.slice(0, 12)}…, pinned ${v.sha256.slice(0, 12)}…) — the artifact was changed after publishing, which is a bigger problem than signing`);
|
|
continue;
|
|
}
|
|
signatures.set(v.version, signArtifact(bytes, key));
|
|
} catch (e) {
|
|
failures.push(`v${v.version}: ${e instanceof Error ? e.message : String(e)}`);
|
|
}
|
|
}
|
|
|
|
if (failures.length) {
|
|
throw new Error(
|
|
`could not retro-sign ${failures.length} older version(s) — the registry requires every version signed once a key is present:\n` +
|
|
failures.map((f) => ` ✗ ${f}`).join('\n') +
|
|
'\n\nNothing was signed. Fix the artifacts above (or remove those versions from the entry) and re-run.',
|
|
);
|
|
}
|
|
|
|
// All-or-nothing: only mutate the entry once every artifact verified and signed.
|
|
for (const v of unsigned) {
|
|
v.signature = signatures.get(v.version)!;
|
|
log(` ✓ retro-signed v${v.version}`);
|
|
}
|
|
return unsigned.map((v) => v.version);
|
|
}
|