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:
parent
853c19fbac
commit
5b69b87394
5 changed files with 514 additions and 8 deletions
|
|
@ -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) ———
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue