From f133233e31362d2c6434f1f6c4e3b235017243e4 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:09:03 -0500 Subject: [PATCH 1/7] Fix verify share payloads in SSR OG --- api/og/verify.ts | 29 ++++++--- api/shareBundle.ts | 125 +++++++++++++++++++++++++++++++++++++++ api/verify.ts | 107 ++++++++------------------------- src/pages/VerifyPage.tsx | 15 ++++- vercel.json | 2 + 5 files changed, 186 insertions(+), 92 deletions(-) create mode 100644 api/shareBundle.ts diff --git a/api/og/verify.ts b/api/og/verify.ts index 3458a9bb1..161ac501a 100644 --- a/api/og/verify.ts +++ b/api/og/verify.ts @@ -2,6 +2,7 @@ import { PNG } from "pngjs"; import QRCode from "qrcode"; import { parseSlug } from "../../src/utils/verifySigil"; +import { decodeShareBundleFromParams, readSharePayloadParam } from "../shareBundle"; type PngInstance = InstanceType & { data: Buffer }; type PngSyncWriter = { sync: { write: (png: PngInstance) => Buffer } }; @@ -200,15 +201,24 @@ export default async function handler( ): Promise { const base = requestOrigin(req); const url = new URL(req.url ?? "/", base); - const slugRaw = url.searchParams.get("slug") ?? ""; + const shareParam = readSharePayloadParam(url.searchParams); + const shareInfo = decodeShareBundleFromParams(url.searchParams); + const slugRaw = shareInfo?.verifierSlug ?? url.searchParams.get("slug") ?? ""; const slug = parseSlug(slugRaw); - const pulse = url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); + const pulse = shareInfo?.pulse + ? String(shareInfo.pulse) + : url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); const chakraDay = url.searchParams.get("chakraDay") ?? ""; - const phiKey = url.searchParams.get("phiKey") ?? slug.shortSig ?? "NA"; - const kasOk = parseBool(url.searchParams.get("kas")); - const g16Ok = parseBool(url.searchParams.get("g16")); - const status = statusFromQuery(url.searchParams.get("status")); + const phiKey = + shareInfo?.phiKey ?? + shareInfo?.keyShort ?? + url.searchParams.get("phiKey") ?? + slug.shortSig ?? + "NA"; + const kasOk = shareInfo?.checks.kas ?? parseBool(url.searchParams.get("kas")); + const g16Ok = shareInfo?.checks.g16 ?? parseBool(url.searchParams.get("g16")); + const status = shareInfo ? (shareInfo.isVerified ? "verified" : "standby") : statusFromQuery(url.searchParams.get("status")); const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; const statusColor: Rgba = status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; @@ -245,8 +255,11 @@ export default async function handler( drawText(png, g16Label, 240, 360, 2, textColor); drawBadge(png, 240 + measureText(g16Label, 2) + 12, 356, g16Ok); - const verifyUrl = `${url.origin}/verify/${encodeURIComponent(slug.raw || slugRaw)}`; - const qr = await makeQrMatrix(verifyUrl); + const verifyUrl = new URL(`${url.origin}/verify/${encodeURIComponent(slug.raw || slugRaw)}`); + if (shareParam) { + verifyUrl.searchParams.set(shareParam.param, shareParam.value); + } + const qr = await makeQrMatrix(verifyUrl.toString()); const moduleSize = Math.floor(220 / qr.size); const qrSize = qr.size * moduleSize; const qrX = 880; diff --git a/api/shareBundle.ts b/api/shareBundle.ts new file mode 100644 index 000000000..3d0a4308c --- /dev/null +++ b/api/shareBundle.ts @@ -0,0 +1,125 @@ +import { decodeLegacyRParam, decodeSharePayload } from "../src/utils/shareBundleCodec"; + +type SharePayloadParam = { param: "p" | "r"; value: string }; + +export type ShareBundleInfo = { + bundle: Record; + mode?: string; + isVerified: boolean; + pulse?: number; + phiKey?: string; + keyShort: string | null; + checks: { kas: boolean | null; g16: boolean | null }; + verifierSlug?: string; + payload: SharePayloadParam; +}; + +const shortPhiKey = (phiKey: string | null | undefined): string | null => { + const trimmed = String(phiKey ?? "").trim(); + if (!trimmed) return null; + if (trimmed.length <= 14) return trimmed; + return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; +}; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null; + +const readString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined); + +const readNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string") { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return undefined; +}; + +const readNestedRecord = (value: unknown): Record | undefined => + isRecord(value) ? (value as Record) : undefined; + +function readSharePayloadParam(params: URLSearchParams): SharePayloadParam | null { + const p = params.get("p"); + if (p) return { param: "p", value: p }; + const r = params.get("r") ?? params.get("receipt"); + if (r) return { param: "r", value: r }; + return null; +} + +function extractProofCapsule(bundle: Record): { + pulse?: number; + phiKey?: string; + verifierSlug?: string; +} | null { + const direct = readNestedRecord(bundle.proofCapsule); + const nested = readNestedRecord(readNestedRecord(bundle.bundleRoot)?.proofCapsule); + const capsule = direct ?? nested; + if (!capsule) return null; + const pulse = readNumber(capsule.pulse); + const phiKey = readString(capsule.phiKey); + const verifierSlug = readString(capsule.verifierSlug); + if (pulse == null && !phiKey && !verifierSlug) return null; + return { pulse, phiKey, verifierSlug }; +} + +function readZkField(bundle: Record, key: "zkProof" | "zkPublicInputs"): unknown { + if (key in bundle) return bundle[key]; + const nested = readNestedRecord(bundle.bundleRoot); + if (nested && key in nested) return nested[key]; + return undefined; +} + +export function decodeShareBundleFromParams(params: URLSearchParams): ShareBundleInfo | null { + const payload = readSharePayloadParam(params); + if (!payload) return null; + + let decoded: unknown; + try { + decoded = payload.param === "p" ? decodeSharePayload(payload.value) : decodeLegacyRParam(payload.value); + } catch { + return null; + } + + if (!isRecord(decoded)) return null; + const bundle = decoded; + + const proofCapsule = extractProofCapsule(bundle); + const verifierSlug = proofCapsule?.verifierSlug; + const mode = readString(bundle.mode) ?? readString(readNestedRecord(bundle.bundleRoot)?.mode); + + const explicitZkVerified = typeof bundle.zkVerified === "boolean" ? bundle.zkVerified : undefined; + const zkpVerified = typeof bundle.zkpVerified === "boolean" ? bundle.zkpVerified : undefined; + const zkProof = readZkField(bundle, "zkProof"); + const zkInputs = readZkField(bundle, "zkPublicInputs"); + const hasProof = zkProof != null && zkInputs != null; + const modeOk = mode === "receive" || mode === "verify" || mode === "origin"; + const inferredVerified = Boolean(verifierSlug && hasProof && modeOk && zkpVerified !== false); + const isVerified = typeof explicitZkVerified === "boolean" ? explicitZkVerified : inferredVerified; + + const pulse = readNumber(bundle.verifiedAtPulse) ?? proofCapsule?.pulse; + const phiKey = readString(bundle.ownerPhiKey) ?? proofCapsule?.phiKey; + const keyShort = shortPhiKey(phiKey); + + const kasOk = bundle.authorSig ? true : null; + const g16Ok = typeof explicitZkVerified === "boolean" ? explicitZkVerified : hasProof ? true : null; + + return { + bundle, + mode, + isVerified, + pulse, + phiKey, + keyShort, + checks: { kas: kasOk, g16: g16Ok }, + verifierSlug, + payload, + }; +} + +export function formatCheck(value: boolean | null): string { + if (value === true) return "✓"; + if (value === false) return "×"; + return "—"; +} + +export { readSharePayloadParam }; diff --git a/api/verify.ts b/api/verify.ts index 3abf766d4..43d38f2b4 100644 --- a/api/verify.ts +++ b/api/verify.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { decodeShareBundleFromParams, formatCheck, readSharePayloadParam } from "./shareBundle"; type SlugInfo = { raw: string; @@ -7,21 +8,6 @@ type SlugInfo = { shortSig: string | null; }; -type ProofCapsule = { - v: "KPV-1"; - pulse: number; - chakraDay: string; - kaiSignature: string; - phiKey: string; - verifierSlug: string; -}; - -type SharedReceipt = { - proofCapsule: ProofCapsule; - authorSig?: unknown; - zkVerified?: boolean; -}; - function parseSlug(rawSlug: string): SlugInfo { let raw = (rawSlug || "").trim(); try { @@ -46,66 +32,23 @@ function statusFromQuery(value: string | null): "verified" | "failed" | "standby return "standby"; } -function parseProofCapsule(raw: unknown): ProofCapsule | null { - if (!raw || typeof raw !== "object") return null; - const record = raw as Record; - if (record.v !== "KPV-1") return null; - if (typeof record.pulse !== "number" || !Number.isFinite(record.pulse)) return null; - if (typeof record.chakraDay !== "string") return null; - if (typeof record.kaiSignature !== "string") return null; - if (typeof record.phiKey !== "string") return null; - if (typeof record.verifierSlug !== "string") return null; - return record as ProofCapsule; -} - -function parseSharedReceipt(raw: unknown): SharedReceipt | null { - if (!raw || typeof raw !== "object") return null; - const record = raw as Record; - const proofCapsule = parseProofCapsule(record.proofCapsule); - if (!proofCapsule) return null; - return { - proofCapsule, - authorSig: record.authorSig, - zkVerified: typeof record.zkVerified === "boolean" ? record.zkVerified : undefined, - }; -} - -function decodeBase64Url(input: string): string | null { - try { - const base64 = input.replace(/-/g, "+").replace(/_/g, "/"); - const pad = base64.length % 4 === 0 ? "" : "=".repeat(4 - (base64.length % 4)); - return Buffer.from(`${base64}${pad}`, "base64").toString("utf-8"); - } catch { - return null; - } -} - -function readReceiptFromParams(params: URLSearchParams): SharedReceipt | null { - const encoded = params.get("r") ?? params.get("receipt"); - if (!encoded) return null; - const decoded = decodeBase64Url(encoded); - if (!decoded) return null; - try { - const raw = JSON.parse(decoded); - return parseSharedReceipt(raw); - } catch { - return null; - } -} - function buildMetaTags(params: { title: string; description: string; url: string; image: string; + imageWidth: string; + imageHeight: string; }): string { - const { title, description, url, image } = params; + const { title, description, url, image, imageWidth, imageHeight } = params; return [ `${title}`, ``, ``, ``, ``, + ``, + ``, ``, ``, ``, @@ -218,31 +161,31 @@ export default async function handler( const requestUrl = new URL(req.url ?? "/", origin); const slugRaw = requestUrl.searchParams.get("slug") ?? ""; const slug = parseSlug(slugRaw); - const receipt = readReceiptFromParams(requestUrl.searchParams); - const receiptSlug = receipt?.proofCapsule.verifierSlug ?? ""; - const canonicalSlug = slug.raw || slugRaw || receiptSlug; - const receiptMatchesSlug = - receipt != null && (!canonicalSlug || receipt.proofCapsule.verifierSlug === canonicalSlug); + const shareParam = readSharePayloadParam(requestUrl.searchParams); + const shareInfo = decodeShareBundleFromParams(requestUrl.searchParams); + const canonicalSlug = slug.raw || slugRaw || shareInfo?.verifierSlug || ""; const statusParam = statusFromQuery(requestUrl.searchParams.get("status")); - const status = receiptMatchesSlug && statusParam === "standby" ? "verified" : statusParam; + const status = shareInfo ? (shareInfo.isVerified ? "verified" : "standby") : statusParam; const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; - const receiptPulse = receiptMatchesSlug ? receipt?.proofCapsule.pulse : null; - const pulseLabel = receiptPulse ? String(receiptPulse) : slug.pulse ? String(slug.pulse) : "—"; + const pulseLabel = shareInfo?.pulse ? String(shareInfo.pulse) : slug.pulse ? String(slug.pulse) : "—"; + const phiLabel = shareInfo?.keyShort ?? (slug.shortSig || "—"); const title = `Proof of Breath™ — ${statusLabel}`; - const description = `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; + const description = shareInfo + ? `Pulse ${pulseLabel} · ΦKey ${phiLabel} · KAS ${formatCheck(shareInfo.checks.kas)} · G16 ${formatCheck(shareInfo.checks.g16)}` + : `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; const verifyUrl = `${origin}/verify/${encodeURIComponent(canonicalSlug)}`; - const ogUrl = new URL(`${origin}/api/og/verify`); - ogUrl.searchParams.set("slug", canonicalSlug); - ogUrl.searchParams.set("status", status); - if (receiptMatchesSlug && receipt) { - ogUrl.searchParams.set("pulse", String(receipt.proofCapsule.pulse)); - ogUrl.searchParams.set("phiKey", receipt.proofCapsule.phiKey); - if (receipt.proofCapsule.chakraDay) ogUrl.searchParams.set("chakraDay", receipt.proofCapsule.chakraDay); - if (receipt.authorSig) ogUrl.searchParams.set("kas", "1"); - if (receipt.zkVerified != null) ogUrl.searchParams.set("g16", receipt.zkVerified ? "1" : "0"); + const ogUrl = new URL(`${origin}/og/verify/${encodeURIComponent(canonicalSlug)}.png`); + if (shareParam) { + ogUrl.searchParams.set(shareParam.param, shareParam.value); + } else { + ogUrl.searchParams.set("status", status); + if (shareInfo?.pulse != null) ogUrl.searchParams.set("pulse", String(shareInfo.pulse)); + if (shareInfo?.phiKey) ogUrl.searchParams.set("phiKey", shareInfo.phiKey); + if (shareInfo?.checks.kas != null) ogUrl.searchParams.set("kas", shareInfo.checks.kas ? "1" : "0"); + if (shareInfo?.checks.g16 != null) ogUrl.searchParams.set("g16", shareInfo.checks.g16 ? "1" : "0"); } const meta = buildMetaTags({ @@ -250,6 +193,8 @@ export default async function handler( description: escapeHtml(description), url: escapeHtml(verifyUrl), image: escapeHtml(ogUrl.toString()), + imageWidth: "1200", + imageHeight: "630", }); const html = await readIndexHtml(origin); diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index d9cb772ae..accaea421 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -1987,9 +1987,18 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le 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()); + const searchParams = new URLSearchParams(window.location.search); + const shareParam = searchParams.get("p") + ? { key: "p", value: searchParams.get("p") ?? "" } + : searchParams.get("r") || searchParams.get("receipt") + ? { key: "r", value: (searchParams.get("r") ?? searchParams.get("receipt") ?? "") } + : null; + const ogImageUrl = new URL(`${origin}/og/verify/${encodeURIComponent(slugValue)}.png`); + if (shareParam?.value) { + ogImageUrl.searchParams.set(shareParam.key, shareParam.value); + } else { + ogImageUrl.searchParams.set("status", statusLabel.toLowerCase()); + } if (result.status === "ok") { ogImageUrl.searchParams.set("pulse", String(result.embedded.pulse ?? slug.pulse ?? "")); const ogPhiKey = diff --git a/vercel.json b/vercel.json index 735f4e9bf..a2bb7670d 100644 --- a/vercel.json +++ b/vercel.json @@ -21,6 +21,8 @@ { "source": "/pulse.html", "destination": "/pulse.html" }, { "source": "/pulse-core.js", "destination": "/pulse-core.js" }, + { "source": "/og/verify/:slug.png", "destination": "/api/og/verify?slug=:slug" }, + { "source": "/verify/:slug", "destination": "/api/verify?slug=:slug" }, { "source": "/verifier", "destination": "/verifier.html" }, From e668425c27c6388cd55a0fee4794d5e7566b7dbc Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:20:29 -0500 Subject: [PATCH 2/7] Refine share bundle verified inference --- api/shareBundle.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api/shareBundle.ts b/api/shareBundle.ts index 3d0a4308c..e56efd930 100644 --- a/api/shareBundle.ts +++ b/api/shareBundle.ts @@ -92,7 +92,7 @@ export function decodeShareBundleFromParams(params: URLSearchParams): ShareBundl const zkProof = readZkField(bundle, "zkProof"); const zkInputs = readZkField(bundle, "zkPublicInputs"); const hasProof = zkProof != null && zkInputs != null; - const modeOk = mode === "receive" || mode === "verify" || mode === "origin"; + const modeOk = mode === "receive" || mode === "verify"; const inferredVerified = Boolean(verifierSlug && hasProof && modeOk && zkpVerified !== false); const isVerified = typeof explicitZkVerified === "boolean" ? explicitZkVerified : inferredVerified; @@ -101,7 +101,14 @@ export function decodeShareBundleFromParams(params: URLSearchParams): ShareBundl const keyShort = shortPhiKey(phiKey); const kasOk = bundle.authorSig ? true : null; - const g16Ok = typeof explicitZkVerified === "boolean" ? explicitZkVerified : hasProof ? true : null; + const g16Ok = + typeof explicitZkVerified === "boolean" + ? explicitZkVerified + : zkpVerified === false + ? false + : hasProof + ? true + : null; return { bundle, From f921d94af44e136f4e85b834d0aee431146dfa61 Mon Sep 17 00:00:00 2001 From: Kojib Date: Tue, 27 Jan 2026 00:27:59 -0500 Subject: [PATCH 3/7] v42.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation Link previews regressed after introducing compact ?p= share payloads because server-side SSR/meta and OG image generator only handled legacy ?r= decoding and defaulted to STANDBY/placeholder images. The goal is to make VerifyPage previews correctly show VERIFIED and the real PNG receipt card for both compact ?p=c1:... and legacy ?r=... share URLs without changing any cryptographic semantics. Server-side decode must remain safe: apply existing deflate-bomb protections and strict caps when decoding ?p= payloads. Description Added a server-safe shared decoder helper api/shareBundle.ts that reads p first then r, uses decodeSharePayload / decodeLegacyRParam, and returns normalized bundle info including { bundle, mode, isVerified, keyShort, pulse, checks } while relying on existing size limits in src/utils/shareBundleCodec.ts. Updated SSR meta generation in api/verify.ts to prefer decoded share bundle data for status/title/description and to set og:image to the absolute PNG generator URL at /og/verify/.png while propagating the same p or r payload param; also added og:image:width and og:image:height metadata. Extended the OG PNG generator api/og/verify.ts to parse p (and fallback to r), decode the bundle via the shared decoder, render the actual verified receipt PNG (pulse, ΦKey, KAS/G16 badges, VERIFIED/STANDBY label), and include the same share param in the verify QR URL embedded in the image. Updated client-side VerifyPage meta code in src/pages/VerifyPage.tsx to attach the p/r payload to the generated OG image URL so client and SSR produce consistent image links. Added a Vercel rewrite for the new path '/og/verify/:slug.png' -> '/api/og/verify?slug=:slug' and avoided changing any verification or proof semantics. --- public/sw.js | 2 +- src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sw.js b/public/sw.js index ecb05fb8a..55688f685 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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 = "42.3.0"; // update on release +const APP_VERSION = "42.3.1"; // 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/version.ts b/src/version.ts index 102d536fb..c106308d1 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 = "42.3.0"; // Canonical offline/PWA version +export const BASE_APP_VERSION = "42.3.1"; // 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 = From 72a4d6d26e9b711041e99dc06736377e408eaadf Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:32:57 -0500 Subject: [PATCH 4/7] Revert "Fix verify share payloads in SSR/OG (support ?p= compact payloads)" --- api/og/verify.ts | 29 +++------ api/shareBundle.ts | 132 --------------------------------------- api/verify.ts | 107 +++++++++++++++++++++++-------- src/pages/VerifyPage.tsx | 15 +---- vercel.json | 2 - 5 files changed, 92 insertions(+), 193 deletions(-) delete mode 100644 api/shareBundle.ts diff --git a/api/og/verify.ts b/api/og/verify.ts index 161ac501a..3458a9bb1 100644 --- a/api/og/verify.ts +++ b/api/og/verify.ts @@ -2,7 +2,6 @@ import { PNG } from "pngjs"; import QRCode from "qrcode"; import { parseSlug } from "../../src/utils/verifySigil"; -import { decodeShareBundleFromParams, readSharePayloadParam } from "../shareBundle"; type PngInstance = InstanceType & { data: Buffer }; type PngSyncWriter = { sync: { write: (png: PngInstance) => Buffer } }; @@ -201,24 +200,15 @@ export default async function handler( ): Promise { const base = requestOrigin(req); const url = new URL(req.url ?? "/", base); - const shareParam = readSharePayloadParam(url.searchParams); - const shareInfo = decodeShareBundleFromParams(url.searchParams); - const slugRaw = shareInfo?.verifierSlug ?? url.searchParams.get("slug") ?? ""; + const slugRaw = url.searchParams.get("slug") ?? ""; const slug = parseSlug(slugRaw); - const pulse = shareInfo?.pulse - ? String(shareInfo.pulse) - : url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); + const pulse = url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); const chakraDay = url.searchParams.get("chakraDay") ?? ""; - const phiKey = - shareInfo?.phiKey ?? - shareInfo?.keyShort ?? - url.searchParams.get("phiKey") ?? - slug.shortSig ?? - "NA"; - const kasOk = shareInfo?.checks.kas ?? parseBool(url.searchParams.get("kas")); - const g16Ok = shareInfo?.checks.g16 ?? parseBool(url.searchParams.get("g16")); - const status = shareInfo ? (shareInfo.isVerified ? "verified" : "standby") : statusFromQuery(url.searchParams.get("status")); + const phiKey = url.searchParams.get("phiKey") ?? slug.shortSig ?? "NA"; + const kasOk = parseBool(url.searchParams.get("kas")); + const g16Ok = parseBool(url.searchParams.get("g16")); + const status = statusFromQuery(url.searchParams.get("status")); const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; const statusColor: Rgba = status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; @@ -255,11 +245,8 @@ export default async function handler( drawText(png, g16Label, 240, 360, 2, textColor); drawBadge(png, 240 + measureText(g16Label, 2) + 12, 356, g16Ok); - const verifyUrl = new URL(`${url.origin}/verify/${encodeURIComponent(slug.raw || slugRaw)}`); - if (shareParam) { - verifyUrl.searchParams.set(shareParam.param, shareParam.value); - } - const qr = await makeQrMatrix(verifyUrl.toString()); + const verifyUrl = `${url.origin}/verify/${encodeURIComponent(slug.raw || slugRaw)}`; + const qr = await makeQrMatrix(verifyUrl); const moduleSize = Math.floor(220 / qr.size); const qrSize = qr.size * moduleSize; const qrX = 880; diff --git a/api/shareBundle.ts b/api/shareBundle.ts deleted file mode 100644 index e56efd930..000000000 --- a/api/shareBundle.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { decodeLegacyRParam, decodeSharePayload } from "../src/utils/shareBundleCodec"; - -type SharePayloadParam = { param: "p" | "r"; value: string }; - -export type ShareBundleInfo = { - bundle: Record; - mode?: string; - isVerified: boolean; - pulse?: number; - phiKey?: string; - keyShort: string | null; - checks: { kas: boolean | null; g16: boolean | null }; - verifierSlug?: string; - payload: SharePayloadParam; -}; - -const shortPhiKey = (phiKey: string | null | undefined): string | null => { - const trimmed = String(phiKey ?? "").trim(); - if (!trimmed) return null; - if (trimmed.length <= 14) return trimmed; - return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; -}; - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null; - -const readString = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined); - -const readNumber = (value: unknown): number | undefined => { - if (typeof value === "number" && Number.isFinite(value)) return value; - if (typeof value === "string") { - const parsed = Number(value); - if (Number.isFinite(parsed)) return parsed; - } - return undefined; -}; - -const readNestedRecord = (value: unknown): Record | undefined => - isRecord(value) ? (value as Record) : undefined; - -function readSharePayloadParam(params: URLSearchParams): SharePayloadParam | null { - const p = params.get("p"); - if (p) return { param: "p", value: p }; - const r = params.get("r") ?? params.get("receipt"); - if (r) return { param: "r", value: r }; - return null; -} - -function extractProofCapsule(bundle: Record): { - pulse?: number; - phiKey?: string; - verifierSlug?: string; -} | null { - const direct = readNestedRecord(bundle.proofCapsule); - const nested = readNestedRecord(readNestedRecord(bundle.bundleRoot)?.proofCapsule); - const capsule = direct ?? nested; - if (!capsule) return null; - const pulse = readNumber(capsule.pulse); - const phiKey = readString(capsule.phiKey); - const verifierSlug = readString(capsule.verifierSlug); - if (pulse == null && !phiKey && !verifierSlug) return null; - return { pulse, phiKey, verifierSlug }; -} - -function readZkField(bundle: Record, key: "zkProof" | "zkPublicInputs"): unknown { - if (key in bundle) return bundle[key]; - const nested = readNestedRecord(bundle.bundleRoot); - if (nested && key in nested) return nested[key]; - return undefined; -} - -export function decodeShareBundleFromParams(params: URLSearchParams): ShareBundleInfo | null { - const payload = readSharePayloadParam(params); - if (!payload) return null; - - let decoded: unknown; - try { - decoded = payload.param === "p" ? decodeSharePayload(payload.value) : decodeLegacyRParam(payload.value); - } catch { - return null; - } - - if (!isRecord(decoded)) return null; - const bundle = decoded; - - const proofCapsule = extractProofCapsule(bundle); - const verifierSlug = proofCapsule?.verifierSlug; - const mode = readString(bundle.mode) ?? readString(readNestedRecord(bundle.bundleRoot)?.mode); - - const explicitZkVerified = typeof bundle.zkVerified === "boolean" ? bundle.zkVerified : undefined; - const zkpVerified = typeof bundle.zkpVerified === "boolean" ? bundle.zkpVerified : undefined; - const zkProof = readZkField(bundle, "zkProof"); - const zkInputs = readZkField(bundle, "zkPublicInputs"); - const hasProof = zkProof != null && zkInputs != null; - const modeOk = mode === "receive" || mode === "verify"; - const inferredVerified = Boolean(verifierSlug && hasProof && modeOk && zkpVerified !== false); - const isVerified = typeof explicitZkVerified === "boolean" ? explicitZkVerified : inferredVerified; - - const pulse = readNumber(bundle.verifiedAtPulse) ?? proofCapsule?.pulse; - const phiKey = readString(bundle.ownerPhiKey) ?? proofCapsule?.phiKey; - const keyShort = shortPhiKey(phiKey); - - const kasOk = bundle.authorSig ? true : null; - const g16Ok = - typeof explicitZkVerified === "boolean" - ? explicitZkVerified - : zkpVerified === false - ? false - : hasProof - ? true - : null; - - return { - bundle, - mode, - isVerified, - pulse, - phiKey, - keyShort, - checks: { kas: kasOk, g16: g16Ok }, - verifierSlug, - payload, - }; -} - -export function formatCheck(value: boolean | null): string { - if (value === true) return "✓"; - if (value === false) return "×"; - return "—"; -} - -export { readSharePayloadParam }; diff --git a/api/verify.ts b/api/verify.ts index 43d38f2b4..3abf766d4 100644 --- a/api/verify.ts +++ b/api/verify.ts @@ -1,6 +1,5 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { decodeShareBundleFromParams, formatCheck, readSharePayloadParam } from "./shareBundle"; type SlugInfo = { raw: string; @@ -8,6 +7,21 @@ type SlugInfo = { shortSig: string | null; }; +type ProofCapsule = { + v: "KPV-1"; + pulse: number; + chakraDay: string; + kaiSignature: string; + phiKey: string; + verifierSlug: string; +}; + +type SharedReceipt = { + proofCapsule: ProofCapsule; + authorSig?: unknown; + zkVerified?: boolean; +}; + function parseSlug(rawSlug: string): SlugInfo { let raw = (rawSlug || "").trim(); try { @@ -32,23 +46,66 @@ function statusFromQuery(value: string | null): "verified" | "failed" | "standby return "standby"; } +function parseProofCapsule(raw: unknown): ProofCapsule | null { + if (!raw || typeof raw !== "object") return null; + const record = raw as Record; + if (record.v !== "KPV-1") return null; + if (typeof record.pulse !== "number" || !Number.isFinite(record.pulse)) return null; + if (typeof record.chakraDay !== "string") return null; + if (typeof record.kaiSignature !== "string") return null; + if (typeof record.phiKey !== "string") return null; + if (typeof record.verifierSlug !== "string") return null; + return record as ProofCapsule; +} + +function parseSharedReceipt(raw: unknown): SharedReceipt | null { + if (!raw || typeof raw !== "object") return null; + const record = raw as Record; + const proofCapsule = parseProofCapsule(record.proofCapsule); + if (!proofCapsule) return null; + return { + proofCapsule, + authorSig: record.authorSig, + zkVerified: typeof record.zkVerified === "boolean" ? record.zkVerified : undefined, + }; +} + +function decodeBase64Url(input: string): string | null { + try { + const base64 = input.replace(/-/g, "+").replace(/_/g, "/"); + const pad = base64.length % 4 === 0 ? "" : "=".repeat(4 - (base64.length % 4)); + return Buffer.from(`${base64}${pad}`, "base64").toString("utf-8"); + } catch { + return null; + } +} + +function readReceiptFromParams(params: URLSearchParams): SharedReceipt | null { + const encoded = params.get("r") ?? params.get("receipt"); + if (!encoded) return null; + const decoded = decodeBase64Url(encoded); + if (!decoded) return null; + try { + const raw = JSON.parse(decoded); + return parseSharedReceipt(raw); + } catch { + return null; + } +} + function buildMetaTags(params: { title: string; description: string; url: string; image: string; - imageWidth: string; - imageHeight: string; }): string { - const { title, description, url, image, imageWidth, imageHeight } = params; + const { title, description, url, image } = params; return [ `${title}`, ``, ``, ``, ``, - ``, - ``, ``, ``, ``, @@ -161,31 +218,31 @@ export default async function handler( const requestUrl = new URL(req.url ?? "/", origin); const slugRaw = requestUrl.searchParams.get("slug") ?? ""; const slug = parseSlug(slugRaw); - const shareParam = readSharePayloadParam(requestUrl.searchParams); - const shareInfo = decodeShareBundleFromParams(requestUrl.searchParams); - const canonicalSlug = slug.raw || slugRaw || shareInfo?.verifierSlug || ""; + const receipt = readReceiptFromParams(requestUrl.searchParams); + const receiptSlug = receipt?.proofCapsule.verifierSlug ?? ""; + const canonicalSlug = slug.raw || slugRaw || receiptSlug; + const receiptMatchesSlug = + receipt != null && (!canonicalSlug || receipt.proofCapsule.verifierSlug === canonicalSlug); const statusParam = statusFromQuery(requestUrl.searchParams.get("status")); - const status = shareInfo ? (shareInfo.isVerified ? "verified" : "standby") : statusParam; + const status = receiptMatchesSlug && statusParam === "standby" ? "verified" : statusParam; const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; - const pulseLabel = shareInfo?.pulse ? String(shareInfo.pulse) : slug.pulse ? String(slug.pulse) : "—"; - const phiLabel = shareInfo?.keyShort ?? (slug.shortSig || "—"); + const receiptPulse = receiptMatchesSlug ? receipt?.proofCapsule.pulse : null; + const pulseLabel = receiptPulse ? String(receiptPulse) : slug.pulse ? String(slug.pulse) : "—"; const title = `Proof of Breath™ — ${statusLabel}`; - const description = shareInfo - ? `Pulse ${pulseLabel} · ΦKey ${phiLabel} · KAS ${formatCheck(shareInfo.checks.kas)} · G16 ${formatCheck(shareInfo.checks.g16)}` - : `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; + const description = `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; const verifyUrl = `${origin}/verify/${encodeURIComponent(canonicalSlug)}`; - const ogUrl = new URL(`${origin}/og/verify/${encodeURIComponent(canonicalSlug)}.png`); - if (shareParam) { - ogUrl.searchParams.set(shareParam.param, shareParam.value); - } else { - ogUrl.searchParams.set("status", status); - if (shareInfo?.pulse != null) ogUrl.searchParams.set("pulse", String(shareInfo.pulse)); - if (shareInfo?.phiKey) ogUrl.searchParams.set("phiKey", shareInfo.phiKey); - if (shareInfo?.checks.kas != null) ogUrl.searchParams.set("kas", shareInfo.checks.kas ? "1" : "0"); - if (shareInfo?.checks.g16 != null) ogUrl.searchParams.set("g16", shareInfo.checks.g16 ? "1" : "0"); + const ogUrl = new URL(`${origin}/api/og/verify`); + ogUrl.searchParams.set("slug", canonicalSlug); + ogUrl.searchParams.set("status", status); + if (receiptMatchesSlug && receipt) { + ogUrl.searchParams.set("pulse", String(receipt.proofCapsule.pulse)); + ogUrl.searchParams.set("phiKey", receipt.proofCapsule.phiKey); + if (receipt.proofCapsule.chakraDay) ogUrl.searchParams.set("chakraDay", receipt.proofCapsule.chakraDay); + if (receipt.authorSig) ogUrl.searchParams.set("kas", "1"); + if (receipt.zkVerified != null) ogUrl.searchParams.set("g16", receipt.zkVerified ? "1" : "0"); } const meta = buildMetaTags({ @@ -193,8 +250,6 @@ export default async function handler( description: escapeHtml(description), url: escapeHtml(verifyUrl), image: escapeHtml(ogUrl.toString()), - imageWidth: "1200", - imageHeight: "630", }); const html = await readIndexHtml(origin); diff --git a/src/pages/VerifyPage.tsx b/src/pages/VerifyPage.tsx index accaea421..d9cb772ae 100644 --- a/src/pages/VerifyPage.tsx +++ b/src/pages/VerifyPage.tsx @@ -1987,18 +1987,9 @@ if (verified && typeof cacheBundleHash === "string" && cacheBundleHash.trim().le const origin = window.location.origin; const slugValue = slug.raw || slugRaw || ""; const ogUrl = new URL(`${origin}/verify/${encodeURIComponent(slugValue)}`); - const searchParams = new URLSearchParams(window.location.search); - const shareParam = searchParams.get("p") - ? { key: "p", value: searchParams.get("p") ?? "" } - : searchParams.get("r") || searchParams.get("receipt") - ? { key: "r", value: (searchParams.get("r") ?? searchParams.get("receipt") ?? "") } - : null; - const ogImageUrl = new URL(`${origin}/og/verify/${encodeURIComponent(slugValue)}.png`); - if (shareParam?.value) { - ogImageUrl.searchParams.set(shareParam.key, shareParam.value); - } else { - ogImageUrl.searchParams.set("status", statusLabel.toLowerCase()); - } + 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 = diff --git a/vercel.json b/vercel.json index a2bb7670d..735f4e9bf 100644 --- a/vercel.json +++ b/vercel.json @@ -21,8 +21,6 @@ { "source": "/pulse.html", "destination": "/pulse.html" }, { "source": "/pulse-core.js", "destination": "/pulse-core.js" }, - { "source": "/og/verify/:slug.png", "destination": "/api/og/verify?slug=:slug" }, - { "source": "/verify/:slug", "destination": "/api/verify?slug=:slug" }, { "source": "/verifier", "destination": "/verifier.html" }, From b5ff81fb31ade7475de9fb31285eab783de4379d Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:37:40 -0500 Subject: [PATCH 5/7] Fix verify metadata and OG card rendering --- api/og/verify.ts | 57 +++++++++++++++++++++++++++++-- api/verify.ts | 88 +++++++++++++++++++++++++++++++++--------------- server.mjs | 12 ++++++- 3 files changed, 127 insertions(+), 30 deletions(-) diff --git a/api/og/verify.ts b/api/og/verify.ts index 3458a9bb1..e9f90e053 100644 --- a/api/og/verify.ts +++ b/api/og/verify.ts @@ -2,6 +2,9 @@ import { PNG } from "pngjs"; import QRCode from "qrcode"; import { parseSlug } from "../../src/utils/verifySigil"; +import { renderVerifiedOgPng } from "../../src/og/renderVerifiedOg"; +import { getCapsuleByHash, getCapsuleByVerifierSlug } from "../../src/og/capsuleStore"; +import { qrDataURL } from "../../src/lib/qr"; type PngInstance = InstanceType & { data: Buffer }; type PngSyncWriter = { sync: { write: (png: PngInstance) => Buffer } }; @@ -169,6 +172,37 @@ function statusFromQuery(value: string | null): "verified" | "failed" | "standby return "standby"; } +function safeParseSlug(raw: string) { + try { + return parseSlug(raw); + } catch { + const cleaned = raw.trim(); + return { raw: cleaned, pulse: null, shortSig: null, verifiedAtPulse: null }; + } +} + +function buildSlugCandidates(slugRaw: string, slug: { raw: string; pulse: number | null; shortSig: string | null }): string[] { + const set = new Set(); + const rawTrim = slugRaw.trim(); + if (rawTrim) set.add(rawTrim); + if (slug.raw) set.add(slug.raw); + if (slug.pulse != null && slug.shortSig) { + set.add(`${slug.pulse}-${slug.shortSig}`); + } + return Array.from(set); +} + +function findCapsuleBySlug(slugRaw: string, slug: { raw: string; pulse: number | null; shortSig: string | null }) { + const candidates = buildSlugCandidates(slugRaw, slug); + for (const candidate of candidates) { + const bySlug = getCapsuleByVerifierSlug(candidate); + if (bySlug) return bySlug; + const byHash = getCapsuleByHash(candidate); + if (byHash) return byHash; + } + return null; +} + async function makeQrMatrix(text: string): Promise<{ size: number; data: boolean[] }> { const qr = QRCode.create(text, { errorCorrectionLevel: "M" }); const data = Array.from(qr.modules.data, (value) => value === 1); @@ -201,7 +235,9 @@ export default async function handler( const base = requestOrigin(req); const url = new URL(req.url ?? "/", base); const slugRaw = url.searchParams.get("slug") ?? ""; - const slug = parseSlug(slugRaw); + const slug = safeParseSlug(slugRaw); + const slugValue = slug.raw || slugRaw; + const capsule = findCapsuleBySlug(slugValue, slug); const pulse = url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); const chakraDay = url.searchParams.get("chakraDay") ?? ""; @@ -210,8 +246,25 @@ export default async function handler( const g16Ok = parseBool(url.searchParams.get("g16")); const status = statusFromQuery(url.searchParams.get("status")); + if (capsule) { + const verifyUrl = `${url.origin}/verify/${encodeURIComponent(capsule.verifierSlug ?? slugValue ?? capsule.capsuleHash)}`; + let qrDataUrl: string | undefined; + try { + qrDataUrl = await qrDataURL(verifyUrl, { size: 280, margin: 1, ecc: "M" }); + } catch { + qrDataUrl = undefined; + } + const pngBuffer = renderVerifiedOgPng({ ...capsule, qrDataUrl }); + res.statusCode = 200; + res.setHeader("Content-Type", "image/png"); + res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=86400"); + res.end(pngBuffer); + return; + } + const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; - const statusColor: Rgba = status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; + const statusColor: Rgba = + status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; const textColor: Rgba = [230, 242, 255, 255]; const png = new PNG({ width: 1200, height: 630 }) as PngInstance; diff --git a/api/verify.ts b/api/verify.ts index 3abf766d4..80d72b410 100644 --- a/api/verify.ts +++ b/api/verify.ts @@ -1,11 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; - -type SlugInfo = { - raw: string; - pulse: number | null; - shortSig: string | null; -}; +import { parseSlug } from "../src/utils/verifySigil"; +import { getCapsuleByHash, getCapsuleByVerifierSlug } from "../src/og/capsuleStore"; type ProofCapsule = { v: "KPV-1"; @@ -22,28 +18,54 @@ type SharedReceipt = { zkVerified?: boolean; }; -function parseSlug(rawSlug: string): SlugInfo { - let raw = (rawSlug || "").trim(); +function statusFromQuery(value: string | null): "verified" | "failed" | "standby" { + const v = value?.toLowerCase().trim(); + if (v === "verified" || v === "ok" || v === "valid") return "verified"; + if (v === "failed" || v === "error" || v === "invalid") return "failed"; + return "standby"; +} + +function safeParseSlug(raw: string) { try { - raw = decodeURIComponent(raw); + return parseSlug(raw); } catch { - // Keep raw as-is when decoding fails (avoid crashing the function). + const cleaned = raw.trim(); + return { raw: cleaned, pulse: null, shortSig: null, verifiedAtPulse: null }; } - const m = raw.match(/^(\d+)-([A-Za-z0-9]+)$/); - if (!m) return { raw, pulse: null, shortSig: null }; - - const pulseNum = Number(m[1]); - const pulse = Number.isFinite(pulseNum) && pulseNum > 0 ? pulseNum : null; - const shortSig = m[2] ? String(m[2]) : null; +} - return { raw, pulse, shortSig }; +function buildSlugCandidates( + slugRaw: string, + slug: { raw: string; pulse: number | null; shortSig: string | null }, + extra?: string, +): string[] { + const set = new Set(); + const rawTrim = slugRaw.trim(); + if (rawTrim) set.add(rawTrim); + if (slug.raw) set.add(slug.raw); + if (slug.pulse != null && slug.shortSig) { + set.add(`${slug.pulse}-${slug.shortSig}`); + } + if (extra) { + const extraTrim = extra.trim(); + if (extraTrim) set.add(extraTrim); + } + return Array.from(set); } -function statusFromQuery(value: string | null): "verified" | "failed" | "standby" { - const v = value?.toLowerCase().trim(); - if (v === "verified" || v === "ok" || v === "valid") return "verified"; - if (v === "failed" || v === "error" || v === "invalid") return "failed"; - return "standby"; +function findCapsuleBySlug( + slugRaw: string, + slug: { raw: string; pulse: number | null; shortSig: string | null }, + extra?: string, +) { + const candidates = buildSlugCandidates(slugRaw, slug, extra); + for (const candidate of candidates) { + const bySlug = getCapsuleByVerifierSlug(candidate); + if (bySlug) return bySlug; + const byHash = getCapsuleByHash(candidate); + if (byHash) return byHash; + } + return null; } function parseProofCapsule(raw: unknown): ProofCapsule | null { @@ -217,25 +239,37 @@ export default async function handler( const requestUrl = new URL(req.url ?? "/", origin); const slugRaw = requestUrl.searchParams.get("slug") ?? ""; - const slug = parseSlug(slugRaw); + const slug = safeParseSlug(slugRaw); const receipt = readReceiptFromParams(requestUrl.searchParams); const receiptSlug = receipt?.proofCapsule.verifierSlug ?? ""; const canonicalSlug = slug.raw || slugRaw || receiptSlug; + const capsule = findCapsuleBySlug(canonicalSlug, slug, receiptSlug); const receiptMatchesSlug = receipt != null && (!canonicalSlug || receipt.proofCapsule.verifierSlug === canonicalSlug); const statusParam = statusFromQuery(requestUrl.searchParams.get("status")); - const status = receiptMatchesSlug && statusParam === "standby" ? "verified" : statusParam; + const hasVerifiedSignal = Boolean(capsule) || receiptMatchesSlug || slug.verifiedAtPulse != null; + const status = statusParam === "standby" && hasVerifiedSignal ? "verified" : statusParam; const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; const receiptPulse = receiptMatchesSlug ? receipt?.proofCapsule.pulse : null; - const pulseLabel = receiptPulse ? String(receiptPulse) : slug.pulse ? String(slug.pulse) : "—"; + const pulseLabel = + receiptPulse != null + ? String(receiptPulse) + : capsule?.verifiedAtPulse != null + ? String(capsule.verifiedAtPulse) + : slug.verifiedAtPulse != null + ? String(slug.verifiedAtPulse) + : slug.pulse + ? String(slug.pulse) + : "—"; const title = `Proof of Breath™ — ${statusLabel}`; const description = `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; - const verifyUrl = `${origin}/verify/${encodeURIComponent(canonicalSlug)}`; + const ogSlug = canonicalSlug || capsule?.verifierSlug || slugRaw; + const verifyUrl = `${origin}/verify/${encodeURIComponent(ogSlug)}`; const ogUrl = new URL(`${origin}/api/og/verify`); - ogUrl.searchParams.set("slug", canonicalSlug); + ogUrl.searchParams.set("slug", ogSlug); ogUrl.searchParams.set("status", status); if (receiptMatchesSlug && receipt) { ogUrl.searchParams.set("pulse", String(receipt.proofCapsule.pulse)); diff --git a/server.mjs b/server.mjs index 4333c44f8..ddf897199 100644 --- a/server.mjs +++ b/server.mjs @@ -47,6 +47,12 @@ const shortPhiKey = (phiKey) => { return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; }; +const baseVerifierSlug = (value) => { + const match = String(value || "").trim().match(/^(\d+)-([A-Za-z0-9]+)(?:-(\d+))?$/); + if (!match) return null; + return `${match[1]}-${match[2]}`; +}; + const stripQuotes = (value) => String(value || "").replace(/\"/g, ""); const sendFile = (res, filePath, cacheControl) => { @@ -124,7 +130,11 @@ async function createServer() { } let record = null; if (pathname.startsWith("/verify/")) { - record = og.getCapsuleByVerifierSlug(slug) ?? og.getCapsuleByHash(slug); + const baseSlug = baseVerifierSlug(slug); + record = + og.getCapsuleByVerifierSlug(slug) ?? + (baseSlug ? og.getCapsuleByVerifierSlug(baseSlug) : null) ?? + og.getCapsuleByHash(slug); } else { record = og.getCapsuleByHash(slug); } From 159bfa965a48a5b27e131025c20db8ee0b275f62 Mon Sep 17 00:00:00 2001 From: Kojib <123880127+kojibai@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:41:51 -0500 Subject: [PATCH 6/7] Revert "Fix verify metadata and deterministic OG image for verified links" --- api/og/verify.ts | 57 ++----------------------------- api/verify.ts | 88 +++++++++++++++--------------------------------- server.mjs | 12 +------ 3 files changed, 30 insertions(+), 127 deletions(-) diff --git a/api/og/verify.ts b/api/og/verify.ts index e9f90e053..3458a9bb1 100644 --- a/api/og/verify.ts +++ b/api/og/verify.ts @@ -2,9 +2,6 @@ import { PNG } from "pngjs"; import QRCode from "qrcode"; import { parseSlug } from "../../src/utils/verifySigil"; -import { renderVerifiedOgPng } from "../../src/og/renderVerifiedOg"; -import { getCapsuleByHash, getCapsuleByVerifierSlug } from "../../src/og/capsuleStore"; -import { qrDataURL } from "../../src/lib/qr"; type PngInstance = InstanceType & { data: Buffer }; type PngSyncWriter = { sync: { write: (png: PngInstance) => Buffer } }; @@ -172,37 +169,6 @@ function statusFromQuery(value: string | null): "verified" | "failed" | "standby return "standby"; } -function safeParseSlug(raw: string) { - try { - return parseSlug(raw); - } catch { - const cleaned = raw.trim(); - return { raw: cleaned, pulse: null, shortSig: null, verifiedAtPulse: null }; - } -} - -function buildSlugCandidates(slugRaw: string, slug: { raw: string; pulse: number | null; shortSig: string | null }): string[] { - const set = new Set(); - const rawTrim = slugRaw.trim(); - if (rawTrim) set.add(rawTrim); - if (slug.raw) set.add(slug.raw); - if (slug.pulse != null && slug.shortSig) { - set.add(`${slug.pulse}-${slug.shortSig}`); - } - return Array.from(set); -} - -function findCapsuleBySlug(slugRaw: string, slug: { raw: string; pulse: number | null; shortSig: string | null }) { - const candidates = buildSlugCandidates(slugRaw, slug); - for (const candidate of candidates) { - const bySlug = getCapsuleByVerifierSlug(candidate); - if (bySlug) return bySlug; - const byHash = getCapsuleByHash(candidate); - if (byHash) return byHash; - } - return null; -} - async function makeQrMatrix(text: string): Promise<{ size: number; data: boolean[] }> { const qr = QRCode.create(text, { errorCorrectionLevel: "M" }); const data = Array.from(qr.modules.data, (value) => value === 1); @@ -235,9 +201,7 @@ export default async function handler( const base = requestOrigin(req); const url = new URL(req.url ?? "/", base); const slugRaw = url.searchParams.get("slug") ?? ""; - const slug = safeParseSlug(slugRaw); - const slugValue = slug.raw || slugRaw; - const capsule = findCapsuleBySlug(slugValue, slug); + const slug = parseSlug(slugRaw); const pulse = url.searchParams.get("pulse") ?? (slug.pulse ? String(slug.pulse) : "NA"); const chakraDay = url.searchParams.get("chakraDay") ?? ""; @@ -246,25 +210,8 @@ export default async function handler( const g16Ok = parseBool(url.searchParams.get("g16")); const status = statusFromQuery(url.searchParams.get("status")); - if (capsule) { - const verifyUrl = `${url.origin}/verify/${encodeURIComponent(capsule.verifierSlug ?? slugValue ?? capsule.capsuleHash)}`; - let qrDataUrl: string | undefined; - try { - qrDataUrl = await qrDataURL(verifyUrl, { size: 280, margin: 1, ecc: "M" }); - } catch { - qrDataUrl = undefined; - } - const pngBuffer = renderVerifiedOgPng({ ...capsule, qrDataUrl }); - res.statusCode = 200; - res.setHeader("Content-Type", "image/png"); - res.setHeader("Cache-Control", "public, max-age=3600, s-maxage=86400"); - res.end(pngBuffer); - return; - } - const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; - const statusColor: Rgba = - status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; + const statusColor: Rgba = status === "verified" ? [56, 231, 166, 255] : status === "failed" ? [255, 107, 107, 255] : [181, 199, 221, 255]; const textColor: Rgba = [230, 242, 255, 255]; const png = new PNG({ width: 1200, height: 630 }) as PngInstance; diff --git a/api/verify.ts b/api/verify.ts index 80d72b410..3abf766d4 100644 --- a/api/verify.ts +++ b/api/verify.ts @@ -1,7 +1,11 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { parseSlug } from "../src/utils/verifySigil"; -import { getCapsuleByHash, getCapsuleByVerifierSlug } from "../src/og/capsuleStore"; + +type SlugInfo = { + raw: string; + pulse: number | null; + shortSig: string | null; +}; type ProofCapsule = { v: "KPV-1"; @@ -18,54 +22,28 @@ type SharedReceipt = { zkVerified?: boolean; }; -function statusFromQuery(value: string | null): "verified" | "failed" | "standby" { - const v = value?.toLowerCase().trim(); - if (v === "verified" || v === "ok" || v === "valid") return "verified"; - if (v === "failed" || v === "error" || v === "invalid") return "failed"; - return "standby"; -} - -function safeParseSlug(raw: string) { +function parseSlug(rawSlug: string): SlugInfo { + let raw = (rawSlug || "").trim(); try { - return parseSlug(raw); + raw = decodeURIComponent(raw); } catch { - const cleaned = raw.trim(); - return { raw: cleaned, pulse: null, shortSig: null, verifiedAtPulse: null }; + // Keep raw as-is when decoding fails (avoid crashing the function). } -} + const m = raw.match(/^(\d+)-([A-Za-z0-9]+)$/); + if (!m) return { raw, pulse: null, shortSig: null }; -function buildSlugCandidates( - slugRaw: string, - slug: { raw: string; pulse: number | null; shortSig: string | null }, - extra?: string, -): string[] { - const set = new Set(); - const rawTrim = slugRaw.trim(); - if (rawTrim) set.add(rawTrim); - if (slug.raw) set.add(slug.raw); - if (slug.pulse != null && slug.shortSig) { - set.add(`${slug.pulse}-${slug.shortSig}`); - } - if (extra) { - const extraTrim = extra.trim(); - if (extraTrim) set.add(extraTrim); - } - return Array.from(set); + const pulseNum = Number(m[1]); + const pulse = Number.isFinite(pulseNum) && pulseNum > 0 ? pulseNum : null; + const shortSig = m[2] ? String(m[2]) : null; + + return { raw, pulse, shortSig }; } -function findCapsuleBySlug( - slugRaw: string, - slug: { raw: string; pulse: number | null; shortSig: string | null }, - extra?: string, -) { - const candidates = buildSlugCandidates(slugRaw, slug, extra); - for (const candidate of candidates) { - const bySlug = getCapsuleByVerifierSlug(candidate); - if (bySlug) return bySlug; - const byHash = getCapsuleByHash(candidate); - if (byHash) return byHash; - } - return null; +function statusFromQuery(value: string | null): "verified" | "failed" | "standby" { + const v = value?.toLowerCase().trim(); + if (v === "verified" || v === "ok" || v === "valid") return "verified"; + if (v === "failed" || v === "error" || v === "invalid") return "failed"; + return "standby"; } function parseProofCapsule(raw: unknown): ProofCapsule | null { @@ -239,37 +217,25 @@ export default async function handler( const requestUrl = new URL(req.url ?? "/", origin); const slugRaw = requestUrl.searchParams.get("slug") ?? ""; - const slug = safeParseSlug(slugRaw); + const slug = parseSlug(slugRaw); const receipt = readReceiptFromParams(requestUrl.searchParams); const receiptSlug = receipt?.proofCapsule.verifierSlug ?? ""; const canonicalSlug = slug.raw || slugRaw || receiptSlug; - const capsule = findCapsuleBySlug(canonicalSlug, slug, receiptSlug); const receiptMatchesSlug = receipt != null && (!canonicalSlug || receipt.proofCapsule.verifierSlug === canonicalSlug); const statusParam = statusFromQuery(requestUrl.searchParams.get("status")); - const hasVerifiedSignal = Boolean(capsule) || receiptMatchesSlug || slug.verifiedAtPulse != null; - const status = statusParam === "standby" && hasVerifiedSignal ? "verified" : statusParam; + const status = receiptMatchesSlug && statusParam === "standby" ? "verified" : statusParam; const statusLabel = status === "verified" ? "VERIFIED" : status === "failed" ? "FAILED" : "STANDBY"; const receiptPulse = receiptMatchesSlug ? receipt?.proofCapsule.pulse : null; - const pulseLabel = - receiptPulse != null - ? String(receiptPulse) - : capsule?.verifiedAtPulse != null - ? String(capsule.verifiedAtPulse) - : slug.verifiedAtPulse != null - ? String(slug.verifiedAtPulse) - : slug.pulse - ? String(slug.pulse) - : "—"; + const pulseLabel = receiptPulse ? String(receiptPulse) : slug.pulse ? String(slug.pulse) : "—"; const title = `Proof of Breath™ — ${statusLabel}`; const description = `Proof of Breath™ • ${statusLabel} • Pulse ${pulseLabel}`; - const ogSlug = canonicalSlug || capsule?.verifierSlug || slugRaw; - const verifyUrl = `${origin}/verify/${encodeURIComponent(ogSlug)}`; + const verifyUrl = `${origin}/verify/${encodeURIComponent(canonicalSlug)}`; const ogUrl = new URL(`${origin}/api/og/verify`); - ogUrl.searchParams.set("slug", ogSlug); + ogUrl.searchParams.set("slug", canonicalSlug); ogUrl.searchParams.set("status", status); if (receiptMatchesSlug && receipt) { ogUrl.searchParams.set("pulse", String(receipt.proofCapsule.pulse)); diff --git a/server.mjs b/server.mjs index ddf897199..4333c44f8 100644 --- a/server.mjs +++ b/server.mjs @@ -47,12 +47,6 @@ const shortPhiKey = (phiKey) => { return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; }; -const baseVerifierSlug = (value) => { - const match = String(value || "").trim().match(/^(\d+)-([A-Za-z0-9]+)(?:-(\d+))?$/); - if (!match) return null; - return `${match[1]}-${match[2]}`; -}; - const stripQuotes = (value) => String(value || "").replace(/\"/g, ""); const sendFile = (res, filePath, cacheControl) => { @@ -130,11 +124,7 @@ async function createServer() { } let record = null; if (pathname.startsWith("/verify/")) { - const baseSlug = baseVerifierSlug(slug); - record = - og.getCapsuleByVerifierSlug(slug) ?? - (baseSlug ? og.getCapsuleByVerifierSlug(baseSlug) : null) ?? - og.getCapsuleByHash(slug); + record = og.getCapsuleByVerifierSlug(slug) ?? og.getCapsuleByHash(slug); } else { record = og.getCapsuleByHash(slug); } From 173bc581235b21e9aa2c33ec56c91bc437aa45c3 Mon Sep 17 00:00:00 2001 From: Kojib Date: Tue, 27 Jan 2026 05:09:12 -0500 Subject: [PATCH 7/7] v42.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Phi Network — Release Notes (v42.4.0) **Codename:** *Kairos Kurrency Receipt Standard* **Classification:** Public Verification Artifact Upgrade **Status:** Deployed / Active --- ### Executive Summary Version **42.4.0** establishes the **Kairos Kurrency Verified Card** as a first-class, audit-grade visual instrument: a sovereign, banknote-style proof surface that renders **steward verification, valuation, and cryptographic identifiers** with consistent, non-overlapping layout and strict provenance rules. This release upgrades the verified card from “pretty receipt” to **usable verification instrument** suitable for public distribution, archiving, and cross-system audit. --- ## What’s New ### 1) Sovereign Banknote Header Standard * Introduced the official header block: * **KAIROS KURRENCY** * **ISSUED UNDER YAHUAH’S LAW OF ETERNAL LIGHT — Φ • KAI-TURAH** * Maintains a stable “instrument identity” independent of the underlying glyph mode. ### 2) Verified Instrument Modes * The card now renders explicit mode stamps with strict logic: * **VERIFIED ORIGIN** (origin glyph) * **VERIFIED RECEIVE** (derivative/receive glyph) * Mode is resolved with **strict receive detection** (no valuation-flavor inference), ensuring provenance correctness. ### 3) Audit-Grade Hash & Proof Surface The card now consistently publishes the verification identifiers needed to locate and validate the underlying proof pack: * **BUNDLE** hash * **RECEIPT** hash * **ZK** hash * **VERSION** identifier * **VERIFIER** identity marker These are laid out as a structured footer with increased spacing to prevent collisions and preserve readability at poster scale. ### 4) Proof of Breath Seal Integration * Integrated **Proof of Breath** badge as an instrument seal. * Adjusted placement/scale to avoid QR collision: * Badge positioned to remain visible and non-overlapping with the scan surface. ### 5) QR Verification Block * QR block remains the canonical “verification pointer.” * Position refined (moved left) to balance the right-side column with the proof seal and prevent crowding. ### 6) Valuation Metadata Lane * Added a stable valuation line with explicit labeling: * **VALUATION** (pulse + algorithm when available) * **HASH (VALUATION)** (moved slightly down for improved separation) * This is “real data only” — no placeholder randomness. ### 7) Φ Watermark Restoration * Restored the Φ watermark as a subtle security mark: * Larger, heavier weight * Low-opacity fill + light stroke for banknote-grade watermark behavior * Designed to be “barely noticeable,” present primarily as anti-counterfeit visual texture. ### 8) Layout Hardening (No Overlap Policy) This release enforces an explicit non-overlap layout approach: * Left identity/valuation block moved up/left for balance. * Footer moved up and spaced to remain fully on-card. * Footer columns spaced to avoid collisions on narrow renders. * Visual lanes now behave predictably across different proof payload lengths. --- ## Security & Integrity Notes * The card is a **visual instrument**, not the proof itself. * Cryptographic verification remains anchored in the underlying proof bundle / receipt payload. * Rendering is designed to prevent “soft spoofing” by ensuring: * mode correctness (origin vs receive) * stable identifiers (hashes + version + verifier marker) * consistent layout that makes anomalies obvious --- ## Compatibility * SVG output remains standards-compliant and web-safe. * Backward compatible with existing VerifyPage flows. * Supports both: * **Sigil-Seal (Proof)** output * **Redeemable Note (Money)** output paths --- ## Operational Impact * Improves shareability without loss of audit clarity. * Reduces verification ambiguity for recipients reviewing artifacts in feeds, chats, or archives. * Provides a consistent visual format suitable for: * screenshots * posters * printed receipts * government/enterprise compliance records --- ## Version **Phi Network v42.4.0** — *Kairos Kurrency Receipt Standard* Effective immediately. --- public/sw.js | 2 +- src/og/buildVerifiedCardSvg.ts | 896 ++++++++++++++++++++++----------- src/og/sigilEmbed.ts | 2 +- src/version.ts | 2 +- 4 files changed, 599 insertions(+), 303 deletions(-) diff --git a/public/sw.js b/public/sw.js index 55688f685..2825fdb46 100644 --- a/public/sw.js +++ b/public/sw.js @@ -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 = "42.3.1"; // update on release +const APP_VERSION = "42.4.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/og/buildVerifiedCardSvg.ts b/src/og/buildVerifiedCardSvg.ts index 91cdfdd95..426d1176a 100644 --- a/src/og/buildVerifiedCardSvg.ts +++ b/src/og/buildVerifiedCardSvg.ts @@ -6,9 +6,17 @@ import { buildProofOfBreathSeal } from "./proofOfBreathSeal"; export const VERIFIED_CARD_W = 1200; export const VERIFIED_CARD_H = 630; + +const NOTE_TITLE_TEXT = "KAIROS KURRENCY"; +const NOTE_SUBTITLE_TEXT = "ISSUED UNDER YAHUAH’S LAW OF ETERNAL LIGHT — Φ • KAI-TURAH"; + const PHI = (1 + Math.sqrt(5)) / 2; + const phiLogoDataUri = svgToDataUri(phiSvg); +type UnknownRecord = Record; +type CardKind = "proof" | "money"; + function hashStringToInt(value: string): number { let hash = 0; for (let i = 0; i < value.length; i += 1) { @@ -32,46 +40,30 @@ function shortPhiKey(phiKey: string): string { return `${trimmed.slice(0, 6)}…${trimmed.slice(-4)}`; } -function badgeMark(ok: boolean): string { - if (ok) { - return "M18 32 L28 42 L46 18"; - } - return "M20 20 L44 44 M44 20 L20 44"; +function shortHash(value: string | undefined, head = 10, tail = 8): string { + if (!value) return "—"; + if (value.length <= head + tail + 2) return value; + return `${value.slice(0, head)}…${value.slice(-tail)}`; } -type TextMetricsLite = { width: number; ascent: number; descent: number }; - -function measureTextMetrics( - text: string, - font: string, - fontPx: number, - letterSpacingEm = 0, -): TextMetricsLite { - let width = text.length * fontPx * 0.6; - let ascent = fontPx * 0.8; - let descent = fontPx * 0.2; - - let ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null; - if (typeof OffscreenCanvas !== "undefined") { - ctx = new OffscreenCanvas(1, 1).getContext("2d"); - } else if (typeof document !== "undefined") { - const canvas = document.createElement("canvas"); - ctx = canvas.getContext("2d"); - } - - if (ctx) { - ctx.font = font; - const metrics = ctx.measureText(text); - width = metrics.width; - ascent = metrics.actualBoundingBoxAscent || ascent; - descent = metrics.actualBoundingBoxDescent || descent; - } +function shortSerial(value: string, head = 10, tail = 10): string { + const v = value.trim(); + if (v.length <= head + tail + 2) return v; + return `${v.slice(0, head)}…${v.slice(-tail)}`; +} - if (letterSpacingEm > 0 && text.length > 1) { - width += (text.length - 1) * letterSpacingEm * fontPx; - } +function badgeMark(ok: boolean): string { + if (ok) return "M18 32 L28 42 L46 18"; + return "M20 20 L44 44 M44 20 L20 44"; +} - return { width, ascent, descent }; +function escXml(s: string): string { + return s + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); } function dropUndefined>(value: T): T { @@ -89,19 +81,119 @@ function formatUsdValue(value: number | null | undefined): string { return fmtUsd(value); } -function sigilImageMarkup(sigilSvg: string | undefined, clipId: string): string { +function stripPhiPrefix(s: string): string { + return s.replace(/^\s*Φ+\s*/u, ""); +} + +function truncText(s: string, max: number): string { + const t = s.trim(); + if (t.length <= max) return t; + return `${t.slice(0, Math.max(0, max - 1))}…`; +} + +function asRecord(v: unknown): UnknownRecord | null { + return v && typeof v === "object" ? (v as UnknownRecord) : null; +} + +function readString(v: unknown): string | undefined { + return typeof v === "string" ? v : undefined; +} + +function readNumber(v: unknown): number | undefined { + return typeof v === "number" && Number.isFinite(v) ? v : undefined; +} + +function firstString(...vals: unknown[]): string | undefined { + for (const v of vals) { + if (typeof v === "string" && v.trim().length) return v.trim(); + } + return undefined; +} + +/** + * STRICT: "receive" only when the glyph/proof/embedded metadata proves it. + * Never infer receive from valuation flavor alone. + */ +function isReceiveGlyphStrict(data: VerifiedCardData, receiptPayload: unknown, valuationSnapshot: unknown): boolean { + const d = asRecord(data); + const r = asRecord(receiptPayload); + const v = asRecord(valuationSnapshot); + + const explicitMode = + readString(d?.["mode"]) ?? + readString(d?.["verifyMode"]) ?? + readString(d?.["verificationMode"]) ?? + readString(r?.["mode"]) ?? + readString(r?.["proofMode"]) ?? + readString(v?.["mode"]); + + if (explicitMode === "receive") return true; + if (explicitMode === "origin") return false; + + const embedded = + asRecord(d?.["embedded"]) ?? + asRecord(d?.["metadata"]) ?? + asRecord(d?.["sigilMeta"]) ?? + asRecord(d?.["sigilMetadata"]) ?? + asRecord(d?.["sigil"]); + + if (embedded) { + if (typeof embedded["childOfHash"] === "string" && embedded["childOfHash"].length > 0) return true; + if (readNumber(embedded["childIssuedPulse"]) !== undefined) return true; + if (typeof embedded["childAllocationPhi"] === "string" && embedded["childAllocationPhi"].length > 0) return true; + if (embedded["childClaim"] && typeof embedded["childClaim"] === "object") return true; + } + + return false; // strict default +} + +function rosettePath(seed: string): string { + const h = hashStringToInt(seed); + const cx = 600; + const cy = 332; + const R = 220; + const turns = 120; + + const wobble = 0.16 + ((h >>> 5) % 45) / 300; + const k1 = 3 + ((h >>> 9) % 4); + const k2 = 4 + ((h >>> 13) % 4); + const spinA = 5 + ((h >>> 17) % 5); + + let d = `M ${cx} ${cy - R}`; + for (let i = 1; i <= turns; i += 1) { + const t = (i / turns) * 2 * Math.PI; + const r = R * (1 - wobble + wobble * Math.sin(spinA * t)); + const x = cx + r * Math.sin(k1 * t); + const y = cy + r * Math.cos(k2 * t); + d += ` L ${x.toFixed(2)} ${y.toFixed(2)}`; + } + return `${d} Z`; +} + +function sigilSlotMarkup( + sigilSvg: string | undefined, + slot: { x: number; y: number; w: number; h: number; r: number }, + clipId: string, +): string { + const { x, y, w, h } = slot; + if (!sigilSvg) { return ` - - Sigil unavailable + + + SIGIL UNAVAILABLE + `; } + const sanitized = sanitizeSigilSvg(sigilSvg); const dataUri = svgToDataUri(sanitized); + return ` - QR unavailable + + + QR UNAVAILABLE + `; } return ` - + + + + + + SCAN • VERIFY + `; } -function shortHash(value: string | undefined, head = 10, tail = 8): string { - if (!value) return "—"; - if (value.length <= head + tail + 2) return value; - return `${value.slice(0, head)}…${value.slice(-tail)}`; -} +function kasG16BadgesMarkup(kasOk: boolean | null | undefined, g16Ok: boolean, x: number, y: number): string { + const hasKas = typeof kasOk === "boolean"; + const kasStroke = kasOk ? "#37e6d4" : "#ff7d7d"; + const g16Stroke = g16Ok ? "#37e6d4" : "#ff7d7d"; + + const badge = (label: string, ok: boolean, stroke: string, bx: number, by: number) => ` + + + ${escXml(label)} + + + + + + + + `; -function sealPlacement(seedValue: string): { x: number; y: number; rotation: number; dash: string } { - const hash = hashStringToInt(seedValue); - const offsetX = (hash % 48) - 24; - const offsetY = ((hash >> 6) % 24) - 12; - const rotation = ((hash % 41) - 20) * 0.5; - const dashA = 4 + (hash % 5); - const dashB = 3 + ((hash >> 3) % 4); - return { x: 1008 + offsetX, y: 92 + offsetY, rotation, dash: `${dashA} ${dashB}` }; -} + if (!hasKas) return badge("G16", g16Ok, g16Stroke, x, y); -function sealBrandIcon(x: number, y: number, palette: { primary: string; accent: string }): string { return ` - - - - + ${badge("KAS", Boolean(kasOk), kasStroke, x, y)} + ${badge("G16", g16Ok, g16Stroke, x + 156, y)} `; } -function ornamentMarkup( - x: number, - y: number, - width: number, - height: number, - palette: { primary: string; secondary: string; accent: string }, - geometry: { tickCount: number; polygonSides: number }, -): string { - const inset = 16; - const left = x + inset; - const right = x + width - inset; - const top = y + inset; - const bottom = y + height - inset; - const cx = x + width - inset - 24; - const cy = y + inset + 24; - const ticks = Array.from({ length: Math.min(geometry.tickCount, 18) }).map((_, idx) => { - const t = (idx / Math.min(geometry.tickCount, 18)) * Math.PI * 0.6 + Math.PI * 1.2; - const inner = 18; - const outer = 26; - const x1 = cx + Math.cos(t) * inner; - const y1 = cy + Math.sin(t) * inner; - const x2 = cx + Math.cos(t) * outer; - const y2 = cy + Math.sin(t) * outer; - return ``; - }); - const poly = Array.from({ length: geometry.polygonSides }).map((_, idx) => { - const t = (idx / geometry.polygonSides) * Math.PI * 2 - Math.PI / 2; - const px = cx + Math.cos(t) * 12; - const py = cy + Math.sin(t) * 12; - return `${idx === 0 ? "M" : "L"}${px.toFixed(2)} ${py.toFixed(2)}`; - }); - return ` - - - - - ${ticks.join("\n")} - - - `; +/** + * Text measurement (best effort): + * - Uses OffscreenCanvas/Canvas measureText when available + * - Falls back to conservative estimate (overestimates width to prevent overflow) + */ +function measureTextWidthPx(text: string, font: string, fontPx: number, letterSpacingPx = 0): number { + let width = text.length * (fontPx * 0.78) + Math.max(0, text.length - 1) * letterSpacingPx; + + let ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null; + if (typeof OffscreenCanvas !== "undefined") { + ctx = new OffscreenCanvas(1, 1).getContext("2d"); + } else if (typeof document !== "undefined") { + const c = document.createElement("canvas"); + ctx = c.getContext("2d"); + } + + if (ctx) { + ctx.font = font; + const m = ctx.measureText(text); + width = m.width + Math.max(0, text.length - 1) * letterSpacingPx; + } + + return width; +} + +function wrapTextToWidth(text: string, maxWidthPx: number, font: string, fontPx: number, letterSpacingPx = 0): string[] { + const clean = text.trim().replace(/\s+/g, " "); + if (!clean) return []; + + const words = clean.split(" "); + const lines: string[] = []; + let cur = ""; + + for (const w of words) { + const next = cur ? `${cur} ${w}` : w; + const wpx = measureTextWidthPx(next, font, fontPx, letterSpacingPx); + if (wpx <= maxWidthPx) { + cur = next; + continue; + } + if (cur) lines.push(cur); + cur = w; + } + + if (cur) lines.push(cur); + return lines; } export function buildVerifiedCardSvg(data: VerifiedCardData): string { + return buildKvrSvg(data, "proof"); +} + +export function buildRedeemableNoteSvg(data: VerifiedCardData): string { + return buildKvrSvg(data, "money"); +} + +function buildKvrSvg(data: VerifiedCardData, kind: CardKind): string { const { capsuleHash, verifiedAtPulse, phikey, kasOk, g16Ok, sigilSvg, qrDataUrl, svgHash, receiptHash } = data; + const { accent, accentSoft, accentGlow } = accentFromHash(capsuleHash); - const id = `og-${hashStringToInt(capsuleHash).toString(16)}`; + + const id = `kvr-${hashStringToInt(`${capsuleHash}|${verifiedAtPulse}|${kind}`).toString(16)}`; const sigilClipId = `${id}-sigil-clip`; const qrClipId = `${id}-qr-clip`; - const ringGradientId = `${id}-ring`; - const glowId = `${id}-glow`; - const waveId = `${id}-wave`; - const badgeGlowId = `${id}-badge-glow`; - const sealId = `${id}-seal`; - const hasKas = typeof kasOk === "boolean"; - const sealSeed = `${capsuleHash}|${svgHash ?? ""}|${verifiedAtPulse}`; - const seal = sealPlacement(sealSeed); + const legalClipId = `${id}-legal-clip`; const phiShort = shortPhiKey(phikey); - const valuationSnapshot = data.valuation ? { ...data.valuation } : data.receipt?.valuation ? { ...data.receipt.valuation } : undefined; + + const valuationSnapshot = + data.valuation ? { ...data.valuation } : data.receipt?.valuation ? { ...data.receipt.valuation } : undefined; + if (valuationSnapshot && "valuationHash" in valuationSnapshot) { delete (valuationSnapshot as { valuationHash?: string }).valuationHash; } + const valuationHash = data.valuation?.valuationHash ?? data.receipt?.valuationHash; - const valuationPhi = formatPhiValue(valuationSnapshot?.phiValue); + const valuationPhiRaw = formatPhiValue(valuationSnapshot?.phiValue); + const valuationPhiText = stripPhiPrefix(valuationPhiRaw); const valuationUsd = formatUsdValue(valuationSnapshot?.usdValue); - const isReceiveMode = valuationSnapshot?.mode === "receive"; - const valuationModeLabel = isReceiveMode ? "RECEIVE" : "ORIGIN"; - const headlineText = isReceiveMode ? "VERIFIED RECEIVE" : "VERIFIED ORIGIN"; + + const vrec = asRecord(valuationSnapshot); + const valuationPulse = + readNumber(vrec?.["pulse"]) ?? + readNumber(vrec?.["valuationPulse"]) ?? + readNumber(vrec?.["atPulse"]) ?? + readNumber(vrec?.["computedPulse"]); + + const valuationAlg = firstString(vrec?.["valuationAlg"], vrec?.["alg"], vrec?.["algorithm"], vrec?.["policyAlg"], vrec?.["policy"]); + const valuationStamp = firstString( + vrec?.["valuationStamp"], + vrec?.["stamp"], + vrec?.["hash"], + typeof valuationHash === "string" ? valuationHash : undefined, + ); const receiptPayload = data.receipt ?? @@ -233,29 +355,37 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { verificationVersion: data.verificationVersion, } : undefined); - const receiptMeta: Record = {}; + const bundleHashValue = receiptPayload?.bundleHash ?? data.bundleHash; const zkPoseidonHash = receiptPayload?.zkPoseidonHash ?? data.zkPoseidonHash; const verifier = receiptPayload?.verifier ?? data.verifier; const verificationVersion = receiptPayload?.verificationVersion ?? data.verificationVersion; + + const isReceiveMode = isReceiveGlyphStrict(data, receiptPayload, valuationSnapshot); + const verifiedStampText = isReceiveMode ? "VERIFIED RECEIVE" : "VERIFIED ORIGIN"; + + const variantLine = + kind === "money" + ? isReceiveMode + ? "RECEIVE MONEY" + : "ORIGIN MONEY" + : isReceiveMode + ? "RECEIVE PROOF" + : "ORIGIN PROOF"; + + const receiptMeta: Record = {}; if (bundleHashValue) receiptMeta.bundleHash = bundleHashValue; if (zkPoseidonHash) receiptMeta.zkPoseidonHash = zkPoseidonHash; if (verifier) receiptMeta.verifier = verifier; if (verificationVersion) receiptMeta.verificationVersion = verificationVersion; receiptMeta.verifiedAtPulse = receiptPayload?.verifiedAtPulse ?? verifiedAtPulse; + receiptMeta.assetMode = isReceiveMode ? "receive" : "origin"; + receiptMeta.artifactKind = kind; if (receiptPayload) receiptMeta.receipt = receiptPayload; if (data.receiptHash) receiptMeta.receiptHash = data.receiptHash; if (data.verificationSig) receiptMeta.verificationSig = data.verificationSig; const receiptJson = JSON.stringify(receiptMeta); - const proofSeal = buildProofOfBreathSeal({ - bundleHash: bundleHashValue, - capsuleHash, - svgHash, - receiptHash, - pulse: verifiedAtPulse, - }); - const auditMeta = dropUndefined({ receiptHash: data.receiptHash, valuation: valuationSnapshot, @@ -263,217 +393,383 @@ export function buildVerifiedCardSvg(data: VerifiedCardData): string { bundleHash: bundleHashValue, zkPoseidonHash, verifiedAtPulse: receiptPayload?.verifiedAtPulse ?? verifiedAtPulse, + artifactKind: kind, + assetMode: isReceiveMode ? "receive" : "origin", }); const auditJson = JSON.stringify(auditMeta); + const proofSeal = buildProofOfBreathSeal({ + bundleHash: bundleHashValue, + capsuleHash, + svgHash, + receiptHash, + pulse: verifiedAtPulse, + }); + + const slot = { x: 360, y: 198, w: 480, h: 300, r: 18 }; + const slotCenterX = slot.x + slot.w / 2; + + // Proof badge down slightly; QR moved left + const proofSealSize = 168; + const proofSealX = 960; + const proofSealY = 144; const proofSealLabel = "PROOF OF BREATH™"; const proofSealMicro = `${shortHash(capsuleHash, 6, 4)} · ${shortHash(bundleHashValue, 6, 4)}`; - const proofSealSizeDefault = 240; - const proofSealRadiusDefault = Math.floor(proofSealSizeDefault / 2); - const proofSealRadiusMin = 120; - const proofSealXDefault = 600; - const proofSealY = 312; - const sigilFrameX = 796; - const sigilFrameY = 136; - const sigilFrameSize = 348; - const unit = 22; - const phiGap = Math.round(unit * PHI); - const titleY = 134; - const subheadY = titleY + 42; - const modeLabelY = subheadY + 34; - const badgeLabelY = modeLabelY + Math.round(unit * 2.15); - const valueLabelY = badgeLabelY + phiGap; - const valueY = valueLabelY + Math.round(unit * 1.4); - const usdLabelY = valueY + phiGap; - const usdValueY = usdLabelY + Math.round(unit * 1.4); - const qrBoxX = 128; - const qrBoxY = usdValueY + Math.round(phiGap * 0.45); - const qrBoxW = 288; - const qrBoxH = 140; - const brandX = VERIFIED_CARD_W / 2; - const brandY = 78; - const brandText = "SIGIL-SEAL"; - const brandFontPx = 14; - const brandLetterSpacingEm = 0.42; - const brandFont = `600 ${brandFontPx}px Inter, "Segoe UI", "Helvetica Neue", Arial, sans-serif`; - const brandMetrics = measureTextMetrics(brandText, brandFont, brandFontPx, brandLetterSpacingEm); - const brandIconSize = 12; - const brandIconGap = Math.round(brandFontPx * 0.6); - const brandGroupWidth = brandMetrics.width + brandIconSize + brandIconGap; - const brandTextX = brandX - brandGroupWidth / 2 + brandIconSize + brandIconGap; - const brandIconX = brandX - brandGroupWidth / 2 + brandIconSize / 2; - const labelFontPx = 22; - const labelLetterSpacingEm = 0.08; - const labelFont = `700 ${labelFontPx}px Inter, "Segoe UI", "Helvetica Neue", Arial, sans-serif`; - const g16LabelX = hasKas ? 470 : 320; - const g16Metrics = measureTextMetrics("G16", labelFont, labelFontPx, labelLetterSpacingEm); - const g16BadgeSize = Math.round(labelFontPx * 1.25); - const g16BadgeGap = Math.round(labelFontPx * 0.45); - const g16BadgeX = g16LabelX + g16Metrics.width + g16BadgeGap; - const g16BadgeY = badgeLabelY - Math.round(Math.max(g16Metrics.ascent * 0.9, g16BadgeSize * 0.85)); - const g16BadgeRadius = Math.round(g16BadgeSize * 0.26); - const g16BadgeStroke = Math.max(1.4, g16BadgeSize * 0.033); - const g16MarkScale = (g16BadgeSize / 54) * 0.5; - const g16MarkTranslate = (g16BadgeSize / 54) * 13.5; - const valueFontPx = 30; - const valueFont = `700 ${valueFontPx}px Inter, "Segoe UI", "Helvetica Neue", Arial, sans-serif`; - const valueMetrics = measureTextMetrics(valuationPhi, valueFont, valueFontPx); - const usdMetrics = measureTextMetrics(valuationUsd, valueFont, valueFontPx); - const leftValueRightEdge = 320 + Math.max(valueMetrics.width, usdMetrics.width); - const safeGap = 24; - const badgeLeftBound = leftValueRightEdge + safeGap; - let proofSealX = proofSealXDefault; - let allowedRadius = proofSealX - badgeLeftBound; - let proofSealRadius = Math.min(proofSealRadiusDefault, allowedRadius); - if (proofSealRadius < proofSealRadiusMin) { - const neededShift = Math.min(24, proofSealRadiusMin - proofSealRadius); - proofSealX += neededShift; - allowedRadius = proofSealX - badgeLeftBound; - proofSealRadius = Math.min(proofSealRadiusDefault, allowedRadius); - } - proofSealRadius = Math.max(0, proofSealRadius); - const proofSealSize = proofSealRadius * 2; const proofSealMarkup = proofSeal.toSvg(proofSealX, proofSealY, proofSealSize, id, proofSealLabel, proofSealMicro); - const phiKeyLabelY = Math.round(proofSealY + proofSealSize / 2 + phiGap * 0.7); - const phiKeyValueY = phiKeyLabelY + Math.round(unit * 1.9); - const brandPalette = proofSeal.palette; - const ornament = ornamentMarkup(810, 150, 320, 320, brandPalette, proofSeal.geometry); + + const qrX = 980; + const qrY = 344; + + const rosette = rosettePath(`${capsuleHash}|${svgHash ?? ""}|${verifiedAtPulse}|${kind}`); + + const serialCore = (zkPoseidonHash ? zkPoseidonHash.slice(0, 12).toUpperCase() : capsuleHash.slice(0, 12).toUpperCase()).replace( + /[^0-9A-F]/g, + "Φ", + ); + const serial = `KVR-${serialCore}-${String(verifiedAtPulse)}`; + const serialDisplay = shortSerial(serial, 10, 10); + + const microLine = escXml( + `KAIROS KURRENSY • LAWFUL UNDER YAHUAH • Φ • ${variantLine} • ${verifiedStampText} • STEWARD VERIFIED @ PULSE ${verifiedAtPulse} • SERIAL ${serial} • BUNDLE ${shortHash(bundleHashValue, 10, 8)} • RECEIPT ${shortHash(receiptHash, 10, 8)}`, + ); + + const drec = asRecord(data); + const embedded = + asRecord(drec?.["embedded"]) ?? + asRecord(drec?.["metadata"]) ?? + asRecord(drec?.["sigilMeta"]) ?? + asRecord(drec?.["sigilMetadata"]) ?? + asRecord(drec?.["sigil"]); + + const childOfHash = readString(embedded?.["childOfHash"]); + const childClaim = asRecord(embedded?.["childClaim"]); + const claimSteps = readNumber(childClaim?.["steps"]); + const claimExpireAtPulse = readNumber(childClaim?.["expireAtPulse"]); + + const moneyRedeemTitle = isReceiveMode ? "REDEEMABLE CLAIM — RECEIVE NOTE" : "REDEEMABLE CLAIM — ORIGIN NOTE"; + const moneyRedeemPolicy = valuationHash ? `POLICY HASH ${shortHash(String(valuationHash))}` : "POLICY HASH —"; + const moneyRedeemLineage = childOfHash ? `LINEAGE ${shortHash(childOfHash, 10, 8)}` : "LINEAGE —"; + const moneyRedeemWindow = + claimSteps !== undefined && claimExpireAtPulse !== undefined + ? `CLAIM WINDOW ${claimSteps} STEPS • EXPIRES @ PULSE ${claimExpireAtPulse}` + : "CLAIM WINDOW —"; + + const headerY1 = 78; + const headerY2 = 102; + const stampY = 152; + const stewardY = 178; + const variantY = 202; + + const leftValueLabel = kind === "money" ? "VALUE" : "Φ VALUE"; + + // Bottom panel + footer spacing fixes + const belt1Y = 556; + const belt2Y = 574; + + // Valuation + footer pushed up and spaced so nothing overlaps + const valuation1Y = 548; // moved up a lot (kept on-card) + const valuation2Y = 564; // spacing from valuation1Y + + const footerTopY = 588; // moved up + const footerRow1Y = footerTopY; + const footerRow2Y = footerTopY + 16; // enough separation + + // Caption + legal + const slotBottom = slot.y + slot.h; + const panelTop = slotBottom + Math.round(4 * PHI); + const captionY = Math.round(panelTop + Math.round(7 * PHI)); + const captionGap = Math.round(4 * PHI); + + const legalFontCandidates: Array<{ fontPx: number; lineH: number }> = [ + { fontPx: 11, lineH: 13 }, + { fontPx: 10, lineH: 12 }, + { fontPx: 9, lineH: 11 }, + { fontPx: 8, lineH: 10 }, + ]; + + const legalText = + kind === "money" + ? `This note is redeemable in Kairos under Yahuah’s Law by the stated policy and claim window. Redemption is steward-verified and bound to the ΦKey and proof bundle embedded in this note.` + : `This Sigil-Seal is proof of stewardship in Kairos under Yahuah’s Law. It attests that the steward verified the embedded proof bundle at the stated pulse. It is a verification instrument, not a redeemable note.`; + + const legalOuterWMax = 640; + const legalOuterWMin = 420; + const legalPadX = Math.round(8 * PHI); + const legalPadY = Math.round(3 * PHI); + + const legalLeftBound = 280; + const legalRightBound = qrX - Math.round(2 * PHI); + const maxPossibleW = Math.max(legalOuterWMin, Math.min(legalOuterWMax, legalRightBound - legalLeftBound)); + + const legalOuterW = maxPossibleW; + const legalOuterX = Math.round(Math.max(legalLeftBound, Math.min(legalRightBound - legalOuterW, slotCenterX - legalOuterW / 2))); + + const legalY = Math.round(Math.max(captionY + captionGap, panelTop + Math.round(8 * PHI))); + const legalMaxH = Math.max(0, belt1Y - Math.round(4 * PHI) - legalY); + + const legalInnerW = Math.max(0, legalOuterW - legalPadX * 2); + let chosenFontPx = 9; + let chosenLineH = 11; + let legalLinesRaw: string[] = []; + + for (const cand of legalFontCandidates) { + const font = `600 ${cand.fontPx}px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial`; + const lines = wrapTextToWidth(legalText, legalInnerW, font, cand.fontPx, 0); + const neededH = legalPadY * 2 + lines.length * cand.lineH; + if (neededH <= legalMaxH) { + chosenFontPx = cand.fontPx; + chosenLineH = cand.lineH; + legalLinesRaw = lines; + break; + } + } + + if (legalLinesRaw.length === 0) { + const cand = legalFontCandidates[legalFontCandidates.length - 1]; + const font = `600 ${cand.fontPx}px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial`; + chosenFontPx = cand.fontPx; + chosenLineH = cand.lineH; + legalLinesRaw = wrapTextToWidth(legalText, legalInnerW, font, cand.fontPx, 0); + } + + const legalLines = legalLinesRaw.map(escXml); + const legalH = Math.min(legalMaxH, legalPadY * 2 + legalLinesRaw.length * chosenLineH); + const textBlockH = legalLinesRaw.length * chosenLineH; + const textTopY = legalY + Math.max(legalPadY, Math.floor((legalH - textBlockH) / 2)); + const legalCenterX = legalOuterX + legalOuterW / 2; + const slotCaptionY = Math.min(captionY, legalY - Math.round(2 * PHI)); + + const showValuationMeta = valuationPulse !== undefined || Boolean(valuationAlg) || Boolean(valuationStamp); + + // Footer row x spacing (more space; no overlap) + const fx1 = 72; + const fx2 = 460; + const fx3 = 860; + return ` - + ${receiptJson} ${auditJson} + - - - - + + + - - - - + + + + - + + - - - - - - - + + + + ${microLine} + + + + + + + + + - - - - - - + + + + - - + + + + - + + + + + + - + + + + + - + - - + ${escXml(NOTE_TITLE_TEXT)} + ${escXml(NOTE_SUBTITLE_TEXT)} - - - - - - - + ${escXml(verifiedStampText)} + Steward Verified @ Pulse ${escXml(String(verifiedAtPulse))} + ${escXml(variantLine)} - - - + + - ${sealBrandIcon(brandIconX, brandY - 4, { primary: brandPalette.primary, accent: brandPalette.accent })} - ${brandText} + + + + ${escXml(leftValueLabel)} + + ${escXml(valuationPhiText)} - ${headlineText} - Steward Verified @ Pulse ${verifiedAtPulse} - ${valuationModeLabel ? `${valuationModeLabel}` : ""} + USD VALUE + ${escXml(valuationUsd)} - ΦKEY - ${phiShort} + ΦKEY + ${escXml(phiShort)} - ${hasKas ? `KAS` : ""} - ${ - hasKas - ? ` - - - + SERIAL ${escXml(serialDisplay)} + SVG ${escXml(shortHash(svgHash))} + SEAL ${escXml(shortHash(capsuleHash))} + + + + ${kasG16BadgesMarkup(kasOk, g16Ok, 0, 0)} - ` - : "" - } - G16 - - - - + ${ + kind === "money" + ? ` + + + ${escXml(moneyRedeemTitle)} + ${escXml(moneyRedeemPolicy)} + ${escXml(moneyRedeemWindow)} + ${escXml(moneyRedeemLineage)} + ` + : "" + } - Φ VALUE - ${valuationPhi} + + + + + + ${sigilSlotMarkup(sigilSvg, slot, sigilClipId)} - USD VALUE - ${valuationUsd} + SIGIL • SEALED • VERIFIED + - - - - VERIFIED - ${shortHash(capsuleHash, 6, 4)} + + ${proofSealMarkup} + ${qrBlockMarkup(qrDataUrl, qrX, qrY, qrClipId)} + + + + + + ${ + legalLines.length + ? legalLines + .map((line, i) => { + const y = textTopY + chosenLineH * (i + 1); + return `${line}`; + }) + .join("\n") + : "" + } + - ${proofSealMarkup} + + + + + + Φ + - - - ${sigilImageMarkup(sigilSvg, sigilClipId)} - ${ornament} - - ${qrImageMarkup(qrDataUrl, qrClipId, qrBoxX + 12, qrBoxY + 8)} + ${ + showValuationMeta + ? ` + + ${ + valuationPulse !== undefined || valuationAlg + ? `VALUATION${valuationPulse !== undefined ? ` PULSE ${escXml(String(valuationPulse))}` : ""}${ + valuationPulse !== undefined && valuationAlg ? " • " : "" + }${valuationAlg ? `ALGORITHM ${escXml(truncText(valuationAlg, 44))}` : ""}` + : "" + } + ${ + valuationStamp + ? `HASH (VALUATION) ${escXml(shortHash(valuationStamp, 14, 10))}` + : "" + } + + ` + : "" + } + + + + BUNDLE ${escXml(shortHash(bundleHashValue))} + ZK ${escXml(shortHash(zkPoseidonHash))} + VERIFIER ${escXml(verifier ?? "—")} - - BUNDLE ${shortHash(bundleHashValue)} - RECEIPT ${shortHash(receiptHash)} - SVG ${shortHash(svgHash)} - CAPSULE ${shortHash(capsuleHash)} - phi.network + RECEIPT ${escXml(shortHash(receiptHash))} + VERSION ${escXml(verificationVersion ?? "—")} + phi.network `.trim(); diff --git a/src/og/sigilEmbed.ts b/src/og/sigilEmbed.ts index b8c32226c..3f3d34150 100644 --- a/src/og/sigilEmbed.ts +++ b/src/og/sigilEmbed.ts @@ -23,7 +23,7 @@ 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}\"`; + return ` ${String(attr)}="${sanitized}"`; }); } diff --git a/src/version.ts b/src/version.ts index c106308d1..bd05b4954 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 = "42.3.1"; // Canonical offline/PWA version +export const BASE_APP_VERSION = "42.4.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 =