feat: MSI full outline, silent props, HelperVersion NSIS sync, CERT fonts plan

TODO-6 L1/I1/L7/I7 — commercial + CERT next-level scaffolds.
This commit is contained in:
George Lambert 2026-10-13 23:23:45 -04:00
commit 46fa1aa0d4
13 changed files with 600 additions and 6 deletions

View file

@ -190,6 +190,7 @@ Site page: [`site/printit.verae.ai/certificate.html`](../site/printit.verae.ai/c
| Live verify | Link to `explorer.verae.ai/time?receipt=&sha256=` (same as `pdfcert.LookupURL`) | | Live verify | Link to `explorer.verae.ai/time?receipt=&sha256=` (same as `pdfcert.LookupURL`) |
| Live QR (web) | **Shipped** — final + receipt/digest paints QR via qrcode.js CDN → data-URL; offline/provisional keeps SVG placeholder (print-safe) | | 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`) | | 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`) |
| Self-host fonts plan | [`site/printit.verae.ai/fonts/README.md`](../site/printit.verae.ai/fonts/README.md) — woff2 placeholder dir (empty + `.gitkeep`); binaries/CDN cutover not started |
Design source of truth for the paper mock: `verae-2026-design/ui-designs/src/html/CERT-01.html`. 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. Cross-product siblings (not owned here): Sign `certificate.html` (SG-P1), Verify ZP-V5, MCP Mac export PDF.

View file

@ -27,6 +27,10 @@ const (
) )
// Minimal fallback when Node/jsPDF is unavailable. // Minimal fallback when Node/jsPDF is unavailable.
//
// Fonts: PDF core / library faces only (Times title, Helvetica body).
// Do not claim Fraunces embed — no AddFont/TTF path unless a licensed
// Fraunces file is wired in later (OPEN-ISSUES #7 / TODO-6 I7).
func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup string) ([]byte, error) { func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup string) ([]byte, error) {
pdf := gofpdf.New("P", "pt", "Letter", "") pdf := gofpdf.New("P", "pt", "Letter", "")
pdf.SetCompression(false) pdf.SetCompression(false)
@ -51,13 +55,13 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
} }
} }
// Gold-d eyebrow (BrandingBook §12) // Gold-d eyebrow (BrandingBook §12) — Helvetica core font (not Fraunces)
pdf.SetTextColor(goldDR, goldDG, goldDB) pdf.SetTextColor(goldDR, goldDG, goldDB)
pdf.SetFont("Helvetica", "B", 9) pdf.SetFont("Helvetica", "B", 9)
pdf.SetXY(margin, 92) pdf.SetXY(margin, 92)
pdf.CellFormat(pageW-margin*2, 11, "TIMESTAMP CERTIFICATE", "", 0, "C", false, 0, "") pdf.CellFormat(pageW-margin*2, 11, "TIMESTAMP CERTIFICATE", "", 0, "C", false, 0, "")
// Title — CERT-01 serif / ink // Title — Times core serif stand-in for CERT-01 Fraunces (no embed unless present)
pdf.SetTextColor(inkR, inkG, inkB) pdf.SetTextColor(inkR, inkG, inkB)
pdf.SetFont("Times", "B", 22) pdf.SetFont("Times", "B", 22)
pdf.SetXY(margin, 106) pdf.SetXY(margin, 106)

View file

@ -103,6 +103,10 @@ func LookupURL(sha256, receiptID string) string {
// Build returns the branded CERT-01 certificate PDF (plum/gold, final-only // Build returns the branded CERT-01 certificate PDF (plum/gold, final-only
// evidence copy). Prefers platform/registration-pdf/build.mjs when Node is // evidence copy). Prefers platform/registration-pdf/build.mjs when Node is
// available; otherwise gofpdf fallback. // available; otherwise gofpdf fallback.
//
// Typography: both paths use PDF standard fonts (Times title, Helvetica body).
// Fraunces is the brand serif on web CERT shells only — the issued PDF does
// not embed Fraunces unless a licensed face is explicitly added later.
func Build(in Input) ([]byte, error) { func Build(in Input) ([]byte, error) {
created := in.CreatedAt created := in.CreatedAt
if created == "" { if created == "" {

View file

@ -259,3 +259,69 @@ func TestFindBuildScriptNoResearchHardcode(t *testing.T) {
t.Fatal("build.mjs must keep hold≠evidence disclaimer") t.Fatal("build.mjs must keep hold≠evidence disclaimer")
} }
} }
// TestBuildUsesSystemFontsNotFraunces: issued PDF uses core Times/Helvetica;
// do not claim Fraunces embed until a licensed face is explicitly added (I7).
func TestBuildUsesSystemFontsNotFraunces(t *testing.T) {
sha := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
in := Input{
DocumentName: "FontCheck.pdf",
SHA256: sha,
ReceiptID: "QmFontCheck",
CreatedAt: "2026-10-03T12:00:00Z",
Merkle: "99",
}
// gofpdf path — deterministic, no Node.
pdf, err := buildFallbackGofpdf(in, "FontCheck.pdf", in.CreatedAt, "GLOBAL", in.Merkle, LookupURL(sha, in.ReceiptID))
if err != nil {
t.Fatal(err)
}
s := string(pdf)
if strings.Contains(s, "Fraunces") || strings.Contains(s, "fraunces") {
t.Fatal("PDF must not embed or name Fraunces unless a licensed face is wired")
}
// Standard 14 base fonts (gofpdf): Times title + Helvetica body.
if !strings.Contains(s, "/BaseFont /Times") && !strings.Contains(s, "/BaseFont/Times") {
if !strings.Contains(s, "Times-Bold") && !strings.Contains(s, "Times-Roman") {
t.Fatal("expected PDF core Times base font for title serif stand-in")
}
}
if !strings.Contains(s, "/BaseFont /Helvetica") && !strings.Contains(s, "/BaseFont/Helvetica") {
if !strings.Contains(s, "Helvetica") {
t.Fatal("expected PDF core Helvetica for body text")
}
}
// Full Build() path must also stay honest (node or gofpdf).
b, err := Build(in)
if err != nil {
t.Fatal(err)
}
full := string(b)
if strings.Contains(full, "Fraunces") || strings.Contains(full, "fraunces") {
t.Fatal("Build() PDF must not claim Fraunces embed")
}
// build.mjs must keep standard faces; no real addFont(...) call for Fraunces.
script := findBuildScript()
if script == "" {
t.Fatal("expected build.mjs")
}
src, err := os.ReadFile(script)
if err != nil {
t.Fatal(err)
}
body := string(src)
lower := strings.ToLower(body)
// Call-site only: addFont(…fraunces…) — prose notes may mention both words.
if strings.Contains(lower, `addfont(`) && strings.Contains(lower, "fraunces") {
// Narrow: same line or adjacent token would be an embed; bare prose is OK.
for _, line := range strings.Split(body, "\n") {
ll := strings.ToLower(line)
if strings.Contains(ll, "addfont(") && strings.Contains(ll, "fraunces") {
t.Fatal("build.mjs must not addFont(Fraunces) until licensed binary lands")
}
}
}
if !strings.Contains(body, `setFont("times"`) || !strings.Contains(body, `setFont("helvetica"`) {
t.Fatal("build.mjs should keep times/helvetica standard faces")
}
}

View file

@ -9,6 +9,9 @@
* - no mcp.verae.ai URLs * - no mcp.verae.ai URLs
* - footer: Verae LLC · verae.ai · explorer.verae.ai * - footer: Verae LLC · verae.ai · explorer.verae.ai
* *
* Fonts: jsPDF standard faces only (times title, helvetica body). Do not claim
* Fraunces embed — no addFont/TTF unless a licensed Fraunces file is wired later.
*
* Usage: node build.mjs <input.json> <output.pdf> * Usage: node build.mjs <input.json> <output.pdf>
* input.json: { file, size_bytes, sha256, multihash?, blake3?, blake3_multihash?, * input.json: { file, size_bytes, sha256, multihash?, blake3?, blake3_multihash?,
* scope, receipt_id, created_at, merkle, lookup_url? } * scope, receipt_id, created_at, merkle, lookup_url? }
@ -143,7 +146,7 @@ async function buildPdf(row) {
doc.setTextColor(...goldD); doc.setTextColor(...goldD);
doc.text("TIMESTAMP CERTIFICATE", pageW / 2, 98, { align: "center" }); doc.text("TIMESTAMP CERTIFICATE", pageW / 2, 98, { align: "center" });
// Title — CERT-01 serif / ink // Title — Times core serif stand-in for CERT-01 Fraunces (no embed unless present)
doc.setFont("times", "bold"); doc.setFont("times", "bold");
doc.setFontSize(22); doc.setFontSize(22);
doc.setTextColor(...ink); doc.setTextColor(...ink);

View file

@ -1,12 +1,34 @@
# Build the Windows installer natively on Windows (Go + NSIS). # Build the Windows installer natively on Windows (Go + NSIS).
# Usage: powershell -ExecutionPolicy Bypass -File platform\windows\build-windows.ps1 [-Arch amd64|arm64] # Usage: powershell -ExecutionPolicy Bypass -File platform\windows\build-windows.ps1 [-Arch amd64|arm64] [-Version x.y.z]
param([string]$Arch = "amd64", [string]$Version = "0.3.2") # Version default: Get-VeraePackageVersion → internal/job.HelperVersion (or $env:VERSION).
param([string]$Arch = "amd64", [string]$Version = "")
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path $Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$Out = Join-Path $Root "dist\windows" $Out = Join-Path $Root "dist\windows"
$Stage = Join-Path $Out "stage-$Arch" $Stage = Join-Path $Out "stage-$Arch"
$Go = (Get-Command go -ErrorAction SilentlyContinue).Source; if (-not $Go) { $Go = "C:\Program Files\Go\bin\go.exe" } $Go = (Get-Command go -ErrorAction SilentlyContinue).Source; if (-not $Go) { $Go = "C:\Program Files\Go\bin\go.exe" }
$Nsis = (Get-Command makensis -ErrorAction SilentlyContinue).Source; if (-not $Nsis) { $Nsis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe" } $Nsis = (Get-Command makensis -ErrorAction SilentlyContinue).Source; if (-not $Nsis) { $Nsis = "${env:ProgramFiles(x86)}\NSIS\makensis.exe" }
# Package version = internal/job.HelperVersion (single source of truth).
$common = Join-Path $PSScriptRoot "scripts\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 {
$jobGo = Join-Path $Root "internal\job\job.go"
if (Test-Path -LiteralPath $jobGo) {
$line = Select-String -Path $jobGo -Pattern 'HelperVersion\s*=\s*"([^"]+)"' -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($line -and $line.Matches.Count -gt 0) {
$Version = $line.Matches[0].Groups[1].Value
}
}
}
if (-not $Version) { $Version = "0.3.6" } # must match HelperVersion fallback
}
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage, (Join-Path $Stage "scripts") | Out-Null New-Item -ItemType Directory -Force -Path $Stage, (Join-Path $Stage "scripts") | Out-Null
Push-Location $Root Push-Location $Root

View file

@ -6,8 +6,12 @@
; only through files in $PLUGINSDIR that are deleted immediately and are never logged. ; only through files in $PLUGINSDIR that are deleted immediately and are never logged.
; Logging: one concise %ProgramData%\Verae\logs\installer-<time>.log with errors and ; Logging: one concise %ProgramData%\Verae\logs\installer-<time>.log with errors and
; the per-step result summary. (Start > Verae > Collect diagnostics for troubleshooting.) ; the per-step result summary. (Start > Verae > Collect diagnostics for troubleshooting.)
;
; VERSION: prefer /DVERSION= from build-windows.ps1 (Get-VeraePackageVersion →
; internal/job.HelperVersion, or $env:VERSION / -Version). Bare makensis fallback
; below must match current HelperVersion when the driver is not used.
!ifndef VERSION !ifndef VERSION
!define VERSION "0.3.2" !define VERSION "0.3.6"
!endif !endif
!ifndef ARCH !ifndef ARCH
!define ARCH "amd64" !define ARCH "amd64"

View file

@ -13,6 +13,11 @@
Scope: non-elevated per-user tray shortcuts only — not the full commercial MSI. Scope: non-elevated per-user tray shortcuts only — not the full commercial MSI.
.EXITCODES
0 Success: MSI built, or WiX missing and tip printed (default; day-to-day OK).
1 Failure: -SkipTip with WiX missing; missing .wxs/tray payload; candle/light
non-zero; or Write-Error / unhandled terminating error ($ErrorActionPreference Stop).
.PARAMETER Version .PARAMETER Version
Override package version (default: Get-VeraePackageVersion / HelperVersion). Override package version (default: Get-VeraePackageVersion / HelperVersion).

View file

@ -0,0 +1,232 @@
# Product-Full.wxs.outline — full elevated Print Stamp MSI (scaffold)
**Status:** authoring outline only. Not a buildable `.wxs`. Does not produce MSI.
**Parity target:** [`../installer-windows.nsi`](../installer-windows.nsi) (elevated full product).
**Not this package:** tray-only skeleton [`VeraeTrayShortcuts.wxs`](VeraeTrayShortcuts.wxs) (per-user, non-elevated).
**Version:** `ProductVersion` ← `Get-VeraePackageVersion` → `internal/job.HelperVersion` (or `VERSION=`).
**Backlog:** [`docs/COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md).
**Silent props:** [`SILENT_PROPS.md`](SILENT_PROPS.md).
---
## Product identity (proposed)
| Field | Value | Notes |
|-------|--------|------|
| Name | Verae Print Stamp | ARP display name |
| Manufacturer | Verae | |
| InstallScope | `perMachine` | Requires elevation (`InstallPrivileges=elevated`) |
| InstallDir default | `ProgramFiles64Folder\Verae\PrintStamp` | Same as NSIS `$PROGRAMFILES64\Verae\PrintStamp` |
| UpgradeCode | **TBD stable GUID** (full product; distinct from tray `A7C3E91B-…`) | MajorUpgrade; never share tray UpgradeCode |
| ProductCode | `*` per build | |
| Package code | auto | |
| Arch | x64 (amd64); arm64 later | NSIS rejects non-x64 today |
| Output name | `VeraePrintStamp-windows-<arch>-<version>.msi` | |
ARP: `ARPURLINFOABOUT=https://printit.verae.ai/`, `ARPHELPLINK=https://customer.verae.ai/portal/`, Publisher Verae, DisplayVersion=`ProductVersion`.
---
## Directory tree
```
TARGETDIR
└── ProgramFiles64Folder
└── Verae (Id=VeraePf)
└── PrintStamp (Id=INSTALLFOLDER) ← [INSTALLDIR]
├── bin\ ← binaries
└── scripts\ ← PowerShell helpers
ProgramMenuFolder
└── Verae (Id=VeraeStartMenuDir) ← Start Menu shortcuts
```
Optional later: `CommonAppDataFolder\Verae\logs` created by CA (NSIS uses `%ProgramData%\Verae\logs` at runtime, not as a permanent component).
---
## Component groups (file payload)
Mirror NSIS `Section` install surface (`installer-windows.nsi` ~613–624).
### ComponentGroup `BinFiles` → `INSTALLFOLDER\bin`
| Component Id | File | Source (stage) | KeyPath | Notes |
|--------------|------|----------------|---------|-------|
| `CmpVeraePrintExe` | `verae-print.exe` | `Stage\bin\verae-print.exe` | yes | CLI + installer-account |
| `CmpVeraePrintHelperExe` | `verae-printhelper.exe` | `Stage\bin\verae-printhelper.exe` | yes | GUI subsystem (`-H windowsgui`); same main package |
### ComponentGroup `ScriptFiles` → `INSTALLFOLDER\scripts`
| Component Id | File | Source | KeyPath | Used by |
|--------------|------|--------|---------|---------|
| `CmpCommonPs1` | `common.ps1` | `Stage\scripts\common.ps1` | yes | version helper, console user |
| `CmpUserTokenPs1` | `user-token.ps1` | `Stage\scripts\user-token.ps1` | yes | key status/save |
| `CmpInstallPrinterPs1` | `install-printer.ps1` | `Stage\scripts\install-printer.ps1` | yes | deferred CA (elevated) |
| `CmpUninstallPrinterPs1` | `uninstall-printer.ps1` | `Stage\scripts\uninstall-printer.ps1` | yes | uninstall CA |
| `CmpVeraeDiagPs1` | `verae-diag.ps1` | `Stage\scripts\verae-diag.ps1` | yes | Start Menu diagnostics |
**Optional (not in NSIS product tree today; include if tray ships with full MSI):**
| Component Id | File | Notes |
|--------------|------|-------|
| `CmpVeraeTrayPs1` | `verae-tray.ps1` | Presence UI; may stay in separate per-user MSI |
| `CmpInstallTrayStartupPs1` | `install-tray-startup.ps1` | User-level Startup; prefer impersonated CA |
### ComponentGroup `DocFiles` → `INSTALLFOLDER`
| Component Id | File | Source | KeyPath |
|--------------|------|--------|---------|
| `CmpLicenseTxt` | `LICENSE.txt` | `Stage\LICENSE.txt` | yes |
| `CmpReadmeWindows` | `README-WINDOWS.txt` | `Stage\README-WINDOWS.txt` | yes |
### ComponentGroup `StartMenuShortcuts` → `VeraeStartMenuDir`
| Component Id | Shortcut name | Target | Arguments |
|--------------|---------------|--------|-----------|
| `CmpSmKeyDialog` | Verae - Setup signing key | `[INSTALLFOLDER]bin\verae-printhelper.exe` | `key-dialog` |
| `CmpSmStartHelper` | Verae - Start print helper | `[INSTALLFOLDER]bin\verae-printhelper.exe` | (none) |
| `CmpSmLogs` | Verae - Logs folder | `explorer.exe` | `%ProgramData%\Verae\logs` |
| `CmpSmDiag` | Verae - Collect diagnostics | `powershell.exe` | `-NoProfile -ExecutionPolicy Bypass -File "[INSTALLFOLDER]scripts\verae-diag.ps1"` |
| `CmpSmUninstall` | Uninstall Verae Print Stamp | ARP / `msiexec /x [ProductCode]` | (WiX standard) |
Each shortcut component: `RegistryValue` HKCU/HKLM keypath under `Software\Verae\PrintStamp\Shortcuts` **or** Advertise shortcuts with Feature linkage. Prefer non-advertised + HKLM keypath for elevated package.
---
## Feature tree
```
Feature ProductComplete (Level=1, typical default)
├── Feature CoreBinaries → BinFiles
├── Feature Scripts → ScriptFiles
├── Feature Docs → DocFiles
├── Feature StartMenu → StartMenuShortcuts
├── Feature PrintersAndHelper (Level=1) → CustomActions only (no files)
└── Feature TrayPresence (Level=2 optional) → tray scripts + user shortcuts
OR leave tray to VeraeTray-*-peruser.msi
```
---
## Registry (machine)
| Key | Name | Value | Component |
|-----|------|-------|-----------|
| `HKLM\Software\Verae\PrintStamp` | `InstallDir` | `[INSTALLFOLDER]` | with install-printer / core |
| `HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\…` | ARP via WiX | — | WiX generates; do not dual-write NSIS `UNINST_KEY` long-term |
| Remove on uninstall | `HKLM\Software\Verae\PrintStamp` | — | match NSIS uninstall |
Do **not** store API keys or tokens in machine registry. Keys stay per-user (`.verae` / user-token.ps1 paths).
---
## Custom actions (NSIS parity) — placeholders
Ordered for install (after `InstallFiles`):
| Id | Type | Impersonate | When | Action |
|----|------|-------------|------|--------|
| `CA_StopHelper` | exe/schtasks | no | before overwrite | `schtasks /End /TN "Verae Print Helper"`; `taskkill /F /IM verae-printhelper.exe` |
| `CA_SaveKey` | PowerShell | **yes** (user) | if not skip | `user-token.ps1 -Mode save` only when property supplies token file path (silent rarely) |
| `CA_WriteUserConfig` | exe | yes | if profile known | `verae-print.exe config --open-on-complete …` |
| `CA_InstallPrinters` | PowerShell | **no** (elevated) | always unless `VERAE_SKIP_PRINTERS=1` | `install-printer.ps1 -InstallDir "[INSTALLFOLDER]"` |
| `CA_InstallTray` | optional | yes | if Feature Tray | Startup + Start Menu tray lnk |
Uninstall:
| Id | Action |
|----|--------|
| `CA_UninstallPrinters` | `uninstall-printer.ps1` |
| `CA_KillHelper` | taskkill helper |
**Printer CA placeholders (authoring stubs — not implemented here):**
```
<!-- PLACEHOLDER: deferred elevated -->
CustomAction Id="CA_InstallPrinters"
ExeCommand="powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File &quot;[INSTALLFOLDER]scripts\install-printer.ps1&quot; -InstallDir &quot;[INSTALLFOLDER]&quot;"
Execute="deferred" Impersonate="no" Return="check" />
<!-- PLACEHOLDER: rollback -->
CustomAction Id="CA_InstallPrintersRollback"
… uninstall-printer.ps1 … Execute="rollback" Impersonate="no" />
<!-- PLACEHOLDER: uninstall sequence -->
CustomAction Id="CA_UninstallPrinters"
… uninstall-printer.ps1 … Execute="deferred" Impersonate="no" />
```
Schedule: `InstallExecuteSequence` — `CA_StopHelper` before `RemoveFiles`/`InstallFiles`; `CA_InstallPrinters` after `InstallFiles`; uninstall CA before `RemoveFiles`.
Condition examples: `NOT Installed`, `REMOVE~="ALL"`, `VERAE_SKIP_PRINTERS<>"1"`.
---
## Printer / helper runtime surface (created by CA, not File components)
Documented so WiX authors do not try to harvest these as static files:
| Item | Created by | Notes |
|------|------------|-------|
| Scheduled task `Verae Print Helper` | `install-printer.ps1` | Runs helper as console user |
| Printer `Verae TimeStamp PDF` | IPP class driver → `http://127.0.0.1:8631/ipp/print` | |
| Printer Local Port fallback | Microsoft Print To PDF + local port | |
| Port / driver prerequisites | same script | Spooler must run |
| HKLM `Software\Verae\PrintStamp\InstallDir` | install-printer.ps1 | |
---
## Staging layout (build host)
Before `candle`/`light` (future `build-full-msi.ps1`):
```
wix/StageFull/
bin/verae-print.exe
bin/verae-printhelper.exe
scripts/common.ps1
scripts/user-token.ps1
scripts/install-printer.ps1
scripts/uninstall-printer.ps1
scripts/verae-diag.ps1
LICENSE.txt
README-WINDOWS.txt
```
Same sources as [`../build-windows.ps1`](../build-windows.ps1) stage (Go build + copy scripts + LICENSE).
---
## Explicitly out of this outline
| Item | Where it lives instead |
|------|------------------------|
| Per-user tray-only MSI | `VeraeTrayShortcuts.wxs` + `build-tray-msi.ps1` |
| Authenticode / signtool | CI secrets; backlog |
| NSIS GUI key wizard pages | Interactive NSIS until MSI UI or always-silent + first-run |
| Real `.wxs` Product element / GUIDs locked | Future `Product-Full.wxs` (not this outline) |
| MSIX / Print Support App | Deferred |
---
## Suggested WiX file split (when implementing)
| File | Contents |
|------|----------|
| `Product-Full.wxs` | Product, Package, MajorUpgrade, Properties, Feature refs, MediaTemplate |
| `Product-Full-Files.wxs` | Directory + Bin/Script/Doc ComponentGroups |
| `Product-Full-Shortcuts.wxs` | Start Menu components |
| `Product-Full-CA.wxs` | CustomAction defs + InstallExecuteSequence |
| `SILENT_PROPS.md` | Public property contract (done) |
| `Product-Full.wxs.outline` | This checklist |
---
## Definition of done for a real Product-Full.wxs (not L1)
1. candle/light builds MSI with HelperVersion stamped.
2. Elevated install places bin + scripts + LICENSE under Program Files.
3. CA runs install-printer.ps1; IPP + fallback printers + helper task.
4. Silent props from SILENT_PROPS.md honored (`/qn`).
5. Uninstall removes printers/task/files/ARP; leaves user keys + logs.
6. Dual-ship NSIS until validated; then thin NSIS.

View file

@ -71,6 +71,15 @@ The script:
5. If candle/light are missing, prints a **WiX install tip** and exits 0 5. If candle/light are missing, prints a **WiX install tip** and exits 0
(day-to-day tray path stays `build-tray-startmenu.ps1`). (day-to-day tray path stays `build-tray-startmenu.ps1`).
#### `build-tray-msi.ps1` exit codes
| Code | Meaning |
|------|---------|
| **0** | MSI built successfully, **or** WiX (candle/light) not found and install tip printed (default; does not fail the non-WiX tray path). |
| **1** | Failure: `-SkipTip` when WiX is missing; missing product/fragment `.wxs` or `verae-tray.ps1`; candle or light returned non-zero; or a terminating PowerShell error. |
CI that requires an MSI artifact should pass `-SkipTip` so a missing toolset fails the job.
### Build (manual WiX v3 candle / light) ### Build (manual WiX v3 candle / light)
From repo root on Windows: From repo root on Windows:

View file

@ -0,0 +1,157 @@
# SILENT_PROPS — full Print Stamp MSI public properties
**Status:** contract scaffold for the future elevated full-product MSI (`Product-Full.wxs`).
**Not implemented** in a shippable MSI yet. Tray skeleton only exposes `VERAE_IPP_ADDR` today.
**Parity intent:** fleet / Intune / SCCM `msiexec /i … /qn` without the NSIS key wizard UI.
**Related:** [`Product-Full.wxs.outline`](Product-Full.wxs.outline), [`docs/COMMERCIAL-INSTALLER-BACKLOG.md`](../../../docs/COMMERCIAL-INSTALLER-BACKLOG.md), [`docs/installer-registration-api.md`](../../../docs/installer-registration-api.md).
---
## msiexec pattern
```bat
msiexec /i VeraePrintStamp-windows-amd64-0.3.6.msi /qn /norestart ^
VERAE_SKIP_KEY_SETUP=1 ^
VERAE_CHANNEL=zapier ^
VERAE_INSTALL_API_URL=https://customer.verae.ai ^
VERAE_IPP_ADDR=127.0.0.1:8631
```
- `/qn` — no UI (required for “silent”).
- `/norestart` — printers/helper must not force reboot; log if reboot pending.
- Log (ops): `msiexec /i … /qn /L*v %TEMP%\verae-msi.log`.
- Uninstall: `msiexec /x {ProductCode} /qn` or ARP.
All names below are **public** Windows Installer properties (uppercase) unless marked private.
---
## Property table
### Required for silent enterprise (skip interactive key UI)
| Property | Type | Default | Meaning |
|----------|------|---------|---------|
| `VERAE_SKIP_KEY_SETUP` | `0` \| `1` | `0` (UI builds) / **`1` recommended for `/qn`** | Skip NSIS-equivalent key wizard (login / create / paste). No portal prompts during install. User supplies key later (first print, Start Menu “Setup signing key”, or pre-placed token). |
| `VERAE_CHANNEL` | string | `zapier` | Billing/API channel for stamp path. `zapier` → zap.verae.ai + xVerae- + `x-billing-channel=printit`. `verae` → api.veraetime.net JWT. Written into machine or first-run config when MSI implements config CA. |
| `VERAE_INSTALL_API_URL` | URL | `https://customer.verae.ai` | Portal **origin** (no trailing path). Client uses `{URL}/portal/api/…` for installer registration (`VERAE_PORTAL_BASE` equivalent). HTTPS only in production. |
### API / endpoint overrides
| Property | Type | Default | Meaning |
|----------|------|---------|---------|
| `VERAE_INSTALL_API_URL` | URL | `https://customer.verae.ai` | See above. Alias concept: env `VERAE_PORTAL_BASE`. |
| `VERAE_ZAPIER_API_URL` | URL | `https://zap.verae.ai` | Zapier/edge stamp base when `VERAE_CHANNEL=zapier`. |
| `VERAE_VERAE_API_URL` | URL | (product default host for `verae` channel) | Optional override for non-zapier channel. |
Do not put API **keys** or passwords in MSI properties (show in ARP repair, logs, process list). Use `VERAE_SKIP_KEY_SETUP=1` and deploy tokens out-of-band, or a secure first-run.
### Install surface toggles
| Property | Type | Default | Meaning |
|----------|------|---------|---------|
| `VERAE_SKIP_PRINTERS` | `0` \| `1` | `0` | Skip `install-printer.ps1` custom action (no IPP / Local Port queues, no helper task). Files still install. |
| `VERAE_SKIP_HELPER_TASK` | `0` \| `1` | `0` | Install printers but do not register scheduled task (rare; usually leave `0`). |
| `VERAE_INSTALL_TRAY` | `0` \| `1` | `0` | If full MSI gains optional tray Feature: install per-user Startup tray. Default off — tray remains separate per-user MSI. |
| `VERAE_IPP_ADDR` | `host:port` | `127.0.0.1:8631` | Helper loopback address for tray shortcuts / docs. Does not change IPP purity (presence = GET only). |
### Optional UX prefs (map to `verae-print config`)
| Property | Type | Default | Meaning |
|----------|------|---------|---------|
| `VERAE_OPEN_ON_COMPLETE` | `folder` \| `pdf` \| `none` | `folder` | After each stamp; same as NSIS Options page. |
| `VERAE_PROMPT_FOR_FILENAME` | `true` \| `false` | `true` | Prompt for output name. |
Only applied when a target user profile is known (impersonated CA). Silent machine install may defer until first interactive run.
### Standard WiX / Windows properties (not Verae-specific)
| Property | Notes |
|----------|--------|
| `INSTALLDIR` / `INSTALLFOLDER` | Override Program Files target. |
| `ALLUSERS=1` | per-machine (full MSI default). |
| `REBOOT=ReallySuppress` | Prefer with `/norestart`. |
| `ARPNOMODIFY` | Optional `1` until modify UI exists. |
---
## Mapping: property → runtime
| MSI property | Runtime / env / config |
|--------------|------------------------|
| `VERAE_INSTALL_API_URL` | `VERAE_PORTAL_BASE` for `verae-print installer-account` / portal client |
| `VERAE_CHANNEL` | stamp `--channel=` / config channel default (`zapier` \| `verae`) |
| `VERAE_ZAPIER_API_URL` | zap client base (if configurable) |
| `VERAE_SKIP_KEY_SETUP=1` | No `user-token.ps1 -Mode save` during install; Finish = “key SKIPPED” |
| `VERAE_IPP_ADDR` | tray `-Addr`, `$env:VERAE_IPP_ADDR` |
| `VERAE_OPEN_ON_COMPLETE` | `config --open-on-complete` |
| `VERAE_PROMPT_FOR_FILENAME` | `config --prompt-for-filename` |
---
## Intune / SCCM sample lines
**Minimum silent (files + printers, skip key UI):**
```bat
msiexec /i VeraePrintStamp-windows-amd64-VERSION.msi /qn /norestart VERAE_SKIP_KEY_SETUP=1
```
**Explicit channel + portal (PrintIt default channel):**
```bat
msiexec /i VeraePrintStamp-windows-amd64-VERSION.msi /qn /norestart ^
VERAE_SKIP_KEY_SETUP=1 ^
VERAE_CHANNEL=zapier ^
VERAE_INSTALL_API_URL=https://customer.verae.ai
```
**Files only (lab / golden image before printer step):**
```bat
msiexec /i VeraePrintStamp-windows-amd64-VERSION.msi /qn VERAE_SKIP_KEY_SETUP=1 VERAE_SKIP_PRINTERS=1
```
**Tray-only package (existing skeleton — not full product):**
```bat
msiexec /i VeraeTray-VERSION-peruser.msi /qn VERAE_IPP_ADDR=127.0.0.1:8631
```
---
## Security rules
1. **Never** pass `xVerae-…`, JWT, password, or TOTP as public MSI properties.
2. Portal base must be **https://** unless a documented insecure lab flag exists (`VERAE_ALLOW_INSECURE_PORTAL` is process env today — do not expose as default MSI prop).
3. Log property **names** in verbose msiexec logs; do not add custom actions that echo secret-bearing properties.
4. `VERAE_SKIP_KEY_SETUP=1` is the supported fleet path; interactive key flows stay NSIS or first-run helper UI.
---
## Tray skeleton vs full product
| Property | Tray MSI (`VeraeTrayShortcuts.wxs`) | Full MSI (this table) |
|----------|-------------------------------------|------------------------|
| `VERAE_IPP_ADDR` | Yes | Yes |
| `VERAE_SKIP_KEY_SETUP` | N/A (no key UI) | Yes |
| `VERAE_CHANNEL` | N/A | Yes |
| `VERAE_INSTALL_API_URL` | N/A | Yes |
| `VERAE_SKIP_PRINTERS` | N/A | Yes |
---
## Implementation checklist (Wave 2+ / I1-adjacent)
- [ ] Declare properties in `Product-Full.wxs` with the defaults above.
- [ ] Condition key-related CAs on `VERAE_SKIP_KEY_SETUP<>"1"`.
- [ ] Condition printer CA on `VERAE_SKIP_PRINTERS<>"1"`.
- [ ] Persist channel + portal URL for helper/CLI (config file or HKLM non-secret values only).
- [ ] Document exit codes from future `build-full-msi.ps1` alongside `build-tray-msi.ps1`.
- [ ] Keep NSIS silent story separate until MSI is primary download.
---
## License
UNLICENSED © Verae — packaging contract only; not a redistributable binary.

View file

View file

@ -0,0 +1,87 @@
# CERT self-host fonts (printit.verae.ai)
Placeholder directory for **self-hosted woff2** used by the CERT-01 web
viewer (`../certificate.html`). Binaries are **not** committed yet — only
`.gitkeep` keeps the path in git.
## Current state
| Surface | Fonts today | Offline fallback |
|---------|-------------|------------------|
| `certificate.html` title | **Fraunces** 600/700 via Google Fonts CDN | Georgia / Times New Roman / serif |
| Mono digests | **JetBrains Mono** 500 via same CDN | ui-monospace / SF Mono / Menlo |
| UI kit (`verae-ui-kit.css`) | Inter + Material Symbols Rounded CDN | system stacks in `brand/tokens.css` |
| Issued PDF (`internal/pdfcert`) | System / library serif — **not** Fraunces embed | N/A (separate from web) |
Design source of truth: `verae-2026-design/ui-designs/src/html/CERT-01.html`
(tokens `--font-serif: 'Fraunces'`; design-kit `src/fonts/` currently ships
Cantarell + Material Symbols TTF only — **no Fraunces binary in-repo yet**).
**Not claimed:** full CERT-01.html pixel match, PDF Fraunces embed, or
self-hosted production fonts.
## Goal
Serve brand serif (and optional mono) from this origin so the certificate
viewer works without `fonts.googleapis.com` / `fonts.gstatic.com`, and
print/PDF-from-browser paths stay consistent offline.
## Planned files (when licensed binaries land)
```
fonts/
README.md (this file)
.gitkeep
fraunces-opsz-wght-600.woff2 # variable or static; opsz + weight 600
fraunces-opsz-wght-700.woff2 # weight 700 (title)
jetbrains-mono-500.woff2 # optional; digests already have mono stack
OFL.txt / LICENSE # required license text beside binaries
```
Exact filenames may follow upstream release naming; keep them stable once
wired so deploys and cache busting stay simple.
## How to wire (later — do not claim done until files exist)
1. Obtain **OFL-licensed** Fraunces (and optional JetBrains Mono) woff2 from
the upstream release or a subsetted build (title glyphs only is enough
for CERT).
2. Drop files into this directory; commit license text next to them.
3. Add local `@font-face` in `certificate.html` (or a small
`fonts/cert-fonts.css`):
```css
@font-face {
font-family: 'Fraunces';
src: url('/fonts/fraunces-opsz-wght-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
```
4. Remove the three Google Fonts `<link>` tags from `certificate.html`
head once local faces load cleanly.
5. Keep the Georgia/serif stack as final fallback.
6. Deploy with the rest of the static site (`rsync` to
`/var/www/printit.verae.ai/` — see parent `README.md`).
7. Smoke: open `/certificate.html` with network blocked; title must still
render serif (local woff2 or Georgia).
## Out of scope here
| Item | Owner / later |
|------|----------------|
| PDF CERT Fraunces embed | **I7 done:** pdfcert/build.mjs note + test — Times/Helvetica only until embed lands |
| Inter / Material Symbols self-host for whole site | optional; CERT title is the residual gap called out in OPEN-ISSUES #7 |
| Cantarell from design-kit TTFs | GNOME mock only; not required for CERT-01 web title |
| Pixel-perfect CERT-01.html match | Still later (OPEN-ISSUES Advanced) |
## Status
| Step | State |
|------|-------|
| Placeholder dir + this plan | **Done** (L7) |
| woff2 binaries + LICENSE | Not started |
| `@font-face` + drop CDN links | Not started |
| Pixel match / PDF embed claim | Not started — do not claim |