From f766e537baef7c0ffaf891f7730933e680cb909c Mon Sep 17 00:00:00 2001 From: George Lambert Date: Tue, 13 Oct 2026 23:08:55 -0400 Subject: [PATCH] feat: WiX ProductVersion from HelperVersion + build-tray-msi + CERT live QR - build-tray-msi.ps1 candle/light or tip; version from common.ps1 - certificate.html final QR via qrcode CDN when digest present --- docs/COMMERCIAL-INSTALLER-BACKLOG.md | 10 +- docs/PACKAGING.md | 3 +- platform/windows/scripts/build-tray-msi.ps1 | 179 ++++++++++++++++++++ platform/windows/wix/README.md | 31 +++- platform/windows/wix/VeraeTrayShortcuts.wxs | 5 +- site/printit.verae.ai/certificate.html | 90 +++++++++- 6 files changed, 299 insertions(+), 19 deletions(-) create mode 100644 platform/windows/scripts/build-tray-msi.ps1 diff --git a/docs/COMMERCIAL-INSTALLER-BACKLOG.md b/docs/COMMERCIAL-INSTALLER-BACKLOG.md index 59d2fee..09c0d5a 100644 --- a/docs/COMMERCIAL-INSTALLER-BACKLOG.md +++ b/docs/COMMERCIAL-INSTALLER-BACKLOG.md @@ -14,11 +14,11 @@ Repo now includes **authoring/docs scaffolds** for the two commercial gaps that | Scaffold | Path | What it is | What it is not | |----------|------|------------|----------------| -| **WiX tray presence MSI skeleton** | [`platform/windows/wix/`](../platform/windows/wix/) — `VeraeTrayShortcuts.wxs`, `VeraeTrayShortcutsFragment.wxs`, [`README.md`](../platform/windows/wix/README.md) | Non-elevated **per-user** WiX v3 product + shortcut fragment: stages `verae-tray.ps1`, Start Menu `Programs\Verae\Verae Tray.lnk`, Startup `Verae Tray.lnk`. Build when **candle/light** (or adapted WiX v4) are present. | Full elevated commercial MSI (printers, Program Files, Authenticode, NSIS parity, CI-signed MSI in `dist/`). No MSI binary is produced in-repo without the WiX toolset. | +| **WiX tray presence MSI skeleton** | [`platform/windows/wix/`](../platform/windows/wix/) — `VeraeTrayShortcuts.wxs`, `VeraeTrayShortcutsFragment.wxs`, [`README.md`](../platform/windows/wix/README.md); driver [`scripts/build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) | Non-elevated **per-user** WiX v3 product + shortcut fragment: stages `verae-tray.ps1`, Start Menu `Programs\Verae\Verae Tray.lnk`, Startup `Verae Tray.lnk`. **ProductVersion** wired from `Get-VeraePackageVersion` → `internal/job.HelperVersion` (or `VERSION=` / `-Version`). `build-tray-msi.ps1` runs **candle/light** when present; otherwise prints WiX install tip (exit 0). | Full elevated commercial MSI (printers, Program Files, Authenticode, NSIS parity, CI-signed MSI in `dist/`). No MSI binary without the WiX toolset on the build host. | | **Menubar notarize checklist** | [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) | Operator step list: unsigned build → codesign hardened runtime → `ditto` zip → `notarytool` (keychain profile **or** API key placeholders) → staple → `spctl`. No secrets. | Automated sign/notarize in `build-menubar-app.sh`, stapled `.app.zip`/DMG release, or CI keychain import. | **Still use for day-to-day presence (no WiX / no Apple certs):** -Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`. +Windows `scripts/build-tray-startmenu.ps1` + `install-tray-startup.ps1`; macOS `verae-print menubar --install` / unsigned `build-menubar-app.sh`. Full product install remains NSIS (`installer-windows.nsi`) and macOS `pkg/build-pkg.sh`. Tray MSI skeleton build: `scripts/build-tray-msi.ps1` (optional WiX). --- @@ -47,7 +47,7 @@ Related but out of scope for “done” above (still backlog-adjacent): Linux `. | Tray NotifyIcon + GET `/status` | [`platform/windows/scripts/verae-tray.ps1`](../platform/windows/scripts/verae-tray.ps1) | User-level; see [PACKAGING.md](PACKAGING.md) § Windows tray | | Startup-folder tray install | [`platform/windows/scripts/install-tray-startup.ps1`](../platform/windows/scripts/install-tray-startup.ps1) | **Not** MSI; no elevation | | WiX-free Start Menu stub | [`platform/windows/scripts/build-tray-startmenu.ps1`](../platform/windows/scripts/build-tray-startmenu.ps1) | Explicitly “future MSI can supersede” | -| **WiX tray presence skeleton** (per-user, non-elevated) | [`platform/windows/wix/`](../platform/windows/wix/) (`VeraeTrayShortcuts.wxs` + fragment + README) | **Scaffold shipped** — Start Menu + Startup → `verae-tray.ps1` only; needs candle/light to emit MSI | +| **WiX tray presence skeleton** (per-user, non-elevated) | [`platform/windows/wix/`](../platform/windows/wix/) + [`scripts/build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) | **Scaffold shipped** — ProductVersion ← HelperVersion; candle/light when present, else WiX tip; not full product MSI | | Shared version helper | [`platform/windows/scripts/common.ps1`](../platform/windows/scripts/common.ps1) `Get-VeraePackageVersion` | Aligns tray scripts with `HelperVersion` | | End-user Windows readme | [`platform/windows/README-WINDOWS.txt`](../platform/windows/README-WINDOWS.txt), [`README.txt`](../platform/windows/README.txt) | Documents NSIS flow; states **no elevated MSI** for tray | | Packaging policy doc | [PACKAGING.md](PACKAGING.md) | “**No elevated MSI**” for tray presence by design | @@ -146,7 +146,7 @@ NSIS product already covers much of the *install experience* (files under Progra |-------------|--------|------------------| | Windows tray / Startup / Start Menu (user) | Yes | Yes for **presence**; not enterprise MSI | | Windows NSIS `setup.exe` | Yes | **Partial** — full GUI install; needs Authenticode + version align; not MSI | -| Windows **MSI** (WiX) tray presence skeleton | **Yes** (`.wxs` + README under `platform/windows/wix/`) | **No** — scaffold only; needs toolset build + is not full product MSI | +| Windows **MSI** (WiX) tray presence skeleton | **Yes** (`.wxs` + `build-tray-msi.ps1`; ProductVersion ← HelperVersion) | **No** — scaffold only; MSI only when candle/light installed; not full product MSI | | Windows **MSI** full product (NSIS parity) | **No** | **No** | | Windows Authenticode pipeline | **No** (in-repo) | **No** | | macOS menu bar LaunchAgent | Yes | Yes (dev/ops) | @@ -162,7 +162,7 @@ NSIS product already covers much of the *install experience* (files under Progra - **[PACKAGING.md](PACKAGING.md)** — tray, menubar, unsigned `.app`, notarization steps (docs), CERT-01, statuslang, IPP split, smoke tests - [`platform/windows/`](../platform/windows/) — NSIS + tray scripts -- [`platform/windows/wix/`](../platform/windows/wix/) — per-user tray shortcut MSI **skeleton** (candle/light) +- [`platform/windows/wix/`](../platform/windows/wix/) + [`build-tray-msi.ps1`](../platform/windows/scripts/build-tray-msi.ps1) — per-user tray MSI skeleton; ProductVersion ← HelperVersion - [`platform/macos/pkg/`](../platform/macos/pkg/) — commercial `.pkg` build - [`platform/macos/menubar/`](../platform/macos/menubar/) — menu bar + unsigned `.app` + notarize docs/checklist - [`platform/macos/menubar/NOTARIZE-CHECKLIST.md`](../platform/macos/menubar/NOTARIZE-CHECKLIST.md) — menubar notarytool step list diff --git a/docs/PACKAGING.md b/docs/PACKAGING.md index 353e75f..77f6dab 100644 --- a/docs/PACKAGING.md +++ b/docs/PACKAGING.md @@ -188,7 +188,8 @@ Site page: [`site/printit.verae.ai/certificate.html`](../site/printit.verae.ai/c | Load paths | Query string (`?sha256=&receipt=&…`), optional `.verae.json` sidecar drop, “Sample data” button | | Print CSS | Letter page, toolbar hidden, frame colors preserved | | Live verify | Link to `explorer.verae.ai/time?receipt=&sha256=` (same as `pdfcert.LookupURL`) | -| Residual gap | Full **CERT-01.html** pixel match (self-hosted design-kit Fraunces/Cantarell, live QR bitmap, Substrate extrinsic polish) still **not** claimed — web title ships CDN Fraunces + Georgia/serif offline fallback; issued artifact remains PDF (`internal/pdfcert` + `platform/registration-pdf/build.mjs`) | +| Live QR (web) | **Shipped** — final + receipt/digest paints QR via qrcode.js CDN → data-URL; offline/provisional keeps SVG placeholder (print-safe) | +| Residual gap | Full **CERT-01.html** pixel match (self-hosted design-kit Fraunces/Cantarell, Substrate extrinsic polish) still **not** claimed — web title ships CDN Fraunces + Georgia/serif offline fallback; issued artifact remains PDF (`internal/pdfcert` + `platform/registration-pdf/build.mjs`) | Design source of truth for the paper mock: `verae-2026-design/ui-designs/src/html/CERT-01.html`. Cross-product siblings (not owned here): Sign `certificate.html` (SG-P1), Verify ZP-V5, MCP Mac export PDF. diff --git a/platform/windows/scripts/build-tray-msi.ps1 b/platform/windows/scripts/build-tray-msi.ps1 new file mode 100644 index 0000000..a953442 --- /dev/null +++ b/platform/windows/scripts/build-tray-msi.ps1 @@ -0,0 +1,179 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Build the per-user Verae Tray presence MSI (WiX v3 candle/light) when available. + +.DESCRIPTION + Stages verae-tray.ps1 + common.ps1, stamps ProductVersion from + internal/job.HelperVersion via Get-VeraePackageVersion (or $env:VERSION), + then runs candle.exe + light.exe if on PATH (or under Program Files WiX). + + If WiX Toolset is missing, prints an install tip and exits 0 without failing + the day-to-day tray path (use build-tray-startmenu.ps1 instead). + + Scope: non-elevated per-user tray shortcuts only — not the full commercial MSI. + +.PARAMETER Version + Override package version (default: Get-VeraePackageVersion / HelperVersion). + +.PARAMETER OutDir + Output directory for .wixobj and .msi (default: platform/windows/wix/out). + +.PARAMETER StageDir + Staging directory for payload files (default: platform/windows/wix/Stage). + +.PARAMETER WixDir + Directory containing VeraeTrayShortcuts.wxs (+ fragment). + +.PARAMETER SkipTip + When candle/light are missing, exit 1 instead of printing the install tip. + +.EXAMPLE + powershell -NoProfile -ExecutionPolicy Bypass -File build-tray-msi.ps1 + $env:VERSION = "0.3.7"; .\build-tray-msi.ps1 +#> +param( + [string]$Version = "", + [string]$OutDir = "", + [string]$StageDir = "", + [string]$WixDir = "", + [switch]$SkipTip +) + +$ErrorActionPreference = "Stop" + +$common = Join-Path $PSScriptRoot "common.ps1" +if (Test-Path -LiteralPath $common) { . $common } + +if (-not $Version) { + if (Get-Command Get-VeraePackageVersion -ErrorAction SilentlyContinue) { + $Version = Get-VeraePackageVersion + } elseif ($env:VERSION -and $env:VERSION.Trim()) { + $Version = $env:VERSION.Trim() + } else { + $Version = "0.3.6" + } +} + +if (-not $WixDir) { + $WixDir = Join-Path (Split-Path -Parent $PSScriptRoot) "wix" +} +if (-not $StageDir) { + $StageDir = Join-Path $WixDir "Stage" +} +if (-not $OutDir) { + $OutDir = Join-Path $WixDir "out" +} + +$productWxs = Join-Path $WixDir "VeraeTrayShortcuts.wxs" +$fragmentWxs = Join-Path $WixDir "VeraeTrayShortcutsFragment.wxs" +if (-not (Test-Path -LiteralPath $productWxs)) { + Write-Error "Missing WiX product: $productWxs" +} +if (-not (Test-Path -LiteralPath $fragmentWxs)) { + Write-Error "Missing WiX fragment: $fragmentWxs" +} + +function Find-WixTool([string]$Name) { + $cmd = Get-Command $Name -ErrorAction SilentlyContinue + if ($cmd -and $cmd.Source) { return $cmd.Source } + $cmdExe = Get-Command "$Name.exe" -ErrorAction SilentlyContinue + if ($cmdExe -and $cmdExe.Source) { return $cmdExe.Source } + + $roots = @() + if (${env:ProgramFiles(x86)}) { $roots += ${env:ProgramFiles(x86)} } + if ($env:ProgramFiles) { $roots += $env:ProgramFiles } + if ($env:WIX) { + $wixBin = Join-Path $env:WIX "bin" + if (Test-Path -LiteralPath $wixBin) { $roots = @($wixBin) + $roots } + } + + foreach ($root in $roots) { + if (-not $root -or -not (Test-Path -LiteralPath $root)) { continue } + $direct = Join-Path $root "$Name.exe" + if (Test-Path -LiteralPath $direct) { return (Resolve-Path -LiteralPath $direct).Path } + Get-ChildItem -LiteralPath $root -Directory -Filter "WiX Toolset*" -ErrorAction SilentlyContinue | + ForEach-Object { + $bin = Join-Path $_.FullName "bin\$Name.exe" + if (Test-Path -LiteralPath $bin) { return (Resolve-Path -LiteralPath $bin).Path } + } + } + return $null +} + +$candle = Find-WixTool "candle" +$light = Find-WixTool "light" + +Write-Host "Verae tray presence MSI (skeleton)" +Write-Host " ProductVersion: $Version (HelperVersion / Get-VeraePackageVersion)" +Write-Host " WixDir: $WixDir" +Write-Host " StageDir: $StageDir" +Write-Host " OutDir: $OutDir" + +if (-not $candle -or -not $light) { + Write-Host "" + Write-Host "WiX Toolset (candle/light) not found on PATH or under Program Files." + Write-Host "No MSI produced. Day-to-day tray presence:" + Write-Host " powershell -NoProfile -ExecutionPolicy Bypass -File `"$(Join-Path $PSScriptRoot 'build-tray-startmenu.ps1')`"" + Write-Host "" + Write-Host "Install WiX tip (Windows host or CI runner):" + Write-Host " 1. Download WiX Toolset v3.14+ from https://wixtoolset.org/ (or winget install --id WiXToolset.WiX)" + Write-Host " 2. Add candle.exe and light.exe to PATH, e.g.:" + Write-Host " set PATH=%PATH%;%ProgramFiles(x86)%\WiX Toolset v3.14\bin" + Write-Host " 3. Re-run this script: build-tray-msi.ps1" + Write-Host " WiX v4 (dotnet tool install --global wix) needs authoring adapted to v4 namespaces." + Write-Host " See platform/windows/wix/README.md" + if ($SkipTip) { exit 1 } + exit 0 +} + +Write-Host " candle: $candle" +Write-Host " light: $light" + +# Stage payload next to .wxs expectations (Stage\verae-tray.ps1, Stage\common.ps1). +New-Item -ItemType Directory -Force -Path $StageDir | Out-Null +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null + +$traySrc = Join-Path $PSScriptRoot "verae-tray.ps1" +$commonSrc = Join-Path $PSScriptRoot "common.ps1" +if (-not (Test-Path -LiteralPath $traySrc)) { + Write-Error "Missing tray script: $traySrc" +} +Copy-Item -LiteralPath $traySrc -Destination (Join-Path $StageDir "verae-tray.ps1") -Force +if (Test-Path -LiteralPath $commonSrc) { + Copy-Item -LiteralPath $commonSrc -Destination (Join-Path $StageDir "common.ps1") -Force +} else { + Write-Warning "common.ps1 not found; MSI will still require it at light time if Component is present." +} + +$stageAbs = (Resolve-Path -LiteralPath $StageDir).Path +$outAbs = (Resolve-Path -LiteralPath $OutDir).Path +$wixAbs = (Resolve-Path -LiteralPath $WixDir).Path + +Write-Host "Staging done. Running candle..." +& $candle -nologo -out (Join-Path $outAbs "\") ` + "-dProductVersion=$Version" ` + "-dStageDir=$stageAbs" ` + (Join-Path $wixAbs "VeraeTrayShortcuts.wxs") ` + (Join-Path $wixAbs "VeraeTrayShortcutsFragment.wxs") +if ($LASTEXITCODE -ne 0) { + Write-Error "candle failed with exit $LASTEXITCODE" +} + +$msiName = "VeraeTray-$Version-peruser.msi" +$msiPath = Join-Path $outAbs $msiName +Write-Host "Running light -> $msiPath" +& $light -nologo -out $msiPath ` + (Join-Path $outAbs "VeraeTrayShortcuts.wixobj") ` + (Join-Path $outAbs "VeraeTrayShortcutsFragment.wixobj") +if ($LASTEXITCODE -ne 0) { + Write-Error "light failed with exit $LASTEXITCODE" +} + +Write-Host "" +Write-Host "Built: $msiPath" +Write-Host " ProductVersion=$Version (from HelperVersion / Get-VeraePackageVersion)." +Write-Host " Per-user, non-elevated tray presence only — not full commercial MSI." +Write-Host " Silent: msiexec /i `"$msiPath`" /qn VERAE_IPP_ADDR=127.0.0.1:8631" +Write-Host " Hold ids are never evidence." +exit 0 diff --git a/platform/windows/wix/README.md b/platform/windows/wix/README.md index e6ad6e9..49062e1 100644 --- a/platform/windows/wix/README.md +++ b/platform/windows/wix/README.md @@ -53,7 +53,25 @@ copy /Y platform\windows\scripts\common.ps1 %STAGE%\ Elevated Program Files install, printers, scheduled helper, Authenticode, NSIS key wizard parity, machine-wide Start Menu under ProgramData. -### Build (WiX v3 candle / light) +### Build (preferred): `build-tray-msi.ps1` + +From `platform/windows/scripts` on Windows (or any cwd): + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\build-tray-msi.ps1 +``` + +The script: + +1. Resolves **ProductVersion** via `Get-VeraePackageVersion` → `internal/job.HelperVersion` + (override with `-Version` or `$env:VERSION`). +2. Stages `verae-tray.ps1` + `common.ps1` under `wix/Stage`. +3. Runs **candle** + **light** when found on PATH / `%WIX%` / Program Files. +4. Writes `wix/out/VeraeTray--peruser.msi`. +5. If candle/light are missing, prints a **WiX install tip** and exits 0 + (day-to-day tray path stays `build-tray-startmenu.ps1`). + +### Build (manual WiX v3 candle / light) From repo root on Windows: @@ -61,6 +79,7 @@ From repo root on Windows: set WIXDIR=platform\windows\wix set STAGE=%WIXDIR%\Stage set OUT=%WIXDIR%\out +REM Prefer HelperVersion; build-tray-msi.ps1 does this automatically: set VER=0.3.6 mkdir %STAGE% 2>nul @@ -81,9 +100,9 @@ light.exe -nologo -out %OUT%\VeraeTray-%VER%-peruser.msi ^ Notes: -- Set `VER` from `internal/job.HelperVersion` (see `docs/PACKAGING.md`), or - pass `-dProductVersion=` explicitly. The `.wxs` defaults to `0.3.6` when - unset (HelperVersion at scaffold time). +- **ProductVersion** is wired by `build-tray-msi.ps1` → `Get-VeraePackageVersion` + / `HelperVersion`. Manual builds should pass `-dProductVersion=` the same way. + The `.wxs` `` fallback is `0.3.6` (must match current HelperVersion). - `Product Id="*"` generates a new ProductCode per build; `UpgradeCode` is fixed for major-upgrade of this **tray presence** package only. - Skeleton is **UI-less** (silent-friendly). For WixUI_Minimal, uncomment @@ -102,9 +121,10 @@ Uninstall via ARP (**Verae Tray (presence)**) or `msiexec /x {ProductCode} /qn`. | File | Role | |------|------| -| `VeraeTrayShortcuts.wxs` | Per-user Product + file components | +| `VeraeTrayShortcuts.wxs` | Per-user Product + file components (`ProductVersion`) | | `VeraeTrayShortcutsFragment.wxs` | Start Menu + Startup shortcut fragment | | `README.md` | This file | +| `../scripts/build-tray-msi.ps1` | candle/light driver; wires ProductVersion; WiX tip if missing | ### Relation to other Windows paths @@ -113,6 +133,7 @@ Uninstall via ARP (**Verae Tray (presence)**) or `msiexec /x {ProductCode} /qn`. | `../scripts/verae-tray.ps1` | Runtime tray (source staged into MSI) | | `../scripts/install-tray-startup.ps1` | WiX-free Startup shortcut | | `../scripts/build-tray-startmenu.ps1` | WiX-free Start Menu + Startup | +| `../scripts/build-tray-msi.ps1` | Builds this skeleton MSI when WiX is installed | | `../installer-windows.nsi` | Elevated **full product** setup.exe | | **`wix/` (this folder)** | Per-user tray shortcut **MSI skeleton** | diff --git a/platform/windows/wix/VeraeTrayShortcuts.wxs b/platform/windows/wix/VeraeTrayShortcuts.wxs index 0caf7b3..4cb3272 100644 --- a/platform/windows/wix/VeraeTrayShortcuts.wxs +++ b/platform/windows/wix/VeraeTrayShortcuts.wxs @@ -11,8 +11,9 @@ Requires WiX Toolset (candle + light, or WiX v4 `wix build`) to produce MSI. See README.md in this folder. - Version default tracks internal/job.HelperVersion; override at candle time: - -dProductVersion=x.y.z + ProductVersion: prefer -dProductVersion from build-tray-msi.ps1 + (Get-VeraePackageVersion → internal/job.HelperVersion, or VERSION=). + Fallback below must match HelperVersion when candle is invoked bare. --> diff --git a/site/printit.verae.ai/certificate.html b/site/printit.verae.ai/certificate.html index 4895b1a..35be43f 100644 --- a/site/printit.verae.ai/certificate.html +++ b/site/printit.verae.ai/certificate.html @@ -69,10 +69,14 @@ } .qr{ width:112px;height:112px;flex:none;border:2px solid var(--ink,#1D1534);border-radius:8px; + /* Offline / provisional placeholder — print-safe without CDN */ background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 29'%3E%3Cpath fill='%231D1534' d='M1 1h7v7H1zm2 2v3h3V3zm10 0h7v7h-7zm2 2v3h3V3zM1 13h7v7H1zm2 2v3h3v-3zm18-14h7v7h-7zm2 2v3h3V3zm0 10h3v2h-3zm-4 0h2v2h-2zm4 4h3v3h-3zm-4 2h2v3h-2zm-2-4h2v2h-2zm6 6h3v2h-3zm-8 0h2v2h-2zm4-2h2v4h-2zm-6 0h2v2h-2z'/%3E%3C/svg%3E") center/72% no-repeat; - position:relative; + position:relative;overflow:hidden; } - .qr span{position:absolute;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59)} + .qr .qr-fallback{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;padding:2px 5px;font:700 10px system-ui;color:var(--plum,#531B59);pointer-events:none} + .qr img.qr-bitmap,.qr canvas.qr-bitmap{width:100%;height:100%;display:block;border-radius:6px;object-fit:contain;background:#fff} + .qr.has-qr{background:#fff} + .qr.has-qr .qr-fallback{display:none} .verify-box ol{margin:6px 0 0 16px;padding:0;line-height:1.6;color:var(--ink-soft,#3B3250);font-size:12.5px} .verify-box a{color:var(--info,#3B2A8C);font-size:10px;word-break:break-all} .foot{ @@ -97,8 +101,11 @@ } /* CDN may be absent offline; keep plum/gold frame + system serif title */ .title{font-family:'Fraunces',Georgia,'Times New Roman',Times,serif!important} - .rule,.banner,.mh,.qr{ -webkit-print-color-adjust:exact;print-color-adjust:exact } + .rule,.banner,.mh,.qr,.qr img.qr-bitmap,.qr canvas.qr-bitmap{ + -webkit-print-color-adjust:exact;print-color-adjust:exact + } .rule{background:#C4A85A!important} + .qr{width:96px;height:96px;border-color:#1D1534!important} a{color:inherit;text-decoration:none} } @page{size:letter;margin:12mm} @@ -164,11 +171,13 @@
- +
Verify this certificate
    -
  1. Open the explorer link (or scan the QR on a printed PDF).
  2. +
  3. Open the explorer link (or scan the QR when final).
  4. Compare the local file’s SHA-256 to the multihash below — file never uploaded.
  5. Only a final / already-registered receipt is evidence. Hold ids are not proof.
@@ -192,7 +201,8 @@ ?document=&sha256=&receipt=&created_at=&merkle=&blake3=&block=&helper_version= Multihash / BLAKE3 lines appear only when data is present — never invented. Title uses Fraunces (Google Fonts CDN) with Georgia/serif offline fallback; plum/gold frame kept. - Full CERT-01.html pixel match (self-hosted design-kit fonts, live QR) remains a residual. + Final certificates paint a live QR (qrcode.js CDN → data-URL) from the explorer lookup; offline / provisional keeps the SVG placeholder so print still works. + Residual: full CERT-01.html self-hosted Fraunces/Cantarell pixel match (design-kit fonts).

@@ -283,6 +293,70 @@ return u; } + /** Live QR when final + payload; CDN fail / offline / provisional → SVG placeholder (print-safe). */ + function clearQR(el) { + if (!el) return; + el.classList.remove('has-qr'); + var bit = el.querySelectorAll('img.qr-bitmap, canvas.qr-bitmap'); + for (var i = 0; i < bit.length; i++) bit[i].remove(); + } + + function paintQRDataUrl(el, url) { + if (!el || !url) return; + clearQR(el); + var img = document.createElement('img'); + img.className = 'qr-bitmap'; + img.alt = ''; + img.width = 112; + img.height = 112; + img.decoding = 'async'; + img.onload = function () { el.classList.add('has-qr'); }; + img.onerror = function () { img.remove(); /* keep placeholder */ }; + img.src = url; + el.appendChild(img); + } + + function withQRCodeLib(fn) { + if (window.QRCode && typeof window.QRCode.toDataURL === 'function') { + fn(window.QRCode); + return; + } + var existing = document.querySelector('script[data-verae-qrcode-lib]'); + if (existing) { + existing.addEventListener('load', function () { + if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode); + }); + return; + } + var s = document.createElement('script'); + s.src = 'https://cdn.jsdelivr.net/npm/qrcode@1.5.4/build/qrcode.min.js'; + s.async = true; + s.setAttribute('data-verae-qrcode-lib', '1'); + s.onload = function () { + if (window.QRCode && window.QRCode.toDataURL) fn(window.QRCode); + }; + /* onerror: leave placeholder — offline print fallback */ + document.head.appendChild(s); + } + + function setVerifyQR(payload, isFinal) { + var el = $('qr'); + if (!el) return; + clearQR(el); + if (!isFinal || !payload) return; + withQRCodeLib(function (QRCode) { + QRCode.toDataURL(String(payload), { + errorCorrectionLevel: 'M', + margin: 1, + width: 224, + color: { dark: '#1D1534', light: '#FFFFFF' } + }, function (err, dataUrl) { + if (err || !dataUrl) return; + paintQRDataUrl(el, dataUrl); + }); + }); + } + function apply(data) { data = data || {}; var doc = data.document_name || data.document || data.file || ''; @@ -351,6 +425,10 @@ var a = $('verify-link'); a.href = url; a.textContent = url; + + /* Real QR only when final evidence + receipt or digest; else placeholder. */ + var qrPayload = (sha || receipt || mh) ? url : ''; + setVerifyQR(qrPayload, evidence && !!qrPayload); } function fromQuery() {