From 2311fd66f7350e60717fa94510c48b26d5cc0a65 Mon Sep 17 00:00:00 2001 From: Kojib Date: Sun, 25 Jan 2026 03:39:22 -0500 Subject: [PATCH 01/46] create phi_og_verified_template --- public/phi_og_verified_template.png | Bin 0 -> 154970 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/phi_og_verified_template.png diff --git a/public/phi_og_verified_template.png b/public/phi_og_verified_template.png new file mode 100644 index 0000000000000000000000000000000000000000..7699e29f9612ebd6ee358b9a13e8990720e27d3f GIT binary patch literal 154970 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

4a_n%{V0tF0S!zH@QTFbI1Yhu(Ba5-hJk_ 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 Date: Sun, 25 Jan 2026 04:19:42 -0500 Subject: [PATCH 02/46] Add verified OG card rendering --- package.json | 1 + server.mjs | 330 +++++++++++++++++++++++---------- src/entry-server.tsx | 6 + src/og/buildVerifiedCardSvg.ts | 164 ++++++++++++++++ src/og/cache.ts | 51 +++++ src/og/capsuleStore.ts | 140 ++++++++++++++ src/og/downloadVerifiedCard.ts | 29 +++ src/og/renderNotFoundOg.ts | 51 +++++ src/og/renderVerifiedOg.ts | 12 ++ src/og/sigilEmbed.ts | 44 +++++ src/og/svgToPng.ts | 58 ++++++ src/og/types.ts | 9 + src/pages/VerifyPage.tsx | 30 +++ 13 files changed, 830 insertions(+), 95 deletions(-) create mode 100644 src/og/buildVerifiedCardSvg.ts create mode 100644 src/og/cache.ts create mode 100644 src/og/capsuleStore.ts create mode 100644 src/og/downloadVerifiedCard.ts create mode 100644 src/og/renderNotFoundOg.ts create mode 100644 src/og/renderVerifiedOg.ts create mode 100644 src/og/sigilEmbed.ts create mode 100644 src/og/svgToPng.ts create mode 100644 src/og/types.ts diff --git a/package.json b/package.json index fac3f39e5..2a58a5bb1 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "html2canvas": "^1.4.1", "jszip": "^3.10.1", "lucide-react": "^0.554.0", + "@resvg/resvg-js": "^2.6.0", "pako": "^2.1.0", "peerjs": "^1.5.5", "qrcode": "^1.5.4", diff --git a/server.mjs b/server.mjs index 0c18e175a..c34d5bf11 100644 --- a/server.mjs +++ b/server.mjs @@ -28,6 +28,27 @@ const mimeTypes = { ".map": "application/json", }; +const OG_PATH_PREFIX = "/og/v/verified/"; +const OG_CACHE_CONTROL = "public, max-age=0, s-maxage=31536000, immutable"; +const OG_CACHE_TTL_MS = 10 * 60 * 1000; +const OG_CACHE_MAX_ENTRIES = 512; + +const escapeHtml = (value) => + String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + +const shortPhiKey = (phiKey) => { + const trimmed = String(phiKey || "").trim(); + if (trimmed.length <= 14) return trimmed; + return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; +}; + +const stripQuotes = (value) => String(value || "").replace(/\"/g, ""); + const sendFile = (res, filePath, cacheControl) => { const ext = path.extname(filePath).toLowerCase(); res.statusCode = 200; @@ -69,126 +90,245 @@ async function createServer() { } let dataCache = null; + let ogCache = null; + let ogModulePromise = null; - const server = createHttpServer((req, res) => { - if (!req.url) { + const loadOgModule = async () => { + if (!ogModulePromise) { + ogModulePromise = (async () => { + const mod = isProd + ? await import(resolvePath("dist/server/entry-server.js")) + : await vite.ssrLoadModule("/src/entry-server.tsx"); + return { + renderVerifiedOgPng: mod.renderVerifiedOgPng, + renderNotFoundOgPng: mod.renderNotFoundOgPng, + getCapsuleByHash: mod.getCapsuleByHash, + getCapsuleByVerifierSlug: mod.getCapsuleByVerifierSlug, + OgLruTtlCache: mod.OgLruTtlCache, + }; + })(); + } + return ogModulePromise; + }; + + const buildVerifiedMeta = async (requestUrl, origin) => { + const pathname = requestUrl.pathname || "/"; + if (!pathname.startsWith("/s/") && !pathname.startsWith("/verify/")) return ""; + + const og = await loadOgModule(); + let slug; + try { + slug = decodeURIComponent(pathname.replace(/^\/(s|verify)\//, "")); + } catch { + return ""; + } + let record = null; + if (pathname.startsWith("/verify/")) { + record = og.getCapsuleByVerifierSlug(slug) ?? og.getCapsuleByHash(slug); + } else { + record = og.getCapsuleByHash(slug); + } + if (!record) return ""; + + const ogImageUrl = `${origin}/og/v/verified/${encodeURIComponent(record.capsuleHash)}.png`; + const title = `VERIFIED • Pulse ${record.pulse} • ΦKey ${shortPhiKey(record.phikey)}`; + const description = `KAS ${record.kasOk ? "✓" : "×"} • G16 ${record.g16Ok ? "✓" : "×"} • Proof of Breath™`; + + return [ + ``, + ``, + ``, + ``, + ``, + ``, + ].join(""); + }; + + const handleOgRoute = async (req, res) => { + const url = new URL(req.url, "http://localhost"); + let pathname; + try { + pathname = decodeURIComponent(url.pathname); + } catch { res.statusCode = 400; res.end("Bad Request"); - return; + return true; } - if (isProd) { - const clientRoot = resolvePath("dist/client"); - if (tryServeStatic(req, res, clientRoot)) return; + if (!pathname.startsWith(OG_PATH_PREFIX)) return false; + const suffix = pathname.slice(OG_PATH_PREFIX.length); + const capsuleHash = suffix.endsWith(".png") ? suffix.slice(0, -4) : suffix; + + if (!capsuleHash) { + res.statusCode = 400; + res.end("Bad Request"); + return true; } - const runSsr = async () => { - const url = req.url || "/"; - const origin = (() => { - const host = req.headers.host || "localhost"; - const proto = req.headers["x-forwarded-proto"] || "http"; - if (Array.isArray(proto)) return `${proto[0]}://${host}`; - return `${proto}://${host}`; - })(); - const requestUrl = new URL(url, origin); + const og = await loadOgModule(); + if (!ogCache) { + ogCache = new og.OgLruTtlCache({ maxEntries: OG_CACHE_MAX_ENTRIES, ttlMs: OG_CACHE_TTL_MS }); + } - const templatePath = isProd ? "dist/client/index.html" : "index.html"; - let template = await fs.readFile(resolvePath(templatePath), "utf-8"); - if (!isProd && vite) { - template = await vite.transformIndexHtml(url, template); + let cached = ogCache.get(capsuleHash); + if (!cached) { + const record = og.getCapsuleByHash(capsuleHash); + const pngBuffer = record ? og.renderVerifiedOgPng(record) : og.renderNotFoundOgPng(capsuleHash); + const etag = createHash("sha256").update(pngBuffer).digest("hex"); + cached = { pngBuffer, etag }; + ogCache.set(capsuleHash, cached); + } + + const ifNoneMatch = req.headers["if-none-match"]; + if (ifNoneMatch && stripQuotes(ifNoneMatch) === cached.etag) { + res.statusCode = 304; + res.setHeader("ETag", `"${cached.etag}"`); + res.setHeader("Cache-Control", OG_CACHE_CONTROL); + res.end(); + return true; + } + + res.statusCode = 200; + res.setHeader("Content-Type", "image/png"); + res.setHeader("Content-Length", cached.pngBuffer.length); + res.setHeader("ETag", `"${cached.etag}"`); + res.setHeader("Cache-Control", OG_CACHE_CONTROL); + res.end(cached.pngBuffer); + return true; + }; + + const server = createHttpServer((req, res) => { + const handleRequest = async () => { + if (!req.url) { + res.statusCode = 400; + res.end("Bad Request"); + return; } - const [head, tail] = template.split(""); - const { render, safeJsonStringify, stableJsonStringify, buildSnapshotEntries, LruTtlCache } = isProd - ? await import(resolvePath("dist/server/entry-server.js")) - : await vite.ssrLoadModule("/src/entry-server.tsx"); + if (await handleOgRoute(req, res)) return; - if (!dataCache) { - dataCache = new LruTtlCache({ maxEntries: 256 }); + if (isProd) { + const clientRoot = resolvePath("dist/client"); + if (tryServeStatic(req, res, clientRoot)) return; } - const snapshotEntries = await buildSnapshotEntries(requestUrl, dataCache); - const snapshot = { - version: "v1", - url: `${requestUrl.pathname}${requestUrl.search}`, - createdAtMs: Date.now(), - data: snapshotEntries, - }; + const runSsr = async () => { + const url = req.url || "/"; + const origin = (() => { + const host = req.headers.host || "localhost"; + const proto = req.headers["x-forwarded-proto"] || "http"; + if (Array.isArray(proto)) return `${proto[0]}://${host}`; + return `${proto}://${host}`; + })(); + const requestUrl = new URL(url, origin); - const etagSource = { ...snapshot, createdAtMs: 0 }; - const etag = createHash("sha256").update(stableJsonStringify(etagSource)).digest("hex"); - snapshot.meta = { etag }; + const templatePath = isProd ? "dist/client/index.html" : "index.html"; + let template = await fs.readFile(resolvePath(templatePath), "utf-8"); + if (!isProd && vite) { + template = await vite.transformIndexHtml(url, template); + } - const ifNoneMatch = req.headers["if-none-match"]; - const cacheControl = "public, max-age=0, s-maxage=30, stale-while-revalidate=300"; + const [head, tail] = template.split(""); + const { render, safeJsonStringify, stableJsonStringify, buildSnapshotEntries, LruTtlCache } = isProd + ? await import(resolvePath("dist/server/entry-server.js")) + : await vite.ssrLoadModule("/src/entry-server.tsx"); - if (ifNoneMatch && ifNoneMatch.replace(/\"/g, "") === etag) { - res.statusCode = 304; - res.setHeader("ETag", `"${etag}"`); - res.setHeader("Cache-Control", cacheControl); - res.end(); - return; - } + if (!dataCache) { + dataCache = new LruTtlCache({ maxEntries: 256 }); + } - const initialData = { url }; - const snapshotScript = ``; - const snapshotEtag = ``; - const ssrHead = `${snapshotScript}${snapshotEtag}`; - const htmlHead = head.replace("", ssrHead).replace("

", "
"); - - let didError = false; - const bodyStream = new PassThrough(); - bodyStream.on("end", () => { - res.write(tail); - res.end(); - }); + const snapshotEntries = await buildSnapshotEntries(requestUrl, dataCache); + const snapshot = { + version: "v1", + url: `${requestUrl.pathname}${requestUrl.search}`, + createdAtMs: Date.now(), + data: snapshotEntries, + }; - const { pipe, abort } = render(url, snapshot, { - onShellReady() { - res.statusCode = didError ? 500 : 200; - res.setHeader("Content-Type", "text/html"); - res.setHeader("Cache-Control", cacheControl); + const etagSource = { ...snapshot, createdAtMs: 0 }; + const etag = createHash("sha256").update(stableJsonStringify(etagSource)).digest("hex"); + snapshot.meta = { etag }; + + const ifNoneMatch = req.headers["if-none-match"]; + const cacheControl = "public, max-age=0, s-maxage=30, stale-while-revalidate=300"; + + if (ifNoneMatch && ifNoneMatch.replace(/\"/g, "") === etag) { + res.statusCode = 304; res.setHeader("ETag", `"${etag}"`); - res.write(htmlHead); - pipe(bodyStream); - bodyStream.pipe(res, { end: false }); - }, - onShellError(error) { - res.statusCode = 500; - res.setHeader("Content-Type", "text/html"); - res.end(template.replace("", "")); - console.error(error); - }, - onAllReady() { - // handled by stream end - }, - onError(error) { - didError = true; - console.error(error); - }, - }); + res.setHeader("Cache-Control", cacheControl); + res.end(); + return; + } - setTimeout(() => abort(), 15000); - }; + const initialData = { url }; + const snapshotScript = ``; + const snapshotEtag = ``; + const ogMeta = await buildVerifiedMeta(requestUrl, origin); + const ssrHead = `${ogMeta}${snapshotScript}${snapshotEtag}`; + const htmlHead = head.replace("", ssrHead).replace("
", "
"); - if (!isProd && vite) { - vite.middlewares(req, res, () => { - runSsr().catch((error) => { - vite.ssrFixStacktrace(error); - console.error(error); - res.statusCode = 500; - res.end("Internal Server Error"); + let didError = false; + const bodyStream = new PassThrough(); + bodyStream.on("end", () => { + res.write(tail); + res.end(); }); + + const { pipe, abort } = render(url, snapshot, { + onShellReady() { + res.statusCode = didError ? 500 : 200; + res.setHeader("Content-Type", "text/html"); + res.setHeader("Cache-Control", cacheControl); + res.setHeader("ETag", `"${etag}"`); + res.write(htmlHead); + pipe(bodyStream); + bodyStream.pipe(res, { end: false }); + }, + onShellError(error) { + res.statusCode = 500; + res.setHeader("Content-Type", "text/html"); + res.end(template.replace("", "")); + console.error(error); + }, + onAllReady() { + // handled by stream end + }, + onError(error) { + didError = true; + console.error(error); + }, + }); + + setTimeout(() => abort(), 15000); + }; + + if (!isProd && vite) { + vite.middlewares(req, res, () => { + runSsr().catch((error) => { + vite.ssrFixStacktrace(error); + console.error(error); + res.statusCode = 500; + res.end("Internal Server Error"); + }); + }); + return; + } + + runSsr().catch((error) => { + console.error(error); + res.statusCode = 500; + res.end("Internal Server Error"); }); - return; - } + }; - runSsr().catch((error) => { + handleRequest().catch((error) => { console.error(error); - res.statusCode = 500; - res.end("Internal Server Error"); + if (!res.headersSent) { + res.statusCode = 500; + res.end("Internal Server Error"); + } }); }); diff --git a/src/entry-server.tsx b/src/entry-server.tsx index 5615dc68e..27ccddd60 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -31,3 +31,9 @@ export const render = ( options ); }; + +export { safeJsonStringify, stableJsonStringify, buildSnapshotEntries, LruTtlCache } from "./ssr/serverExports"; +export { renderVerifiedOgPng } from "./og/renderVerifiedOg"; +export { renderNotFoundOgPng } from "./og/renderNotFoundOg"; +export { getCapsuleByHash, getCapsuleByVerifierSlug } from "./og/capsuleStore"; +export { LruTtlCache as OgLruTtlCache } from "./og/cache"; diff --git a/src/og/buildVerifiedCardSvg.ts b/src/og/buildVerifiedCardSvg.ts new file mode 100644 index 000000000..df5483c22 --- /dev/null +++ b/src/og/buildVerifiedCardSvg.ts @@ -0,0 +1,164 @@ +import type { VerifiedCardData } from "./types"; +import { sanitizeSigilSvg, svgToDataUri } from "./sigilEmbed"; + +const WIDTH = 1200; +const HEIGHT = 630; + +function hashStringToInt(value: string): number { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return hash; +} + +function accentFromHash(capsuleHash: string): { accent: string; accentSoft: string; accentGlow: string } { + const hash = hashStringToInt(capsuleHash); + const hue = hash % 360; + const accent = `hsl(${hue} 78% 62%)`; + const accentSoft = `hsl(${hue} 78% 52%)`; + const accentGlow = `hsla(${hue}, 90%, 70%, 0.75)`; + return { accent, accentSoft, accentGlow }; +} + +function shortPhiKey(phiKey: string): string { + const trimmed = phiKey.trim(); + if (trimmed.length <= 14) return trimmed; + return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; +} + +function badgeMark(ok: boolean): string { + if (ok) { + return "M20 34 L28 42 L44 20"; + } + return "M20 20 L44 44 M44 20 L20 44"; +} + +function headerCheckPath(): string { + return "M16 26 L26 36 L44 16"; +} + +function sigilImageMarkup(sigilSvg: string | undefined, clipId: string): string { + if (!sigilSvg) { + return ` + + Sigil unavailable + `; + } + const sanitized = sanitizeSigilSvg(sigilSvg); + const dataUri = svgToDataUri(sanitized); + return ` + + `; +} + +export function buildVerifiedCardSvg(data: VerifiedCardData): string { + const { capsuleHash, pulse, phikey, kasOk, g16Ok, sigilSvg } = data; + const { accent, accentSoft, accentGlow } = accentFromHash(capsuleHash); + const id = `og-${hashStringToInt(capsuleHash).toString(16)}`; + const sigilClipId = `${id}-sigil-clip`; + const ringGradientId = `${id}-ring`; + const glowId = `${id}-glow`; + const waveId = `${id}-wave`; + const badgeGlowId = `${id}-badge-glow`; + + const phiShort = shortPhiKey(phikey); + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Φ + + VERIFIED + + + + + + Pulse ${pulse} • ΦKey + ${phiShort} + + KAS + + + + + + G16 + + + + + + + + ${sigilImageMarkup(sigilSvg, sigilClipId)} + + + Proof of Breath™ — VERIFIED + phi.network + + `.trim(); +} diff --git a/src/og/cache.ts b/src/og/cache.ts new file mode 100644 index 000000000..076ea8df2 --- /dev/null +++ b/src/og/cache.ts @@ -0,0 +1,51 @@ +export type LruTtlCacheOptions = { + maxEntries: number; + ttlMs: number; +}; + +type CacheEntry = { + value: V; + expiresAtMs: number; +}; + +export class LruTtlCache { + private readonly maxEntries: number; + private readonly ttlMs: number; + private readonly store = new Map>(); + + constructor(options: LruTtlCacheOptions) { + this.maxEntries = Math.max(1, Math.floor(options.maxEntries)); + this.ttlMs = Math.max(0, Math.floor(options.ttlMs)); + } + + get(key: K): V | undefined { + const entry = this.store.get(key); + if (!entry) return undefined; + const now = Date.now(); + if (entry.expiresAtMs > 0 && entry.expiresAtMs <= now) { + this.store.delete(key); + return undefined; + } + this.store.delete(key); + this.store.set(key, entry); + return entry.value; + } + + set(key: K, value: V, ttlMs?: number): void { + const ttl = ttlMs == null ? this.ttlMs : Math.max(0, Math.floor(ttlMs)); + const expiresAtMs = ttl > 0 ? Date.now() + ttl : 0; + if (this.store.has(key)) { + this.store.delete(key); + } + this.store.set(key, { value, expiresAtMs }); + while (this.store.size > this.maxEntries) { + const firstKey = this.store.keys().next().value as K | undefined; + if (firstKey === undefined) break; + this.store.delete(firstKey); + } + } + + clear(): void { + this.store.clear(); + } +} diff --git a/src/og/capsuleStore.ts b/src/og/capsuleStore.ts new file mode 100644 index 000000000..bbcdc2e10 --- /dev/null +++ b/src/og/capsuleStore.ts @@ -0,0 +1,140 @@ +import fs from "node:fs"; +import path from "node:path"; +import type { VerifiedCardData } from "./types"; + +const DEFAULT_PATHS = [ + "data/verified-capsules.json", + "data/verified_capsules.json", + "public/verified-capsules.json", + "public/verified_capsules.json", +]; + +type CapsuleIndex = { + byHash: Map; + bySlug: Map; + mtimeMs: number; + storePath: string | null; +}; + +let cache: CapsuleIndex = { + byHash: new Map(), + bySlug: new Map(), + mtimeMs: 0, + storePath: null, +}; + +function resolveStorePath(): string | null { + const envPath = process.env.PHI_CAPSULE_INDEX_PATH || process.env.PHI_CAPSULE_INDEX; + const candidates = envPath ? [envPath, ...DEFAULT_PATHS] : DEFAULT_PATHS; + + for (const candidate of candidates) { + const resolved = path.resolve(process.cwd(), candidate); + if (fs.existsSync(resolved)) return resolved; + } + + return null; +} + +function parseBoolean(value: unknown): boolean | null { + if (typeof value === "boolean") return value; + if (typeof value === "number") return value !== 0; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (["1", "true", "yes", "ok", "verified"].includes(normalized)) return true; + if (["0", "false", "no", "invalid", "failed"].includes(normalized)) return false; + } + return null; +} + +function parseRecord(raw: unknown): VerifiedCardData | null { + if (!raw || typeof raw !== "object") return null; + const record = raw as Record; + const capsuleHash = typeof record.capsuleHash === "string" ? record.capsuleHash : null; + const pulseValue = record.pulse; + const pulse = typeof pulseValue === "number" && Number.isFinite(pulseValue) ? pulseValue : null; + const phiKey = typeof record.phikey === "string" + ? record.phikey + : typeof record.phiKey === "string" + ? record.phiKey + : null; + const kasOk = parseBoolean(record.kasOk); + const g16Ok = parseBoolean(record.g16Ok); + const verifierSlug = typeof record.verifierSlug === "string" ? record.verifierSlug : undefined; + const sigilSvg = typeof record.sigilSvg === "string" ? record.sigilSvg : undefined; + + if (!capsuleHash || pulse == null || !phiKey || kasOk == null || g16Ok == null) return null; + + return { + capsuleHash, + pulse, + phikey: phiKey, + kasOk, + g16Ok, + verifierSlug, + sigilSvg, + }; +} + +function loadIndex(): CapsuleIndex { + const storePath = resolveStorePath(); + if (!storePath) { + cache = { byHash: new Map(), bySlug: new Map(), mtimeMs: 0, storePath: null }; + return cache; + } + + const stats = fs.statSync(storePath); + if (cache.storePath === storePath && cache.mtimeMs === stats.mtimeMs) { + return cache; + } + + const rawText = fs.readFileSync(storePath, "utf8"); + const parsed = JSON.parse(rawText) as unknown; + const records: VerifiedCardData[] = []; + + if (Array.isArray(parsed)) { + for (const entry of parsed) { + const rec = parseRecord(entry); + if (rec) records.push(rec); + } + } else if (parsed && typeof parsed === "object") { + const parsedRecord = parseRecord(parsed); + if (parsedRecord) { + records.push(parsedRecord); + } else { + const container = parsed as Record; + if (Array.isArray(container.records)) { + for (const entry of container.records) { + const rec = parseRecord(entry); + if (rec) records.push(rec); + } + } else { + for (const entry of Object.values(container)) { + const rec = parseRecord(entry); + if (rec) records.push(rec); + } + } + } + } + + const byHash = new Map(); + const bySlug = new Map(); + for (const record of records) { + byHash.set(record.capsuleHash, record); + if (record.verifierSlug) { + bySlug.set(record.verifierSlug.toLowerCase(), record); + } + } + + cache = { byHash, bySlug, mtimeMs: stats.mtimeMs, storePath }; + return cache; +} + +export function getCapsuleByHash(capsuleHash: string): VerifiedCardData | null { + const index = loadIndex(); + return index.byHash.get(capsuleHash) ?? null; +} + +export function getCapsuleByVerifierSlug(verifierSlug: string): VerifiedCardData | null { + const index = loadIndex(); + return index.bySlug.get(verifierSlug.toLowerCase()) ?? null; +} diff --git a/src/og/downloadVerifiedCard.ts b/src/og/downloadVerifiedCard.ts new file mode 100644 index 000000000..b12cefc6f --- /dev/null +++ b/src/og/downloadVerifiedCard.ts @@ -0,0 +1,29 @@ +import { downloadBlob } from "../lib/download"; +import type { VerifiedCardData } from "./types"; +import { buildVerifiedCardSvg } from "./buildVerifiedCardSvg"; +import { svgToPngBlob } from "./svgToPng"; + +function fileNameForCapsule(hash: string): string { + const safe = hash.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 16) || "verified"; + return `verified-${safe}.png`; +} + +export async function downloadVerifiedCardPng(data: VerifiedCardData): Promise { + const filename = fileNameForCapsule(data.capsuleHash); + const ogUrl = `/og/v/verified/${encodeURIComponent(data.capsuleHash)}.png`; + + try { + const res = await fetch(ogUrl, { method: "GET" }); + if (res.ok) { + const blob = await res.blob(); + downloadBlob(blob, filename); + return; + } + } catch { + // Fall back to client render + } + + const svg = buildVerifiedCardSvg(data); + const pngBlob = await svgToPngBlob(svg, 1200, 630); + downloadBlob(pngBlob, filename); +} diff --git a/src/og/renderNotFoundOg.ts b/src/og/renderNotFoundOg.ts new file mode 100644 index 000000000..c92c3a537 --- /dev/null +++ b/src/og/renderNotFoundOg.ts @@ -0,0 +1,51 @@ +import { Resvg } from "@resvg/resvg-js"; + +const WIDTH = 1200; +const HEIGHT = 630; + +function buildNotFoundSvg(capsuleHash: string): string { + const safeHash = capsuleHash || "unknown"; + return ` + + + + + + + + + + + + + + + + + + + + + Φ + + NOT FOUND + Proof capsule unavailable + ${safeHash} + + + Proof of Breath™ — NOT FOUND + phi.network + + `.trim(); +} + +export function renderNotFoundOgPng(capsuleHash: string): Buffer { + const svg = buildNotFoundSvg(capsuleHash); + const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200 } }); + const pngData = resvg.render().asPng(); + return Buffer.from(pngData); +} diff --git a/src/og/renderVerifiedOg.ts b/src/og/renderVerifiedOg.ts new file mode 100644 index 000000000..d6a4c1ce3 --- /dev/null +++ b/src/og/renderVerifiedOg.ts @@ -0,0 +1,12 @@ +import { Resvg } from "@resvg/resvg-js"; +import type { VerifiedCardData } from "./types"; +import { buildVerifiedCardSvg } from "./buildVerifiedCardSvg"; + +export function renderVerifiedOgPng(data: VerifiedCardData): Buffer { + const svg = buildVerifiedCardSvg(data); + const resvg = new Resvg(svg, { + fitTo: { mode: "width", value: 1200 }, + }); + const pngData = resvg.render().asPng(); + return Buffer.from(pngData); +} diff --git a/src/og/sigilEmbed.ts b/src/og/sigilEmbed.ts new file mode 100644 index 000000000..b8c32226c --- /dev/null +++ b/src/og/sigilEmbed.ts @@ -0,0 +1,44 @@ +const SCRIPT_TAG = /]*>[\s\S]*?<\/script>/gi; +const FOREIGN_OBJECT_TAG = /]*>[\s\S]*?<\/foreignObject>/gi; +const EVENT_HANDLER_ATTR = /\son[a-zA-Z]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi; +const JS_PROTOCOL_ATTR = /\s(xlink:href|href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi; + +function stripProhibitedTags(svg: string): string { + return svg.replace(SCRIPT_TAG, "").replace(FOREIGN_OBJECT_TAG, ""); +} + +function stripEventHandlers(svg: string): string { + return svg.replace(EVENT_HANDLER_ATTR, ""); +} + +function sanitizeHrefValue(raw: string): string { + const value = raw.trim().replace(/^['"]|['"]$/g, ""); + const lower = value.toLowerCase(); + if (lower.startsWith("javascript:")) return ""; + if (lower.startsWith("http://") || lower.startsWith("https://")) return ""; + return value; +} + +function stripUnsafeHrefs(svg: string): string { + return svg.replace(JS_PROTOCOL_ATTR, (match, attr, value) => { + const sanitized = sanitizeHrefValue(String(value)); + if (!sanitized) return ""; + return ` ${String(attr)}=\"${sanitized}\"`; + }); +} + +export function sanitizeSigilSvg(svg: string): string { + const raw = svg ?? ""; + const withoutTags = stripProhibitedTags(raw); + const withoutHandlers = stripEventHandlers(withoutTags); + return stripUnsafeHrefs(withoutHandlers); +} + +export function svgToDataUri(svg: string): string { + const cleaned = sanitizeSigilSvg(svg); + const encoded = encodeURIComponent(cleaned) + .replace(/%0A/g, "") + .replace(/%0D/g, "") + .replace(/%09/g, ""); + return `data:image/svg+xml;utf8,${encoded}`; +} diff --git a/src/og/svgToPng.ts b/src/og/svgToPng.ts new file mode 100644 index 000000000..35eb4f152 --- /dev/null +++ b/src/og/svgToPng.ts @@ -0,0 +1,58 @@ +function loadImage(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + let settled = false; + + const finalizeResolve = () => { + if (settled) return; + settled = true; + resolve(img); + }; + + const finalizeReject = (err: Error) => { + if (settled) return; + settled = true; + reject(err); + }; + + img.onload = () => finalizeResolve(); + img.onerror = () => finalizeReject(new Error("Failed to load SVG image")); + img.decoding = "async"; + img.src = url; + + if (typeof img.decode === "function") { + img + .decode() + .then(() => finalizeResolve()) + .catch(() => { + if (!settled) finalizeResolve(); + }); + } + }); +} + +export async function svgToPngBlob(svg: string, width: number, height: number): Promise { + const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" }); + const url = URL.createObjectURL(svgBlob); + try { + const img = await loadImage(url); + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) { + throw new Error("Canvas 2D context unavailable"); + } + ctx.clearRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((result) => { + if (result) resolve(result); + else reject(new Error("Failed to encode PNG")); + }, "image/png"); + }); + return blob; + } finally { + URL.revokeObjectURL(url); + } +} diff --git a/src/og/types.ts b/src/og/types.ts new file mode 100644 index 000000000..d57b85886 --- /dev/null +++ b/src/og/types.ts @@ -0,0 +1,9 @@ +export type VerifiedCardData = { + capsuleHash: string; + pulse: number; + phikey: string; + kasOk: boolean; + g16Ok: boolean; + verifierSlug?: string; + sigilSvg?: string; +}; diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index ad6c0a9ef..5d87c3ec2 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -38,6 +38,8 @@ import { getKaiPulseEternalInt } from "../SovereignSolar"; import { useKaiTicker } from "../hooks/useKaiTicker"; import { useValuation } from "./SigilPage/useValuation"; import type { SigilMetadataLite } from "../utils/valuation"; +import { downloadVerifiedCardPng } from "../og/downloadVerifiedCard"; +import type { VerifiedCardData } from "../og/types"; import { jcsCanonicalize } from "../utils/jcs"; import { svgCanonicalForHash } from "../utils/svgProof"; import useRollingChartSeries from "../components/VerifierStamper/hooks/useRollingChartSeries"; @@ -1309,6 +1311,24 @@ if (authorSigNext) { return zkVerify ? "valid" : "invalid"; }, [busy, zkMeta?.zkPoseidonHash, zkVerify]); + const verifiedCardData = useMemo(() => { + if (result.status !== "ok" || !proofCapsule || !capsuleHash) return null; + return { + capsuleHash, + pulse: proofCapsule.pulse, + phikey: proofCapsule.phiKey, + kasOk: sealKAS === "valid", + g16Ok: sealZK === "valid", + verifierSlug: proofCapsule.verifierSlug, + sigilSvg: svgText.trim() ? svgText : undefined, + }; + }, [capsuleHash, proofCapsule, result.status, sealKAS, sealZK, svgText]); + + const onDownloadVerifiedCard = useCallback(async () => { + if (!verifiedCardData) return; + await downloadVerifiedCardPng(verifiedCardData); + }, [verifiedCardData]); + const sealStateLabel = useCallback((state: SealState): string => { switch (state) { case "valid": @@ -1599,6 +1619,16 @@ body: [
) : null} + {verifiedCardData ? ( +
+
Card
+
+ +
+
+ ) : null}
From 63583d539272ae8e753cd23d3e3925368ebbeded Mon Sep 17 00:00:00 2001 From: Kojib Date: Sun, 25 Jan 2026 04:20:34 -0500 Subject: [PATCH 03/46] update pnpm lock --- pnpm-lock.yaml | 130 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf065d2af..6f88599b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@react-three/fiber': specifier: ^9.4.0 version: 9.5.0(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(three@0.181.2) + '@resvg/resvg-js': + specifier: ^2.6.0 + version: 2.6.2 '@stripe/react-stripe-js': specifier: ^5.4.1 version: 5.4.1(@stripe/stripe-js@8.6.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -548,6 +551,82 @@ packages: react-redux: optional: true + '@resvg/resvg-js-android-arm-eabi@2.6.2': + resolution: {integrity: sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.6.2': + resolution: {integrity: sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.6.2': + resolution: {integrity: sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.6.2': + resolution: {integrity: sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + resolution: {integrity: sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + resolution: {integrity: sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + resolution: {integrity: sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + resolution: {integrity: sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + resolution: {integrity: sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + resolution: {integrity: sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + resolution: {integrity: sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + resolution: {integrity: sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.6.2': + resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} + engines: {node: '>= 10'} + '@rolldown/binding-android-arm64@1.0.0-beta.47': resolution: {integrity: sha512-vPP9/MZzESh9QtmvQYojXP/midjgkkc1E4AdnPPAzQXo668ncHJcVLKjJKzoBdsQmaIvNjrMdsCwES8vTQHRQw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2452,6 +2531,57 @@ snapshots: react: 19.2.3 react-redux: 9.2.0(@types/react@19.2.9)(react@19.2.3)(redux@5.0.1) + '@resvg/resvg-js-android-arm-eabi@2.6.2': + optional: true + + '@resvg/resvg-js-android-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-arm64@2.6.2': + optional: true + + '@resvg/resvg-js-darwin-x64@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm-gnueabihf@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-arm64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-gnu@2.6.2': + optional: true + + '@resvg/resvg-js-linux-x64-musl@2.6.2': + optional: true + + '@resvg/resvg-js-win32-arm64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-ia32-msvc@2.6.2': + optional: true + + '@resvg/resvg-js-win32-x64-msvc@2.6.2': + optional: true + + '@resvg/resvg-js@2.6.2': + optionalDependencies: + '@resvg/resvg-js-android-arm-eabi': 2.6.2 + '@resvg/resvg-js-android-arm64': 2.6.2 + '@resvg/resvg-js-darwin-arm64': 2.6.2 + '@resvg/resvg-js-darwin-x64': 2.6.2 + '@resvg/resvg-js-linux-arm-gnueabihf': 2.6.2 + '@resvg/resvg-js-linux-arm64-gnu': 2.6.2 + '@resvg/resvg-js-linux-arm64-musl': 2.6.2 + '@resvg/resvg-js-linux-x64-gnu': 2.6.2 + '@resvg/resvg-js-linux-x64-musl': 2.6.2 + '@resvg/resvg-js-win32-arm64-msvc': 2.6.2 + '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 + '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@rolldown/binding-android-arm64@1.0.0-beta.47': optional: true From bc30b469ff95b0ae8d96a51e0d4fdbb6ef939505 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 04:34:45 -0500 Subject: [PATCH 04/46] Add verifiedAtPulse to verified OG cards --- server.mjs | 47 ++++++++++++++++++++++++++++------ src/og/buildVerifiedCardSvg.ts | 4 +-- src/og/capsuleStore.ts | 13 ++++++++++ src/og/downloadVerifiedCard.ts | 2 +- src/og/types.ts | 1 + src/pages/VerifyPage.tsx | 38 ++++++++++++++++++++++++--- src/utils/verifySigil.ts | 4 ++- 7 files changed, 93 insertions(+), 16 deletions(-) diff --git a/server.mjs b/server.mjs index c34d5bf11..0f366f1ed 100644 --- a/server.mjs +++ b/server.mjs @@ -130,9 +130,11 @@ async function createServer() { } if (!record) return ""; - const ogImageUrl = `${origin}/og/v/verified/${encodeURIComponent(record.capsuleHash)}.png`; - const title = `VERIFIED • Pulse ${record.pulse} • ΦKey ${shortPhiKey(record.phikey)}`; - const description = `KAS ${record.kasOk ? "✓" : "×"} • G16 ${record.g16Ok ? "✓" : "×"} • Proof of Breath™`; + const ogImageUrl = `${origin}/og/v/verified/${encodeURIComponent(record.capsuleHash)}/${encodeURIComponent( + String(record.verifiedAtPulse), + )}.png`; + const title = `VERIFIED • Steward @ Pulse ${record.verifiedAtPulse} • ΦKey ${shortPhiKey(record.phikey)}`; + const description = "KAS ✓ • G16 ✓ • Proof of Breath™"; return [ ``, @@ -157,9 +159,10 @@ async function createServer() { if (!pathname.startsWith(OG_PATH_PREFIX)) return false; const suffix = pathname.slice(OG_PATH_PREFIX.length); - const capsuleHash = suffix.endsWith(".png") ? suffix.slice(0, -4) : suffix; + const cleaned = suffix.endsWith(".png") ? suffix.slice(0, -4) : suffix; + const [capsuleHash, verifiedAtPulseRaw, extra] = cleaned.split("/"); - if (!capsuleHash) { + if (!capsuleHash || extra) { res.statusCode = 400; res.end("Bad Request"); return true; @@ -170,13 +173,41 @@ async function createServer() { ogCache = new og.OgLruTtlCache({ maxEntries: OG_CACHE_MAX_ENTRIES, ttlMs: OG_CACHE_TTL_MS }); } - let cached = ogCache.get(capsuleHash); + const record = og.getCapsuleByHash(capsuleHash); + if (!verifiedAtPulseRaw) { + if (record) { + const redirectUrl = `${OG_PATH_PREFIX}${encodeURIComponent(record.capsuleHash)}/${encodeURIComponent( + String(record.verifiedAtPulse), + )}.png`; + res.statusCode = 302; + res.setHeader("Location", redirectUrl); + res.setHeader("Cache-Control", OG_CACHE_CONTROL); + res.end(); + return true; + } + } + + if (record && verifiedAtPulseRaw) { + const requestedPulse = Number(verifiedAtPulseRaw); + if (!Number.isFinite(requestedPulse) || requestedPulse !== record.verifiedAtPulse) { + const redirectUrl = `${OG_PATH_PREFIX}${encodeURIComponent(record.capsuleHash)}/${encodeURIComponent( + String(record.verifiedAtPulse), + )}.png`; + res.statusCode = 302; + res.setHeader("Location", redirectUrl); + res.setHeader("Cache-Control", OG_CACHE_CONTROL); + res.end(); + return true; + } + } + + const cacheKey = record ? `${capsuleHash}:${record.verifiedAtPulse}` : `notfound:${capsuleHash}`; + let cached = ogCache.get(cacheKey); if (!cached) { - const record = og.getCapsuleByHash(capsuleHash); const pngBuffer = record ? og.renderVerifiedOgPng(record) : og.renderNotFoundOgPng(capsuleHash); const etag = createHash("sha256").update(pngBuffer).digest("hex"); cached = { pngBuffer, etag }; - ogCache.set(capsuleHash, cached); + ogCache.set(cacheKey, cached); } const ifNoneMatch = req.headers["if-none-match"]; diff --git a/src/og/buildVerifiedCardSvg.ts b/src/og/buildVerifiedCardSvg.ts index df5483c22..fa3df868b 100644 --- a/src/og/buildVerifiedCardSvg.ts +++ b/src/og/buildVerifiedCardSvg.ts @@ -59,7 +59,7 @@ function sigilImageMarkup(sigilSvg: string | undefined, clipId: string): string } export function buildVerifiedCardSvg(data: VerifiedCardData): string { - const { capsuleHash, pulse, phikey, kasOk, g16Ok, sigilSvg } = data; + const { capsuleHash, verifiedAtPulse, phikey, kasOk, g16Ok, sigilSvg } = data; const { accent, accentSoft, accentGlow } = accentFromHash(capsuleHash); const id = `og-${hashStringToInt(capsuleHash).toString(16)}`; const sigilClipId = `${id}-sigil-clip`; @@ -137,7 +137,7 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { - Pulse ${pulse} • ΦKey + Steward Verified @ Pulse ${verifiedAtPulse} • ΦKey ${phiShort} KAS diff --git a/src/og/capsuleStore.ts b/src/og/capsuleStore.ts index bbcdc2e10..11e51effb 100644 --- a/src/og/capsuleStore.ts +++ b/src/og/capsuleStore.ts @@ -52,6 +52,18 @@ function parseRecord(raw: unknown): VerifiedCardData | null { const capsuleHash = typeof record.capsuleHash === "string" ? record.capsuleHash : null; const pulseValue = record.pulse; const pulse = typeof pulseValue === "number" && Number.isFinite(pulseValue) ? pulseValue : null; + const verifiedAtPulseValue = + record.verifiedAtPulse ?? + record.verified_at_pulse ?? + record.verifiedPulse ?? + record.verified_pulse ?? + record.verificationPulse; + const verifiedAtPulse = + typeof verifiedAtPulseValue === "number" && Number.isFinite(verifiedAtPulseValue) + ? verifiedAtPulseValue + : typeof verifiedAtPulseValue === "string" && verifiedAtPulseValue.trim() !== "" && Number.isFinite(Number(verifiedAtPulseValue)) + ? Number(verifiedAtPulseValue) + : null; const phiKey = typeof record.phikey === "string" ? record.phikey : typeof record.phiKey === "string" @@ -67,6 +79,7 @@ function parseRecord(raw: unknown): VerifiedCardData | null { return { capsuleHash, pulse, + verifiedAtPulse: verifiedAtPulse ?? pulse, phikey: phiKey, kasOk, g16Ok, diff --git a/src/og/downloadVerifiedCard.ts b/src/og/downloadVerifiedCard.ts index b12cefc6f..b31cd4795 100644 --- a/src/og/downloadVerifiedCard.ts +++ b/src/og/downloadVerifiedCard.ts @@ -10,7 +10,7 @@ function fileNameForCapsule(hash: string): string { export async function downloadVerifiedCardPng(data: VerifiedCardData): Promise { const filename = fileNameForCapsule(data.capsuleHash); - const ogUrl = `/og/v/verified/${encodeURIComponent(data.capsuleHash)}.png`; + const ogUrl = `/og/v/verified/${encodeURIComponent(data.capsuleHash)}/${encodeURIComponent(String(data.verifiedAtPulse))}.png`; try { const res = await fetch(ogUrl, { method: "GET" }); diff --git a/src/og/types.ts b/src/og/types.ts index d57b85886..3cff106f5 100644 --- a/src/og/types.ts +++ b/src/og/types.ts @@ -1,6 +1,7 @@ export type VerifiedCardData = { capsuleHash: string; pulse: number; + verifiedAtPulse: number; phikey: string; kasOk: boolean; g16Ok: boolean; diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 5d87c3ec2..cdd3945ee 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -173,6 +173,7 @@ type SharedReceipt = { zkProof?: ProofBundleMeta["zkProof"]; proofHints?: ProofBundleMeta["proofHints"]; zkPublicInputs?: ProofBundleMeta["zkPublicInputs"]; + verifiedAtPulse?: number; }; function parseProofCapsule(raw: unknown): ProofCapsuleV1 | null { @@ -190,6 +191,12 @@ function buildSharedReceiptFromObject(raw: unknown): SharedReceipt | null { if (!isRecord(raw)) return null; const proofCapsule = parseProofCapsule(raw.proofCapsule); if (!proofCapsule) return null; + const verifiedAtPulse = + typeof raw.verifiedAtPulse === "number" && Number.isFinite(raw.verifiedAtPulse) + ? raw.verifiedAtPulse + : typeof raw.verifiedAtPulse === "string" && Number.isFinite(Number(raw.verifiedAtPulse)) + ? Number(raw.verifiedAtPulse) + : undefined; return { proofCapsule, capsuleHash: typeof raw.capsuleHash === "string" ? raw.capsuleHash : undefined, @@ -202,6 +209,7 @@ function buildSharedReceiptFromObject(raw: unknown): SharedReceipt | null { zkProof: "zkProof" in raw ? raw.zkProof : undefined, proofHints: "proofHints" in raw ? raw.proofHints : undefined, zkPublicInputs: "zkPublicInputs" in raw ? raw.zkPublicInputs : undefined, + verifiedAtPulse, }; } @@ -909,7 +917,8 @@ export default function VerifyPage(): ReactElement { } setBusy(true); try { - const next = await verifySigilSvg(slug, raw); + const verifiedAtPulse = currentPulse ?? getKaiPulseEternalInt(new Date()); + const next = await verifySigilSvg(slug, raw, verifiedAtPulse); setResult(next); if (next.status === "ok") { setIdentityAttested("missing"); @@ -920,7 +929,7 @@ export default function VerifyPage(): ReactElement { } finally { setBusy(false); } - }, [slug, svgText]); + }, [currentPulse, slug, svgText]); // Proof bundle construction (logic unchanged) React.useEffect(() => { @@ -1045,6 +1054,7 @@ if (authorSigNext) { const slugShortSigMatches = slug.shortSig == null ? null : slug.shortSig === capsule.kaiSignature.slice(0, slug.shortSig.length); const derivedPhiKeyMatchesEmbedded = capsule.phiKey ? derivedPhiKey === capsule.phiKey : null; + const verifiedAtPulse = sharedReceipt.verifiedAtPulse ?? capsule.pulse; if (!active) return; const checks = { @@ -1081,6 +1091,7 @@ if (authorSigNext) { embedded: baseEmbedded, derivedPhiKey, checks, + verifiedAtPulse, }, ); setEmbeddedProof(embed); @@ -1316,13 +1327,14 @@ if (authorSigNext) { return { capsuleHash, pulse: proofCapsule.pulse, + verifiedAtPulse: result.verifiedAtPulse, phikey: proofCapsule.phiKey, kasOk: sealKAS === "valid", g16Ok: sealZK === "valid", verifierSlug: proofCapsule.verifierSlug, sigilSvg: svgText.trim() ? svgText : undefined, }; - }, [capsuleHash, proofCapsule, result.status, sealKAS, sealZK, svgText]); + }, [capsuleHash, proofCapsule, result.status, result.verifiedAtPulse, sealKAS, sealZK, svgText]); const onDownloadVerifiedCard = useCallback(async () => { if (!verifiedCardData) return; @@ -1461,7 +1473,12 @@ body: [ verifierUrl: proofVerifierUrl || currentVerifyUrl, } as const; + const verifiedAtPulse = result.status === "ok" ? result.verifiedAtPulse : sharedReceipt?.verifiedAtPulse; + const extended: Record = { ...receipt }; + if (typeof verifiedAtPulse === "number" && Number.isFinite(verifiedAtPulse)) { + extended.verifiedAtPulse = verifiedAtPulse; + } if (svgHash) extended.svgHash = svgHash; if (bundleHash) extended.bundleHash = bundleHash; if (embeddedProof?.shareUrl) extended.shareUrl = embeddedProof.shareUrl; @@ -1476,7 +1493,20 @@ body: [ } return jcsCanonicalize(extended as Parameters[0]); - }, [bundleHash, capsuleHash, currentVerifyUrl, embeddedProof?.shareUrl, proofCapsule, proofVerifierUrl, svgHash, zkMeta?.zkPoseidonHash, zkVerify]); + }, [ + bundleHash, + capsuleHash, + currentVerifyUrl, + embeddedProof?.shareUrl, + proofCapsule, + proofVerifierUrl, + result.status, + result.verifiedAtPulse, + sharedReceipt?.verifiedAtPulse, + svgHash, + zkMeta?.zkPoseidonHash, + zkVerify, + ]); const shareReceiptUrl = useMemo(() => { if (!receiptJson) return ""; diff --git a/src/utils/verifySigil.ts b/src/utils/verifySigil.ts index f9610b2d0..672054764 100644 --- a/src/utils/verifySigil.ts +++ b/src/utils/verifySigil.ts @@ -32,6 +32,7 @@ export type VerifyResult = embedded: EmbeddedMeta; derivedPhiKey: string; checks: VerifyChecks; + verifiedAtPulse: number; }; export function parseSlug(rawSlug: string): SlugInfo { @@ -51,7 +52,7 @@ function firstN(s: string, n: number): string { return s.slice(0, n); } -export async function verifySigilSvg(slug: SlugInfo, svgText: string): Promise { +export async function verifySigilSvg(slug: SlugInfo, svgText: string, verifiedAtPulse: number): Promise { try { const embedded = extractEmbeddedMetaFromSvg(svgText); const sig = (embedded.kaiSignature ?? "").trim(); @@ -118,6 +119,7 @@ export async function verifySigilSvg(slug: SlugInfo, svgText: string): Promise Date: Sun, 25 Jan 2026 04:48:45 -0500 Subject: [PATCH 05/46] Add verification pulse to proof bundles --- src/components/KaiVoh/verifierProof.ts | 6 ++ src/pages/VerifyPage.tsx | 118 ++++++++++++++++++++----- src/utils/sigilMetadata.ts | 36 +++++++- src/utils/verifySigil.ts | 2 +- 4 files changed, 136 insertions(+), 26 deletions(-) diff --git a/src/components/KaiVoh/verifierProof.ts b/src/components/KaiVoh/verifierProof.ts index 74a5cbf13..44ed051b3 100644 --- a/src/components/KaiVoh/verifierProof.ts +++ b/src/components/KaiVoh/verifierProof.ts @@ -21,6 +21,9 @@ import type { AuthorSig } from "../../utils/authorSig"; export const PROOF_HASH_ALG = "sha256" as const; export const PROOF_CANON = "JCS" as const; export const PROOF_METADATA_ID = "kai-voh-proof" as const; +export const VERIFICATION_BUNDLE_VERSION = "KVB-1.1" as const; + +export type VerificationSource = "local" | "pbi"; /* -------------------------------------------------------------------------- */ /* Base URL */ @@ -173,6 +176,9 @@ export type ProofBundleLike = { svgHash?: string; shareUrl?: string; verifierUrl?: string; + verifier?: VerificationSource; + verificationVersion?: string; + verifiedAtPulse?: number; zkPoseidonHash?: string; zkProof?: unknown; proofHints?: unknown; diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index cdd3945ee..9548c2f3b 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -19,6 +19,8 @@ import { normalizeChakraDay, PROOF_CANON, PROOF_HASH_ALG, + VERIFICATION_BUNDLE_VERSION, + type VerificationSource, type ProofCapsuleV1, } from "../components/KaiVoh/verifierProof"; import { extractProofBundleMetaFromSvg, type ProofBundleMeta } from "../utils/sigilMetadata"; @@ -168,6 +170,8 @@ type SharedReceipt = { bundleHash?: string; verifierUrl?: string; shareUrl?: string; + verifier?: VerificationSource; + verificationVersion?: string; authorSig?: ProofBundleMeta["authorSig"]; zkPoseidonHash?: string; zkProof?: ProofBundleMeta["zkProof"]; @@ -203,6 +207,8 @@ function buildSharedReceiptFromObject(raw: unknown): SharedReceipt | null { svgHash: typeof raw.svgHash === "string" ? raw.svgHash : undefined, bundleHash: typeof raw.bundleHash === "string" ? raw.bundleHash : undefined, verifierUrl: typeof raw.verifierUrl === "string" ? raw.verifierUrl : undefined, + verifier: raw.verifier === "local" || raw.verifier === "pbi" ? (raw.verifier as VerificationSource) : undefined, + verificationVersion: typeof raw.verificationVersion === "string" ? raw.verificationVersion : undefined, shareUrl: typeof raw.shareUrl === "string" ? raw.shareUrl : undefined, authorSig: raw.authorSig as ProofBundleMeta["authorSig"], zkPoseidonHash: typeof raw.zkPoseidonHash === "string" ? raw.zkPoseidonHash : undefined, @@ -959,6 +965,9 @@ export default function VerifyPage(): ReactElement { bundleHash: sharedReceipt.bundleHash, shareUrl: sharedReceipt.shareUrl, verifierUrl: sharedReceipt.verifierUrl, + verifier: sharedReceipt.verifier, + verificationVersion: sharedReceipt.verificationVersion, + verifiedAtPulse: sharedReceipt.verifiedAtPulse, authorSig: sharedReceipt.authorSig, zkPoseidonHash: sharedReceipt.zkPoseidonHash, zkProof: sharedReceipt.zkProof, @@ -981,10 +990,20 @@ export default function VerifyPage(): ReactElement { const embedded = extractProofBundleMetaFromSvg(svgText); const capsule = embedded?.proofCapsule ?? fallbackCapsule; const capsuleHashNext = await hashProofCapsuleV1(capsule); + const verificationSource: VerificationSource = "local"; + const verifiedAtPulse = result.verifiedAtPulse; const bundleSeed = embedded?.raw && typeof embedded.raw === "object" && embedded.raw !== null - ? { ...(embedded.raw as Record), svgHash: svgHashNext, capsuleHash: capsuleHashNext, proofCapsule: capsule } + ? { + ...(embedded.raw as Record), + svgHash: svgHashNext, + capsuleHash: capsuleHashNext, + proofCapsule: capsule, + verifier: verificationSource, + verificationVersion: VERIFICATION_BUNDLE_VERSION, + verifiedAtPulse, + } : { hashAlg: embedded?.hashAlg ?? PROOF_HASH_ALG, canon: embedded?.canon ?? PROOF_CANON, @@ -993,6 +1012,9 @@ export default function VerifyPage(): ReactElement { svgHash: svgHashNext, shareUrl: embedded?.shareUrl, verifierUrl: embedded?.verifierUrl, + verifier: verificationSource, + verificationVersion: VERIFICATION_BUNDLE_VERSION, + verifiedAtPulse, zkPoseidonHash: embedded?.zkPoseidonHash, zkProof: embedded?.zkProof, proofHints: embedded?.proofHints, @@ -1003,17 +1025,26 @@ export default function VerifyPage(): ReactElement { const bundleUnsigned = buildBundleUnsigned(bundleSeed); const bundleHashNext = await hashBundle(bundleUnsigned); -const authorSigNext = embedded?.authorSig; -let authorSigOk: boolean | null = null; - -if (authorSigNext) { - if (isKASAuthorSig(authorSigNext)) { - // ✅ Verify KAS against the artifact's recomputed unsigned bundle hash - authorSigOk = await verifyBundleAuthorSig(bundleHashNext, authorSigNext); - } else { - authorSigOk = false; - } -} + const authorSigNext = embedded?.authorSig; + let authorSigOk: boolean | null = null; + + if (authorSigNext) { + if (isKASAuthorSig(authorSigNext)) { + // ✅ Verify KAS against the artifact's recomputed unsigned bundle hash + authorSigOk = await verifyBundleAuthorSig(bundleHashNext, authorSigNext); + if (!authorSigOk) { + const legacySeed = { ...bundleSeed }; + delete (legacySeed as Record).verifiedAtPulse; + delete (legacySeed as Record).verifier; + delete (legacySeed as Record).verificationVersion; + const legacyUnsigned = buildBundleUnsigned(legacySeed); + const legacyHash = await hashBundle(legacyUnsigned); + authorSigOk = await verifyBundleAuthorSig(legacyHash, authorSigNext); + } + } else { + authorSigOk = false; + } + } if (!active) return; setProofCapsule(capsule); @@ -1041,6 +1072,9 @@ if (authorSigNext) { bundleHash: sharedReceipt.bundleHash, shareUrl: sharedReceipt.shareUrl, verifierUrl: sharedReceipt.verifierUrl, + verifier: sharedReceipt.verifier, + verificationVersion: sharedReceipt.verificationVersion, + verifiedAtPulse: sharedReceipt.verifiedAtPulse, authorSig: sharedReceipt.authorSig, zkPoseidonHash: sharedReceipt.zkPoseidonHash, zkProof: sharedReceipt.zkProof, @@ -1054,7 +1088,7 @@ if (authorSigNext) { const slugShortSigMatches = slug.shortSig == null ? null : slug.shortSig === capsule.kaiSignature.slice(0, slug.shortSig.length); const derivedPhiKeyMatchesEmbedded = capsule.phiKey ? derivedPhiKey === capsule.phiKey : null; - const verifiedAtPulse = sharedReceipt.verifiedAtPulse ?? capsule.pulse; + const verifiedAtPulse = sharedReceipt.verifiedAtPulse ?? null; if (!active) return; const checks = { @@ -1322,19 +1356,24 @@ if (authorSigNext) { return zkVerify ? "valid" : "invalid"; }, [busy, zkMeta?.zkPoseidonHash, zkVerify]); + const stewardVerifiedPulse = useMemo(() => { + if (result.status === "ok") return result.verifiedAtPulse; + return sharedReceipt?.verifiedAtPulse ?? null; + }, [result, sharedReceipt?.verifiedAtPulse]); + const verifiedCardData = useMemo(() => { - if (result.status !== "ok" || !proofCapsule || !capsuleHash) return null; + if (result.status !== "ok" || !proofCapsule || !capsuleHash || stewardVerifiedPulse == null) return null; return { capsuleHash, pulse: proofCapsule.pulse, - verifiedAtPulse: result.verifiedAtPulse, + verifiedAtPulse: stewardVerifiedPulse, phikey: proofCapsule.phiKey, kasOk: sealKAS === "valid", g16Ok: sealZK === "valid", verifierSlug: proofCapsule.verifierSlug, sigilSvg: svgText.trim() ? svgText : undefined, }; - }, [capsuleHash, proofCapsule, result.status, result.verifiedAtPulse, sealKAS, sealZK, svgText]); + }, [capsuleHash, proofCapsule, result.status, sealKAS, sealZK, stewardVerifiedPulse, svgText]); const onDownloadVerifiedCard = useCallback(async () => { if (!verifiedCardData) return; @@ -1435,6 +1474,9 @@ body: [ bundleHash, shareUrl: embeddedProof?.shareUrl ?? null, verifierUrl: proofVerifierUrl, + verifier: embeddedProof?.verifier ?? verificationSource, + verificationVersion: embeddedProof?.verificationVersion ?? verificationVersion, + verifiedAtPulse: stewardVerifiedPulse ?? null, authorSig: embeddedProof?.authorSig ?? null, zkPoseidonHash: zkMeta?.zkPoseidonHash ?? null, zkProof: zkMeta?.zkProof ?? null, @@ -1444,7 +1486,18 @@ body: [ null, 2, ); - }, [proofCapsule, capsuleHash, svgHash, bundleHash, embeddedProof, proofVerifierUrl, zkMeta]); + }, [ + proofCapsule, + capsuleHash, + svgHash, + bundleHash, + embeddedProof, + proofVerifierUrl, + stewardVerifiedPulse, + verificationSource, + verificationVersion, + zkMeta, + ]); const svgPreview = useMemo(() => { const raw = svgText.trim(); @@ -1463,6 +1516,11 @@ body: [ const shareKas = sealKAS === "valid" ? "✅" : "❌"; const shareG16 = sealZK === "valid" ? "✅" : "❌"; + const stewardPulseLabel = + stewardVerifiedPulse == null ? "Verified pulse unavailable (legacy bundle)" : `Steward Verified @ Pulse ${stewardVerifiedPulse}`; + const verificationSource: VerificationSource = sharedReceipt?.verifier ?? "local"; + const verificationVersion = sharedReceipt?.verificationVersion ?? VERIFICATION_BUNDLE_VERSION; + const receiptJson = useMemo(() => { if (!proofCapsule) return ""; const receipt = { @@ -1473,11 +1531,11 @@ body: [ verifierUrl: proofVerifierUrl || currentVerifyUrl, } as const; - const verifiedAtPulse = result.status === "ok" ? result.verifiedAtPulse : sharedReceipt?.verifiedAtPulse; - const extended: Record = { ...receipt }; - if (typeof verifiedAtPulse === "number" && Number.isFinite(verifiedAtPulse)) { - extended.verifiedAtPulse = verifiedAtPulse; + extended.verifier = verificationSource; + extended.verificationVersion = verificationVersion; + if (typeof stewardVerifiedPulse === "number" && Number.isFinite(stewardVerifiedPulse)) { + extended.verifiedAtPulse = stewardVerifiedPulse; } if (svgHash) extended.svgHash = svgHash; if (bundleHash) extended.bundleHash = bundleHash; @@ -1500,9 +1558,9 @@ body: [ embeddedProof?.shareUrl, proofCapsule, proofVerifierUrl, - result.status, - result.verifiedAtPulse, - sharedReceipt?.verifiedAtPulse, + stewardVerifiedPulse, + verificationSource, + verificationVersion, svgHash, zkMeta?.zkPoseidonHash, zkVerify, @@ -1912,6 +1970,18 @@ body: [ void remember(proofVerifierUrl, "Verifier URL")} disabled={!proofVerifierUrl} />
+
+ steward pulse + {stewardPulseLabel} + void remember(String(stewardVerifiedPulse ?? ""), "Steward verification pulse")} + disabled={stewardVerifiedPulse == null} + /> +
+
sigilHash diff --git a/src/utils/sigilMetadata.ts b/src/utils/sigilMetadata.ts index 2ba619c2d..c81211c55 100644 --- a/src/utils/sigilMetadata.ts +++ b/src/utils/sigilMetadata.ts @@ -1,6 +1,6 @@ import { XMLParser } from "fast-xml-parser"; import { gunzipB64 } from "../lib/sigil/codec"; -import type { ProofCapsuleV1 } from "../components/KaiVoh/verifierProof"; +import type { ProofCapsuleV1, VerificationSource } from "../components/KaiVoh/verifierProof"; import type { AuthorSig } from "./authorSig"; import { parseAuthorSig } from "./authorSig"; @@ -17,6 +17,9 @@ export type EmbeddedMeta = { timestamp?: string; shareUrl?: string; verifierUrl?: string; + verifier?: VerificationSource; + verificationVersion?: string; + verifiedAtPulse?: number; proofCapsule?: ProofCapsuleV1; capsuleHash?: string; svgHash?: string; @@ -130,6 +133,25 @@ function toEmbeddedMetaFromUnknown(raw: unknown): EmbeddedMeta { const verifierUrl = typeof raw.verifierUrl === "string" ? raw.verifierUrl : undefined; + const verifier = + typeof raw.verifier === "string" && (raw.verifier === "local" || raw.verifier === "pbi") + ? (raw.verifier as VerificationSource) + : undefined; + + const verificationVersion = + typeof raw.verificationVersion === "string" ? raw.verificationVersion : undefined; + + const verifiedAtPulseRaw = + raw.verifiedAtPulse ?? + decodedPayload?.verifiedAtPulse ?? + undefined; + const verifiedAtPulse = + typeof verifiedAtPulseRaw === "number" && Number.isFinite(verifiedAtPulseRaw) + ? verifiedAtPulseRaw + : typeof verifiedAtPulseRaw === "string" && Number.isFinite(Number(verifiedAtPulseRaw)) + ? Number(verifiedAtPulseRaw) + : undefined; + const proofCapsule = capsuleRaw && typeof capsuleRaw.v === "string" && @@ -186,6 +208,9 @@ function toEmbeddedMetaFromUnknown(raw: unknown): EmbeddedMeta { timestamp, shareUrl, verifierUrl, + verifier, + verificationVersion, + verifiedAtPulse, proofCapsule, capsuleHash, svgHash, @@ -458,6 +483,9 @@ export type ProofBundleMeta = { bundleHash?: string; shareUrl?: string; verifierUrl?: string; + verifier?: VerificationSource; + verificationVersion?: string; + verifiedAtPulse?: number; authorSig?: AuthorSig | null; zkPoseidonHash?: string; zkProof?: unknown; @@ -486,6 +514,9 @@ export function extractProofBundleMetaFromSvg(svgText: string): ProofBundleMeta bundleHash: meta.bundleHash, shareUrl: meta.shareUrl, verifierUrl: meta.verifierUrl, + verifier: meta.verifier, + verificationVersion: meta.verificationVersion, + verifiedAtPulse: meta.verifiedAtPulse, authorSig: meta.authorSig, zkPoseidonHash: meta.zkPoseidonHash, zkProof: meta.zkProof, @@ -516,6 +547,9 @@ export function extractProofBundleMetaFromSvg(svgText: string): ProofBundleMeta bundleHash: meta.bundleHash, shareUrl: meta.shareUrl, verifierUrl: meta.verifierUrl, + verifier: meta.verifier, + verificationVersion: meta.verificationVersion, + verifiedAtPulse: meta.verifiedAtPulse, authorSig: meta.authorSig, zkPoseidonHash: meta.zkPoseidonHash, zkProof: meta.zkProof, diff --git a/src/utils/verifySigil.ts b/src/utils/verifySigil.ts index 672054764..6c43a0fa9 100644 --- a/src/utils/verifySigil.ts +++ b/src/utils/verifySigil.ts @@ -32,7 +32,7 @@ export type VerifyResult = embedded: EmbeddedMeta; derivedPhiKey: string; checks: VerifyChecks; - verifiedAtPulse: number; + verifiedAtPulse: number | null; }; export function parseSlug(rawSlug: string): SlugInfo { From 1c4ae9e229c9c115a953a5a0e1a90e3b5441d75a Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 04:55:31 -0500 Subject: [PATCH 06/46] Fix verification metadata order and OG description --- server.mjs | 2 +- src/pages/VerifyPage.tsx | 44 ++++++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/server.mjs b/server.mjs index 0f366f1ed..f57878b9c 100644 --- a/server.mjs +++ b/server.mjs @@ -134,7 +134,7 @@ async function createServer() { String(record.verifiedAtPulse), )}.png`; const title = `VERIFIED • Steward @ Pulse ${record.verifiedAtPulse} • ΦKey ${shortPhiKey(record.phikey)}`; - const description = "KAS ✓ • G16 ✓ • Proof of Breath™"; + const description = `KAS ${record.kasOk ? "✓" : "×"} • G16 ${record.g16Ok ? "✓" : "×"} • Proof of Breath™`; return [ ``, diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 9548c2f3b..baa27d7c8 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -1462,6 +1462,28 @@ body: [ const receiveNonce = useMemo(() => (receiveSig ? receiveSig.nonce : ""), [receiveSig]); const receiveBundleHash = useMemo(() => (receiveSig?.binds.bundleHash ? receiveSig.binds.bundleHash : bundleHash || ""), [receiveSig, bundleHash]); + const svgPreview = useMemo(() => { + const raw = svgText.trim(); + if (!raw) return ""; + const lines = raw.split("\n"); + return lines.slice(0, Math.min(lines.length, 8)).join("\n"); + }, [svgText]); + + const verifierPulse = result.status === "ok" ? (result.embedded.pulse ?? (slug.pulse ?? 0)) : slug.pulse ?? 0; + const verifierSig = result.status === "ok" ? (result.embedded.kaiSignature ?? (slug.shortSig ?? "unknown")) : slug.shortSig ?? "unknown"; + const verifierPhi = result.status === "ok" ? result.derivedPhiKey : "—"; + const verifierChakra = result.status === "ok" ? result.embedded.chakraDay : undefined; + + const shareStatus = result.status === "ok" ? "VERIFIED" : result.status === "error" ? "FAILED" : "STANDBY"; + const sharePhiShort = verifierPhi && verifierPhi !== "—" ? ellipsizeMiddle(verifierPhi, 12, 10) : "—"; + const shareKas = sealKAS === "valid" ? "✅" : "❌"; + const shareG16 = sealZK === "valid" ? "✅" : "❌"; + + const stewardPulseLabel = + stewardVerifiedPulse == null ? "Verified pulse unavailable (legacy bundle)" : `Steward Verified @ Pulse ${stewardVerifiedPulse}`; + const verificationSource: VerificationSource = sharedReceipt?.verifier ?? "local"; + const verificationVersion = sharedReceipt?.verificationVersion ?? VERIFICATION_BUNDLE_VERSION; + const auditBundleText = useMemo(() => { if (!proofCapsule) return ""; return JSON.stringify( @@ -1499,28 +1521,6 @@ body: [ zkMeta, ]); - const svgPreview = useMemo(() => { - const raw = svgText.trim(); - if (!raw) return ""; - const lines = raw.split("\n"); - return lines.slice(0, Math.min(lines.length, 8)).join("\n"); - }, [svgText]); - - const verifierPulse = result.status === "ok" ? (result.embedded.pulse ?? (slug.pulse ?? 0)) : slug.pulse ?? 0; - const verifierSig = result.status === "ok" ? (result.embedded.kaiSignature ?? (slug.shortSig ?? "unknown")) : slug.shortSig ?? "unknown"; - const verifierPhi = result.status === "ok" ? result.derivedPhiKey : "—"; - const verifierChakra = result.status === "ok" ? result.embedded.chakraDay : undefined; - - const shareStatus = result.status === "ok" ? "VERIFIED" : result.status === "error" ? "FAILED" : "STANDBY"; - const sharePhiShort = verifierPhi && verifierPhi !== "—" ? ellipsizeMiddle(verifierPhi, 12, 10) : "—"; - const shareKas = sealKAS === "valid" ? "✅" : "❌"; - const shareG16 = sealZK === "valid" ? "✅" : "❌"; - - const stewardPulseLabel = - stewardVerifiedPulse == null ? "Verified pulse unavailable (legacy bundle)" : `Steward Verified @ Pulse ${stewardVerifiedPulse}`; - const verificationSource: VerificationSource = sharedReceipt?.verifier ?? "local"; - const verificationVersion = sharedReceipt?.verificationVersion ?? VERIFICATION_BUNDLE_VERSION; - const receiptJson = useMemo(() => { if (!proofCapsule) return ""; const receipt = { From 47579f81722e9ec92143bff016f76788e159c1bc Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 04:58:45 -0500 Subject: [PATCH 07/46] Fix verifiedAtPulse type in bundle seed --- src/pages/VerifyPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index baa27d7c8..84408d5c5 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -991,7 +991,10 @@ export default function VerifyPage(): ReactElement { const capsule = embedded?.proofCapsule ?? fallbackCapsule; const capsuleHashNext = await hashProofCapsuleV1(capsule); const verificationSource: VerificationSource = "local"; - const verifiedAtPulse = result.verifiedAtPulse; + const verifiedAtPulse = + typeof result.verifiedAtPulse === "number" && Number.isFinite(result.verifiedAtPulse) + ? result.verifiedAtPulse + : undefined; const bundleSeed = embedded?.raw && typeof embedded.raw === "object" && embedded.raw !== null From 21689a915f3c39972f52ea2b7292a97a2a375e71 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:03:58 -0500 Subject: [PATCH 08/46] Fix PNG download handling --- src/lib/download.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/download.ts b/src/lib/download.ts index ed9a14d95..ee8ff6d8a 100644 --- a/src/lib/download.ts +++ b/src/lib/download.ts @@ -1,9 +1,13 @@ export function downloadBlob(blob: Blob, filename: string) { - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.style.display = "none"; + document.body.appendChild(a); + a.click(); + window.setTimeout(() => { URL.revokeObjectURL(url); - } - \ No newline at end of file + a.remove(); + }, 1000); +} From 53b981275eff07639fa832cb8252d8dbf90524d1 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:08:37 -0500 Subject: [PATCH 09/46] Fix verifier PNG download and unique URLs --- src/components/KaiVoh/verifierProof.ts | 9 ++++++--- src/og/downloadVerifiedCard.ts | 3 ++- src/pages/VerifyPage.tsx | 5 ++++- src/utils/verifySigil.ts | 9 ++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/KaiVoh/verifierProof.ts b/src/components/KaiVoh/verifierProof.ts index 44ed051b3..13e7abe27 100644 --- a/src/components/KaiVoh/verifierProof.ts +++ b/src/components/KaiVoh/verifierProof.ts @@ -68,14 +68,17 @@ export function shortKaiSig10(sig: string): string { return safe.length > 10 ? safe.slice(0, 10) : safe; } -export function buildVerifierSlug(pulse: number, kaiSignature: string): string { +export function buildVerifierSlug(pulse: number, kaiSignature: string, verifiedAtPulse?: number): string { const shortSig = shortKaiSig10(kaiSignature); + if (verifiedAtPulse != null && Number.isFinite(verifiedAtPulse)) { + return `${pulse}-${shortSig}-${verifiedAtPulse}`; + } return `${pulse}-${shortSig}`; } -export function buildVerifierUrl(pulse: number, kaiSignature: string, verifierBaseUrl?: string): string { +export function buildVerifierUrl(pulse: number, kaiSignature: string, verifierBaseUrl?: string, verifiedAtPulse?: number): string { const base = (verifierBaseUrl ?? defaultHostedVerifierBaseUrl()).replace(/\/+$/, ""); - const slug = encodeURIComponent(buildVerifierSlug(pulse, kaiSignature)); + const slug = encodeURIComponent(buildVerifierSlug(pulse, kaiSignature, verifiedAtPulse)); return `${base}/${slug}`; } diff --git a/src/og/downloadVerifiedCard.ts b/src/og/downloadVerifiedCard.ts index b31cd4795..dbd32cea9 100644 --- a/src/og/downloadVerifiedCard.ts +++ b/src/og/downloadVerifiedCard.ts @@ -14,7 +14,8 @@ export async function downloadVerifiedCardPng(data: VerifiedCardData): Promise (zkMeta?.zkPublicInputs ? formatProofValue(zkMeta.zkPublicInputs) : ""), [zkMeta]); const embeddedProofHints = useMemo(() => (zkMeta?.proofHints ? formatProofValue(zkMeta.proofHints) : ""), [zkMeta]); - const proofVerifierUrl = useMemo(() => (proofCapsule ? buildVerifierUrl(proofCapsule.pulse, proofCapsule.kaiSignature) : ""), [proofCapsule]); + const proofVerifierUrl = useMemo( + () => (proofCapsule ? buildVerifierUrl(proofCapsule.pulse, proofCapsule.kaiSignature, undefined, stewardVerifiedPulse ?? undefined) : ""), + [proofCapsule, stewardVerifiedPulse], + ); const currentVerifyUrl = useMemo(() => { if (typeof window === "undefined") return ""; return window.location.href; diff --git a/src/utils/verifySigil.ts b/src/utils/verifySigil.ts index 6c43a0fa9..91b5b4e2b 100644 --- a/src/utils/verifySigil.ts +++ b/src/utils/verifySigil.ts @@ -5,6 +5,7 @@ export type SlugInfo = { raw: string; pulse: number | null; shortSig: string | null; + verifiedAtPulse: number | null; }; export type VerifyChecks = { @@ -37,14 +38,16 @@ export type VerifyResult = export function parseSlug(rawSlug: string): SlugInfo { const raw = decodeURIComponent(rawSlug || "").trim(); - const m = raw.match(/^(\d+)-([A-Za-z0-9]+)$/); - if (!m) return { raw, pulse: null, shortSig: null }; + const m = raw.match(/^(\d+)-([A-Za-z0-9]+)(?:-(\d+))?$/); + if (!m) return { raw, pulse: null, shortSig: null, verifiedAtPulse: null }; const pulseNum = Number(m[1]); const pulse = Number.isFinite(pulseNum) && pulseNum > 0 ? pulseNum : null; const shortSig = m[2] ? String(m[2]) : null; + const verifiedAtPulseNum = m[3] ? Number(m[3]) : null; + const verifiedAtPulse = Number.isFinite(verifiedAtPulseNum) && verifiedAtPulseNum > 0 ? verifiedAtPulseNum : null; - return { raw, pulse, shortSig }; + return { raw, pulse, shortSig, verifiedAtPulse }; } function firstN(s: string, n: number): string { From ff4892f88935f1cb00fd3c2a439f4df781dcf35c Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:12:45 -0500 Subject: [PATCH 10/46] Fix VerifyPage pulse initialization --- src/pages/VerifyPage.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 6373cb0c2..c3f07f374 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -855,10 +855,6 @@ export default function VerifyPage(): ReactElement { const embeddedZkPublicInputs = useMemo(() => (zkMeta?.zkPublicInputs ? formatProofValue(zkMeta.zkPublicInputs) : ""), [zkMeta]); const embeddedProofHints = useMemo(() => (zkMeta?.proofHints ? formatProofValue(zkMeta.proofHints) : ""), [zkMeta]); - const proofVerifierUrl = useMemo( - () => (proofCapsule ? buildVerifierUrl(proofCapsule.pulse, proofCapsule.kaiSignature, undefined, stewardVerifiedPulse ?? undefined) : ""), - [proofCapsule, stewardVerifiedPulse], - ); const currentVerifyUrl = useMemo(() => { if (typeof window === "undefined") return ""; return window.location.href; @@ -1367,6 +1363,11 @@ export default function VerifyPage(): ReactElement { return sharedReceipt?.verifiedAtPulse ?? null; }, [result, sharedReceipt?.verifiedAtPulse]); + const proofVerifierUrl = useMemo( + () => (proofCapsule ? buildVerifierUrl(proofCapsule.pulse, proofCapsule.kaiSignature, undefined, stewardVerifiedPulse ?? undefined) : ""), + [proofCapsule, stewardVerifiedPulse], + ); + const verifiedCardData = useMemo(() => { if (result.status !== "ok" || !proofCapsule || !capsuleHash || stewardVerifiedPulse == null) return null; return { From 7db9cb2c58cc56abb9398623ad65721e17e01971 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 05:19:44 -0500 Subject: [PATCH 11/46] Use phi.svg in verified card --- src/assets/phi.svg | 478 +++++++++++++++++++++++++++++++++ src/og/buildVerifiedCardSvg.ts | 15 +- 2 files changed, 490 insertions(+), 3 deletions(-) create mode 100644 src/assets/phi.svg diff --git a/src/assets/phi.svg b/src/assets/phi.svg new file mode 100644 index 000000000..46a0712ee --- /dev/null +++ b/src/assets/phi.svg @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/og/buildVerifiedCardSvg.ts b/src/og/buildVerifiedCardSvg.ts index fa3df868b..256a28070 100644 --- a/src/og/buildVerifiedCardSvg.ts +++ b/src/og/buildVerifiedCardSvg.ts @@ -1,8 +1,10 @@ +import phiSvg from "../assets/phi.svg?raw"; import type { VerifiedCardData } from "./types"; import { sanitizeSigilSvg, svgToDataUri } from "./sigilEmbed"; const WIDTH = 1200; const HEIGHT = 630; +const phiLogoDataUri = svgToDataUri(phiSvg); function hashStringToInt(value: string): number { let hash = 0; @@ -107,7 +109,7 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { @@ -167,6 +205,7 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { /> VERIFIED + ${valuationModeLabel ? `${valuationModeLabel}` : ""} @@ -189,6 +228,12 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { + Φ VALUE (MINTED) + ${valuationPhi} + + USD VALUE (MINTED) + ${valuationUsd} + ${sigilImageMarkup(sigilSvg, sigilClipId)} diff --git a/src/og/capsuleStore.ts b/src/og/capsuleStore.ts index 9235e1bb9..c510ea1da 100644 --- a/src/og/capsuleStore.ts +++ b/src/og/capsuleStore.ts @@ -83,6 +83,8 @@ function parseRecord(raw: unknown): VerifiedCardData | null { typeof record.verificationSig === "object" && record.verificationSig !== null ? (record.verificationSig as VerifiedCardData["verificationSig"]) : undefined; + const valuation = + typeof record.valuation === "object" && record.valuation !== null ? (record.valuation as VerifiedCardData["valuation"]) : undefined; if (!capsuleHash || pulse == null || !phiKey || kasOk == null || g16Ok == null) return null; @@ -102,6 +104,7 @@ function parseRecord(raw: unknown): VerifiedCardData | null { receiptHash, verificationSig, sigilSvg, + valuation, }; } diff --git a/src/og/types.ts b/src/og/types.ts index 64cf3674e..11b4fa697 100644 --- a/src/og/types.ts +++ b/src/og/types.ts @@ -1,4 +1,7 @@ import type { VerificationReceipt, VerificationSig } from "../utils/verificationReceipt"; +import type { ValuationSnapshot } from "../utils/valuationSnapshot"; + +export type VerifiedCardValuation = ValuationSnapshot & { valuationHash?: string }; export type VerifiedCardData = { capsuleHash: string; @@ -16,4 +19,5 @@ export type VerifiedCardData = { receiptHash?: string; verificationSig?: VerificationSig; sigilSvg?: string; + valuation?: VerifiedCardValuation; }; diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index b597efccf..bdef85e7d 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -32,6 +32,7 @@ import { ZK_STATEMENT_DOMAIN, type VerificationSource, type ProofCapsuleV1, + type ProofBundleLike, } from "../components/KaiVoh/verifierProof"; import { extractProofBundleMetaFromSvg, type ProofBundleMeta } from "../utils/sigilMetadata"; import { derivePhiKeyFromSig } from "../components/VerifierStamper/sigilUtils"; @@ -61,12 +62,14 @@ import { BREATH_MS } from "../components/valuation/constants"; import { assertReceiptHashMatch, buildVerificationReceipt, + hashValuationSnapshot, hashVerificationReceipt, verificationSigFromKas, verifyVerificationSig, type VerificationReceipt, type VerificationSig, } from "../utils/verificationReceipt"; +import { buildReceiveBundleRoot, hashReceiveBundleRoot } from "../utils/receiveBundle"; import { buildVerificationCacheKey, buildVerificationCacheRecord, @@ -74,6 +77,12 @@ import { type VerificationCache, writeVerificationCache, } from "../utils/verificationCache"; +import { + buildValuationSnapshotKey, + createValuationSnapshot, + getOrCreateValuationSnapshot, + type ValuationSnapshotState, +} from "../utils/valuationSnapshot"; /* ──────────────────────────────────────────────────────────────── Utilities @@ -678,6 +687,8 @@ export default function VerifyPage(): ReactElement { const [verificationSigVerified, setVerificationSigVerified] = useState(null); const [verificationSigBusy, setVerificationSigBusy] = useState(false); const [receiptHash, setReceiptHash] = useState(""); + const [valuationSnapshotState, setValuationSnapshotState] = useState(null); + const [valuationHash, setValuationHash] = useState(""); const [receiveSig, setReceiveSig] = useState(null); const [localReceiveBundle, setLocalReceiveBundle] = useState(null); @@ -764,6 +775,29 @@ export default function VerifyPage(): ReactElement { ? "Glyph embedded value" : "Live glyph valuation"; + const isReceiveGlyph = useMemo(() => { + const mode = localReceiveBundle?.mode ?? embeddedProof?.mode ?? sharedReceipt?.mode; + if (mode === "receive") return true; + if (localReceiveBundle?.receiveSig || embeddedProof?.receiveSig || sharedReceipt?.receiveSig) return true; + if (localReceiveBundle?.originBundleHash || embeddedProof?.originBundleHash || sharedReceipt?.originBundleHash) return true; + if (localReceiveBundle?.ownerPhiKey || embeddedProof?.ownerPhiKey || sharedReceipt?.ownerPhiKey) return true; + return false; + }, [ + embeddedProof?.mode, + embeddedProof?.originBundleHash, + embeddedProof?.ownerPhiKey, + embeddedProof?.receiveSig, + localReceiveBundle?.mode, + localReceiveBundle?.originBundleHash, + localReceiveBundle?.ownerPhiKey, + localReceiveBundle?.receiveSig, + sharedReceipt?.mode, + sharedReceipt?.originBundleHash, + sharedReceipt?.ownerPhiKey, + sharedReceipt?.receiveSig, + ]); + + // Focus Views const [openSvgEditor, setOpenSvgEditor] = useState(false); const [openAuditJson, setOpenAuditJson] = useState(false); @@ -775,6 +809,7 @@ export default function VerifyPage(): ReactElement { const [chartOpen, setChartOpen] = useState(false); const [chartFocus, setChartFocus] = useState<"phi" | "usd">("phi"); const [chartReflowKey, setChartReflowKey] = useState(0); + const chartMode = isReceiveGlyph ? "usd" : chartFocus; // Seal info popovers const [sealPopover, setSealPopover] = useState<"proof" | "kas" | "g16" | null>(null); @@ -866,10 +901,11 @@ export default function VerifyPage(): ReactElement { }, [authorSigVerified, result, slug.pulse, slug.raw, slugRaw, zkVerify]); const openChartPopover = useCallback((focus: "phi" | "usd") => { - setChartFocus(focus); + const nextFocus = isReceiveGlyph ? "usd" : focus; + setChartFocus(nextFocus); setChartOpen(true); setChartReflowKey((k) => k + 1); - }, []); + }, [isReceiveGlyph]); const closeChartPopover = useCallback(() => { setChartOpen(false); @@ -895,7 +931,7 @@ export default function VerifyPage(): ReactElement { React.useEffect(() => { if (chartOpen) setChartReflowKey((k) => k + 1); - }, [chartOpen, chartFocus]); + }, [chartMode, chartOpen, chartFocus]); React.useEffect(() => { if (!chartOpen) return; @@ -920,6 +956,14 @@ export default function VerifyPage(): ReactElement { return Number.isFinite(candidate) ? candidate : 0; }, [displayPhi, liveValuePhi]); + const chartSeriesValue = useMemo(() => { + if (!isReceiveGlyph) return chartPhi; + const phiStatic = displayPhi ?? liveValuePhi ?? 0; + if (!Number.isFinite(phiStatic)) return 0; + if (!Number.isFinite(usdPerPhi) || usdPerPhi <= 0) return 0; + return phiStatic * usdPerPhi; + }, [chartPhi, displayPhi, isReceiveGlyph, liveValuePhi, usdPerPhi]); + const seriesKey = useMemo(() => { if (result.status === "ok") { return `${result.embedded.pulse ?? slug.pulse ?? "x"}|${result.embedded.kaiSignature ?? ""}|${result.embedded.phiKey ?? ""}`; @@ -930,7 +974,7 @@ export default function VerifyPage(): ReactElement { const chartData = useRollingChartSeries({ seriesKey, sampleMs: BREATH_MS, - valuePhi: chartPhi, + valuePhi: chartSeriesValue, usdPerPhi, maxPoints: 4096, snapKey: chartReflowKey, @@ -1426,6 +1470,49 @@ React.useEffect(() => { return sharedReceipt?.verifiedAtPulse ?? null; }, [result, sharedReceipt?.verifiedAtPulse]); + const valuationSnapshotKey = useMemo(() => { + if (!bundleHash || stewardVerifiedPulse == null) return ""; + return buildValuationSnapshotKey(bundleHash, stewardVerifiedPulse); + }, [bundleHash, stewardVerifiedPulse]); + + const valuationSnapshotInput = useMemo(() => { + if (result.status !== "ok" || stewardVerifiedPulse == null) return null; + if (displayPhi == null || !Number.isFinite(displayPhi)) return null; + const usdPerPhiValue = Number.isFinite(usdPerPhi) && usdPerPhi > 0 ? usdPerPhi : null; + return { + verifiedAtPulse: stewardVerifiedPulse, + phiValue: displayPhi, + usdPerPhi: usdPerPhiValue, + source: displaySource ?? "unknown", + mode: isReceiveGlyph ? "receive" : "origin", + }; + }, [displayPhi, displaySource, isReceiveGlyph, result.status, stewardVerifiedPulse, usdPerPhi]); + + React.useEffect(() => { + setValuationSnapshotState((prev) => { + if (!valuationSnapshotKey) return null; + if (prev?.key === valuationSnapshotKey) return prev; + return getOrCreateValuationSnapshot(prev, valuationSnapshotKey, valuationSnapshotInput); + }); + }, [valuationSnapshotInput, valuationSnapshotKey]); + + const valuationSnapshot = useMemo(() => valuationSnapshotState?.snapshot ?? null, [valuationSnapshotState]); + + React.useEffect(() => { + let active = true; + if (!valuationSnapshot) { + setValuationHash(""); + return; + } + (async () => { + const hash = await hashValuationSnapshot(valuationSnapshot); + if (active) setValuationHash(hash); + })(); + return () => { + active = false; + }; + }, [valuationSnapshot]); + const verificationSource: VerificationSource = sharedReceipt?.verifier ?? "local"; const verificationVersion = sharedReceipt?.verificationVersion ?? VERIFICATION_BUNDLE_VERSION; const cacheVerificationVersion = sharedReceipt?.verificationVersion ?? embeddedProof?.verificationVersion; @@ -1610,14 +1697,16 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le ); const verificationReceipt = useMemo(() => { if (!bundleHash || !zkMeta?.zkPoseidonHash || stewardVerifiedPulse == null) return null; + const valuationPayload = valuationSnapshot && valuationHash ? { valuation: valuationSnapshot, valuationHash } : undefined; return buildVerificationReceipt({ bundleHash, zkPoseidonHash: zkMeta.zkPoseidonHash, verifiedAtPulse: stewardVerifiedPulse, verifier: verificationSource, verificationVersion, + ...(valuationPayload ?? {}), }); - }, [bundleHash, stewardVerifiedPulse, verificationSource, verificationVersion, zkMeta?.zkPoseidonHash]); + }, [bundleHash, stewardVerifiedPulse, valuationHash, valuationSnapshot, verificationSource, verificationVersion, zkMeta?.zkPoseidonHash]); const effectiveReceiveSig = useMemo(() => localReceiveBundle?.receiveSig ?? receiveSig ?? null, [localReceiveBundle?.receiveSig, receiveSig]); const effectiveReceivePulse = useMemo(() => { @@ -1631,8 +1720,8 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le if (embeddedProof?.receiveBundleHash) return embeddedProof.receiveBundleHash; if (sharedReceipt?.receiveBundleHash) return sharedReceipt.receiveBundleHash; if (effectiveReceiveSig?.binds.bundleHash) return effectiveReceiveSig.binds.bundleHash; - return bundleHash || ""; - }, [bundleHash, embeddedProof?.receiveBundleHash, effectiveReceiveSig?.binds.bundleHash, localReceiveBundle?.receiveBundleHash, sharedReceipt?.receiveBundleHash]); + return ""; + }, [embeddedProof?.receiveBundleHash, effectiveReceiveSig?.binds.bundleHash, localReceiveBundle?.receiveBundleHash, sharedReceipt?.receiveBundleHash]); const effectiveReceiveMode = useMemo(() => { if (localReceiveBundle?.mode) return localReceiveBundle.mode; if (embeddedProof?.mode) return embeddedProof.mode; @@ -1672,6 +1761,48 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le [embeddedProof?.ownerKeyDerivation, localReceiveBundle?.ownerKeyDerivation, sharedReceipt?.ownerKeyDerivation], ); + const receiveBundleRoot = useMemo(() => { + if (!proofCapsule || !capsuleHash || !svgHash) return null; + const bundleSeed: ProofBundleLike = { + hashAlg: embeddedProof?.hashAlg ?? PROOF_HASH_ALG, + canon: embeddedProof?.canon ?? PROOF_CANON, + bindings: embeddedProof?.bindings ?? PROOF_BINDINGS, + zkStatement: embeddedProof?.zkStatement, + bundleRoot: bundleRoot ?? embeddedProof?.bundleRoot, + zkMeta: embeddedProof?.zkMeta, + proofCapsule, + capsuleHash, + svgHash, + zkPoseidonHash: zkMeta?.zkPoseidonHash ?? undefined, + zkProof: zkMeta?.zkProof ?? undefined, + zkPublicInputs: zkMeta?.zkPublicInputs ?? undefined, + }; + return buildReceiveBundleRoot({ + bundleRoot: bundleRoot ?? embeddedProof?.bundleRoot ?? undefined, + bundle: bundleSeed, + originBundleHash: effectiveOriginBundleHash ?? bundleHash ?? undefined, + originAuthorSig: effectiveOriginAuthorSig ?? null, + receivePulse: effectiveReceivePulse ?? undefined, + }); + }, [ + bundleHash, + bundleRoot, + capsuleHash, + embeddedProof?.bindings, + embeddedProof?.bundleRoot, + embeddedProof?.canon, + embeddedProof?.hashAlg, + embeddedProof?.zkMeta, + embeddedProof?.zkStatement, + effectiveOriginAuthorSig, + effectiveOriginBundleHash, + effectiveReceivePulse, + proofCapsule, + svgHash, + zkMeta?.zkPoseidonHash, + zkMeta?.zkProof, + zkMeta?.zkPublicInputs, + ]); React.useEffect(() => { let active = true; @@ -1689,8 +1820,8 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le return; } - const receiveBundleHashValue = effectiveReceiveBundleHash; - if (!receiveBundleHashValue || (receiveMode && receiveBundleHashValue !== bundleHash)) { + const receiveBundleHashValue = effectiveReceiveBundleHash || effectiveReceiveSig.binds.bundleHash; + if (!receiveBundleHashValue) { setOwnerPhiKeyVerified(false); setOwnershipAttested(false); return; @@ -1730,6 +1861,12 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le return; } + if (!receiveBundleRoot) { + setOwnerPhiKeyVerified(null); + setOwnershipAttested("missing"); + return; + } + if (!effectiveOwnerPhiKey) { setOwnerPhiKeyVerified(null); setOwnershipAttested("missing"); @@ -1737,6 +1874,14 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le } (async () => { + const expectedReceiveBundleHash = await hashReceiveBundleRoot(receiveBundleRoot); + if (expectedReceiveBundleHash !== receiveBundleHashValue) { + if (active) { + setOwnerPhiKeyVerified(false); + setOwnershipAttested(false); + } + return; + } const expectedOwnerPhiKey = await deriveOwnerPhiKeyFromReceive({ receiverPubKeyJwk: effectiveReceiveSig.pubKeyJwk, receivePulse: effectiveReceivePulse, @@ -1753,12 +1898,12 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le active = false; }; }, [ - bundleHash, effectiveOwnerPhiKey, effectiveReceiveBundleHash, effectiveReceiveMode, effectiveReceivePulse, effectiveReceiveSig, + receiveBundleRoot, receiveSigVerified, ]); @@ -1879,6 +2024,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le const receiptValue = verificationReceipt ?? embeddedProof?.receipt ?? sharedReceipt?.receipt; const receiptHashValue = receiptHash || embeddedProof?.receiptHash || sharedReceipt?.receiptHash; const verificationSigValue = verificationSig ?? embeddedProof?.verificationSig ?? sharedReceipt?.verificationSig; + const valuationValue = valuationSnapshot && valuationHash ? { ...valuationSnapshot, valuationHash } : undefined; return { capsuleHash, pulse: proofCapsule.pulse, @@ -1895,6 +2041,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le receiptHash: receiptHashValue || undefined, verificationSig: verificationSigValue ?? undefined, sigilSvg: svgText.trim() ? svgText : undefined, + valuation: valuationValue, }; }, [ bundleHash, @@ -1912,6 +2059,8 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le sharedReceipt?.verificationSig, stewardVerifiedPulse, svgText, + valuationHash, + valuationSnapshot, verificationReceipt, verificationSig, verificationSource, @@ -1948,7 +2097,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le }, [proofCapsule, receiptHash, verificationSigBusy]); const onReceiveGlyph = useCallback(async () => { - if (!bundleHash) return; + if (!bundleHash || !proofCapsule || !capsuleHash || !svgHash) return; if (receiveBusy) return; if (!isWebAuthnAvailable()) { setNotice("WebAuthn is not available in this browser. Please verify on a device with passkeys enabled."); @@ -1958,8 +2107,33 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le setReceiveBusy(true); try { const receivePulse = currentPulse ?? getKaiPulseEternalInt(new Date()); + const originSigCandidate = effectiveOriginAuthorSig ?? (embeddedProof?.authorSig ?? null); + const originAuthorSig = isKASAuthorSig(originSigCandidate) ? originSigCandidate : null; + const originBundleHash = effectiveOriginBundleHash ?? bundleHash; + const receiveBundleSeed: ProofBundleLike = { + hashAlg: embeddedProof?.hashAlg ?? PROOF_HASH_ALG, + canon: embeddedProof?.canon ?? PROOF_CANON, + bindings: embeddedProof?.bindings ?? PROOF_BINDINGS, + zkStatement: embeddedProof?.zkStatement, + bundleRoot: bundleRoot ?? embeddedProof?.bundleRoot, + zkMeta: embeddedProof?.zkMeta, + proofCapsule, + capsuleHash, + svgHash, + zkPoseidonHash: zkMeta?.zkPoseidonHash ?? undefined, + zkProof: zkMeta?.zkProof ?? undefined, + zkPublicInputs: zkMeta?.zkPublicInputs ?? undefined, + }; + const receiveBundleRoot = buildReceiveBundleRoot({ + bundleRoot: bundleRoot ?? embeddedProof?.bundleRoot ?? undefined, + bundle: receiveBundleSeed, + originBundleHash, + originAuthorSig, + receivePulse, + }); + const receiveBundleHash = await hashReceiveBundleRoot(receiveBundleRoot); const passkey = await ensureReceiverPasskey(); - const { nonce, challengeBytes } = await buildKasChallenge("receive", bundleHash); + const { nonce, challengeBytes } = await buildKasChallenge("receive", receiveBundleHash); const assertion = await getWebAuthnAssertionJson({ challenge: challengeBytes, allowCredIds: [passkey.credId], @@ -1980,7 +2154,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le v: "KRS-1", alg: "webauthn-es256", nonce, - binds: { bundleHash }, + binds: { bundleHash: receiveBundleHash }, createdAtPulse: receivePulse, credId: passkey.credId, pubKeyJwk: passkey.pubKeyJwk as ReceiveSig["pubKeyJwk"], @@ -1990,24 +2164,20 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le const ownerPhiKey = await deriveOwnerPhiKeyFromReceive({ receiverPubKeyJwk: nextSig.pubKeyJwk, receivePulse, - receiveBundleHash: bundleHash, + receiveBundleHash, }); const ownerKeyDerivation = buildOwnerKeyDerivation({ originPhiKey: proofCapsule?.phiKey, receivePulse, - receiveBundleHash: bundleHash, + receiveBundleHash, }); - const originSigCandidate = effectiveOriginAuthorSig ?? (embeddedProof?.authorSig ?? null); - const originAuthorSig = isKASAuthorSig(originSigCandidate) ? originSigCandidate : null; - const originBundleHash = effectiveOriginBundleHash ?? bundleHash; - setReceiveSig(nextSig); setReceiveSigVerified(true); setLocalReceiveBundle({ mode: "receive", originBundleHash, - receiveBundleHash: bundleHash, + receiveBundleHash, originAuthorSig, receiveSig: nextSig, receivePulse, @@ -2023,12 +2193,25 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le } }, [ bundleHash, + bundleRoot, + capsuleHash, currentPulse, effectiveOriginAuthorSig, effectiveOriginBundleHash, embeddedProof?.authorSig, + embeddedProof?.bindings, + embeddedProof?.bundleRoot, + embeddedProof?.canon, + embeddedProof?.hashAlg, + embeddedProof?.zkMeta, + embeddedProof?.zkStatement, proofCapsule?.phiKey, + proofCapsule, receiveBusy, + svgHash, + zkMeta?.zkPoseidonHash, + zkMeta?.zkProof, + zkMeta?.zkPublicInputs, ]); const sealStateLabel = useCallback((state: SealState): string => { @@ -2136,13 +2319,13 @@ body: [ React.useEffect(() => { let active = true; - if (!effectiveReceiveSig || !bundleHash) { + if (!effectiveReceiveSig) { setReceiveSigVerified(null); return; } (async () => { - const receiveBundleHashValue = effectiveReceiveSig.binds.bundleHash; + const receiveBundleHashValue = effectiveReceiveBundleHash || effectiveReceiveSig.binds.bundleHash; if (!receiveBundleHashValue) { if (active) setReceiveSigVerified(false); return; @@ -2161,7 +2344,7 @@ body: [ return () => { active = false; }; - }, [effectiveReceiveSig, bundleHash]); + }, [effectiveReceiveBundleHash, effectiveReceiveSig]); React.useEffect(() => { @@ -2596,7 +2779,7 @@ React.useEffect(() => { >
e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
-
{chartFocus === "phi" ? "Φ Resonance · Live" : "$ Price · Live"}
+
{chartMode === "phi" ? "Φ Resonance · Live" : "$ Price · Live"}
@@ -2611,7 +2794,8 @@ React.useEffect(() => { momentX={1} colors={["rgba(167,255,244,1)"]} usdPerPhi={usdPerPhi} - mode={chartFocus === "usd" ? "usd" : "phi"} + mode={chartMode === "usd" ? "usd" : "phi"} + dataUnit={isReceiveGlyph ? "usd" : "phi"} reflowKey={chartReflowKey} /> diff --git a/src/utils/receiveBundle.ts b/src/utils/receiveBundle.ts new file mode 100644 index 000000000..686bff627 --- /dev/null +++ b/src/utils/receiveBundle.ts @@ -0,0 +1,54 @@ +import type { AuthorSig } from "./authorSig"; +import type { OwnerKeyDerivation } from "./ownerPhiKey"; +import { jcsCanonicalize } from "./jcs"; +import { sha256Hex } from "./sha256"; +import { buildBundleRoot, type BundleRoot, type ProofBundleLike } from "../components/KaiVoh/verifierProof"; + +type ReceiveBundleRootBase = BundleRoot & { + mode: "receive"; + originBundleHash?: string; + originAuthorSig?: AuthorSig | null; + receivePulse?: number; + ownerPhiKey?: string; + ownerKeyDerivation?: OwnerKeyDerivation; +}; + +export type ReceiveBundleRoot = Readonly; + +type ReceiveBundleRootInput = { + bundleRoot?: BundleRoot; + bundle: ProofBundleLike; + originBundleHash?: string; + originAuthorSig?: AuthorSig | null; + receivePulse?: number; + ownerPhiKey?: string; + ownerKeyDerivation?: OwnerKeyDerivation; +}; + +function dropUndefined>(value: T): T { + const entries = Object.entries(value).filter((entry) => entry[1] !== undefined); + return Object.fromEntries(entries) as T; +} + +/** + * Receive bundle root is derived from the proof bundle root plus receive bindings. + * NOTE: ownerPhiKey/ownerKeyDerivation are optional inputs; avoid including them + * when hashing receiveBundleHash to prevent circular dependencies. + */ +export function buildReceiveBundleRoot(input: ReceiveBundleRootInput): ReceiveBundleRoot { + const baseRoot = input.bundleRoot ?? buildBundleRoot(input.bundle); + const merged = dropUndefined({ + ...baseRoot, + mode: "receive" as const, + originBundleHash: input.originBundleHash, + originAuthorSig: input.originAuthorSig ?? undefined, + receivePulse: input.receivePulse, + ownerPhiKey: input.ownerPhiKey, + ownerKeyDerivation: input.ownerKeyDerivation, + }); + return merged as ReceiveBundleRoot; +} + +export async function hashReceiveBundleRoot(root: ReceiveBundleRoot): Promise { + return await sha256Hex(jcsCanonicalize(root as Parameters[0])); +} diff --git a/src/utils/valuationSnapshot.ts b/src/utils/valuationSnapshot.ts new file mode 100644 index 000000000..c78dab730 --- /dev/null +++ b/src/utils/valuationSnapshot.ts @@ -0,0 +1,70 @@ +export type ValuationSnapshot = Readonly<{ + v: "KVS-1"; + verifiedAtPulse: number; + phiValue: number; + usdValue: number | null; + usdPerPhi: number | null; + source: "balance" | "embedded" | "live" | "unknown"; + mode: "origin" | "receive"; +}>; + +export type ValuationSnapshotInput = { + verifiedAtPulse: number; + phiValue: number; + usdPerPhi: number | null; + source: ValuationSnapshot["source"]; + mode: ValuationSnapshot["mode"]; +}; + +export type ValuationSnapshotState = { + key: string; + snapshot: ValuationSnapshot; +}; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +export function isValuationSnapshot(value: unknown): value is ValuationSnapshot { + if (!value || typeof value !== "object") return false; + const record = value as Record; + if (record.v !== "KVS-1") return false; + if (!isFiniteNumber(record.verifiedAtPulse)) return false; + if (!isFiniteNumber(record.phiValue)) return false; + const usdPerPhi = record.usdPerPhi; + if (usdPerPhi !== null && !isFiniteNumber(usdPerPhi)) return false; + const usdValue = record.usdValue; + if (usdValue !== null && !isFiniteNumber(usdValue)) return false; + if (record.source !== "balance" && record.source !== "embedded" && record.source !== "live" && record.source !== "unknown") return false; + if (record.mode !== "origin" && record.mode !== "receive") return false; + return true; +} + +export function buildValuationSnapshotKey(bundleHash: string, verifiedAtPulse: number): string { + return `${bundleHash}|${verifiedAtPulse}`; +} + +export function createValuationSnapshot(input: ValuationSnapshotInput): ValuationSnapshot { + const usdPerPhi = isFiniteNumber(input.usdPerPhi) && input.usdPerPhi > 0 ? input.usdPerPhi : null; + const usdValue = usdPerPhi ? input.phiValue * usdPerPhi : null; + return { + v: "KVS-1", + verifiedAtPulse: input.verifiedAtPulse, + phiValue: input.phiValue, + usdValue, + usdPerPhi, + source: input.source, + mode: input.mode, + }; +} + +export function getOrCreateValuationSnapshot( + prev: ValuationSnapshotState | null, + key: string, + input: ValuationSnapshotInput | null, +): ValuationSnapshotState | null { + if (!key) return null; + if (prev?.key === key) return prev; + if (!input) return null; + return { key, snapshot: createValuationSnapshot(input) }; +} diff --git a/src/utils/verificationReceipt.ts b/src/utils/verificationReceipt.ts index 977d93443..f98e864ca 100644 --- a/src/utils/verificationReceipt.ts +++ b/src/utils/verificationReceipt.ts @@ -1,5 +1,6 @@ import { jcsCanonicalize } from "./jcs"; import { base64UrlDecode, base64UrlEncode, hexToBytes, sha256Bytes, sha256Hex } from "./sha256"; +import { isValuationSnapshot, type ValuationSnapshot } from "./valuationSnapshot"; export type VerificationReceipt = Readonly<{ v: "KVR-1"; @@ -8,6 +9,8 @@ export type VerificationReceipt = Readonly<{ verifiedAtPulse: number; verifier: string; verificationVersion: string; + valuationHash?: string; + valuation?: ValuationSnapshot; }>; export type VerificationSig = Readonly<{ @@ -28,15 +31,18 @@ function isRecord(value: unknown): value is Record { export function isVerificationReceipt(value: unknown): value is VerificationReceipt { if (!isRecord(value)) return false; - return ( + const hasCore = value.v === "KVR-1" && typeof value.bundleHash === "string" && typeof value.zkPoseidonHash === "string" && typeof value.verifiedAtPulse === "number" && Number.isFinite(value.verifiedAtPulse) && typeof value.verifier === "string" && - typeof value.verificationVersion === "string" - ); + typeof value.verificationVersion === "string"; + if (!hasCore) return false; + if (value.valuationHash !== undefined && typeof value.valuationHash !== "string") return false; + if (value.valuation !== undefined && !isValuationSnapshot(value.valuation)) return false; + return true; } export function isVerificationSig(value: unknown): value is VerificationSig { @@ -60,6 +66,8 @@ export function buildVerificationReceipt(params: { verifiedAtPulse: number; verifier: string; verificationVersion: string; + valuationHash?: string; + valuation?: ValuationSnapshot; }): VerificationReceipt { return { v: "KVR-1", @@ -68,6 +76,8 @@ export function buildVerificationReceipt(params: { verifiedAtPulse: params.verifiedAtPulse, verifier: params.verifier, verificationVersion: params.verificationVersion, + valuationHash: params.valuationHash, + valuation: params.valuation, }; } @@ -75,6 +85,10 @@ export async function hashVerificationReceipt(receipt: VerificationReceipt): Pro return await sha256Hex(jcsCanonicalize(receipt)); } +export async function hashValuationSnapshot(snapshot: ValuationSnapshot): Promise { + return await sha256Hex(jcsCanonicalize(snapshot)); +} + // Challenge derivation: base64url(bytes(receiptHash hex)) export function verificationReceiptChallenge(receiptHash: string): { challengeBytes: Uint8Array; challengeB64: string } { const challengeBytes = hexToBytes(receiptHash); @@ -86,6 +100,15 @@ export async function assertReceiptHashMatch(receipt: unknown, receiptHash: stri if (!isVerificationReceipt(receipt)) { throw new Error("verification receipt mismatch"); } + if (receipt.valuationHash || receipt.valuation) { + if (!receipt.valuationHash || !receipt.valuation || !isValuationSnapshot(receipt.valuation)) { + throw new Error("verification receipt mismatch"); + } + const expectedValuationHash = await hashValuationSnapshot(receipt.valuation); + if (expectedValuationHash !== receipt.valuationHash) { + throw new Error("verification receipt mismatch"); + } + } const expected = await hashVerificationReceipt(receipt); if (expected !== receiptHash) { throw new Error("verification receipt mismatch"); diff --git a/tests/receive_bundle.test.mjs b/tests/receive_bundle.test.mjs index 1c992f420..ac6eeda40 100644 --- a/tests/receive_bundle.test.mjs +++ b/tests/receive_bundle.test.mjs @@ -89,16 +89,19 @@ const kasPath = new URL("../src/utils/webauthnKAS.ts", import.meta.url); const receivePath = new URL("../src/utils/webauthnReceive.ts", import.meta.url); const ownerPath = new URL("../src/utils/ownerPhiKey.ts", import.meta.url); const shaPath = new URL("../src/utils/sha256.ts", import.meta.url); +const receiveBundlePath = new URL("../src/utils/receiveBundle.ts", import.meta.url); const kas = await import(pathToFileURL(transpileRecursive(kasPath.href)).href); const receive = await import(pathToFileURL(transpileRecursive(receivePath.href)).href); const owner = await import(pathToFileURL(transpileRecursive(ownerPath.href)).href); const sha = await import(pathToFileURL(transpileRecursive(shaPath.href)).href); +const receiveBundle = await import(pathToFileURL(transpileRecursive(receiveBundlePath.href)).href); const { verifyBundleAuthorSig } = kas; const { buildKasChallenge, verifyWebAuthnAssertion } = receive; const { deriveOwnerPhiKeyFromReceive } = owner; const { base64UrlEncode, base64UrlDecode, sha256Bytes, sha256Hex } = sha; +const { buildReceiveBundleRoot, hashReceiveBundleRoot } = receiveBundle; function makeRandomBytes(size) { const out = new Uint8Array(size); @@ -246,6 +249,51 @@ test("receive glyph provenance + ownership bindings", async () => { assert.equal(ownerPhiKey, ownerPhiKeyAgain); }); +test("receive bundle hash binds to receive signature challenge", async () => { + const proofCapsule = { + v: "KPV-1", + pulse: 11, + chakraDay: "Crown", + kaiSignature: "sig", + phiKey: "phi", + verifierSlug: "slug", + }; + const receivePulse = 98765; + const receiveRoot = buildReceiveBundleRoot({ + bundle: { + proofCapsule, + capsuleHash: "capsule-hash", + svgHash: "svg-hash", + }, + originBundleHash: "origin-hash", + originAuthorSig: null, + receivePulse, + }); + const receiveBundleHash = await hashReceiveBundleRoot(receiveRoot); + const receiveSig = await makeReceiveSig({ + bundleHash: receiveBundleHash, + nonce: "nonce-test", + receivePulse, + }); + const expectedChallenge = (await buildKasChallenge("receive", receiveBundleHash, receiveSig.nonce)).challengeBytes; + const receiveOk = await verifyWebAuthnAssertion({ + assertion: receiveSig.assertion, + expectedChallenge, + pubKeyJwk: receiveSig.pubKeyJwk, + expectedCredId: receiveSig.credId, + }); + assert.equal(receiveOk, true); + + const wrongChallenge = (await buildKasChallenge("receive", await sha256Hex("wrong-hash"), receiveSig.nonce)).challengeBytes; + const wrongOk = await verifyWebAuthnAssertion({ + assertion: receiveSig.assertion, + expectedChallenge: wrongChallenge, + pubKeyJwk: receiveSig.pubKeyJwk, + expectedCredId: receiveSig.credId, + }); + assert.equal(wrongOk, false); +}); + test("legacy compatibility: receive bundles without receiveSig mark ownership missing", async () => { const bundleHash = await sha256Hex("legacy-bundle"); const authorSig = await makeAuthorSig(bundleHash); diff --git a/tests/valuation_snapshot.test.mjs b/tests/valuation_snapshot.test.mjs new file mode 100644 index 000000000..12fad513f --- /dev/null +++ b/tests/valuation_snapshot.test.mjs @@ -0,0 +1,119 @@ +import assert from "node:assert/strict"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { basename, dirname, join, resolve } from "node:path"; +import { pathToFileURL, fileURLToPath } from "node:url"; +import { test } from "node:test"; +import ts from "typescript"; + +const tempRoot = mkdtempSync(join(process.cwd(), ".tmp-valuation-snapshot-")); +const moduleCache = new Map(); + +const IMPORT_FROM_RE = /from\s+["']([^"']+)["']/g; +const IMPORT_CALL_RE = /import\(\s*["']([^"']+)["']\s*\)/g; + +function resolveImport(spec, baseFile) { + if (!spec.startsWith(".")) return null; + const baseDir = dirname(baseFile); + const candidates = [spec, `${spec}.ts`, `${spec}.tsx`, `${spec}.js`, `${spec}.jsx`]; + for (const candidate of candidates) { + const full = resolve(baseDir, candidate); + if (existsSync(full)) return full; + } + return null; +} + +function gatherImports(source) { + const specs = new Set(); + for (const match of source.matchAll(IMPORT_FROM_RE)) { + specs.add(match[1]); + } + for (const match of source.matchAll(IMPORT_CALL_RE)) { + specs.add(match[1]); + } + return [...specs]; +} + +function rewriteImports(code, replacements) { + let out = code; + for (const [spec, replacement] of replacements) { + out = out.replaceAll(`"${spec}"`, `"${replacement}"`); + out = out.replaceAll(`'${spec}'`, `'${replacement}'`); + } + return out; +} + +function transpileRecursive(fileUrl) { + const filePath = fileURLToPath(fileUrl); + if (moduleCache.has(filePath)) return moduleCache.get(filePath); + + const source = readFileSync(filePath, "utf8"); + const imports = gatherImports(source); + const replacements = new Map(); + + for (const spec of imports) { + const resolved = resolveImport(spec, filePath); + if (!resolved) continue; + const depUrl = pathToFileURL(resolved).href; + const compiledPath = transpileRecursive(depUrl); + replacements.set(spec, pathToFileURL(compiledPath).href); + } + + const transpiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ES2022, + target: ts.ScriptTarget.ES2022, + moduleResolution: ts.ModuleResolutionKind.Bundler, + }, + }).outputText; + + const rewritten = rewriteImports(transpiled, replacements); + const tempFile = join( + tempRoot, + `${basename(filePath).replace(/\W+/g, "_")}-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs` + ); + writeFileSync(tempFile, rewritten, "utf8"); + moduleCache.set(filePath, tempFile); + return tempFile; +} + +process.on("exit", () => { + rmSync(tempRoot, { recursive: true, force: true }); +}); + +const valuationPath = new URL("../src/utils/valuationSnapshot.ts", import.meta.url); +const valuation = await import(pathToFileURL(transpileRecursive(valuationPath.href)).href); + +const { buildValuationSnapshotKey, createValuationSnapshot, getOrCreateValuationSnapshot } = valuation; + +function makeInput(usdPerPhi) { + return { + verifiedAtPulse: 100, + phiValue: 2, + usdPerPhi, + source: "live", + mode: "origin", + }; +} + +test("valuation snapshot remains stable when usdPerPhi changes for same key", () => { + const key = buildValuationSnapshotKey("bundle-1", 1000); + const first = getOrCreateValuationSnapshot(null, key, makeInput(3)); + const second = getOrCreateValuationSnapshot(first, key, makeInput(4)); + assert.equal(first.key, second.key); + assert.deepEqual(first.snapshot, second.snapshot); +}); + +test("valuation snapshot remints when key changes", () => { + const keyA = buildValuationSnapshotKey("bundle-1", 1000); + const keyB = buildValuationSnapshotKey("bundle-1", 1001); + const first = getOrCreateValuationSnapshot(null, keyA, makeInput(3)); + const second = getOrCreateValuationSnapshot(first, keyB, makeInput(3)); + assert.notEqual(first.key, second.key); + assert.notStrictEqual(first, second); +}); + +test("valuation snapshot drops usd when usdPerPhi is unavailable", () => { + const snapshot = createValuationSnapshot(makeInput(null)); + assert.equal(snapshot.usdPerPhi, null); + assert.equal(snapshot.usdValue, null); +}); diff --git a/tests/verification_receipt.test.mjs b/tests/verification_receipt.test.mjs index 2692beb7c..9b1b643ec 100644 --- a/tests/verification_receipt.test.mjs +++ b/tests/verification_receipt.test.mjs @@ -87,8 +87,18 @@ process.on("exit", () => { const receiptPath = new URL("../src/utils/verificationReceipt.ts", import.meta.url); const receipt = await import(pathToFileURL(transpileRecursive(receiptPath.href)).href); - -const { buildVerificationReceipt, hashVerificationReceipt, verificationReceiptChallenge, verifyVerificationSig } = receipt; +const valuationPath = new URL("../src/utils/valuationSnapshot.ts", import.meta.url); +const valuation = await import(pathToFileURL(transpileRecursive(valuationPath.href)).href); + +const { + assertReceiptHashMatch, + buildVerificationReceipt, + hashValuationSnapshot, + hashVerificationReceipt, + verificationReceiptChallenge, + verifyVerificationSig, +} = receipt; +const { createValuationSnapshot } = valuation; test("receipt hash changes when verifiedAtPulse changes", async () => { const receiptA = buildVerificationReceipt({ @@ -162,3 +172,64 @@ test("verification sig fails when receipt hash changes", async () => { const ok = await verifyVerificationSig(hashB, fakeSig); assert.equal(ok, false); }); + +test("receipt hash changes when valuation hash changes", async () => { + const valuationA = createValuationSnapshot({ + verifiedAtPulse: 10, + phiValue: 2, + usdPerPhi: 3, + source: "live", + mode: "origin", + }); + const valuationB = createValuationSnapshot({ + verifiedAtPulse: 10, + phiValue: 2, + usdPerPhi: 4, + source: "live", + mode: "origin", + }); + const hashA = await hashValuationSnapshot(valuationA); + const hashB = await hashValuationSnapshot(valuationB); + const receiptA = buildVerificationReceipt({ + bundleHash: "0xaaa", + zkPoseidonHash: "0xbbb", + verifiedAtPulse: 10, + verifier: "local", + verificationVersion: "KVB-1.2", + valuationHash: hashA, + valuation: valuationA, + }); + const receiptB = buildVerificationReceipt({ + bundleHash: "0xaaa", + zkPoseidonHash: "0xbbb", + verifiedAtPulse: 10, + verifier: "local", + verificationVersion: "KVB-1.2", + valuationHash: hashB, + valuation: valuationB, + }); + const receiptHashA = await hashVerificationReceipt(receiptA); + const receiptHashB = await hashVerificationReceipt(receiptB); + assert.notEqual(receiptHashA, receiptHashB); +}); + +test("receipt hash check fails on mismatched valuation hash", async () => { + const valuation = createValuationSnapshot({ + verifiedAtPulse: 10, + phiValue: 2, + usdPerPhi: 3, + source: "live", + mode: "origin", + }); + const receipt = buildVerificationReceipt({ + bundleHash: "0xaaa", + zkPoseidonHash: "0xbbb", + verifiedAtPulse: 10, + verifier: "local", + verificationVersion: "KVB-1.2", + valuationHash: "deadbeef", + valuation, + }); + const receiptHash = await hashVerificationReceipt(receipt); + await assert.rejects(async () => assertReceiptHashMatch(receipt, receiptHash), /verification receipt mismatch/); +}); From 8595b42c8463c9af651155652d3fa201ef6a00bb Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:00:47 -0500 Subject: [PATCH 33/46] Fix verification snapshot and KAS fallback --- src/pages/VerifyPage.tsx | 16 ++++++++++++---- src/utils/receiveBundle.ts | 3 ++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index bdef85e7d..1f82cd398 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -81,6 +81,7 @@ import { buildValuationSnapshotKey, createValuationSnapshot, getOrCreateValuationSnapshot, + type ValuationSnapshotInput, type ValuationSnapshotState, } from "../utils/valuationSnapshot"; @@ -1479,13 +1480,14 @@ React.useEffect(() => { if (result.status !== "ok" || stewardVerifiedPulse == null) return null; if (displayPhi == null || !Number.isFinite(displayPhi)) return null; const usdPerPhiValue = Number.isFinite(usdPerPhi) && usdPerPhi > 0 ? usdPerPhi : null; + const mode: ValuationSnapshotInput["mode"] = isReceiveGlyph ? "receive" : "origin"; return { verifiedAtPulse: stewardVerifiedPulse, phiValue: displayPhi, usdPerPhi: usdPerPhiValue, source: displaySource ?? "unknown", - mode: isReceiveGlyph ? "receive" : "origin", - }; + mode, + } satisfies ValuationSnapshotInput; }, [displayPhi, displaySource, isReceiveGlyph, result.status, stewardVerifiedPulse, usdPerPhi]); React.useEffect(() => { @@ -1920,11 +1922,17 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le (async () => { if (originSig) { - if (!effectiveOriginBundleHash || !isKASAuthorSig(originSig)) { + if (!isKASAuthorSig(originSig)) { + if (active) setAuthorSigVerified(false); + return; + } + const originBundleHash = effectiveOriginBundleHash ?? bundleHashFromAuthorSig(originSig); + const candidateHashes = Array.from(new Set([originBundleHash, bundleHash].filter(Boolean))) as string[]; + if (!candidateHashes.length) { if (active) setAuthorSigVerified(false); return; } - const ok = await verifyBundleAuthorSig(effectiveOriginBundleHash, originSig); + const ok = await verifyAuthorSigWithFallback(originSig, candidateHashes); if (active) setAuthorSigVerified(ok); return; } diff --git a/src/utils/receiveBundle.ts b/src/utils/receiveBundle.ts index 686bff627..15d80ebf3 100644 --- a/src/utils/receiveBundle.ts +++ b/src/utils/receiveBundle.ts @@ -50,5 +50,6 @@ export function buildReceiveBundleRoot(input: ReceiveBundleRootInput): ReceiveBu } export async function hashReceiveBundleRoot(root: ReceiveBundleRoot): Promise { - return await sha256Hex(jcsCanonicalize(root as Parameters[0])); + const jsonRoot = root as unknown as Parameters[0]; + return await sha256Hex(jcsCanonicalize(jsonRoot)); } From c66e34e3ef0d110178169a6a38e53acd9660e219 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:08:01 -0500 Subject: [PATCH 34/46] Tighten KAS author-sig fallback --- src/pages/VerifyPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 1f82cd398..856cc01df 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -1926,8 +1926,9 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le if (active) setAuthorSigVerified(false); return; } - const originBundleHash = effectiveOriginBundleHash ?? bundleHashFromAuthorSig(originSig); - const candidateHashes = Array.from(new Set([originBundleHash, bundleHash].filter(Boolean))) as string[]; + const derivedHash = bundleHashFromAuthorSig(originSig); + const originBundleHash = effectiveOriginBundleHash ?? (derivedHash && derivedHash === bundleHash ? derivedHash : null); + const candidateHashes = originBundleHash ? [originBundleHash] : []; if (!candidateHashes.length) { if (active) setAuthorSigVerified(false); return; From 54b08ef5126ce3f96c6303ea4f19e15c04f42e7a Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:13:50 -0500 Subject: [PATCH 35/46] Fix receive signature lookup and USD chart scaling --- .../VerifierStamper/VerifierStamper.tsx | 28 ++++++++--- src/components/valuation/chart/LiveChart.tsx | 49 +++++++++++-------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 9ea51cffc..832b597ea 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -197,6 +197,14 @@ function readReceiveSigFromBundle(raw: unknown): ReceiveSig | null { return isReceiveSig(candidate) ? candidate : null; } +function readReceiveBundleHashFromBundle(raw: unknown): string | null { + if (!isRecord(raw)) return null; + const candidate = raw.receiveBundleHash; + if (typeof candidate !== "string") return null; + const trimmed = candidate.trim(); + return trimmed ? trimmed : null; +} + const RECEIVE_LOCK_PREFIX = "kai:receive:lock:v1"; const RECEIVE_REMOTE_LIMIT = 200; const RECEIVE_REMOTE_PAGES = 3; @@ -1248,20 +1256,24 @@ const VerifierStamperInner: React.FC = () => { } if (bundleHash) { - const key = `received:${bundleHash}`; - const stored = window.localStorage.getItem(key); - if (stored) { + const receiveBundleHash = readReceiveBundleHashFromBundle(proofBundleMeta?.raw); + const keys = new Set(); + if (receiveBundleHash) keys.add(`received:${receiveBundleHash}`); + keys.add(`received:${bundleHash}`); + for (const key of keys) { + const stored = window.localStorage.getItem(key); + if (!stored) continue; try { const parsed = JSON.parse(stored) as unknown; if (!alive) return; - setReceiveSig(isReceiveSig(parsed) ? parsed : null); + if (isReceiveSig(parsed)) { + setReceiveSig(parsed); + setReceiveStatus("already"); + return; + } } catch { if (!alive) return; - setReceiveSig(null); } - if (!alive) return; - setReceiveStatus("already"); - return; } } diff --git a/src/components/valuation/chart/LiveChart.tsx b/src/components/valuation/chart/LiveChart.tsx index f96d9aa75..4b3bc5d81 100644 --- a/src/components/valuation/chart/LiveChart.tsx +++ b/src/components/valuation/chart/LiveChart.tsx @@ -149,23 +149,7 @@ export default function LiveChart({ const dataMax = hasData ? safeData[safeData.length - 1].i : 1; const lastIndex = hasData ? safeData[safeData.length - 1].i : 0; const lastParentValue = hasData ? safeData[safeData.length - 1].value : live; - - // Detect child glyph (explicit or live differs from parent last tick) - const childΦ = useMemo(() => { - if (childPhiExact != null && Number.isFinite(childPhiExact)) return childPhiExact; - const diff = Math.abs(live - lastParentValue); - return diff > 1e-9 ? live : null; - }, [childPhiExact, live, lastParentValue]); - - // Force child mode from prop if known - const isChild = isChildGlyph || childΦ != null; - - // Unit mode (Φ vs USD) - const isUsdMode = useMemo(() => { - if (mode === "usd") return true; - if (mode === "phi") return false; - return isChild; // auto - }, [mode, isChild]); + const lastPoint = hasData ? (safeData[safeData.length - 1] as FXPoint) : null; /* ─────────────────── STABLE FX LATCH ─────────────────── * Remember last known positive FX and use it whenever a new @@ -185,6 +169,31 @@ export default function LiveChart({ [usdPerPhi], ); + const lastParentPhi = useMemo(() => { + if (!hasData) return live; + if (dataUnit !== "usd") return Number(lastParentValue); + const fx = lastPoint ? fxOf(lastPoint) : usdPerPhi; + if (!finitePos(fx)) return Number(lastParentValue); + return Number(lastParentValue) / (fx as number); + }, [dataUnit, fxOf, hasData, lastParentValue, lastPoint, live, usdPerPhi]); + + // Detect child glyph (explicit or live differs from parent last tick) + const childΦ = useMemo(() => { + if (childPhiExact != null && Number.isFinite(childPhiExact)) return childPhiExact; + const diff = Math.abs(live - lastParentPhi); + return diff > 1e-9 ? live : null; + }, [childPhiExact, live, lastParentPhi]); + + // Force child mode from prop if known + const isChild = isChildGlyph || childΦ != null; + + // Unit mode (Φ vs USD) + const isUsdMode = useMemo(() => { + if (mode === "usd") return true; + if (mode === "phi") return false; + return isChild; // auto + }, [mode, isChild]); + /** USD for a point based on Φ×FX or provided USD, with stability guard */ const usdFromPoint = useCallback( (p: FXPoint, prevUsd: number | null): [number, number] => { @@ -226,12 +235,12 @@ export default function LiveChart({ // PV display in Φ, optionally scaled by child ratio const pvPhi = useMemo(() => { - if (!scalePvToChild || childΦ == null || !Number.isFinite(lastParentValue) || lastParentValue <= 0) { + if (!scalePvToChild || childΦ == null || !Number.isFinite(lastParentPhi) || lastParentPhi <= 0) { return pv; } - const r = childΦ / lastParentValue; + const r = childΦ / lastParentPhi; return pv * r; - }, [pv, scalePvToChild, childΦ, lastParentValue]); + }, [pv, scalePvToChild, childΦ, lastParentPhi]); // PV line in chart units const pvChart = useMemo(() => (isUsdMode ? pvPhi * fxOf() : pvPhi), [isUsdMode, pvPhi, fxOf]); From 6872a65936e829b82c30a374cdcfd47eebef917d Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:36:26 -0500 Subject: [PATCH 36/46] Allow chained receives and show owner PhiKey --- .../VerifierStamper/VerifierStamper.tsx | 53 ++++++++----------- src/pages/VerifyPage.tsx | 42 ++++++++++++--- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 832b597ea..2c9967534 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -1257,41 +1257,24 @@ const VerifierStamperInner: React.FC = () => { if (bundleHash) { const receiveBundleHash = readReceiveBundleHashFromBundle(proofBundleMeta?.raw); - const keys = new Set(); - if (receiveBundleHash) keys.add(`received:${receiveBundleHash}`); - keys.add(`received:${bundleHash}`); - for (const key of keys) { - const stored = window.localStorage.getItem(key); - if (!stored) continue; - try { - const parsed = JSON.parse(stored) as unknown; - if (!alive) return; - if (isReceiveSig(parsed)) { - setReceiveSig(parsed); - setReceiveStatus("already"); - return; + if (receiveBundleHash) { + const stored = window.localStorage.getItem(`received:${receiveBundleHash}`); + if (stored) { + try { + const parsed = JSON.parse(stored) as unknown; + if (!alive) return; + if (isReceiveSig(parsed)) { + setReceiveSig(parsed); + setReceiveStatus("already"); + return; + } + } catch { + if (!alive) return; } - } catch { - if (!alive) return; } } } - const embedded = readReceiveSigFromBundle(proofBundleMeta?.raw); - if (embedded) { - if (!alive) return; - setReceiveSig(embedded); - setReceiveStatus("already"); - return; - } - - if (meta && (await hasReceiveLock(meta))) { - if (!alive) return; - setReceiveSig(null); - setReceiveStatus("already"); - return; - } - if (!alive) return; setReceiveSig(null); setReceiveStatus(bundleHash ? "new" : "idle"); @@ -2507,6 +2490,16 @@ const VerifierStamperInner: React.FC = () => { }; } + const rawBundle = proofBundleMeta?.raw; + const priorReceiveSig = readReceiveSigFromBundle(rawBundle); + if (rawBundle && isRecord(rawBundle)) { + const receiveSigHistory = collectReceiveSigHistory(rawBundle, priorReceiveSig); + if (receiveSigHistory.length > 0) { + nextBundle.receiveSigHistory = receiveSigHistory; + } + } + delete nextBundle.receiveSig; + const bundleRoot = buildBundleRoot(nextBundle); const rootHash = await computeBundleHash(bundleRoot); const legacySeed = { ...nextBundle } as Record; diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 856cc01df..33e2a3c0a 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -884,9 +884,16 @@ export default function VerifyPage(): ReactElement { ogImageUrl.searchParams.set("status", statusLabel.toLowerCase()); if (result.status === "ok") { ogImageUrl.searchParams.set("pulse", String(result.embedded.pulse ?? slug.pulse ?? "")); - ogImageUrl.searchParams.set("phiKey", result.derivedPhiKey ?? ""); + const ogPhiKey = + localReceiveBundle?.ownerPhiKey ?? + embeddedProof?.ownerPhiKey ?? + sharedReceipt?.ownerPhiKey ?? + result.derivedPhiKey ?? + ""; + ogImageUrl.searchParams.set("phiKey", ogPhiKey); if (result.embedded.chakraDay) ogImageUrl.searchParams.set("chakraDay", result.embedded.chakraDay); - if (authorSigVerified != null) ogImageUrl.searchParams.set("kas", authorSigVerified ? "1" : "0"); + const kasStatus = effectiveReceiveSig ? receiveSigVerified : authorSigVerified; + if (kasStatus != null) ogImageUrl.searchParams.set("kas", kasStatus ? "1" : "0"); if (zkVerify != null) ogImageUrl.searchParams.set("g16", zkVerify ? "1" : "0"); } @@ -899,7 +906,19 @@ export default function VerifyPage(): ReactElement { ensureMetaTag("name", "twitter:title", `Proof of Breath™ — ${statusLabel}`); ensureMetaTag("name", "twitter:description", `Proof of Breath™ • ${statusLabel} • Pulse ${slug.pulse ?? "—"}`); ensureMetaTag("name", "twitter:image", ogImageUrl.toString()); - }, [authorSigVerified, result, slug.pulse, slug.raw, slugRaw, zkVerify]); + }, [ + authorSigVerified, + effectiveReceiveSig, + embeddedProof?.ownerPhiKey, + localReceiveBundle?.ownerPhiKey, + receiveSigVerified, + result, + sharedReceipt?.ownerPhiKey, + slug.pulse, + slug.raw, + slugRaw, + zkVerify, + ]); const openChartPopover = useCallback((focus: "phi" | "usd") => { const nextFocus = isReceiveGlyph ? "usd" : focus; @@ -1984,7 +2003,11 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le () => (result.status === "ok" ? String(result.embedded.pulse ?? (slug.pulse ?? 0)) : String(slug.pulse ?? 0)), [result, slug.pulse], ); - const kpiPhiKey = useMemo(() => (result.status === "ok" ? result.derivedPhiKey || "—" : "—"), [result]); + const effectivePhiKey = useMemo(() => { + if (effectiveOwnerPhiKey) return effectiveOwnerPhiKey; + return result.status === "ok" ? result.derivedPhiKey || "—" : "—"; + }, [effectiveOwnerPhiKey, result]); + const kpiPhiKey = useMemo(() => effectivePhiKey, [effectivePhiKey]); const provenanceSig = useMemo( () => effectiveOriginAuthorSig ?? embeddedProof?.authorSig ?? null, @@ -1993,10 +2016,14 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le const sealKAS: SealState = useMemo(() => { if (busy) return "busy"; + if (effectiveReceiveSig) { + if (receiveSigVerified === null) return "na"; + return receiveSigVerified ? "valid" : "invalid"; + } if (!provenanceSig) return "off"; if (authorSigVerified === null) return "na"; return authorSigVerified ? "valid" : "invalid"; - }, [authorSigVerified, busy, provenanceSig]); + }, [authorSigVerified, busy, effectiveReceiveSig, provenanceSig, receiveSigVerified]); const sealZK: SealState = useMemo(() => { if (busy) return "busy"; @@ -2038,7 +2065,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le capsuleHash, pulse: proofCapsule.pulse, verifiedAtPulse: stewardVerifiedPulse, - phikey: proofCapsule.phiKey, + phikey: effectivePhiKey !== "—" ? effectivePhiKey : proofCapsule.phiKey, kasOk: sealKAS === "valid", g16Ok: sealZK === "valid", verifierSlug: proofCapsule.verifierSlug, @@ -2061,6 +2088,7 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le proofCapsule, receiptHash, result.status, + effectivePhiKey, sealKAS, sealZK, sharedReceipt?.receipt, @@ -2310,7 +2338,7 @@ body: [ const verifierPulse = result.status === "ok" ? (result.embedded.pulse ?? (slug.pulse ?? 0)) : slug.pulse ?? 0; const verifierSig = result.status === "ok" ? (result.embedded.kaiSignature ?? (slug.shortSig ?? "unknown")) : slug.shortSig ?? "unknown"; - const verifierPhi = result.status === "ok" ? result.derivedPhiKey : "—"; + const verifierPhi = effectivePhiKey; const verifierChakra = result.status === "ok" ? result.embedded.chakraDay : undefined; const shareStatus = result.status === "ok" ? "VERIFIED" : result.status === "error" ? "FAILED" : "STANDBY"; From 90b857226d74259e786599f1d2fdc9143c9f0937 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:40:42 -0500 Subject: [PATCH 37/46] Fix verify OG meta ordering for receive sig --- src/pages/VerifyPage.tsx | 94 ++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 33e2a3c0a..9f61f2dea 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -873,53 +873,6 @@ export default function VerifyPage(): ReactElement { return () => window.clearTimeout(t); }, [notice]); - React.useEffect(() => { - if (typeof window === "undefined") return; - const statusLabel = result.status === "ok" ? "VERIFIED" : result.status === "error" ? "FAILED" : "STANDBY"; - const origin = window.location.origin; - const slugValue = slug.raw || slugRaw || ""; - const ogUrl = new URL(`${origin}/verify/${encodeURIComponent(slugValue)}`); - const ogImageUrl = new URL(`${origin}/api/og/verify`); - ogImageUrl.searchParams.set("slug", slugValue); - ogImageUrl.searchParams.set("status", statusLabel.toLowerCase()); - if (result.status === "ok") { - ogImageUrl.searchParams.set("pulse", String(result.embedded.pulse ?? slug.pulse ?? "")); - const ogPhiKey = - localReceiveBundle?.ownerPhiKey ?? - embeddedProof?.ownerPhiKey ?? - sharedReceipt?.ownerPhiKey ?? - result.derivedPhiKey ?? - ""; - ogImageUrl.searchParams.set("phiKey", ogPhiKey); - if (result.embedded.chakraDay) ogImageUrl.searchParams.set("chakraDay", result.embedded.chakraDay); - const kasStatus = effectiveReceiveSig ? receiveSigVerified : authorSigVerified; - if (kasStatus != null) ogImageUrl.searchParams.set("kas", kasStatus ? "1" : "0"); - if (zkVerify != null) ogImageUrl.searchParams.set("g16", zkVerify ? "1" : "0"); - } - - document.title = `Proof of Breath™ — ${statusLabel}`; - ensureMetaTag("property", "og:title", `Proof of Breath™ — ${statusLabel}`); - ensureMetaTag("property", "og:description", `Proof of Breath™ • ${statusLabel} • Pulse ${slug.pulse ?? "—"}`); - ensureMetaTag("property", "og:url", ogUrl.toString()); - ensureMetaTag("property", "og:image", ogImageUrl.toString()); - ensureMetaTag("name", "twitter:card", "summary_large_image"); - ensureMetaTag("name", "twitter:title", `Proof of Breath™ — ${statusLabel}`); - ensureMetaTag("name", "twitter:description", `Proof of Breath™ • ${statusLabel} • Pulse ${slug.pulse ?? "—"}`); - ensureMetaTag("name", "twitter:image", ogImageUrl.toString()); - }, [ - authorSigVerified, - effectiveReceiveSig, - embeddedProof?.ownerPhiKey, - localReceiveBundle?.ownerPhiKey, - receiveSigVerified, - result, - sharedReceipt?.ownerPhiKey, - slug.pulse, - slug.raw, - slugRaw, - zkVerify, - ]); - const openChartPopover = useCallback((focus: "phi" | "usd") => { const nextFocus = isReceiveGlyph ? "usd" : focus; setChartFocus(nextFocus); @@ -1749,6 +1702,53 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le if (sharedReceipt?.mode) return sharedReceipt.mode; return effectiveReceiveSig ? "receive" : undefined; }, [embeddedProof?.mode, effectiveReceiveSig, localReceiveBundle?.mode, sharedReceipt?.mode]); + + React.useEffect(() => { + if (typeof window === "undefined") return; + const statusLabel = result.status === "ok" ? "VERIFIED" : result.status === "error" ? "FAILED" : "STANDBY"; + const origin = window.location.origin; + const slugValue = slug.raw || slugRaw || ""; + const ogUrl = new URL(`${origin}/verify/${encodeURIComponent(slugValue)}`); + const ogImageUrl = new URL(`${origin}/api/og/verify`); + ogImageUrl.searchParams.set("slug", slugValue); + ogImageUrl.searchParams.set("status", statusLabel.toLowerCase()); + if (result.status === "ok") { + ogImageUrl.searchParams.set("pulse", String(result.embedded.pulse ?? slug.pulse ?? "")); + const ogPhiKey = + localReceiveBundle?.ownerPhiKey ?? + embeddedProof?.ownerPhiKey ?? + sharedReceipt?.ownerPhiKey ?? + result.derivedPhiKey ?? + ""; + ogImageUrl.searchParams.set("phiKey", ogPhiKey); + if (result.embedded.chakraDay) ogImageUrl.searchParams.set("chakraDay", result.embedded.chakraDay); + const kasStatus = effectiveReceiveSig ? receiveSigVerified : authorSigVerified; + if (kasStatus != null) ogImageUrl.searchParams.set("kas", kasStatus ? "1" : "0"); + if (zkVerify != null) ogImageUrl.searchParams.set("g16", zkVerify ? "1" : "0"); + } + + document.title = `Proof of Breath™ — ${statusLabel}`; + ensureMetaTag("property", "og:title", `Proof of Breath™ — ${statusLabel}`); + ensureMetaTag("property", "og:description", `Proof of Breath™ • ${statusLabel} • Pulse ${slug.pulse ?? "—"}`); + ensureMetaTag("property", "og:url", ogUrl.toString()); + ensureMetaTag("property", "og:image", ogImageUrl.toString()); + ensureMetaTag("name", "twitter:card", "summary_large_image"); + ensureMetaTag("name", "twitter:title", `Proof of Breath™ — ${statusLabel}`); + ensureMetaTag("name", "twitter:description", `Proof of Breath™ • ${statusLabel} • Pulse ${slug.pulse ?? "—"}`); + ensureMetaTag("name", "twitter:image", ogImageUrl.toString()); + }, [ + authorSigVerified, + effectiveReceiveSig, + embeddedProof?.ownerPhiKey, + localReceiveBundle?.ownerPhiKey, + receiveSigVerified, + result, + sharedReceipt?.ownerPhiKey, + slug.pulse, + slug.raw, + slugRaw, + zkVerify, + ]); const effectiveOriginBundleHash = useMemo( () => localReceiveBundle?.originBundleHash ?? From a5793b50e66fa35bd9ed908018b1939a7e7eb0b4 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:51:12 -0500 Subject: [PATCH 38/46] Allow repeat receive claims per upload --- .../VerifierStamper/VerifierStamper.tsx | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 2c9967534..dae89937a 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -1255,26 +1255,6 @@ const VerifierStamperInner: React.FC = () => { autoReceiveRef.current = null; } - if (bundleHash) { - const receiveBundleHash = readReceiveBundleHashFromBundle(proofBundleMeta?.raw); - if (receiveBundleHash) { - const stored = window.localStorage.getItem(`received:${receiveBundleHash}`); - if (stored) { - try { - const parsed = JSON.parse(stored) as unknown; - if (!alive) return; - if (isReceiveSig(parsed)) { - setReceiveSig(parsed); - setReceiveStatus("already"); - return; - } - } catch { - if (!alive) return; - } - } - } - } - if (!alive) return; setReceiveSig(null); setReceiveStatus(bundleHash ? "new" : "idle"); @@ -1282,7 +1262,7 @@ const VerifierStamperInner: React.FC = () => { return () => { alive = false; }; - }, [bundleHash, proofBundleMeta?.raw, meta, hasReceiveLock]); + }, [bundleHash]); useEffect(() => { if (!bundleHash || unlockState.isUnlocked || !unlockState.isRequired) return; From c94496f13e047dceb4a9daf3ad32f2db509df2bf Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:01:46 -0500 Subject: [PATCH 39/46] Use receive bundle root and lock helper --- src/components/VerifierStamper/VerifierStamper.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index dae89937a..91edfbe4f 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -941,9 +941,10 @@ const VerifierStamperInner: React.FC = () => { ); const hasReceiveLock = useCallback( - async (m: SigilMetadata): Promise => { + async (m: SigilMetadata, options?: { includeRemote?: boolean }): Promise => { if (await hasLocalReceiveLock(m)) return true; if (await hasRegistryReceiveLock(m)) return true; + if (options?.includeRemote === false) return false; return hasRemoteReceiveLock(m); }, [hasLocalReceiveLock, hasRegistryReceiveLock, hasRemoteReceiveLock] @@ -2256,7 +2257,7 @@ const VerifierStamperInner: React.FC = () => { return; } - if ((await hasLocalReceiveLock(meta)) || (await hasRegistryReceiveLock(meta))) { + if (await hasReceiveLock(meta, { includeRemote: false })) { setError("This transfer has already been received."); setReceiveStatus("already"); return; @@ -2526,6 +2527,7 @@ const VerifierStamperInner: React.FC = () => { } nextBundle.bundleHash = bundleHashNext; nextBundle.receiveBundleHash = receiveBundleHash; + nextBundle.receiveBundleRoot = receiveBundleRoot; if (receiveSigLocal) nextBundle.receiveSig = receiveSigLocal; if (receiveSigLocal) { From eea658d8329dc0507abbaa7183862b46607615b3 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:05:29 -0500 Subject: [PATCH 40/46] Use receive bundle hash from embedded proof --- src/components/VerifierStamper/VerifierStamper.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 91edfbe4f..0b77d051f 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -1193,6 +1193,10 @@ const VerifierStamperInner: React.FC = () => { if (receiveFromBundle) { metaNext = { ...metaNext, receiveSig: receiveFromBundle }; } + const receiveBundleHashFromBundle = readReceiveBundleHashFromBundle(proofMetaNext?.raw); + if (receiveBundleHashFromBundle && !metaNext.receiveBundleHash) { + metaNext = { ...metaNext, receiveBundleHash: receiveBundleHashFromBundle }; + } if (proofMetaNext?.raw && isRecord(proofMetaNext.raw)) { metaNext = { ...metaNext, proofBundleRaw: proofMetaNext.raw }; } From a66c6243f941ae424dec14f030ca6c3adab07f0e Mon Sep 17 00:00:00 2001 From: Kojib Date: Sun, 25 Jan 2026 19:17:52 -0500 Subject: [PATCH 41/46] update verifypage --- src/pages/VerifyPage.tsx | 1 - tsconfig.tsbuildinfo | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index 9f61f2dea..e413a028d 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -79,7 +79,6 @@ import { } from "../utils/verificationCache"; import { buildValuationSnapshotKey, - createValuationSnapshot, getOrCreateValuationSnapshot, type ValuationSnapshotInput, type ValuationSnapshotState, diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index 939658a59..4db48f281 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/sovereignsolar.ts","./src/entry-client.tsx","./src/entry-server-exports.ts","./src/entry-server.tsx","./src/main.tsx","./src/types.ts","./src/version.ts","./src/components/daydetailmodal.tsx","./src/components/errorboundary.tsx","./src/components/eternalklock.tsx","./src/components/exhalenote.tsx","./src/components/feedcard.tsx","./src/components/glyphimportmodal.tsx","./src/components/homepricechartcard.tsx","./src/components/homepricetickerfallback.tsx","./src/components/inhaleuploadicon.tsx","./src/components/kaiklock.canon.ts","./src/components/kaiklock.tsx","./src/components/kaiklockhomeface.tsx","./src/components/kaipricechart.tsx","./src/components/kaisigil.tsx","./src/components/kaisplashscreen.tsx","./src/components/largeglyphminter.tsx","./src/components/largeglyphviewer.tsx","./src/components/monthkalendarmodal.tsx","./src/components/notemodal.tsx","./src/components/phistreampopover.tsx","./src/components/resultcard.tsx","./src/components/sealmomentmodal.tsx","./src/components/sealmomentmodaltransfer.tsx","./src/components/sendsigilmodal.tsx","./src/components/sigilconflictbanner copy.tsx","./src/components/sigilconflictbanner.tsx","./src/components/sigilexplorer.tsx","./src/components/sigilglyphbutton.tsx","./src/components/sigilmodal.tsx","./src/components/sigilmomentrow.tsx","./src/components/sigilpublisherpanel.tsx","./src/components/solaranchoreddial.tsx","./src/components/sovereigndeclarations.tsx","./src/components/stargateviewer.tsx","./src/components/valuationmodal.tsx","./src/components/valuehistorymodal.tsx","./src/components/verifierform.tsx","./src/components/weekkalendarmodal.tsx","./src/components/kairealms/gameportal.tsx","./src/components/kairealms/glyphutils.ts","./src/components/kairealms/inventory.tsx","./src/components/kairealms/kaikasino.tsx","./src/components/kairealms/kaipulseengine.ts","./src/components/kairealms/missionrunner.tsx","./src/components/kairealms/realmview.tsx","./src/components/kairealms/sigilavatar.tsx","./src/components/kairealms/worldstate.ts","./src/components/kairealms/constants.ts","./src/components/kairealms/index.tsx","./src/components/kairealms/styles.ts","./src/components/kairealms/types.ts","./src/components/kairealms/usegamesession.ts","./src/components/kairealms/kaimaze/kaimaze.tsx","./src/components/kairealms/kaimaze/index.ts","./src/components/kairealms/kaimaze/engine/ai.ts","./src/components/kairealms/kaimaze/engine/constants.ts","./src/components/kairealms/kaimaze/engine/engine.ts","./src/components/kairealms/kaimaze/engine/input.ts","./src/components/kairealms/kaimaze/engine/map.ts","./src/components/kairealms/kaimaze/engine/physics.ts","./src/components/kairealms/kaimaze/engine/types.ts","./src/components/kairealms/lib/gamefocus.ts","./src/components/kaisigil/art.tsx","./src/components/kaisigil/defs.tsx","./src/components/kaisigil/metadata.tsx","./src/components/kaisigil/zkglyph.tsx","./src/components/kaisigil/constants.ts","./src/components/kaisigil/crypto.ts","./src/components/kaisigil/embed.ts","./src/components/kaisigil/exporters.ts","./src/components/kaisigil/freq.ts","./src/components/kaisigil/helpers.ts","./src/components/kaisigil/hooks.ts","./src/components/kaisigil/identity.ts","./src/components/kaisigil/step.ts","./src/components/kaisigil/types.ts","./src/components/kaisigil/utils.ts","./src/components/kaisigil/valuationbridge.ts","./src/components/kaivoh/breathsealer.tsx","./src/components/kaivoh/kaiverifierlink.tsx","./src/components/kaivoh/kaivoh.tsx","./src/components/kaivoh/kaivohapp.tsx","./src/components/kaivoh/kaivohboundary.tsx","./src/components/kaivoh/kaivohmodal.tsx","./src/components/kaivoh/multisharedispatcher.tsx","./src/components/kaivoh/phikeyresolver.ts","./src/components/kaivoh/postbody.tsx","./src/components/kaivoh/postcomposer.tsx","./src/components/kaivoh/sessionmanager.tsx","./src/components/kaivoh/sigilauth.base.ts","./src/components/kaivoh/sigilauthcontext.tsx","./src/components/kaivoh/sigilauthprovider.tsx","./src/components/kaivoh/sigillogin.tsx","./src/components/kaivoh/sigilmemorybuilder.ts","./src/components/kaivoh/signatureembedder.ts","./src/components/kaivoh/socialconnector.shared.ts","./src/components/kaivoh/socialconnector.tsx","./src/components/kaivoh/storyrecorder.tsx","./src/components/kaivoh/verifierframe.tsx","./src/components/kaivoh/encodetoken.worker.ts","./src/components/kaivoh/kaivohencode.worker.ts","./src/components/kaivoh/usesigilauth.ts","./src/components/kaivoh/verifierproof.ts","./src/components/sigilexplorer/pulsehoneycombmodal.tsx","./src/components/sigilexplorer/sigilexplorer.tsx","./src/components/sigilexplorer/sigilhoneycombexplorer.tsx","./src/components/sigilexplorer/apiclient.ts","./src/components/sigilexplorer/chakra.ts","./src/components/sigilexplorer/format.ts","./src/components/sigilexplorer/index.ts","./src/components/sigilexplorer/inhalequeue.ts","./src/components/sigilexplorer/kaicadence.ts","./src/components/sigilexplorer/registrystore.ts","./src/components/sigilexplorer/remotepull.ts","./src/components/sigilexplorer/transfers.ts","./src/components/sigilexplorer/treebuilder.ts","./src/components/sigilexplorer/treetypes.ts","./src/components/sigilexplorer/types.ts","./src/components/sigilexplorer/url.ts","./src/components/sigilexplorer/urlhealth.ts","./src/components/sigilexplorer/witness.ts","./src/components/sigilexplorer/tree/buildforest.ts","./src/components/sigilexplorer/tree/types.ts","./src/components/verifierstamper/sendphiamountfield.tsx","./src/components/verifierstamper/sigilmomentrow.tsx","./src/components/verifierstamper/verifierstamper.tsx","./src/components/verifierstamper/constants.ts","./src/components/verifierstamper/crypto.ts","./src/components/verifierstamper/files.ts","./src/components/verifierstamper/keys.ts","./src/components/verifierstamper/merkle.ts","./src/components/verifierstamper/segments.ts","./src/components/verifierstamper/sigilutils.ts","./src/components/verifierstamper/styles.ts","./src/components/verifierstamper/svg.ts","./src/components/verifierstamper/types.ts","./src/components/verifierstamper/ui.tsx","./src/components/verifierstamper/verifyhistorical.ts","./src/components/verifierstamper/verifysovereignoffline.ts","./src/components/verifierstamper/window.ts","./src/components/verifierstamper/zk.ts","./src/components/verifierstamper/hooks/useautoshrink.ts","./src/components/verifierstamper/hooks/userollingchartseries.ts","./src/components/exhale-note/banknotesvg.ts","./src/components/exhale-note/bridge.ts","./src/components/exhale-note/constants.ts","./src/components/exhale-note/css.ts","./src/components/exhale-note/cutmarks.ts","./src/components/exhale-note/dom.ts","./src/components/exhale-note/exporters.ts","./src/components/exhale-note/format.ts","./src/components/exhale-note/hash.ts","./src/components/exhale-note/printer.ts","./src/components/exhale-note/proofpages.ts","./src/components/exhale-note/qr.ts","./src/components/exhale-note/sanitize.ts","./src/components/exhale-note/sigilembed.ts","./src/components/exhale-note/svgtopng.ts","./src/components/exhale-note/titles.ts","./src/components/exhale-note/types.ts","./src/components/session/sessioncontext.ts","./src/components/session/sessionprovider.tsx","./src/components/session/sigilsessioncontext.ts","./src/components/session/sigilsessionprovider.tsx","./src/components/session/sigilsessiontypes.ts","./src/components/session/sessionstorage.ts","./src/components/session/sessiontypes.ts","./src/components/session/usesession.ts","./src/components/session/usesigilsession.ts","./src/components/shortner/shortredirect.tsx","./src/components/shortner/shorturltool.tsx","./src/components/shortner/index.tsx","./src/components/shortner/kaiphishort.ts","./src/components/sigil/kaiqr.tsx","./src/components/sigil/mobilesafefileinput.tsx","./src/components/sigil/ownershippanel.tsx","./src/components/sigil/ownershipverifier.tsx","./src/components/sigil/ownershipverifymodal.tsx","./src/components/sigil/phidepositpanel.tsx","./src/components/sigil/provenancelist.tsx","./src/components/sigil/sigilcta.tsx","./src/components/sigil/sigilframe.tsx","./src/components/sigil/sigilheader.tsx","./src/components/sigil/sigilmetapanel.tsx","./src/components/sigil/sovereigncontrols.tsx","./src/components/sigil/stargateoverlay.tsx","./src/components/sigil/upgradesigilmodal.tsx","./src/components/sigil/openownershipverifymodal.tsx","./src/components/sigil/theme.tsx","./src/components/valuation/donorseditor.tsx","./src/components/valuation/mintcompositemodal.tsx","./src/components/valuation/asset.ts","./src/components/valuation/constants.ts","./src/components/valuation/display.ts","./src/components/valuation/drivers.ts","./src/components/valuation/globals.d.ts","./src/components/valuation/hooks.ts","./src/components/valuation/math.ts","./src/components/valuation/platform.ts","./src/components/valuation/rarity.ts","./src/components/valuation/series.ts","./src/components/valuation/chart/livechart.tsx","./src/components/valuation/chart/valuationcard.tsx","./src/components/valuation/chart/valuedonut.tsx","./src/components/valuation/types/window.d.ts","./src/components/verifier/sendphiamountfield.tsx","./src/components/verifier/verifiererrorboundary.tsx","./src/components/verifier/hooks/usemetasignals.ts","./src/components/verifier/types/local.ts","./src/components/verifier/types/window.d.ts","./src/components/verifier/ui/jsontree.tsx","./src/components/verifier/ui/statuschips.tsx","./src/components/verifier/utils/base64.ts","./src/components/verifier/utils/childexpiry.ts","./src/components/verifier/utils/decimal.ts","./src/components/verifier/utils/dialog.ts","./src/components/verifier/utils/log.ts","./src/components/verifier/utils/metadataset.ts","./src/components/verifier/utils/modal.ts","./src/components/verifier/utils/notepayload.ts","./src/components/verifier/utils/rotationbus.ts","./src/components/verifier/utils/saferead.ts","./src/components/verifier/utils/sigilglobal.ts","./src/components/verifier/utils/sigilmemoryvault.ts","./src/components/verifier/utils/statemachine.ts","./src/components/verifier/utils/urlpayload.ts","./src/constants/sigilexplorer.ts","./src/glyph/glyphmodal.tsx","./src/glyph/glyphengine.ts","./src/glyph/glyphutils.ts","./src/glyph/types.ts","./src/glyph/useglyphlogic.ts","./src/hooks/useauthorityproof.ts","./src/hooks/usebodyscrolllock.ts","./src/hooks/usedisablezoom.ts","./src/hooks/usefastpress.ts","./src/hooks/usekaiparitypricepoints.ts","./src/hooks/usekaiticker.ts","./src/hooks/useperfmode.ts","./src/hooks/useresponsivesigilsize.ts","./src/hooks/userotationbus.ts","./src/hooks/usevaluehistory.ts","./src/hooks/usevisualviewportsize.ts","./src/kai/kainow.ts","./src/lib/download.ts","./src/lib/hash.ts","./src/lib/mobilepopoverfix.ts","./src/lib/qr.ts","./src/lib/sigilregistryclient.ts","./src/lib/ledger/log.ts","./src/lib/ledger/merkle.ts","./src/lib/ledger/types.ts","./src/lib/sigil/breathproof.ts","./src/lib/sigil/canonicalize.ts","./src/lib/sigil/codec.ts","./src/lib/sigil/embed.ts","./src/lib/sigil/extract.ts","./src/lib/sigil/hash.ts","./src/lib/sigil/recover.ts","./src/lib/sigil/signature.ts","./src/lib/sigil/__tests__/canonicalize.test.ts","./src/lib/sigil/__tests__/hash.test.ts","./src/lib/sync/dht.ts","./src/lib/sync/ipfsadapter.ts","./src/lib/sync/nopadapter.ts","./src/og/buildverifiedcardsvg.ts","./src/og/cache.ts","./src/og/capsulestore.ts","./src/og/downloadverifiedcard.ts","./src/og/rendernotfoundog.ts","./src/og/renderverifiedog.ts","./src/og/sigilembed.ts","./src/og/svgtopng.ts","./src/og/types.ts","./src/pages/pshort.tsx","./src/pages/sigilfeedpage.tsx","./src/pages/verifyembedpage.tsx","./src/pages/verifypage.tsx","./src/pages/verifysigil.tsx","./src/pages/sigilpage/sigilpage.tsx","./src/pages/sigilpage/constants.ts","./src/pages/sigilpage/debits.ts","./src/pages/sigilpage/descendants.ts","./src/pages/sigilpage/exportzip.ts","./src/pages/sigilpage/linkshare.ts","./src/pages/sigilpage/modalutils.ts","./src/pages/sigilpage/momentkeys.ts","./src/pages/sigilpage/ogimage.ts","./src/pages/sigilpage/posterexport.tsx","./src/pages/sigilpage/registry.ts","./src/pages/sigilpage/registrysign.ts","./src/pages/sigilpage/rotation.ts","./src/pages/sigilpage/rotationbus.ts","./src/pages/sigilpage/sendlock.ts","./src/pages/sigilpage/styleinject.ts","./src/pages/sigilpage/svgops.ts","./src/pages/sigilpage/types.ts","./src/pages/sigilpage/usesigilsend.ts","./src/pages/sigilpage/usevaluation.ts","./src/pages/sigilpage/utils.ts","./src/pages/sigilpage/verifiercanon.public.ts","./src/pages/sigilpage/verifiercanon.ts","./src/pages/sigilstream/sigilstreamroot.tsx","./src/pages/sigilstream/index.ts","./src/pages/sigilstream/attachments/embeds.tsx","./src/pages/sigilstream/attachments/files.ts","./src/pages/sigilstream/attachments/gallery.tsx","./src/pages/sigilstream/attachments/types.ts","./src/pages/sigilstream/composer/composer.tsx","./src/pages/sigilstream/composer/linkhelpers.ts","./src/pages/sigilstream/core/alias.ts","./src/pages/sigilstream/core/kai_time.ts","./src/pages/sigilstream/core/phistreamautoadd.ts","./src/pages/sigilstream/core/ticker.ts","./src/pages/sigilstream/core/types.ts","./src/pages/sigilstream/core/urldisplay.ts","./src/pages/sigilstream/core/utils.ts","./src/pages/sigilstream/data/memorystreamv2.ts","./src/pages/sigilstream/data/seed.ts","./src/pages/sigilstream/data/storage.ts","./src/pages/sigilstream/data/toast/toasts.tsx","./src/pages/sigilstream/data/toast/toast.ts","./src/pages/sigilstream/identity/identitybar.tsx","./src/pages/sigilstream/identity/sigilactionurl.tsx","./src/pages/sigilstream/inhaler/inhalesection.tsx","./src/pages/sigilstream/list/streamlist.tsx","./src/pages/sigilstream/payload/payloadbanner.tsx","./src/pages/sigilstream/payload/types.ts","./src/pages/sigilstream/payload/usepayload.ts","./src/pages/sigilstream/status/kaistatus.tsx","./src/pages/sigilstream/status/proofbadge.tsx","./src/perf/perfprofiler.tsx","./src/perf/perfdebug.tsx","./src/router/approuter.tsx","./src/session/sigilsession.tsx","./src/session/sigilsessioncontext.ts","./src/session/sigilsessiontypes.ts","./src/session/usesigilsession.ts","./src/ssr/ssrsnapshotcontext.tsx","./src/ssr/cache.ts","./src/ssr/loaders.ts","./src/ssr/safejson.ts","./src/ssr/serverexports.ts","./src/ssr/snapshotclient.ts","./src/ssr/snapshottypes.ts","./src/types/crypto-shims.d.ts","./src/types/global.d.ts","./src/types/jsqr.d.ts","./src/types/klocktypes.ts","./src/types/pako.d.ts","./src/types/react-router-dom-server.d.ts","./src/types/sigil-global.d.ts","./src/types/sigil.ts","./src/types/snarkjs-shim.d.ts","./src/types/snarkjs.d.ts","./src/types/usernameclaim.ts","./src/types/zkp-prover.d.ts","./src/utils/authorsig.ts","./src/utils/base64url.ts","./src/utils/cbor.ts","./src/utils/constants.ts","./src/utils/cryptoledger.ts","./src/utils/derivedglyph.ts","./src/utils/domhead.ts","./src/utils/extractkaimetadata.ts","./src/utils/feedpayload.ts","./src/utils/globaltokenregistry.ts","./src/utils/hash.ts","./src/utils/jcs.ts","./src/utils/kai.ts","./src/utils/kaimath.ts","./src/utils/kaitimedisplay.ts","./src/utils/kai_cadence.ts","./src/utils/kai_pulse.ts","./src/utils/kai_turah.ts","./src/utils/kairosmath.ts","./src/utils/klock_adapters.ts","./src/utils/kopyfeedback.ts","./src/utils/lahmahtor.ts","./src/utils/largeasset.ts","./src/utils/payload.ts","./src/utils/phi-issuance.ts","./src/utils/phi-precision.ts","./src/utils/platform.ts","./src/utils/poseidon.ts","./src/utils/postseal.ts","./src/utils/provenance.ts","./src/utils/qrexport.ts","./src/utils/reloaddetective.ts","./src/utils/sanitizehtml.ts","./src/utils/sendledger.ts","./src/utils/sendlock.ts","./src/utils/sha256.ts","./src/utils/shareurl.ts","./src/utils/shortener.ts","./src/utils/sigilauthextract.ts","./src/utils/sigilcapsule.ts","./src/utils/sigildecode.ts","./src/utils/sigilexplorersync.ts","./src/utils/sigilmetadata.ts","./src/utils/sigilregistry.ts","./src/utils/sigiltransferregistry.ts","./src/utils/sigilurl.ts","./src/utils/solarsync.ts","./src/utils/streamlink.ts","./src/utils/svgmeta.ts","./src/utils/svgproof.ts","./src/utils/transferpackage.ts","./src/utils/urlshort.ts","./src/utils/useclientready.ts","./src/utils/useportaltarget.ts","./src/utils/usesigilpayload copy.ts","./src/utils/usesigilpayload.ts","./src/utils/usesovereignsolarclock.ts","./src/utils/usernameclaim.ts","./src/utils/usernameclaimregistry.ts","./src/utils/valuation.ts","./src/utils/verificationcache.ts","./src/utils/verificationreceipt.ts","./src/utils/verifysigil.ts","./src/utils/webauthnkas.ts","./src/utils/webauthnreceive.ts","./src/utils/zkproof.ts","./src/verifier/canonical.ts","./src/verifier/validator.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/sovereignsolar.ts","./src/entry-client.tsx","./src/entry-server-exports.ts","./src/entry-server.tsx","./src/main.tsx","./src/types.ts","./src/version.ts","./src/components/daydetailmodal.tsx","./src/components/errorboundary.tsx","./src/components/eternalklock.tsx","./src/components/exhalenote.tsx","./src/components/feedcard.tsx","./src/components/glyphimportmodal.tsx","./src/components/homepricechartcard.tsx","./src/components/homepricetickerfallback.tsx","./src/components/inhaleuploadicon.tsx","./src/components/kaiklock.canon.ts","./src/components/kaiklock.tsx","./src/components/kaiklockhomeface.tsx","./src/components/kaipricechart.tsx","./src/components/kaisigil.tsx","./src/components/kaisplashscreen.tsx","./src/components/largeglyphminter.tsx","./src/components/largeglyphviewer.tsx","./src/components/monthkalendarmodal.tsx","./src/components/notemodal.tsx","./src/components/phistreampopover.tsx","./src/components/resultcard.tsx","./src/components/sealmomentmodal.tsx","./src/components/sealmomentmodaltransfer.tsx","./src/components/sendsigilmodal.tsx","./src/components/sigilconflictbanner copy.tsx","./src/components/sigilconflictbanner.tsx","./src/components/sigilexplorer.tsx","./src/components/sigilglyphbutton.tsx","./src/components/sigilmodal.tsx","./src/components/sigilmomentrow.tsx","./src/components/sigilpublisherpanel.tsx","./src/components/solaranchoreddial.tsx","./src/components/sovereigndeclarations.tsx","./src/components/stargateviewer.tsx","./src/components/valuationmodal.tsx","./src/components/valuehistorymodal.tsx","./src/components/verifierform.tsx","./src/components/weekkalendarmodal.tsx","./src/components/kairealms/gameportal.tsx","./src/components/kairealms/glyphutils.ts","./src/components/kairealms/inventory.tsx","./src/components/kairealms/kaikasino.tsx","./src/components/kairealms/kaipulseengine.ts","./src/components/kairealms/missionrunner.tsx","./src/components/kairealms/realmview.tsx","./src/components/kairealms/sigilavatar.tsx","./src/components/kairealms/worldstate.ts","./src/components/kairealms/constants.ts","./src/components/kairealms/index.tsx","./src/components/kairealms/styles.ts","./src/components/kairealms/types.ts","./src/components/kairealms/usegamesession.ts","./src/components/kairealms/kaimaze/kaimaze.tsx","./src/components/kairealms/kaimaze/index.ts","./src/components/kairealms/kaimaze/engine/ai.ts","./src/components/kairealms/kaimaze/engine/constants.ts","./src/components/kairealms/kaimaze/engine/engine.ts","./src/components/kairealms/kaimaze/engine/input.ts","./src/components/kairealms/kaimaze/engine/map.ts","./src/components/kairealms/kaimaze/engine/physics.ts","./src/components/kairealms/kaimaze/engine/types.ts","./src/components/kairealms/lib/gamefocus.ts","./src/components/kaisigil/art.tsx","./src/components/kaisigil/defs.tsx","./src/components/kaisigil/metadata.tsx","./src/components/kaisigil/zkglyph.tsx","./src/components/kaisigil/constants.ts","./src/components/kaisigil/crypto.ts","./src/components/kaisigil/embed.ts","./src/components/kaisigil/exporters.ts","./src/components/kaisigil/freq.ts","./src/components/kaisigil/helpers.ts","./src/components/kaisigil/hooks.ts","./src/components/kaisigil/identity.ts","./src/components/kaisigil/step.ts","./src/components/kaisigil/types.ts","./src/components/kaisigil/utils.ts","./src/components/kaisigil/valuationbridge.ts","./src/components/kaivoh/breathsealer.tsx","./src/components/kaivoh/kaiverifierlink.tsx","./src/components/kaivoh/kaivoh.tsx","./src/components/kaivoh/kaivohapp.tsx","./src/components/kaivoh/kaivohboundary.tsx","./src/components/kaivoh/kaivohmodal.tsx","./src/components/kaivoh/multisharedispatcher.tsx","./src/components/kaivoh/phikeyresolver.ts","./src/components/kaivoh/postbody.tsx","./src/components/kaivoh/postcomposer.tsx","./src/components/kaivoh/sessionmanager.tsx","./src/components/kaivoh/sigilauth.base.ts","./src/components/kaivoh/sigilauthcontext.tsx","./src/components/kaivoh/sigilauthprovider.tsx","./src/components/kaivoh/sigillogin.tsx","./src/components/kaivoh/sigilmemorybuilder.ts","./src/components/kaivoh/signatureembedder.ts","./src/components/kaivoh/socialconnector.shared.ts","./src/components/kaivoh/socialconnector.tsx","./src/components/kaivoh/storyrecorder.tsx","./src/components/kaivoh/verifierframe.tsx","./src/components/kaivoh/encodetoken.worker.ts","./src/components/kaivoh/kaivohencode.worker.ts","./src/components/kaivoh/usesigilauth.ts","./src/components/kaivoh/verifierproof.ts","./src/components/sigilexplorer/pulsehoneycombmodal.tsx","./src/components/sigilexplorer/sigilexplorer.tsx","./src/components/sigilexplorer/sigilhoneycombexplorer.tsx","./src/components/sigilexplorer/apiclient.ts","./src/components/sigilexplorer/chakra.ts","./src/components/sigilexplorer/format.ts","./src/components/sigilexplorer/index.ts","./src/components/sigilexplorer/inhalequeue.ts","./src/components/sigilexplorer/kaicadence.ts","./src/components/sigilexplorer/registrystore.ts","./src/components/sigilexplorer/remotepull.ts","./src/components/sigilexplorer/transfers.ts","./src/components/sigilexplorer/treebuilder.ts","./src/components/sigilexplorer/treetypes.ts","./src/components/sigilexplorer/types.ts","./src/components/sigilexplorer/url.ts","./src/components/sigilexplorer/urlhealth.ts","./src/components/sigilexplorer/witness.ts","./src/components/sigilexplorer/tree/buildforest.ts","./src/components/sigilexplorer/tree/types.ts","./src/components/verifierstamper/sendphiamountfield.tsx","./src/components/verifierstamper/sigilmomentrow.tsx","./src/components/verifierstamper/verifierstamper.tsx","./src/components/verifierstamper/constants.ts","./src/components/verifierstamper/crypto.ts","./src/components/verifierstamper/files.ts","./src/components/verifierstamper/keys.ts","./src/components/verifierstamper/merkle.ts","./src/components/verifierstamper/segments.ts","./src/components/verifierstamper/sigilutils.ts","./src/components/verifierstamper/styles.ts","./src/components/verifierstamper/svg.ts","./src/components/verifierstamper/types.ts","./src/components/verifierstamper/ui.tsx","./src/components/verifierstamper/verifyhistorical.ts","./src/components/verifierstamper/verifysovereignoffline.ts","./src/components/verifierstamper/window.ts","./src/components/verifierstamper/zk.ts","./src/components/verifierstamper/hooks/useautoshrink.ts","./src/components/verifierstamper/hooks/userollingchartseries.ts","./src/components/exhale-note/banknotesvg.ts","./src/components/exhale-note/bridge.ts","./src/components/exhale-note/constants.ts","./src/components/exhale-note/css.ts","./src/components/exhale-note/cutmarks.ts","./src/components/exhale-note/dom.ts","./src/components/exhale-note/exporters.ts","./src/components/exhale-note/format.ts","./src/components/exhale-note/hash.ts","./src/components/exhale-note/printer.ts","./src/components/exhale-note/proofpages.ts","./src/components/exhale-note/qr.ts","./src/components/exhale-note/sanitize.ts","./src/components/exhale-note/sigilembed.ts","./src/components/exhale-note/svgtopng.ts","./src/components/exhale-note/titles.ts","./src/components/exhale-note/types.ts","./src/components/session/sessioncontext.ts","./src/components/session/sessionprovider.tsx","./src/components/session/sigilsessioncontext.ts","./src/components/session/sigilsessionprovider.tsx","./src/components/session/sigilsessiontypes.ts","./src/components/session/sessionstorage.ts","./src/components/session/sessiontypes.ts","./src/components/session/usesession.ts","./src/components/session/usesigilsession.ts","./src/components/shortner/shortredirect.tsx","./src/components/shortner/shorturltool.tsx","./src/components/shortner/index.tsx","./src/components/shortner/kaiphishort.ts","./src/components/sigil/kaiqr.tsx","./src/components/sigil/mobilesafefileinput.tsx","./src/components/sigil/ownershippanel.tsx","./src/components/sigil/ownershipverifier.tsx","./src/components/sigil/ownershipverifymodal.tsx","./src/components/sigil/phidepositpanel.tsx","./src/components/sigil/provenancelist.tsx","./src/components/sigil/sigilcta.tsx","./src/components/sigil/sigilframe.tsx","./src/components/sigil/sigilheader.tsx","./src/components/sigil/sigilmetapanel.tsx","./src/components/sigil/sovereigncontrols.tsx","./src/components/sigil/stargateoverlay.tsx","./src/components/sigil/upgradesigilmodal.tsx","./src/components/sigil/openownershipverifymodal.tsx","./src/components/sigil/theme.tsx","./src/components/valuation/donorseditor.tsx","./src/components/valuation/mintcompositemodal.tsx","./src/components/valuation/asset.ts","./src/components/valuation/constants.ts","./src/components/valuation/display.ts","./src/components/valuation/drivers.ts","./src/components/valuation/globals.d.ts","./src/components/valuation/hooks.ts","./src/components/valuation/math.ts","./src/components/valuation/platform.ts","./src/components/valuation/rarity.ts","./src/components/valuation/series.ts","./src/components/valuation/chart/livechart.tsx","./src/components/valuation/chart/valuationcard.tsx","./src/components/valuation/chart/valuedonut.tsx","./src/components/valuation/types/window.d.ts","./src/components/verifier/sendphiamountfield.tsx","./src/components/verifier/verifiererrorboundary.tsx","./src/components/verifier/hooks/usemetasignals.ts","./src/components/verifier/types/local.ts","./src/components/verifier/types/window.d.ts","./src/components/verifier/ui/jsontree.tsx","./src/components/verifier/ui/statuschips.tsx","./src/components/verifier/utils/base64.ts","./src/components/verifier/utils/childexpiry.ts","./src/components/verifier/utils/decimal.ts","./src/components/verifier/utils/dialog.ts","./src/components/verifier/utils/log.ts","./src/components/verifier/utils/metadataset.ts","./src/components/verifier/utils/modal.ts","./src/components/verifier/utils/notepayload.ts","./src/components/verifier/utils/rotationbus.ts","./src/components/verifier/utils/saferead.ts","./src/components/verifier/utils/sigilglobal.ts","./src/components/verifier/utils/sigilmemoryvault.ts","./src/components/verifier/utils/statemachine.ts","./src/components/verifier/utils/urlpayload.ts","./src/constants/sigilexplorer.ts","./src/glyph/glyphmodal.tsx","./src/glyph/glyphengine.ts","./src/glyph/glyphutils.ts","./src/glyph/types.ts","./src/glyph/useglyphlogic.ts","./src/hooks/useauthorityproof.ts","./src/hooks/usebodyscrolllock.ts","./src/hooks/usedisablezoom.ts","./src/hooks/usefastpress.ts","./src/hooks/usekaiparitypricepoints.ts","./src/hooks/usekaiticker.ts","./src/hooks/useperfmode.ts","./src/hooks/useresponsivesigilsize.ts","./src/hooks/userotationbus.ts","./src/hooks/usevaluehistory.ts","./src/hooks/usevisualviewportsize.ts","./src/kai/kainow.ts","./src/lib/download.ts","./src/lib/hash.ts","./src/lib/mobilepopoverfix.ts","./src/lib/qr.ts","./src/lib/sigilregistryclient.ts","./src/lib/ledger/log.ts","./src/lib/ledger/merkle.ts","./src/lib/ledger/types.ts","./src/lib/sigil/breathproof.ts","./src/lib/sigil/canonicalize.ts","./src/lib/sigil/codec.ts","./src/lib/sigil/embed.ts","./src/lib/sigil/extract.ts","./src/lib/sigil/hash.ts","./src/lib/sigil/recover.ts","./src/lib/sigil/signature.ts","./src/lib/sigil/__tests__/canonicalize.test.ts","./src/lib/sigil/__tests__/hash.test.ts","./src/lib/sync/dht.ts","./src/lib/sync/ipfsadapter.ts","./src/lib/sync/nopadapter.ts","./src/og/buildverifiedcardsvg.ts","./src/og/cache.ts","./src/og/capsulestore.ts","./src/og/downloadverifiedcard.ts","./src/og/rendernotfoundog.ts","./src/og/renderverifiedog.ts","./src/og/sigilembed.ts","./src/og/svgtopng.ts","./src/og/types.ts","./src/pages/pshort.tsx","./src/pages/sigilfeedpage.tsx","./src/pages/verifyembedpage.tsx","./src/pages/verifypage.tsx","./src/pages/verifysigil.tsx","./src/pages/sigilpage/sigilpage.tsx","./src/pages/sigilpage/constants.ts","./src/pages/sigilpage/debits.ts","./src/pages/sigilpage/descendants.ts","./src/pages/sigilpage/exportzip.ts","./src/pages/sigilpage/linkshare.ts","./src/pages/sigilpage/modalutils.ts","./src/pages/sigilpage/momentkeys.ts","./src/pages/sigilpage/ogimage.ts","./src/pages/sigilpage/posterexport.tsx","./src/pages/sigilpage/registry.ts","./src/pages/sigilpage/registrysign.ts","./src/pages/sigilpage/rotation.ts","./src/pages/sigilpage/rotationbus.ts","./src/pages/sigilpage/sendlock.ts","./src/pages/sigilpage/styleinject.ts","./src/pages/sigilpage/svgops.ts","./src/pages/sigilpage/types.ts","./src/pages/sigilpage/usesigilsend.ts","./src/pages/sigilpage/usevaluation.ts","./src/pages/sigilpage/utils.ts","./src/pages/sigilpage/verifiercanon.public.ts","./src/pages/sigilpage/verifiercanon.ts","./src/pages/sigilstream/sigilstreamroot.tsx","./src/pages/sigilstream/index.ts","./src/pages/sigilstream/attachments/embeds.tsx","./src/pages/sigilstream/attachments/files.ts","./src/pages/sigilstream/attachments/gallery.tsx","./src/pages/sigilstream/attachments/types.ts","./src/pages/sigilstream/composer/composer.tsx","./src/pages/sigilstream/composer/linkhelpers.ts","./src/pages/sigilstream/core/alias.ts","./src/pages/sigilstream/core/kai_time.ts","./src/pages/sigilstream/core/phistreamautoadd.ts","./src/pages/sigilstream/core/ticker.ts","./src/pages/sigilstream/core/types.ts","./src/pages/sigilstream/core/urldisplay.ts","./src/pages/sigilstream/core/utils.ts","./src/pages/sigilstream/data/memorystreamv2.ts","./src/pages/sigilstream/data/seed.ts","./src/pages/sigilstream/data/storage.ts","./src/pages/sigilstream/data/toast/toasts.tsx","./src/pages/sigilstream/data/toast/toast.ts","./src/pages/sigilstream/identity/identitybar.tsx","./src/pages/sigilstream/identity/sigilactionurl.tsx","./src/pages/sigilstream/inhaler/inhalesection.tsx","./src/pages/sigilstream/list/streamlist.tsx","./src/pages/sigilstream/payload/payloadbanner.tsx","./src/pages/sigilstream/payload/types.ts","./src/pages/sigilstream/payload/usepayload.ts","./src/pages/sigilstream/status/kaistatus.tsx","./src/pages/sigilstream/status/proofbadge.tsx","./src/perf/perfprofiler.tsx","./src/perf/perfdebug.tsx","./src/router/approuter.tsx","./src/session/sigilsession.tsx","./src/session/sigilsessioncontext.ts","./src/session/sigilsessiontypes.ts","./src/session/usesigilsession.ts","./src/ssr/ssrsnapshotcontext.tsx","./src/ssr/cache.ts","./src/ssr/loaders.ts","./src/ssr/safejson.ts","./src/ssr/serverexports.ts","./src/ssr/snapshotclient.ts","./src/ssr/snapshottypes.ts","./src/types/crypto-shims.d.ts","./src/types/global.d.ts","./src/types/jsqr.d.ts","./src/types/klocktypes.ts","./src/types/pako.d.ts","./src/types/react-router-dom-server.d.ts","./src/types/sigil-global.d.ts","./src/types/sigil.ts","./src/types/snarkjs-shim.d.ts","./src/types/snarkjs.d.ts","./src/types/usernameclaim.ts","./src/types/zkp-prover.d.ts","./src/utils/authorsig.ts","./src/utils/base64url.ts","./src/utils/cbor.ts","./src/utils/constants.ts","./src/utils/cryptoledger.ts","./src/utils/derivedglyph.ts","./src/utils/domhead.ts","./src/utils/extractkaimetadata.ts","./src/utils/feedpayload.ts","./src/utils/globaltokenregistry.ts","./src/utils/hash.ts","./src/utils/jcs.ts","./src/utils/kai.ts","./src/utils/kaimath.ts","./src/utils/kaitimedisplay.ts","./src/utils/kai_cadence.ts","./src/utils/kai_pulse.ts","./src/utils/kai_turah.ts","./src/utils/kairosmath.ts","./src/utils/klock_adapters.ts","./src/utils/kopyfeedback.ts","./src/utils/lahmahtor.ts","./src/utils/largeasset.ts","./src/utils/ownerphikey.ts","./src/utils/payload.ts","./src/utils/phi-issuance.ts","./src/utils/phi-precision.ts","./src/utils/platform.ts","./src/utils/poseidon.ts","./src/utils/postseal.ts","./src/utils/provenance.ts","./src/utils/qrexport.ts","./src/utils/receivebundle.ts","./src/utils/reloaddetective.ts","./src/utils/sanitizehtml.ts","./src/utils/sendledger.ts","./src/utils/sendlock.ts","./src/utils/sha256.ts","./src/utils/shareurl.ts","./src/utils/shortener.ts","./src/utils/sigilauthextract.ts","./src/utils/sigilcapsule.ts","./src/utils/sigildecode.ts","./src/utils/sigilexplorersync.ts","./src/utils/sigilmetadata.ts","./src/utils/sigilregistry.ts","./src/utils/sigiltransferregistry.ts","./src/utils/sigilurl.ts","./src/utils/solarsync.ts","./src/utils/streamlink.ts","./src/utils/svgmeta.ts","./src/utils/svgproof.ts","./src/utils/transferpackage.ts","./src/utils/urlshort.ts","./src/utils/useclientready.ts","./src/utils/useportaltarget.ts","./src/utils/usesigilpayload copy.ts","./src/utils/usesigilpayload.ts","./src/utils/usesovereignsolarclock.ts","./src/utils/usernameclaim.ts","./src/utils/usernameclaimregistry.ts","./src/utils/valuation.ts","./src/utils/valuationsnapshot.ts","./src/utils/verificationcache.ts","./src/utils/verificationreceipt.ts","./src/utils/verificationversion.ts","./src/utils/verifysigil.ts","./src/utils/webauthnkas.ts","./src/utils/webauthnreceive.ts","./src/utils/zkproof.ts","./src/verifier/canonical.ts","./src/verifier/validator.ts"],"version":"5.9.3"} \ No newline at end of file From 68edd9f5d88f02c1f1f6b05f80bccd4171a26ac1 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:50:16 -0500 Subject: [PATCH 42/46] Fix segmented glyph verification and archive on seal --- .../VerifierStamper/VerifierStamper.tsx | 268 +++++++++++++++++- 1 file changed, 254 insertions(+), 14 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 0b77d051f..d58b553a9 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -541,8 +541,12 @@ const VerifierStamperInner: React.FC = () => { ); const claimReceiveSig = useCallback( - async (receiveBundleHash: string, receivePulse: number): Promise => { - if (receiveBusy || receiveStatus !== "new") return null; + async ( + receiveBundleHash: string, + receivePulse: number, + options?: { force?: boolean } + ): Promise => { + if (receiveBusy || (!options?.force && receiveStatus !== "new")) return null; if (!receiveBundleHash) return null; setReceiveBusy(true); try { @@ -794,9 +798,13 @@ const VerifierStamperInner: React.FC = () => { ); const buildReceiveLockKeys = useCallback( - async (m: SigilMetadata): Promise<{ keys: string[]; canonical: string | null; nonce: string | null }> => { + async ( + m: SigilMetadata, + options?: { bundleHash?: string | null; canonicalOverride?: string | null } + ): Promise<{ keys: string[]; canonical: string | null; nonce: string | null }> => { const keys = new Set(); - if (bundleHash) keys.add(`${RECEIVE_LOCK_PREFIX}:bundle:${bundleHash}`); + const bundleHashValue = options?.bundleHash ?? bundleHash; + if (bundleHashValue) keys.add(`${RECEIVE_LOCK_PREFIX}:bundle:${bundleHashValue}`); const last = m.transfers?.slice(-1)[0]; if (last) { @@ -810,7 +818,7 @@ const VerifierStamperInner: React.FC = () => { null; if (nonce) keys.add(`${RECEIVE_LOCK_PREFIX}:nonce:${nonce}`); - let effCanonical = canonical; + let effCanonical = options?.canonicalOverride ?? canonical; if (!effCanonical) { try { const eff = await computeEffectiveCanonical(m); @@ -951,8 +959,12 @@ const VerifierStamperInner: React.FC = () => { ); const writeReceiveLock = useCallback( - async (m: SigilMetadata, nowPulse: number) => { - const { keys } = await buildReceiveLockKeys(m); + async ( + m: SigilMetadata, + nowPulse: number, + options?: { bundleHash?: string | null; canonicalOverride?: string | null } + ) => { + const { keys } = await buildReceiveLockKeys(m, options); for (const key of keys) { if (!window.localStorage.getItem(key)) { window.localStorage.setItem(key, JSON.stringify({ pulse: nowPulse })); @@ -962,6 +974,170 @@ const VerifierStamperInner: React.FC = () => { [buildReceiveLockKeys] ); + const rebuildProofBundleForSegment = useCallback( + async ( + svgText: string, + metaValue: SigilMetadata + ): Promise<{ bundle: Record; bundleHash: string | null; receiveBundleHash: string | null } | null> => { + const proofCapsule = proofBundleMeta?.proofCapsule; + const capsuleHash = proofBundleMeta?.capsuleHash ?? (proofCapsule ? await hashProofCapsuleV1(proofCapsule) : null); + const svgHash = await hashSvgText(svgText); + const rawBundle = proofBundleMeta?.raw; + + let nextBundle: Record | null = null; + if (rawBundle && isRecord(rawBundle)) { + nextBundle = { ...(rawBundle as Record), svgHash, capsuleHash, proofCapsule: proofCapsule ?? undefined }; + } else if (metaValue.kaiSignature && typeof metaValue.pulse === "number") { + const chakraDay = normalizeChakraDay(metaValue.chakraDay ?? "") ?? "Crown"; + const verifierSlug = buildVerifierSlug(metaValue.pulse, metaValue.kaiSignature); + const phiKey = metaValue.userPhiKey ?? (await derivePhiKeyFromSig(metaValue.kaiSignature)); + const fallbackCapsule = { + v: "KPV-1" as const, + pulse: metaValue.pulse, + chakraDay, + kaiSignature: metaValue.kaiSignature, + phiKey, + verifierSlug, + }; + const capsuleHashNext = capsuleHash ?? (await hashProofCapsuleV1(fallbackCapsule)); + nextBundle = { + hashAlg: proofBundleMeta?.hashAlg ?? PROOF_HASH_ALG, + canon: proofBundleMeta?.canon ?? PROOF_CANON, + bindings: proofBundleMeta?.bindings, + zkStatement: proofBundleMeta?.zkStatement, + bundleRoot: proofBundleMeta?.bundleRoot, + proofCapsule: fallbackCapsule, + capsuleHash: capsuleHashNext, + svgHash, + shareUrl: proofBundleMeta?.shareUrl, + verifierUrl: proofBundleMeta?.verifierUrl, + verifier: proofBundleMeta?.verifier, + verifiedAtPulse: proofBundleMeta?.verifiedAtPulse, + zkPoseidonHash: proofBundleMeta?.zkPoseidonHash, + zkProof: proofBundleMeta?.zkProof, + proofHints: proofBundleMeta?.proofHints, + zkPublicInputs: proofBundleMeta?.zkPublicInputs, + authorSig: proofBundleMeta?.authorSig ?? null, + mode: proofBundleMeta?.mode, + originBundleHash: proofBundleMeta?.originBundleHash, + originAuthorSig: proofBundleMeta?.originAuthorSig ?? null, + receiveSig: proofBundleMeta?.receiveSig ?? null, + receivePulse: proofBundleMeta?.receivePulse, + receiveBundleHash: proofBundleMeta?.receiveBundleHash, + ownerPhiKey: proofBundleMeta?.ownerPhiKey, + ownerKeyDerivation: proofBundleMeta?.ownerKeyDerivation, + }; + } + + if (!nextBundle) return null; + + const bundleRoot = buildBundleRoot(nextBundle); + const rootHash = await computeBundleHash(bundleRoot); + const legacySeed = { ...nextBundle } as Record; + delete legacySeed.bundleRoot; + delete legacySeed.transport; + delete legacySeed.verificationCache; + delete legacySeed.cacheKey; + delete legacySeed.receipt; + delete legacySeed.receiptHash; + delete legacySeed.verificationSig; + delete legacySeed.zkMeta; + const legacyUnsigned = buildBundleUnsigned(legacySeed); + const legacyHash = await hashBundle(legacyUnsigned); + const useRootHash = + proofBundleMeta?.bundleRoot !== undefined || + proofBundleMeta?.bindings?.bundleHashOf === PROOF_BINDINGS.bundleHashOf || + (isRecord(rawBundle) && "bundleRoot" in rawBundle); + const bundleHashNext = useRootHash ? rootHash : legacyHash; + if (useRootHash) { + nextBundle.bundleRoot = bundleRoot; + } else { + delete nextBundle.bundleRoot; + } + nextBundle.bundleHash = bundleHashNext; + + const priorReceiveSig = readReceiveSigFromBundle(rawBundle ?? nextBundle); + const receiveMode = nextBundle.mode === "receive" || Boolean(priorReceiveSig) || Boolean(nextBundle.receiveBundleHash); + let receiveBundleHashNext: string | null = null; + if (receiveMode) { + let receivePulse = typeof nextBundle.receivePulse === "number" ? nextBundle.receivePulse : priorReceiveSig?.createdAtPulse; + if (typeof receivePulse !== "number" || !Number.isFinite(receivePulse)) receivePulse = kaiPulseNow(); + + const originBundleHash = typeof nextBundle.originBundleHash === "string" ? nextBundle.originBundleHash : undefined; + const originAuthorSig = isKASAuthorSig(nextBundle.originAuthorSig) ? nextBundle.originAuthorSig : null; + + const receiveBundleRoot = buildReceiveBundleRoot({ + bundleRoot, + bundle: nextBundle as ProofBundleLike, + originBundleHash, + originAuthorSig, + receivePulse, + }); + receiveBundleHashNext = await hashReceiveBundleRoot(receiveBundleRoot); + + let receiveSigNext = priorReceiveSig; + if (receiveSigNext && receiveSigNext.binds.bundleHash !== receiveBundleHashNext) { + receiveSigNext = await claimReceiveSig(receiveBundleHashNext, receivePulse, { force: true }); + } + + if (receiveSigNext) { + nextBundle.receiveSig = receiveSigNext; + nextBundle.receivePulse = receiveSigNext.createdAtPulse ?? receivePulse; + nextBundle.receiveBundleHash = receiveBundleHashNext; + nextBundle.receiveBundleRoot = receiveBundleRoot; + const ownerPhiKey = await deriveOwnerPhiKeyFromReceive({ + receiverPubKeyJwk: receiveSigNext.pubKeyJwk, + receivePulse: nextBundle.receivePulse as number, + receiveBundleHash: receiveBundleHashNext, + }); + nextBundle.ownerPhiKey = ownerPhiKey; + nextBundle.ownerKeyDerivation = buildOwnerKeyDerivation({ + originPhiKey: proofCapsule?.phiKey, + receivePulse: nextBundle.receivePulse as number, + receiveBundleHash: receiveBundleHashNext, + }); + } else { + delete nextBundle.receiveSig; + delete nextBundle.receiveBundleHash; + delete nextBundle.receiveBundleRoot; + delete nextBundle.ownerPhiKey; + delete nextBundle.ownerKeyDerivation; + } + + if (rawBundle && isRecord(rawBundle) && priorReceiveSig && receiveSigNext?.binds.bundleHash !== priorReceiveSig.binds.bundleHash) { + const receiveSigHistory = collectReceiveSigHistory(rawBundle, priorReceiveSig); + if (receiveSigHistory.length > 0) nextBundle.receiveSigHistory = receiveSigHistory; + } + } + + return { bundle: nextBundle, bundleHash: bundleHashNext, receiveBundleHash: receiveBundleHashNext }; + }, + [claimReceiveSig, proofBundleMeta] + ); + + const buildSegmentedSvgDataUrl = useCallback( + async ( + m: SigilMetadata + ): Promise<{ dataUrl: string; svgText: string; bundleHash: string | null; receiveBundleHash: string | null } | null> => { + if (!svgURL) return null; + const rawSvg = await fetch(svgURL).then((r) => r.text()); + let svgText = embedMetadataText(rawSvg, m); + let bundleHashNext: string | null = null; + let receiveBundleHashNext: string | null = null; + if (proofBundleMeta) { + const rebuilt = await rebuildProofBundleForSegment(svgText, m); + if (rebuilt) { + svgText = embedProofMetadata(svgText, rebuilt.bundle); + bundleHashNext = rebuilt.bundleHash; + receiveBundleHashNext = rebuilt.receiveBundleHash; + } + } + const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`; + return { dataUrl, svgText, bundleHash: bundleHashNext, receiveBundleHash: receiveBundleHashNext }; + }, + [proofBundleMeta, rebuildProofBundleForSegment, svgURL] + ); + const publishReceiveLock = useCallback( async (m: SigilMetadata, amountPhi?: string) => { let canonicalHash = (m.canonicalHash as string | undefined)?.toLowerCase() ?? null; @@ -1520,6 +1696,22 @@ const VerifierStamperInner: React.FC = () => { [liveSig, computeEffectiveCanonical, contentSigExpected, receiveSig] ); + const markSegmentedAsReceived = useCallback( + async ( + m: SigilMetadata, + nowPulse: number, + options?: { bundleHash?: string | null; canonicalOverride?: string | null } + ) => { + try { + await writeReceiveLock(m, nowPulse, options); + setReceiveStatus("already"); + } catch (err) { + logError("segment.writeReceiveLock", err); + } + }, + [writeReceiveLock] + ); + useEffect(() => { if (meta) { void syncMetaAndUi(meta); @@ -2222,8 +2414,24 @@ const VerifierStamperInner: React.FC = () => { segmentFileBlob, `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` ); - const durl2 = await embedMetadata(svgURL, rolled); - download(durl2, `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, nowPulse)}.svg`); + const segmented = await buildSegmentedSvgDataUrl(rolled); + if (segmented) { + download( + segmented.dataUrl, + `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, nowPulse)}.svg` + ); + } + const hasReceiveProof = + Boolean((rolled as SigilMetadataWithOptionals).receiveSig) || + Boolean(proofBundleMeta?.receiveSig) || + (isRecord(proofBundleMeta?.raw) && proofBundleMeta?.raw?.mode === "receive"); + if (hasReceiveProof) { + const eff = await computeEffectiveCanonical(rolled); + await markSegmentedAsReceived(rolled, nowPulse, { + bundleHash: segmented?.bundleHash ?? bundleHash, + canonicalOverride: eff.canonical, + }); + } const rolled2 = await refreshHeadWindow(rolled); await syncMetaAndUi(rolled2); setError(null); @@ -2574,20 +2782,52 @@ const VerifierStamperInner: React.FC = () => { setError("Segmentation is disabled on SEND sigils."); return; } - const { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(meta); + let { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(meta); + const nowPulse = kaiPulseNow(); + if ((rolled as SigilMetadataWithOptionals).sendLock) { + (rolled as SigilMetadataWithOptionals).sendLock = { + ...(rolled as SigilMetadataWithOptionals).sendLock, + used: true, + usedPulse: nowPulse, + }; + } if (segmentFileBlob) download( segmentFileBlob, `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` ); - if (svgURL) { - const durl = await embedMetadata(svgURL, rolled); - download(durl, `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, kaiPulseNow())}.svg`); + const segmented = await buildSegmentedSvgDataUrl(rolled); + if (segmented) { + download( + segmented.dataUrl, + `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, nowPulse)}.svg` + ); + } + const hasReceiveProof = + Boolean((rolled as SigilMetadataWithOptionals).receiveSig) || + Boolean(proofBundleMeta?.receiveSig) || + (isRecord(proofBundleMeta?.raw) && proofBundleMeta?.raw?.mode === "receive"); + if (hasReceiveProof) { + const eff = await computeEffectiveCanonical(rolled); + await markSegmentedAsReceived(rolled, nowPulse, { + bundleHash: segmented?.bundleHash ?? bundleHash, + canonicalOverride: eff.canonical, + }); } const rolled2 = await refreshHeadWindow(rolled); await syncMetaAndUi(rolled2); setError(null); - }, [meta, svgURL, isSendFilename, refreshHeadWindow, syncMetaAndUi]); + }, [ + meta, + isSendFilename, + refreshHeadWindow, + syncMetaAndUi, + buildSegmentedSvgDataUrl, + bundleHash, + computeEffectiveCanonical, + markSegmentedAsReceived, + proofBundleMeta, + ]); const frequencyHz = useMemo( () => From 0027102a605fe9ff47392987b279b8eb6db3a0a2 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:53:04 -0500 Subject: [PATCH 43/46] Fix sendLock type when sealing segments --- src/components/VerifierStamper/VerifierStamper.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index d58b553a9..81c1b8c4c 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -2784,9 +2784,10 @@ const VerifierStamperInner: React.FC = () => { } let { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(meta); const nowPulse = kaiPulseNow(); - if ((rolled as SigilMetadataWithOptionals).sendLock) { + const rolledSendLock = (rolled as SigilMetadataWithOptionals).sendLock; + if (rolledSendLock?.nonce) { (rolled as SigilMetadataWithOptionals).sendLock = { - ...(rolled as SigilMetadataWithOptionals).sendLock, + ...rolledSendLock, used: true, usedPulse: nowPulse, }; From 146ac52622b66347948af40f1e0e08b8aee5d5f9 Mon Sep 17 00:00:00 2001 From: Kojib Date: Sun, 25 Jan 2026 19:55:43 -0500 Subject: [PATCH 44/46] update --- .../VerifierStamper/VerifierStamper.tsx | 2 +- src/pages/VerifyPage.tsx | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 81c1b8c4c..668ef549c 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -2782,7 +2782,7 @@ const VerifierStamperInner: React.FC = () => { setError("Segmentation is disabled on SEND sigils."); return; } - let { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(meta); + const { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(meta); const nowPulse = kaiPulseNow(); const rolledSendLock = (rolled as SigilMetadataWithOptionals).sendLock; if (rolledSendLock?.nonce) { diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index e413a028d..ec351caaa 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -2770,13 +2770,6 @@ React.useEffect(() => { > ✍ -
-
- ) : null} - {canReceiveGlyph ? ( -
-
Receive
-
-
-
- ) : null} - {verifiedCardData ? ( -
-
Card
-
+ ) : null}
From 44b819eb576fa784206ea15ecc63ec7fe5b46fa8 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:03:47 -0500 Subject: [PATCH 45/46] Add bundle zip downloads for verifier actions --- .../VerifierStamper/VerifierStamper.tsx | 161 ++++++++++++++---- 1 file changed, 129 insertions(+), 32 deletions(-) diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 668ef549c..2c3a94a0d 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -1118,22 +1118,36 @@ const VerifierStamperInner: React.FC = () => { const buildSegmentedSvgDataUrl = useCallback( async ( m: SigilMetadata - ): Promise<{ dataUrl: string; svgText: string; bundleHash: string | null; receiveBundleHash: string | null } | null> => { + ): Promise<{ + dataUrl: string; + svgText: string; + bundleHash: string | null; + receiveBundleHash: string | null; + proofBundle?: Record | null; + } | null> => { if (!svgURL) return null; const rawSvg = await fetch(svgURL).then((r) => r.text()); let svgText = embedMetadataText(rawSvg, m); let bundleHashNext: string | null = null; let receiveBundleHashNext: string | null = null; + let proofBundle: Record | null = null; if (proofBundleMeta) { const rebuilt = await rebuildProofBundleForSegment(svgText, m); if (rebuilt) { svgText = embedProofMetadata(svgText, rebuilt.bundle); bundleHashNext = rebuilt.bundleHash; receiveBundleHashNext = rebuilt.receiveBundleHash; + proofBundle = rebuilt.bundle; } } const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`; - return { dataUrl, svgText, bundleHash: bundleHashNext, receiveBundleHash: receiveBundleHashNext }; + return { + dataUrl, + svgText, + bundleHash: bundleHashNext, + receiveBundleHash: receiveBundleHashNext, + proofBundle, + }; }, [proofBundleMeta, rebuildProofBundleForSegment, svgURL] ); @@ -2095,27 +2109,89 @@ const VerifierStamperInner: React.FC = () => { [conv.phiStringToSend, remainingPhiScaled] ); + const buildBundleZip = useCallback( + async (options: { + svgText: string; + meta: SigilMetadata; + base: string; + context: "download" | "receive" | "segment"; + proofBundle?: Record | null; + segmentFile?: { name: string; blob: Blob } | null; + }): Promise => { + const { svgText, meta: metaValue, base, context, proofBundle, segmentFile } = options; + const svgBlob = new Blob([svgText], { type: "image/svg+xml;charset=utf-8" }); + let pngBlob: Blob | null = null; + try { + const svgDataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`; + pngBlob = await pngBlobFromSvgDataUrl(svgDataUrl, 1024); + } catch (err) { + logError("pngBlobFromSvgDataUrl", err); + } + + const { default: JSZip } = await import("jszip"); + const zip = new JSZip(); + zip.file(`${base}.svg`, svgBlob); + if (pngBlob) zip.file(`${base}.png`, pngBlob); + zip.file(`${base}.metadata.json`, JSON.stringify(metaValue, null, 2)); + if (proofBundle) { + zip.file(`${base}.proof_bundle.json`, JSON.stringify(proofBundle, null, 2)); + } + if (segmentFile) { + zip.file(segmentFile.name, segmentFile.blob); + } + const manifest = { + bundleVersion: "verifier-stamper-v1", + context, + createdAt: new Date().toISOString(), + pulse: typeof metaValue.pulse === "number" ? metaValue.pulse : null, + kaiPulse: typeof metaValue.kaiPulse === "number" ? metaValue.kaiPulse : null, + bundleHash: proofBundle && typeof proofBundle.bundleHash === "string" ? proofBundle.bundleHash : null, + receiveBundleHash: + proofBundle && typeof (proofBundle as { receiveBundleHash?: string }).receiveBundleHash === "string" + ? (proofBundle as { receiveBundleHash?: string }).receiveBundleHash + : null, + files: { + svg: `${base}.svg`, + png: pngBlob ? `${base}.png` : null, + metadata: `${base}.metadata.json`, + proofBundle: proofBundle ? `${base}.proof_bundle.json` : null, + segment: segmentFile?.name ?? null, + }, + }; + zip.file(`${base}.manifest.json`, JSON.stringify(manifest, null, 2)); + + return zip.generateAsync({ + type: "blob", + mimeType: "application/zip", + compression: "DEFLATE", + compressionOptions: { level: 6 }, + streamFiles: true, + }); + }, + [] + ); + const downloadZip = useCallback(async () => { if (!meta || !svgURL) return; const svgDataUrl = await embedMetadata(svgURL, meta); - const svgBlob = await fetch(svgDataUrl).then((r) => r.blob()); - let pngBlob: Blob | null = null; - try { - pngBlob = await pngBlobFromSvgDataUrl(svgDataUrl, 1024); - } catch (err) { - logError("pngBlobFromSvgDataUrl", err); + let svgText = await fetch(svgDataUrl).then((r) => r.text()); + const proofBundle = proofBundleMeta?.raw && isRecord(proofBundleMeta.raw) ? proofBundleMeta.raw : null; + if (proofBundle) { + svgText = embedProofMetadata(svgText, proofBundle); } - const { default: JSZip } = await import("jszip"); - const zip = new JSZip(); const sigilPulse = meta.pulse ?? 0; const last = meta.transfers?.slice(-1)[0]; const sendPulse = last?.senderKaiPulse ?? meta.kaiPulse ?? kaiPulseNow(); const base = pulseFilename("sigil_bundle", sigilPulse, sendPulse); - zip.file(`${base}.svg`, svgBlob); - if (pngBlob) zip.file(`${base}.png`, pngBlob); - const zipBlob = await zip.generateAsync({ type: "blob" }); + const zipBlob = await buildBundleZip({ + svgText, + meta, + base, + context: "download", + proofBundle, + }); download(zipBlob, `${base}.zip`); - }, [meta, svgURL]); + }, [buildBundleZip, meta, proofBundleMeta, svgURL]); const isSendFilename = useMemo(() => (sourceFilename || "").toLowerCase().includes("sigil_send"), [sourceFilename]); @@ -2409,16 +2485,23 @@ const VerifierStamperInner: React.FC = () => { const cap = updated.segmentSize ?? SEGMENT_SIZE; if (windowSize >= cap) { const { meta: rolled, segmentFileBlob } = await sealCurrentWindowIntoSegment(updated); - if (segmentFileBlob) - download( - segmentFileBlob, - `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` - ); const segmented = await buildSegmentedSvgDataUrl(rolled); if (segmented) { + const segmentName = `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json`; + const base = pulseFilename("sigil_segment_bundle", rolled.pulse ?? 0, nowPulse); + const zipBlob = await buildBundleZip({ + svgText: segmented.svgText, + meta: rolled, + base, + context: "segment", + proofBundle: segmented.proofBundle ?? null, + segmentFile: segmentFileBlob ? { name: segmentName, blob: segmentFileBlob } : null, + }); + download(zipBlob, `${base}.zip`); + } else if (segmentFileBlob) { download( - segmented.dataUrl, - `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, nowPulse)}.svg` + segmentFileBlob, + `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` ); } const hasReceiveProof = @@ -2757,9 +2840,16 @@ const VerifierStamperInner: React.FC = () => { } const updatedSvg = embedProofMetadata(baseSvg, nextBundle); - durl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(updatedSvg)))}`; const sigilPulse = updated.pulse ?? 0; - download(durl, `${pulseFilename("sigil_receive", sigilPulse, nowPulse)}.svg`); + const receiveBase = pulseFilename("sigil_bundle", sigilPulse, nowPulse); + const receiveZip = await buildBundleZip({ + svgText: updatedSvg, + meta: updated, + base: receiveBase, + context: "receive", + proofBundle: nextBundle, + }); + download(receiveZip, `${receiveBase}.zip`); const receivedPhi = readPhiAmountFromMeta(updated); const receivedPhiNumber = receivedPhi ? Number(receivedPhi) : NaN; dispatchPhiMoveSuccess({ @@ -2767,8 +2857,7 @@ const VerifierStamperInner: React.FC = () => { amountPhiDisplay: receivedPhi ? `Φ ${fmtPhiFixed4(receivedPhi)}` : undefined, amountDisplay: receivedPhi ? `Φ ${fmtPhiFixed4(receivedPhi)}` : undefined, amountPhi: Number.isFinite(receivedPhiNumber) ? receivedPhiNumber : undefined, - downloadUrl: durl, - downloadLabel: "Sigil Receive", + downloadLabel: "Sigil Bundle", message: "Transfer received.", }); const updated2 = await refreshHeadWindow(updated); @@ -2792,16 +2881,23 @@ const VerifierStamperInner: React.FC = () => { usedPulse: nowPulse, }; } - if (segmentFileBlob) - download( - segmentFileBlob, - `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` - ); const segmented = await buildSegmentedSvgDataUrl(rolled); if (segmented) { + const segmentName = `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json`; + const base = pulseFilename("sigil_segment_bundle", rolled.pulse ?? 0, nowPulse); + const zipBlob = await buildBundleZip({ + svgText: segmented.svgText, + meta: rolled, + base, + context: "segment", + proofBundle: segmented.proofBundle ?? null, + segmentFile: segmentFileBlob ? { name: segmentName, blob: segmentFileBlob } : null, + }); + download(zipBlob, `${base}.zip`); + } else if (segmentFileBlob) { download( - segmented.dataUrl, - `${pulseFilename("sigil_head_after_seal", rolled.pulse ?? 0, nowPulse)}.svg` + segmentFileBlob, + `sigil_segment_${rolled.pulse ?? 0}_${String((rolled.segments?.length ?? 1) - 1).padStart(6, "0")}.json` ); } const hasReceiveProof = @@ -2823,6 +2919,7 @@ const VerifierStamperInner: React.FC = () => { isSendFilename, refreshHeadWindow, syncMetaAndUi, + buildBundleZip, buildSegmentedSvgDataUrl, bundleHash, computeEffectiveCanonical, From d6d8081609bc65173159d5405db468874a0a1c06 Mon Sep 17 00:00:00 2001 From: Kojib Date: Sun, 25 Jan 2026 20:19:25 -0500 Subject: [PATCH 46/46] v42.0.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Φ Network (PHI_NETWORK) — v42.0.0 **Release date:** 2026-01-25 (America/New_York) **Baseline:** v41.2.0 (tag: `41.2.0`, commit: `f955135`) → HEAD :contentReference[oaicite:1]{index=1} ## Executive summary v42.0.0 is a major capability release for the ΦNet Sovereign Gate focused on **shareable verification**, **receive-ownership**, and **offline-first integrity**. This release introduces a deterministic **VERIFIED OpenGraph (OG) card pipeline** (server-rendered + client fallback), adds a **receive ownership layer** (receive signature + owner ΦKey derivation + provenance handling), and hardens the verification bundle schema/hashing so **offline verification remains final and spoof-resistant**. :contentReference[oaicite:2]{index=2} --- ## Highlights ### 1) Deterministic VERIFIED OG cards + /og route (shareable verification) - Added deterministic **VERIFIED OG card renderer**, **/og route**, SSR metadata, and a client download fallback. :contentReference[oaicite:3]{index=3} - Hardened OG not-found handling + share text, and tightened OG metadata ordering for receive signatures. :contentReference[oaicite:4]{index=4} - Added **verifiedAtPulse** to verified OG cards and added verification pulse into proof bundles so each verification is uniquely stamped. :contentReference[oaicite:5]{index=5} ### 2) Receive ownership layer (receiveSig + owner ΦKey derivation + provenance) - Implemented a receive ownership layer including **receiveSig**, **owner ΦKey derivation**, and provenance handling. :contentReference[oaicite:6]{index=6} - Fixed receive signature lookup issues and improved verification snapshot/KAS fallback behavior around receive flows. :contentReference[oaicite:7]{index=7} - Allowed chained receives and surfaced owner ΦKey display. :contentReference[oaicite:8]{index=8} ### 3) Verifier artifact packaging (ZIP bundles) + segmented verification robustness - Added **bundle ZIP downloads** for VerifierStamper receive + segment flows. :contentReference[oaicite:9]{index=9} - Fixed segmented glyph verification and ensured archive-on-seal correctness; fixed sendLock typing when sealing segments. :contentReference[oaicite:10]{index=10} ### 4) Valuation integrity (minted snapshot + receipt binding) + receive USD chart correctness - Minted valuation snapshot + bound it to receipts; fixed receive-signature bundle hash, and corrected USD chart behavior for receive glyphs. :contentReference[oaicite:11]{index=11} ### 5) SSR + offline-first performance (snapshot seeding/caching, preload/header, lazy-loading) - SSR/offline: use `/` as shell, add SSR preloads/header, and lazy-load heavy browser libs. :contentReference[oaicite:12]{index=12} - Added SSR snapshot seeding + caching and short-circuited SSR loaders when cached. :contentReference[oaicite:13]{index=13} - Multiple Vercel SSR hardening iterations (render invocation, render fn signature, HTML fallback). :contentReference[oaicite:14]{index=14} --- ## Breaking changes / upgrade notes - **Verification bundle schema** has been expanded/clarified with explicit binding fields (origin/receive mode, bundle hashes, receiveSig, receivePulse, ownerPhiKey, derivation, etc.). Consumers should treat the bundle as forward-compatible and prefer the explicit fields when present. :contentReference[oaicite:15]{index=15} - Tooling note: repository package manager metadata was updated (`pnpm@10.28.1`) alongside related build metadata changes. :contentReference[oaicite:16]{index=16} --- ## Security & Integrity - Hardened verification cache + receipts; tightened proof bundle contracts and normalized ZK curve metadata for consistency. :contentReference[oaicite:17]{index=17} - Fixed verify bundle hash parity (including legacy proofs) and aligned ZK hash with payload hash. :contentReference[oaicite:18]{index=18} - Persisted KAS author signatures in the sigil registry + hardened WebAuthn signing flows (PWA stability + cross-session recall). :contentReference[oaicite:19]{index=19} --- ## Performance & Reliability - Improved SSR/offline shell behavior and lazy loading for heavier client libraries to reduce initial load and improve time-to-interactive without sacrificing offline operation. :contentReference[oaicite:20]{index=20} - Fixed app loading issues on Vercel by correcting SSR invocation/signatures and HTML fallbacks. :contentReference[oaicite:21]{index=21} - Mobile export hardening: fixed PNG export fidelity, ensured img onload is registered before src assignment, and clamped PNG export sizes for mobile constraints. :contentReference[oaicite:22]{index=22} --- ## Full “Today / last 24h” commit list (GitHub date buckets) > Note: GitHub groups these as **Jan 25–26, 2026** (often UTC-skewed vs local time). :contentReference[oaicite:23]{index=23} ### Jan 26, 2026 - Merge PR #256 — Add deterministic VERIFIED OG card renderer, /og route, SSR meta, and client download fallback (`d646d44`) :contentReference[oaicite:24]{index=24} - Merge PR #279 — Add bundle ZIP downloads for VerifierStamper receive and segment flows (`4cdfc14`) :contentReference[oaicite:25]{index=25} - Add bundle zip downloads for verifier actions (`44b819e`) :contentReference[oaicite:26]{index=26} - Merge PR #278 — Fix segmented glyph verification and archive on seal (`ba519b5`) :contentReference[oaicite:27]{index=27} - update (`146ac52`) :contentReference[oaicite:28]{index=28} - Fix sendLock type when sealing segments (`0027102`) :contentReference[oaicite:29]{index=29} - Fix segmented glyph verification and archive on seal (`68edd9f`) :contentReference[oaicite:30]{index=30} - Merge PR #273 — Mint valuation snapshot + bind to receipt; fix receive-signature bundle hash; USD chart for receive glyphs (`f7d225a`) :contentReference[oaicite:31]{index=31} - Merge PR #276 — fix infinite receives and verification display (`2c73fde`) :contentReference[oaicite:32]{index=32} - Merge PR #277 — fix unused variable warnings (`2a1994a`) :contentReference[oaicite:33]{index=33} - update verifypage (`a66c624`) :contentReference[oaicite:34]{index=34} - Use receive bundle hash from embedded proof (`eea658d`) :contentReference[oaicite:35]{index=35} - Use receive bundle root and lock helper (`c94496f`) :contentReference[oaicite:36]{index=36} ### Jan 25, 2026 - Allow repeat receive claims per upload (`a5793b5`) :contentReference[oaicite:37]{index=37} - Fix verify OG meta ordering for receive sig (`90b8572`) :contentReference[oaicite:38]{index=38} - Allow chained receives and show owner PhiKey (`6872a65`) :contentReference[oaicite:39]{index=39} - Merge PR #275 — Fix receive signature storage key mismatch (`fd528a3`) :contentReference[oaicite:40]{index=40} - Fix receive signature lookup and USD chart scaling (`54b08ef`) :contentReference[oaicite:41]{index=41} - Merge PR #274 — Fix TypeScript type errors on VerifyPage (`4d43a9f`) :contentReference[oaicite:42]{index=42} - Tighten KAS author-sig fallback (`c66e34e`) :contentReference[oaicite:43]{index=43} - Fix verification snapshot and KAS fallback (`8595b42`) :contentReference[oaicite:44]{index=44} - Add receive-bound valuation and signature fixes (`fe54c2d`) :contentReference[oaicite:45]{index=45} - Merge PR #272 — Add receive ownership layer: receiveSig, owner ΦKey derivation, and provenance handling (`4da37ea`) :contentReference[oaicite:46]{index=46} - update verifypage fixed linter errors (`888515c`) :contentReference[oaicite:47]{index=47} - Add receive ownership metadata and verification (`c6675aa`) :contentReference[oaicite:48]{index=48} - Merge PR #270 — default cache version to verification_bundle_version (`d5c44d6`) :contentReference[oaicite:49]{index=49} - Use verifier bundle version for cache fallback (`29fc43f`) :contentReference[oaicite:50]{index=50} - Merge PR #268 — harden sigil proof bundle for invariants (`ec5f7cf`) :contentReference[oaicite:51]{index=51} - Merge PR #269 — finalize verifier audit hardening (`8c8f218`) :contentReference[oaicite:52]{index=52} - update verifier proof verifier stamper and verifypage for lint errrors (`fd80430`) :contentReference[oaicite:53]{index=53} - Harden verification cache and receipts (`0ffd781`) :contentReference[oaicite:54]{index=54} - Harden sigil proof bundle contracts (`beec7ed`) :contentReference[oaicite:55]{index=55} - Merge PR #267 — fix proof bundle curve metadata consistency (`f4a8587`) :contentReference[oaicite:56]{index=56} - Normalize zk curve metadata in proof bundles (`23ddb86`) :contentReference[oaicite:57]{index=57} - Merge PR #266 — escape capsulehash in svg text (`04d9a92`) :contentReference[oaicite:58]{index=58} - Merge PR #258 — fix corrupted PNG image on VerifyPage (`cabfee1`) :contentReference[oaicite:59]{index=59} - Fix bundle hash parity and PNG filename (`674b964`) :contentReference[oaicite:60]{index=60} - Fix bundle hash parity for legacy proofs (`9c96fcd`) :contentReference[oaicite:61]{index=61} - Use phi.svg in verified card (`7db9cb2`) :contentReference[oaicite:62]{index=62} - Fix VerifyPage pulse initialization (`ff4892f`) :contentReference[oaicite:63]{index=63} - Fix verifier PNG download and unique URLs (`53b9812`) :contentReference[oaicite:64]{index=64} - Fix PNG download handling (`21689a9`) :contentReference[oaicite:65]{index=65} - Merge PR #257 — add verification timing marker to card (`0a908f0`) :contentReference[oaicite:66]{index=66} - Fix verifiedAtPulse type in bundle seed (`47579f8`) :contentReference[oaicite:67]{index=67} - Fix verification metadata order and OG description (`1c4ae9e`) :contentReference[oaicite:68]{index=68} - Add verification pulse to proof bundles (`507e5a3`) :contentReference[oaicite:69]{index=69} - Add verifiedAtPulse to verified OG cards (`bc30b46`) :contentReference[oaicite:70]{index=70} - Add verified OG card rendering (`548f807`) :contentReference[oaicite:71]{index=71} - create phi_og_verified_template (`2311fd6`) :contentReference[oaicite:72]{index=72} - Preserve badge styles on button variants (`ea60d43`) + official seal popovers + copy refinements :contentReference[oaicite:73]{index=73} - SSR fixes: render invocation/signature + loader declaration + caching short-circuit + HTML fallback :contentReference[oaicite:74]{index=74} --- ## Full changelog (since v41.2.0) Baseline tag: `41.2.0` — “Bump app version to 41.2.0 and update sigil file naming” (`f955135`). :contentReference[oaicite:75]{index=75} ### v41.3.0 → v41.6.x (Jan 12–13) - v41.3.0 Update build artifacts and SigilModal component (`e498c3f`) :contentReference[oaicite:76]{index=76} - Bump app version to 41.5.0 and update Sigil file naming (`3c858ef`) :contentReference[oaicite:77]{index=77} - v41.6.0 Update baseName format in SigilModal export (`e5fbe8a`) :contentReference[oaicite:78]{index=78} - Store KAS auth in sigil registry (`fff9e79`) + Fix WebAuthn allowCredentials buffer (`464fd40`) :contentReference[oaicite:79]{index=79} - Merge PR #226 — Persist KAS author signatures in sigil registry and harden WebAuthn signing (`87ff285`) :contentReference[oaicite:80]{index=80} - Reload detection: Add reload detective and harden KaiVoh reload safety (`228a735`), PR #228 (`5d8c524`) :contentReference[oaicite:81]{index=81} ### v41.6.8 → v41.7.x (Jan 24) - Merge PR #229 — implement SSR and optimize performance (`d96d955`) + SSR entrypoints/perf instrumentation (`ad71837`) :contentReference[oaicite:82]{index=82} - Export + proof bundle: Enhance sigil export with proof bundle (`11e6433`), Match SigilPage export proof bundle to SealMoment (`aeb1339`), Align exported SVG hashing with verifier (`2602cdf`) :contentReference[oaicite:83]{index=83} - Offline correctness: Ensure sigil page and exports work offline (`93ec687`), Keep JSZip bundled for offline exports (`7f1f2d0`) :contentReference[oaicite:84]{index=84} - Mobile PNG stability: Clamp PNG export size for mobile (`c539f9c`), Fix fallback image loading (`6457adc`), Fix mobile PNG export fidelity (`56e6906`) :contentReference[oaicite:85]{index=85} ### v41.8.x → v41.9.x (Jan 25) - SSR/offline hardening: PR #237 (`b56ad55`) + SSR snapshot cache/loader short-circuit (`2b36112`) + Vercel SSR invocation/signature fixes (`c17a987`, `8e55600`) :contentReference[oaicite:86]{index=86} - Verify UX: live chart popover on verify price (`7c88c05`) + badge/seal popovers and copy updates :contentReference[oaicite:87]{index=87} - Verified OG groundwork: create template (`2311fd6`), add verified OG rendering (`548f807`), add verification pulse fields (`507e5a3`, `bc30b46`) :contentReference[oaicite:88]{index=88} - Hash parity hardening: multiple fixes for verify bundle hash parity + legacy proofs + PNG download correctness :contentReference[oaicite:89]{index=89} ### v41.9.9 → v42.0.0 (Jan 25–26) - Receive ownership layer (PR #272) + receive valuation/signature correctness and chained-receive display improvements :contentReference[oaicite:90]{index=90} - Valuation snapshot minting + receipt binding (PR #273) :contentReference[oaicite:91]{index=91} - Segmented verification fixes (PR #278) + ZIP bundle downloads (PR #279) :contentReference[oaicite:92]{index=92} - Deterministic VERIFIED OG cards end-to-end (PR #256) :contentReference[oaicite:93]{index=93} --- ## Credits - Primary author: @kojibai :contentReference[oaicite:94]{index=94} --- public/sw.js | 4 ++-- src/components/VerifierStamper/VerifierStamper.tsx | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/sw.js b/public/sw.js index 733c07ad6..f315492e8 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,4 +1,4 @@ -/* KAIKLOK SW — offline-first + route mapping for /s/* (sigil links) +/* PHI NETWORK SW — offline-first + route mapping for /s/* (sigil links) - Canonical service worker (legacy /service-worker.js removed to avoid conflicts) - Instant offline boot (app-shell) - Seeds known sigil links from /sigils-index.json (optional) @@ -14,7 +14,7 @@ // Update this version string manually to keep the app + cache versions in sync. // The value is forwarded to the UI via the service worker "SW_ACTIVATED" message. -const APP_VERSION = "41.9.9"; // update on release +const APP_VERSION = "42.0.0"; // update on release const VERSION = new URL(self.location.href).searchParams.get("v") || APP_VERSION; // derived from build const PREFIX = "PHINETWORK"; diff --git a/src/components/VerifierStamper/VerifierStamper.tsx b/src/components/VerifierStamper/VerifierStamper.tsx index 2c3a94a0d..715e17d7c 100644 --- a/src/components/VerifierStamper/VerifierStamper.tsx +++ b/src/components/VerifierStamper/VerifierStamper.tsx @@ -2688,7 +2688,7 @@ const VerifierStamperInner: React.FC = () => { logError("receive.recordTransferMovement", err); } - let durl = await embedMetadata(svgURL, updated); + const durl = await embedMetadata(svgURL, updated); const baseSvg = await fetch(durl).then((r) => r.text()); const svgHash = await hashSvgText(baseSvg); const proofCapsule = proofBundleMeta?.proofCapsule; diff --git a/src/version.ts b/src/version.ts index 792367600..ebb6f9cf0 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,7 +1,7 @@ // src/version.ts // Shared PWA version constants so the app shell, SW registration, and UI stay in sync. -export const BASE_APP_VERSION = "41.9.9"; // Canonical offline/PWA version +export const BASE_APP_VERSION = "42.0.0"; // Canonical offline/PWA version export const SW_VERSION_EVENT = "kairos:sw-version"; export const DEFAULT_APP_VERSION = BASE_APP_VERSION; // Keep in sync with public/sw.js const ENV_APP_VERSION =