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 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`) |
| 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`.
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.
//
// 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) {
pdf := gofpdf.New("P", "pt", "Letter", "")
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.SetFont("Helvetica", "B", 9)
pdf.SetXY(margin, 92)
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.SetFont("Times", "B", 22)
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
// evidence copy). Prefers platform/registration-pdf/build.mjs when Node is
// 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) {
created := in.CreatedAt
if created == "" {

View file

@ -259,3 +259,69 @@ func TestFindBuildScriptNoResearchHardcode(t *testing.T) {
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
* - 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>
* input.json: { file, size_bytes, sha256, multihash?, blake3?, blake3_multihash?,
* scope, receipt_id, created_at, merkle, lookup_url? }
@ -143,7 +146,7 @@ async function buildPdf(row) {
doc.setTextColor(...goldD);
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.setFontSize(22);
doc.setTextColor(...ink);

View file

@ -1,12 +1,34 @@
# Build the Windows installer natively on Windows (Go + NSIS).
# Usage: powershell -ExecutionPolicy Bypass -File platform\windows\build-windows.ps1 [-Arch amd64|arm64]
param([string]$Arch = "amd64", [string]$Version = "0.3.2")
# Usage: powershell -ExecutionPolicy Bypass -File platform\windows\build-windows.ps1 [-Arch amd64|arm64] [-Version x.y.z]
# Version default: Get-VeraePackageVersion → internal/job.HelperVersion (or $env:VERSION).
param([string]$Arch = "amd64", [string]$Version = "")
$ErrorActionPreference = "Stop"
$Root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$Out = Join-Path $Root "dist\windows"
$Stage = Join-Path $Out "stage-$Arch"
$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" }
# 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
New-Item -ItemType Directory -Force -Path $Stage, (Join-Path $Stage "scripts") | Out-Null
Push-Location $Root

View file

@ -6,8 +6,12 @@
; 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
; 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
!define VERSION "0.3.2"
!define VERSION "0.3.6"
!endif
!ifndef ARCH
!define ARCH "amd64"

View file

@ -13,6 +13,11 @@
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
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
(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)
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 |