Showcase: user-initiated sync coordinator wired into Status screen

runSync() composes transport -> verifier -> quarantine no-loop -> staging ->
activation as one user action (the phone tap). Trust comes from same-origin
/sync-config.json (edition + origin + pinned fingerprint->SPKI map). Prep
guidance view gains a real Download button with phase messages and outcome
notes (ok/no-update/offline/rejected/not-configured). Pipeline now writes the
per-edition pointer /editions/<ed>/latest.json the HttpTransport consumes, and
takes --min-app-version so staging packages pass shell compatibility.
6 new end-to-end tests (fake fetch + fake-indexedDB): activate, no-update,
untrusted-key rejection keeps v1 + quarantines v2, no-loop fetches ONLY
latest.json on a quarantined pointer. Full CI green: 291 tests.
This commit is contained in:
Lumen Stage1 2026-10-02 12:40:17 -05:00
commit 5b69b87394
5 changed files with 514 additions and 8 deletions

View file

@ -11,7 +11,7 @@
* Trace: IMPLEMENTATION-CONTRACT.md Stage 6, SPIKE-04 §3 gate 5, ARCH 18.7.
*/
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { join, dirname } from "node:path";
import { buildPackage } from "./package.js";
import { generateTestKeyPair, fingerprintFromPublicPem } from "./sign.js";
import { sha256Hex } from "./hash.js";
@ -98,7 +98,7 @@ const input: PipelineInput = {
schemaVersion: 1,
generatedAt: new Date().toISOString(),
festival: { name: "SolarPunk Summit 2026", timezone: FEST_TZ, startUtc, endUtc },
appCompatibility: { minAppVersion: "1.0.0", maxAppVersion: null },
appCompatibility: { minAppVersion: arg("min-app-version", "1.0.0"), maxAppVersion: null },
content: {
emergency,
schedule: { ...schedule, events } as unknown as PipelineInput["content"]["schedule"],
@ -216,6 +216,10 @@ log("sign", `signed with test key ${kp.fingerprint.slice(0, 18)}…`);
// ——— 5: upload immutable files + flip latest.json (local dir = origin layout §37) ———
const pkgDir = join(outDir, "editions", edition, "packages", String(version));
function originEditionDir(packageDirectory: string): string {
// <out>/editions/<edition>/packages/<v> → <out>/editions/<edition>
return dirname(dirname(packageDirectory));
}
mkdirSync(join(pkgDir, "assets"), { recursive: true });
for (const [, f] of pkg.files) {
writeFileSync(join(pkgDir, f.file), f.canonicalBytes);
@ -231,7 +235,10 @@ writeFileSync(join(pkgDir, "signature.json"), JSON.stringify(pkg.signature, null
writeFileSync(join(pkgDir, "publicKey.pem"), kp.publicKeyPem);
writeFileSync(join(pkgDir, "emergency-floor.json"), JSON.stringify(pkg.emergencyFloor, null, 2));
// Mutable pointer — last write, so immutable files always exist before flip.
// Root pointer per contract §37 + per-edition pointer consumed by the
// page-side HttpTransport (/editions/<ed>/latest.json).
writeFileSync(join(outDir, "latest.json"), JSON.stringify(pkg.latest, null, 2));
writeFileSync(join(originEditionDir(pkgDir), "latest.json"), JSON.stringify(pkg.latest, null, 2));
log("upload", `wrote immutable tree under ${pkgDir}/ + latest.json flip`);
// ——— 6: smoke — verify what we just uploaded (key known from this run) ———