Compare commits

..
Author SHA1 Message Date
Cursor Agent
f8444ec9be
Deploy the Linux verae-print checkout after tests pass.
All checks were successful
test-and-deploy / test (push) Successful in 6s
test-and-deploy / deploy-138 (push) Successful in 10s
2026-10-11 06:44:16 +00:00
Cursor Agent
8f55e17f73
Send a document title as a verae-meta-git/1 history.
A non-blank document name is the first snapshot title, with restamp left false so a reprint keeps the first job. A blank name stays digest-only.
2026-10-11 05:37:25 +00:00
128 changed files with 1273 additions and 14361 deletions

View file

@ -0,0 +1,24 @@
name: test-and-deploy
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: 138
steps:
- uses: actions/checkout@v4
- name: Unit tests
run: bash scripts/test-138.sh
deploy-138:
needs: test
if: success() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: 138
steps:
- uses: actions/checkout@v4
- name: Deploy the host checkout
run: bash scripts/deploy-138.sh

View file

@ -1,164 +0,0 @@
# EXAMPLE ONLY — not a live workflow.
# Copy into .github/workflows/windows-msi-sign.yml (or .forgejo/workflows/) and enable
# only when org secret store holds the names below. Default product CI remains
# windows-msi.yml (unsigned full MSI).
#
# Secret / var NAMES only — never commit PFX bytes, passwords, thumbprints as values,
# or Azure client secrets. See platform/windows/wix/CI-SIGN-NOTARY.md.
#
# GitHub Actions: secrets.* / vars.* as below.
# Forgejo Actions: same secret names; map runs-on to your Windows label; keep names identical.
#
# After sign + upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1 (hash only; no upload).
#
# name: windows-msi-sign
# on:
# workflow_dispatch:
# # push:
# # tags: ["printstamp-v*"]
name: windows-msi-sign-example
on:
workflow_dispatch:
permissions:
contents: read
# id-token: write # uncomment if using Azure OIDC → Trusted Signing (no client secret)
jobs:
# Live default stays unsigned — see windows-msi.yml.
# This example job is gated off until secrets exist.
full-msi-sign:
if: ${{ false }} # flip only after secrets exist; do not claim SmartScreen until clean-VM proof
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install WiX Toolset v3
shell: pwsh
run: |
choco install wixtoolset --version 3.14.1 -y --no-progress
$wixBin = "${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin"
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
$found = Get-ChildItem -Path "${env:ProgramFiles(x86)}" -Filter "candle.exe" -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'WiX Toolset' } |
Select-Object -First 1
if ($found) { $wixBin = $found.Directory.FullName }
}
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
Write-Error "WiX candle.exe not found after install."
}
"WIX_BIN=$wixBin" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"PATH=$wixBin;$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Build full MSI (pre-sign)
shell: pwsh
run: |
powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\build-full-msi.ps1 -Arch amd64
$msi = Get-ChildItem -Path platform\windows\wix\out -Filter "VeraePrintStamp-windows-*.msi" |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $msi) { Write-Error "No MSI under platform/windows/wix/out" }
"MSI_PATH=$($msi.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"MSI_NAME=$($msi.Name)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
$stage = "platform\windows\wix\StageFull\bin"
"STAGE_BIN=$stage" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
# --- Path A: PFX via secrets (signtool) ---
# Secret / var names (placeholders — values never in git):
# secrets.WINDOWS_CERT_PFX_BASE64
# secrets.WINDOWS_CERT_PFX_PASSWORD
# secrets.WINDOWS_CERT_THUMBPRINT # optional if /sha1 preferred after import
# vars.WINDOWS_SIGN_TIMESTAMP_URL # RFC 3161 TSA
#
# - name: Import code-signing PFX
# shell: pwsh
# env:
# WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
# WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
# run: |
# $pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
# [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64))
# $pwd = ConvertTo-SecureString $env:WINDOWS_CERT_PFX_PASSWORD -AsPlainText -Force
# Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pwd | Out-Null
# Remove-Item -LiteralPath $pfx -Force -ErrorAction SilentlyContinue
# # Never Upload-Artifact the PFX; never Write-Host password or base64.
#
# - name: signtool Authenticode (EXEs + MSI)
# shell: pwsh
# env:
# WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
# WINDOWS_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CERT_THUMBPRINT }}
# run: |
# $tsa = $env:WINDOWS_SIGN_TIMESTAMP_URL
# if (-not $tsa) { $tsa = "https://timestamp.digicert.com" }
# $signtool = (Get-Command signtool.exe -ErrorAction Stop).Source
# $targets = @(
# (Join-Path $env:STAGE_BIN "verae-print.exe"),
# (Join-Path $env:STAGE_BIN "verae-printhelper.exe"),
# $env:MSI_PATH
# ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
# foreach ($t in $targets) {
# if ($env:WINDOWS_CERT_THUMBPRINT) {
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /sha1 $env:WINDOWS_CERT_THUMBPRINT $t
# } else {
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /a $t
# }
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool sign failed: $t" }
# & $signtool verify /pa $t
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool verify failed: $t" }
# }
# --- Path B: Azure Trusted Signing (prefer over PFX when available) ---
# Secret / var names (placeholders — values never in git):
# secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID # or OIDC; skip client secret
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET # omit when id-token: write + federated creds
# vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT # e.g. https://cus.codesigning.azure.net
# vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT
# vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE
# vars.WINDOWS_SIGN_TIMESTAMP_URL # optional; action may default TSA
#
# - name: Azure Trusted Signing (EXEs + MSI)
# # uses: azure/trusted-signing-action@v0 # pin a reviewed tag when enabling
# # with:
# # azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID }}
# # azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID }}
# # azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET }}
# # endpoint: ${{ vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT }}
# # trusted-signing-account-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT }}
# # certificate-profile-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
# # files-folder: ${{ env.STAGE_BIN }}
# # files-folder-filter: exe
# # files-folder-recurse: true
# # file-digest: SHA256
# # timestamp-rfc3161: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
# # timestamp-digest: SHA256
# shell: pwsh
# run: |
# Write-Error "Trusted Signing path not enabled — wire action + secret names above."
# - name: Upload signed MSI artifact
# uses: actions/upload-artifact@v4
# with:
# name: VeraePrintStamp-windows-msi-signed
# path: platform/windows/wix/out/VeraePrintStamp-windows-*.msi
# if-no-files-found: error
# retention-days: 14
# Operator after download: upload CDN blob → fill-manifest-sha256.ps1 → paste digests.
# See docs/PUBLISH-DIGESTS.md (does not upload; secret values never land in manifests).
- name: Placeholder (example not enabled)
shell: pwsh
run: |
Write-Host "windows-msi-sign.yml.example — sign job gated if: false."
Write-Host "Live unsigned build: .github/workflows/windows-msi.yml"
Write-Host "Local dry-run: platform/windows/scripts/sign-dry-run.ps1"
Write-Host "Digests after sign+upload: docs/PUBLISH-DIGESTS.md"

View file

@ -1,164 +0,0 @@
# Unsigned full elevated MSI (Product-Full.wxs + build-full-msi.ps1).
# Artifact is NOT Authenticode-signed. Sign step is commented; secret names only.
# See platform/windows/wix/CI-SIGN-NOTARY.md and platform/windows/wix/DUAL-SHIP.md § CI artifact.
name: windows-msi
on:
workflow_dispatch:
push:
paths:
- "platform/windows/**"
- "cmd/**"
- "internal/**"
- "go.mod"
- "go.sum"
- ".github/workflows/windows-msi.yml"
pull_request:
paths:
- "platform/windows/**"
- "cmd/**"
- "internal/**"
- "go.mod"
- "go.sum"
- ".github/workflows/windows-msi.yml"
permissions:
contents: read
jobs:
full-msi-unsigned:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install WiX Toolset v3
shell: pwsh
run: |
choco install wixtoolset --version 3.14.1 -y --no-progress
$wixBin = "${env:ProgramFiles(x86)}\WiX Toolset v3.14\bin"
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
# Fallback: any WiX v3.* bin on Program Files
$found = Get-ChildItem -Path "${env:ProgramFiles(x86)}" -Filter "candle.exe" -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'WiX Toolset' } |
Select-Object -First 1
if ($found) { $wixBin = $found.Directory.FullName }
}
if (-not (Test-Path -LiteralPath (Join-Path $wixBin "candle.exe"))) {
Write-Error "WiX candle.exe not found after install. Full MSI job requires WiX v3."
}
"WIX_BIN=$wixBin" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"PATH=$wixBin;$env:PATH" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "WiX bin: $wixBin"
& (Join-Path $wixBin "candle.exe") -?
- name: Build full MSI (unsigned)
shell: pwsh
run: |
$candle = Get-Command candle -ErrorAction SilentlyContinue
$light = Get-Command light -ErrorAction SilentlyContinue
if (-not $candle -or -not $light) {
Write-Error "WiX candle/light not on PATH — refusing unsigned MSI build (fail closed)."
}
powershell -NoProfile -ExecutionPolicy Bypass -File platform\windows\scripts\build-full-msi.ps1 -Arch amd64
$msi = Get-ChildItem -Path platform\windows\wix\out -Filter "VeraePrintStamp-windows-*.msi" -ErrorAction SilentlyContinue |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $msi) {
Write-Error "build-full-msi.ps1 did not produce VeraePrintStamp-windows-*.msi under platform/windows/wix/out"
}
Write-Host "MSI: $($msi.FullName) ($([math]::Round($msi.Length/1MB, 2)) MB)"
Get-FileHash -Algorithm SHA256 -Path $msi.FullName | Format-List
"MSI_PATH=$($msi.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"MSI_NAME=$($msi.Name)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
# --- Future Authenticode (NOT enabled) ---
# Wire only after certs exist in the org secret store. Never commit PFX or passwords.
# Full copy-paste example (gated if: false): .github/workflows/windows-msi-sign.yml.example
# Local dry-run (no secrets): platform/windows/scripts/sign-dry-run.ps1
# After sign + CDN upload: docs/PUBLISH-DIGESTS.md + fill-manifest-sha256.ps1
# Secret / var names (placeholders only — see CI-SIGN-NOTARY.md):
# secrets.WINDOWS_CERT_PFX_BASE64
# secrets.WINDOWS_CERT_PFX_PASSWORD
# secrets.WINDOWS_CERT_THUMBPRINT # optional /sha1 after import
# vars.WINDOWS_SIGN_TIMESTAMP_URL # RFC 3161 TSA
# secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID # Path B: Azure Trusted Signing
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID
# secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET # omit when OIDC id-token: write
# vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT
# vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT
# vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE
#
# --- Path A: PFX → signtool (commented; secret NAMES only) ---
# - name: Import code-signing PFX
# if: ${{ false }}
# shell: pwsh
# env:
# WINDOWS_CERT_PFX_BASE64: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
# WINDOWS_CERT_PFX_PASSWORD: ${{ secrets.WINDOWS_CERT_PFX_PASSWORD }}
# run: |
# $pfx = Join-Path $env:RUNNER_TEMP "codesign.pfx"
# [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX_BASE64))
# $pwd = ConvertTo-SecureString $env:WINDOWS_CERT_PFX_PASSWORD -AsPlainText -Force
# Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $pwd | Out-Null
# Remove-Item -LiteralPath $pfx -Force -ErrorAction SilentlyContinue
# # Never Upload-Artifact the PFX; never echo password or base64.
#
# - name: signtool Authenticode (EXEs + MSI)
# if: ${{ false }}
# shell: pwsh
# env:
# WINDOWS_SIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
# WINDOWS_CERT_THUMBPRINT: ${{ secrets.WINDOWS_CERT_THUMBPRINT }}
# run: |
# $tsa = $env:WINDOWS_SIGN_TIMESTAMP_URL
# if (-not $tsa) { $tsa = "https://timestamp.digicert.com" }
# $signtool = (Get-Command signtool.exe -ErrorAction Stop).Source
# $stage = "platform\windows\wix\StageFull\bin"
# $targets = @(
# (Join-Path $stage "verae-print.exe"),
# (Join-Path $stage "verae-printhelper.exe"),
# $env:MSI_PATH
# ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
# foreach ($t in $targets) {
# if ($env:WINDOWS_CERT_THUMBPRINT) {
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /sha1 $env:WINDOWS_CERT_THUMBPRINT $t
# } else {
# & $signtool sign /fd SHA256 /tr $tsa /td SHA256 /a $t
# }
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool sign failed: $t" }
# & $signtool verify /pa $t
# if ($LASTEXITCODE -ne 0) { Write-Error "signtool verify failed: $t" }
# }
#
# --- Path B: Azure Trusted Signing (prefer over PFX when available) ---
# - name: Azure Trusted Signing (EXEs + MSI)
# if: ${{ false }}
# # uses: azure/trusted-signing-action@v0 # pin a reviewed tag when enabling
# # with:
# # azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_TENANT_ID }}
# # azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_ID }}
# # azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CLIENT_SECRET }}
# # endpoint: ${{ vars.WINDOWS_TRUSTED_SIGNING_ENDPOINT }}
# # trusted-signing-account-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_ACCOUNT }}
# # certificate-profile-name: ${{ vars.WINDOWS_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
# # files: ${{ env.MSI_PATH }}
# # file-digest: SHA256
# # timestamp-rfc3161: ${{ vars.WINDOWS_SIGN_TIMESTAMP_URL }}
# # timestamp-digest: SHA256
# shell: pwsh
# run: |
# Write-Error "Trusted Signing not enabled — unsigned artifact only. See windows-msi-sign.yml.example."
- name: Upload unsigned MSI artifact
uses: actions/upload-artifact@v4
with:
name: VeraePrintStamp-windows-msi-unsigned
path: platform/windows/wix/out/VeraePrintStamp-windows-*.msi
if-no-files-found: error
retention-days: 14

6
.gitignore vendored
View file

@ -7,9 +7,3 @@
.DS_Store
node_modules/
/build/
Verae-Secret-Credentials.MD
# Compiled PT-06 menu bar (built by run-menubar.sh / swiftc)
platform/macos/menubar/verae-menubar
verae-print
platform/macos/pkg/resources/AppIcon.iconset/
build/macos-pkg/

162
README.md
View file

@ -1,66 +1,136 @@
# PrintToVeraeTimeStamp
[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
Print / CLI helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`.
PrintIt desktop/CLI path: stamp documents via Verae edge billing (xVerae keys).
## Table of Contents
- [Background](#background)
- [Install](#install)
- [Usage](#usage)
- [Packaging](#packaging)
- [Contributing](#contributing)
- [License](#license)
## Background
Default channel is zapier (zap.verae.ai) with x-billing-channel=printit.
Desktop and print workflows share the same commercial meter as portal API customers.
**Co-contributors:** Grok (xAI), George Lambert, Scott "R00tPiG Security" Lindsey — Verae Inc. ([verae.com](https://verae.com)).
Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine.
## Install
```sh
go build ./...
Downloads: [printit.verae.ai](https://printit.verae.ai/) (macOS `.pkg`, Windows `.zip`, Linux `.tar.gz`).
**macOS (recommended):** open `VeraePrintStamp-macos.pkg`. First launch opens **customer.verae.ai** for an **xVerae-** signing key.
**Windows (GUI installer):**
```text
Download VeraePrintStamp-windows-amd64-setup.exe from printit.verae.ai
Run the installer → Finish with “Run setup” → paste your xVerae- key
verae-print stamp .\document.pdf
```
**Debian / Ubuntu:**
```bash
sudo apt install ./verae-print_amd64.deb
verae-print setup
verae-print stamp document.pdf
```
**Linux (tarball, any distro):**
```bash
tar xzf VeraePrintStamp-linux-amd64.tar.gz
cd VeraePrintStamp-linux-amd64-*
sudo ./install.sh # CLI + optional CUPS queue
verae-print setup
verae-print stamp document.pdf
```
From source (macOS):
```bash
cd /Users/marchon/research/PrintToVeraeTimeStamp
go build -o "$HOME/bin/verae-print" ./cmd/verae-print
verae-print setup # create account + save xVerae- signing key
verae-print install-printer # Print → Destination → Verae Timestamping
```
Optional (buried PDF submenu): `verae-print install-macos`
Ensure `$HOME/bin` is on your `PATH`. For the legacy Verae Time JWT channel:
```bash
uvx verae-mcp-timestamping-server setup
# or session check:
uvx verae-mcp-timestamping-server session
```
For the Zapier channel, store an `xVerae-` token from [customer.verae.ai](https://customer.verae.ai/portal/) / [zap.verae.ai](https://zap.verae.ai/portal/) Security tab:
```bash
mkdir -p ~/.verae
chmod 700 ~/.verae
printenv XVERAE_TOKEN > ~/.verae/xverae-token # or paste once
chmod 600 ~/.verae/xverae-token
```
## Usage
```sh
verae-print stamp --channel=zapier file.pdf # default billed path
verae-print serve # IPP helper + loopback status
verae-print tray-status # held/key/helper snapshot
verae-print tray-status --open # open http://127.0.0.1:8631/queue
verae-print menubar # PT-W3/PT-06: tray (Win) or menu bar (macOS)
verae-print menubar --install # login presence (Win Startup / macOS LaunchAgent)
verae-print menubar --uninstall # remove login presence
verae-print pending --html # offline queue page
# Live while helper runs: GET /queue (HTML), GET /status (JSON), GET / (health)
# Windows tray: platform/windows/scripts/verae-tray.ps1
# Windows Startup: platform/windows/scripts/install-tray-startup.ps1
# Windows Start Menu (WiX-free stub): platform/windows/scripts/build-tray-startmenu.ps1
# macOS menu bar: platform/macos/menubar/run-menubar.sh
# macOS durable LaunchAgent assets: ~/Library/Application Support/Verae/menubar/
# macOS optional .app (unsigned): platform/macos/menubar/build-menubar-app.sh
# macOS notarization steps (docs only): platform/macos/menubar/README.txt
# Direct chain only with VERAE_ALLOW_DIRECT_VERAE=1
In any app: **File → Print → Destination → Verae Timestamping → Print**.
Also: drop a PDF on **Verae Print Stamp** in `/Applications`, or **Open With**.
CLI:
```bash
verae-print stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] file.pdf
```
## Packaging
Default output directory: `~/Documents/Verae Timestamps/`
Tray, menu bar, CERT-01 digests, version stamps (`HelperVersion` **0.3.6**), and IPP GET vs POST split:
| File | Content |
|------|---------|
| `{name}.pdf` | Exact printed bytes (proven artifact) |
| `{name}-verae-timestamp-{RECEIPTID}.pdf` | Registration / certificate PDF |
| `{name}.verae.json` | Receipt sidecar |
- [docs/PACKAGING.md](docs/PACKAGING.md) — Windows tray (no elevated MSI), macOS LaunchAgent / unsigned `.app` / notarization (docs only), `pending` / `tray-status` / `menubar`, statuslang notify titles, hold ≠ evidence, CERT-01 web viewer.
- [site/printit.verae.ai/certificate.html](site/printit.verae.ai/certificate.html) — CERT-01 branded certificate HTML (plum/gold, multihash when present).
Channels:
- `zapier` (default after setup) — `zap.verae.ai` + customer `xVerae-` signing key + `attachPdf:true`
- `verae` — `api.veraetime.net` + JWT in `~/.verae/auth.properties` (opt-in: `-channel=verae`)
## Signed macOS installer (.pkg)
Same FlightLookup Developer ID + notarization path as Verae Timestamping.app (`~/fie/installers/macos/signing.env`).
```bash
cd /Users/marchon/research/PrintToVeraeTimeStamp
chmod +x platform/macos/pkg/build-pkg.sh
./platform/macos/pkg/build-pkg.sh
# → build/macos-pkg/dist/VeraePrintStamp-0.1.2.pkg
# → build/macos-pkg/dist/VeraePrintStamp-macos.pkg
```
The pkg installs:
| Path | Role |
|------|------|
| `/Applications/Verae Print Stamp.app` | Drop / Open With |
| `/Library/Application Support/Verae/PrintStamp/bin/verae-print` | Stamp CLI (universal) |
| `/usr/local/bin/verae-print` | Symlink |
| LaunchAgent + CUPS queue | **Print → Destination → Verae Timestamping** |
Requires: `Developer ID Application` + `Developer ID Installer` (FlightLookup), App Store Connect API key in `signing.env`, Node for bundling the registration PDF builder.
## Windows / Linux release archives
```bash
./scripts/build-win-linux.sh
./platform/windows/build-installer.sh # NSIS GUI setup.exe (needs makensis)
# on Debian/Ubuntu host:
VERSION=0.1.2 BIN_AMD64=... BIN_ARM64=... ./platform/linux/deb/build-deb.sh all
```
Windows ships an NSIS GUI installer; stamp via CLI after setup (virtual printer still on the [spec](docs/superpowers/specs/2026-10-03-print-to-verae-timestamp.md) roadmap). Debian/Ubuntu use `verae-print_*.deb`; other distros use the tarball + `install.sh`.
## Status
macOS Destination printer (notarized `.pkg`), Windows/Linux CLI packages, and branded registration PDF are published at [printit.verae.ai](https://printit.verae.ai/).
## Contributing
Questions: open an issue on the project Forgejo/GitHub remote, or contact Verae engineering.
PRs are accepted when they include tests and do not leak secrets.
Open questions and design changes in this repository. Pull requests are accepted for review against the spec.
## License
UNLICENSED © Verae Inc. (verae.com)
UNLICENSED © Verae / George Lambert

View file

@ -1,4 +1,4 @@
//go:build !windows && !darwin && !linux
//go:build !windows && !darwin
package main

View file

@ -1,7 +0,0 @@
//go:build !windows
package main
import "os/exec"
func hideWindow(c *exec.Cmd) {}

View file

@ -1,10 +0,0 @@
//go:build !windows && !linux
package main
import "fmt"
func runKeyDialog() int {
fmt.Println("key-dialog is for Windows and Linux; on macOS run: verae-print mac-key-setup")
return 1
}

View file

@ -45,8 +45,6 @@ function Btn([string]$text, [int]$x, [int]$w, [string]$res) {
switch ($kind) {
'login' {
$e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $t = Field '2FA code (if enabled)' $false ''
$fl = New-Object Windows.Forms.LinkLabel; $fl.Text = 'Forgot password?'; $fl.Left = 170; $fl.Top = $script:y; $fl.Width = 200
$fl.Add_LinkClicked({ $script:result = 'forgot'; $f.Close() }); $f.Controls.Add($fl); $script:y += 24
$script:y += 8
$ok = Btn 'Sign in' 15 100 'login'; $f.AcceptButton = $ok
Btn 'Create account' 125 120 'create' | Out-Null
@ -55,35 +53,7 @@ switch ($kind) {
$f.Height = $script:y + 75
if ($pre) { $f.Add_Shown({ $p.Focus() }) }
[void]$f.ShowDialog()
if ($script:result -eq 'login') { "login|$(B $e.Text)|$(B $p.Text)|$(B $t.Text)" }
elseif ($script:result -eq 'forgot') { "forgot|$(B $e.Text)" } else { $script:result }
}
'forgot' {
$e = Field 'Email' $false $pre
$script:y += 8
$ok = Btn 'Send code' 15 100 'send'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'send') { "send|$(B $e.Text)" } else { 'cancel' }
}
'reset' {
$k = Field 'Code from the email' $false ''; $p = Field 'New password' $true ''; $p2 = Field 'Confirm new password' $true ''
$script:y += 8
$ok = Btn 'Reset password' 15 130 'reset'; $f.AcceptButton = $ok
Btn 'Send a new code' 155 130 'resend' | Out-Null
$c = Btn 'Cancel' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'reset') { "reset|$(B $k.Text)|$(B $p.Text)|$(B $p2.Text)" } else { $script:result }
}
'unavail' {
$ok = Btn 'Open customer.verae.ai' 15 200 'open'; $f.AcceptButton = $ok
$c = Btn 'Back' 410 100 'cancel'; $f.CancelButton = $c
$f.Height = $script:y + 75
[void]$f.ShowDialog()
if ($script:result -eq 'open') { Start-Process 'https://customer.verae.ai/portal/' }
'cancel'
if ($script:result -eq 'login') { "login|$(B $e.Text)|$(B $p.Text)|$(B $t.Text)" } else { $script:result }
}
'create' {
$n = Field 'Your name' $false ''; $e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $p2 = Field 'Confirm password' $true ''
@ -173,21 +143,9 @@ func acquireKeyInteractive(reason string) bool {
}
if err != nil {
vlog.Errorf("sign-in: login failed for %s: %v", auth.MaskEmail(email), err)
msg = "Sign-in failed: " + err.Error() + "\r\nForgot your password? Click \"Forgot password?\" below."
msg = "Sign-in failed: " + err.Error()
continue
}
case "forgot":
if e := strings.TrimSpace(f[0]); e != "" {
email = e
}
k, ok, m := winForgotFlow(caps, email)
if m != "" {
msg = m
}
if !ok {
continue
}
key = k
case "create":
k, ok := winCreateFlow(caps, email)
if !ok {
@ -273,95 +231,6 @@ func winCreateFlow(caps auth.Capabilities, email string) (string, bool) {
return "", false
}
// winResetUnavailable: in-app reset not supported; offer the web portal.
func winResetUnavailable() {
_, _, _ = winForm("unavail", "Password reset isn't available in the app yet.\r\n\r\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "")
}
// winForgotFlow: emailed-code password reset. Returns (key, ok, message for the sign-in form).
func winForgotFlow(caps auth.Capabilities, email string) (string, bool, string) {
msg := "Reset your Verae password.\r\nWe'll email a 6-digit code to this address."
for i := 0; i < 10; i++ {
act, f, err := winForm("forgot", msg, email)
if err != nil || act != "send" {
return "", false, ""
}
email = strings.TrimSpace(f[0])
if !auth.ValidEmail(email) {
msg = "Please enter a valid email address."
continue
}
vlog.Printf("sign-in: password reset requested for %s", auth.MaskEmail(email))
if ferr := auth.PasswordForgot(email); ferr != nil {
vlog.Errorf("sign-in: password forgot %s: %v", auth.MaskEmail(email), ferr)
if errors.Is(ferr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
msg = "Could not send a reset code: " + ferr.Error()
continue
}
cmsg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes). Check your spam folder.\r\n" + caps.Policy.Describe()
for j := 0; j < 15; j++ {
act, cf, err := winForm("reset", cmsg, "")
if err != nil || act == "cancel" || act == "" {
return "", false, ""
}
if act == "resend" {
if rerr := auth.PasswordForgot(email); rerr != nil {
if errors.Is(rerr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
cmsg = "Could not send a new code: " + rerr.Error()
} else {
cmsg = "A new code was sent to " + email + ".\r\n" + caps.Policy.Describe()
}
continue
}
code, pw := strings.TrimSpace(cf[0]), cf[1]
switch {
case len(code) != 6 || strings.Trim(code, "0123456789") != "":
cmsg = "Enter the 6-digit code from the email.\r\n" + caps.Policy.Describe()
continue
case pw != cf[2]:
cmsg = "The passwords do not match.\r\n" + caps.Policy.Describe()
continue
}
if perr := caps.Policy.Check(pw, email); perr != nil {
cmsg = perr.Error() + "\r\n" + caps.Policy.Describe()
continue
}
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
cf[1], cf[2] = "", ""
if rerr != nil {
vlog.Errorf("sign-in: password reset %s: %v", auth.MaskEmail(email), rerr)
if errors.Is(rerr, auth.ErrResetUnavailable) {
winResetUnavailable()
return "", false, ""
}
cmsg = "Could not reset the password: " + rerr.Error()
continue
}
vlog.Printf("sign-in: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
if key != "" {
return key, true, ""
}
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
pw = ""
if lerr != nil {
if errors.Is(lerr, auth.ErrTOTPRequired) {
return "", false, "Your password was changed. Sign in with your new password and your 6-digit authenticator code."
}
return "", false, "Your password was changed, but signing in failed: " + lerr.Error() + "\r\nSign in with your new password."
}
return key, true, ""
}
return "", false, ""
}
return "", false, ""
}
// pasteFlow: masked key field + portal button; validated before saving.
func pasteFlow(reason string) bool {
msg := "Open customer.verae.ai, sign in, open Security and create an xVerae- token (shown once). Paste it below."

View file

@ -1,365 +0,0 @@
//go:build darwin || linux
package main
// Shared native sign-in flow (macOS AppleScript dialogs, Linux zenity dialogs).
// Platform files provide ask/say/choose/openURL, machineWord and keyStoreDesc.
// Passwords travel only over the dialog's stdout pipe and HTTPS; never argv,
// files or logs. Keys are logged masked only.
import (
"errors"
"strings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
var errCancel = errors.New("cancelled")
const (
optKeep = "Keep the existing key"
optLogin = "Sign in to my Verae account (email + password)"
optCreate = "Create a new Verae account"
optPaste = "Paste an xVerae- key"
optSkip = "Skip for now"
)
// acquireKeyInteractive: native sign-in dialogs after a 401/403 or missing key.
func acquireKeyInteractive(reason string) bool {
if !guiAvailable() {
vlog.Printf("sign-in needed but no desktop session for dialogs: %s (run: verae-print key-dialog)", reason)
return false
}
_, saved := keyFlow(reason)
return saved
}
// keyFlow returns a masked result string and whether a NEW key was saved.
func keyFlow(reason string) (string, bool) {
existing, _ := auth.LoadXVeraeToken()
caps := auth.GetCapabilities()
vlog.Printf("mac-key-setup: installApi=%v emailVerification=%v existing=%v", caps.InstallAPI, caps.EmailVerification, existing != "")
var opts []string
intro := "No Verae signing key was found for this " + machineWord + " user. You need one to timestamp documents."
if existing != "" {
st, det := auth.ValidateXVeraeKey(existing)
vlog.Printf("mac-key-setup: existing key %s check=%s %s", auth.MaskKey(existing), st, det)
switch st {
case "valid":
intro = "A Verae signing key is already saved: " + auth.MaskKey(existing) + " (verified with Verae)."
opts = append(opts, optKeep)
case "invalid":
intro = "A saved key (" + auth.MaskKey(existing) + ") was REJECTED by Verae. Please sign in or paste a new key."
default:
intro = "A Verae signing key is saved: " + auth.MaskKey(existing) + " (could not verify right now)."
opts = append(opts, optKeep)
}
}
opts = append(opts, optLogin, optCreate, optPaste, optSkip)
if reason != "" {
intro = reason + "\n\n" + intro
}
prompt := intro + "\n\nYour password is sent only to customer.verae.ai over HTTPS and is never stored. Only the issued key is saved (" + keyStoreDesc + ")."
for {
c, err := choose(prompt, opts)
if err != nil {
c = optSkip
}
var key string
switch c {
case optKeep:
return "kept " + auth.MaskKey(existing), false
case optLogin:
key, err = macLogin(caps)
case optCreate:
key, err = macCreate(caps)
case optPaste:
key, err = macPaste()
default:
btn, _, aerr := ask("Continue without a Verae key?\n\nThe printer is installed, but documents cannot be timestamped until you add a key. You will be asked again on your first print (or run Verae Print Stamp from your apps menu).", "", false, false, []string{"Go back", "Skip"}, "Go back")
if aerr != nil || btn == "Skip" {
return "skipped", false
}
continue
}
if errors.Is(err, errCancel) {
continue // back to the menu
}
if err != nil {
vlog.Errorf("mac-key-setup: %v", err)
say("Could not get a key: " + err.Error())
continue
}
if err := auth.SaveXVeraeToken(key); err != nil {
vlog.Errorf("mac-key-setup: save failed: %v", err)
say("The key could not be saved: " + err.Error())
continue
}
vlog.Printf("mac-key-setup: key saved %s", auth.MaskKey(key))
say("Your Verae signing key (" + auth.MaskKey(key) + ") is saved.\n\nPrint from any app and choose \"Verae Timestamping\".")
return "saved " + auth.MaskKey(key), true
}
}
const btnForgot = "Forgot password?"
func macLogin(caps auth.Capabilities) (string, error) {
email := ""
for {
btn, e, err := ask("Sign in to Verae\n\nEmail:", email, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
return "", errCancel
}
email = strings.TrimSpace(e)
if !auth.ValidEmail(email) {
say("Please enter a valid email address.")
continue
}
btn, pw, err := ask("Password for "+email+":", "", true, true, []string{"Back", btnForgot, "Sign in"}, "Sign in")
if err != nil || btn == "Back" {
continue
}
if btn == btnForgot {
key, ferr := macForgot(caps, email)
if errors.Is(ferr, errCancel) {
continue
}
return key, ferr
}
vlog.Printf("mac-key-setup: login %s", auth.MaskEmail(email))
key, err := auth.InstallLogin(caps, email, pw, "", tokenName())
if errors.Is(err, auth.ErrTOTPRequired) {
btn, code, cerr := ask("This account uses two-factor authentication.\n\nEnter the 6-digit code from your authenticator app:", "", false, true, []string{"Back", "Verify"}, "Verify")
if cerr != nil || btn == "Back" {
continue
}
key, err = auth.InstallLogin(caps, email, pw, strings.TrimSpace(code), tokenName())
}
pw = ""
if err != nil {
vlog.Errorf("mac-key-setup: login failed for %s: %v", auth.MaskEmail(email), err)
b, _, _ := ask("Sign-in failed: "+err.Error(), "", false, false, []string{btnForgot, "Try again"}, "Try again")
if b == btnForgot {
key, ferr := macForgot(caps, email)
if errors.Is(ferr, errCancel) {
continue
}
return key, ferr
}
continue
}
return key, nil
}
}
// offerPortalReset tells the user in-app reset is unavailable and offers the web portal.
func offerPortalReset() {
b, _, _ := ask("Password reset isn't available in the app yet.\n\nYou can reset your password on the Verae customer portal in your browser ("+auth.PortalResetURL+").", "", false, false, []string{"Back", "Open portal"}, "Open portal")
if b == "Open portal" {
openURL(auth.PortalResetURL)
}
}
// macForgot: emailed-code password reset. Returns a minted key, or errCancel.
func macForgot(caps auth.Capabilities, email string) (string, error) {
for {
btn, e, err := ask("Reset your Verae password\n\nWe'll email a 6-digit code to this address:", email, false, true, []string{"Back", "Send code"}, "Send code")
if err != nil || btn == "Back" {
return "", errCancel
}
email = strings.TrimSpace(e)
if !auth.ValidEmail(email) {
say("Please enter a valid email address.")
continue
}
vlog.Printf("mac-key-setup: password reset requested for %s", auth.MaskEmail(email))
if ferr := auth.PasswordForgot(email); ferr != nil {
vlog.Errorf("mac-key-setup: password forgot %s: %v", auth.MaskEmail(email), ferr)
if errors.Is(ferr, auth.ErrResetUnavailable) {
offerPortalReset()
return "", errCancel
}
say("Could not send a reset code: " + ferr.Error())
continue
}
msg := "If " + email + " has a Verae account, we emailed it a 6-digit code (valid 15 minutes).\nIt may take a minute; check spam.\n\nCode:"
codeLoop:
for {
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Next"}, "Next")
if err != nil || btn == "Cancel" {
return "", errCancel
}
if btn == "Send a new code" {
if rerr := auth.PasswordForgot(email); rerr != nil {
if errors.Is(rerr, auth.ErrResetUnavailable) {
offerPortalReset()
return "", errCancel
}
say("Could not send a new code: " + rerr.Error())
} else {
say("A new code is on its way.")
}
continue
}
code = strings.TrimSpace(code)
if len(code) != 6 || strings.Trim(code, "0123456789") != "" {
say("Enter the 6-digit code from the email.")
continue
}
for {
btn, pw, err := ask("Choose a new password.\n"+caps.Policy.Describe(), "", true, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
continue codeLoop
}
if perr := caps.Policy.Check(pw, email); perr != nil {
say(perr.Error())
continue
}
btn, pw2, err := ask("Confirm the new password:", "", true, true, []string{"Back", "Reset password"}, "Reset password")
if err != nil || btn == "Back" {
continue
}
if pw != pw2 {
say("The passwords do not match.")
continue
}
pw2 = ""
key, rerr := auth.PasswordReset(email, code, pw, tokenName())
if rerr != nil {
pw = ""
vlog.Errorf("mac-key-setup: password reset %s: %v", auth.MaskEmail(email), rerr)
if errors.Is(rerr, auth.ErrResetUnavailable) {
offerPortalReset()
return "", errCancel
}
say("Could not reset the password: " + rerr.Error())
continue codeLoop
}
vlog.Printf("mac-key-setup: password reset ok for %s (key returned=%v)", auth.MaskEmail(email), key != "")
if key != "" {
return key, nil
}
// No key minted (e.g. 2FA account): sign in with the new password.
key, lerr := auth.InstallLogin(caps, email, pw, "", tokenName())
if errors.Is(lerr, auth.ErrTOTPRequired) {
b, totp, cerr := ask("Your password was changed.\n\nThis account uses two-factor authentication. Enter the 6-digit code from your authenticator app:", "", false, true, []string{"Cancel", "Verify"}, "Verify")
if cerr != nil || b == "Cancel" {
pw = ""
return "", errCancel
}
key, lerr = auth.InstallLogin(caps, email, pw, strings.TrimSpace(totp), tokenName())
}
pw = ""
if lerr != nil {
say("Your password was changed, but signing in failed: " + lerr.Error() + "\n\nChoose \"Sign in\" and use your new password.")
return "", errCancel
}
return key, nil
}
}
}
}
func macCreate(caps auth.Capabilities) (string, error) {
var name, email string
for {
btn, n, err := ask("Create a Verae account\n\nYour name:", name, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
return "", errCancel
}
name = strings.TrimSpace(n)
if name == "" {
continue
}
btn, e, err := ask("Email address:", email, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
continue
}
email = strings.TrimSpace(e)
if !auth.ValidEmail(email) {
say("Please enter a valid email address.")
continue
}
policy := caps.Policy.Describe()
btn, pw, err := ask("Choose a password.\n"+policy, "", true, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
continue
}
if perr := caps.Policy.Check(pw, email); perr != nil {
say(perr.Error())
continue
}
btn, pw2, err := ask("Confirm the password:", "", true, true, []string{"Back", "Create account"}, "Create account")
if err != nil || btn == "Back" {
continue
}
if pw != pw2 {
say("The passwords do not match.")
continue
}
pw2 = ""
vlog.Printf("mac-key-setup: register %s installApi=%v", auth.MaskEmail(email), caps.InstallAPI)
rr, err := auth.InstallRegister(caps, name, email, pw, tokenName())
pw = ""
if err != nil {
vlog.Errorf("mac-key-setup: register failed for %s: %v", auth.MaskEmail(email), err)
say("Could not create the account: " + err.Error())
continue
}
if rr.PendingID == "" {
return rr.Key, nil // server without email verification (legacy signup)
}
vlog.Printf("mac-key-setup: verification code sent to %s", auth.MaskEmail(email))
msg := "We emailed a 6-digit code to " + auth.MaskEmail(email) + ".\nIt may take a minute; check spam.\n\nCode:"
for {
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Verify"}, "Verify")
if err != nil || btn == "Cancel" {
return "", errCancel
}
if btn == "Send a new code" {
if rerr := auth.InstallResend(rr.PendingID); rerr != nil {
say("Could not send a new code: " + rerr.Error())
} else {
say("A new code was sent.")
}
continue
}
key, verr := auth.InstallVerify(rr.PendingID, code, tokenName())
if verr != nil {
say(verr.Error())
continue
}
return key, nil
}
}
}
func macPaste() (string, error) {
for {
btn, k, err := ask("Paste your xVerae- key.\n\nNo key yet? Click \"Open portal\", sign in, open Security and create an xVerae- token (shown once).", "", true, true, []string{"Back", "Open portal", "Save"}, "Save")
if err != nil || btn == "Back" {
return "", errCancel
}
if btn == "Open portal" {
openURL(auth.PortalSignupURL)
continue
}
k = strings.TrimSpace(k)
st, det := auth.ValidateXVeraeKey(k)
vlog.Printf("mac-key-setup: pasted key %s: %s %s", auth.MaskKey(k), st, det)
switch st {
case "valid":
return k, nil
case "invalid":
say("Verae rejected this key (" + det + "). Please check it and paste again.")
case "badformat":
say("That does not look like a Verae key: " + det)
default:
b, _, _ := ask("Could not reach Verae to check the key ("+det+").\nSave it anyway?", "", false, false, []string{"No", "Save anyway"}, "Save anyway")
if b == "Save anyway" {
return k, nil
}
}
}
}

View file

@ -3,12 +3,14 @@
package main
import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
"strings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
@ -20,15 +22,6 @@ import (
const dlgTitle = "Verae Print Stamp"
const (
machineWord = "Mac"
keyStoreDesc = "Keychain + ~/.verae"
)
func openURL(u string) { _ = exec.Command("open", u).Start() }
func guiAvailable() bool { return true }
// AppleScript gets all user-visible text via argv (no string interpolation →
// no injection). Output: "<button>\t<text returned>" or "CANCEL".
const dialogScript = `on run argv
@ -67,6 +60,8 @@ const chooseScript = `on run argv
return item 1 of r
end run`
var errCancel = errors.New("cancelled")
func osa(script string, args ...string) (string, error) {
cmd := exec.Command("osascript", append([]string{"-e", script, "--"}, args...)...)
out, err := cmd.Output()
@ -108,6 +103,14 @@ func choose(msg string, opts []string) (string, error) {
return out, nil
}
const (
optKeep = "Keep the existing key"
optLogin = "Sign in to my Verae account (email + password)"
optCreate = "Create a new Verae account"
optPaste = "Paste an xVerae- key"
optSkip = "Skip for now"
)
func runMacKeySetup(args []string) int {
fs := flag.NewFlagSet("mac-key-setup", flag.ContinueOnError)
askOpen := fs.Bool("ask-open", true, "also ask what to open after each print")
@ -140,6 +143,217 @@ func runMacKeySetup(args []string) int {
return 0
}
// acquireKeyInteractive: native sign-in dialogs after a 401/403 or missing key.
func acquireKeyInteractive(reason string) bool {
_, saved := keyFlow(reason)
return saved
}
// keyFlow returns a masked result string and whether a NEW key was saved.
func keyFlow(reason string) (string, bool) {
existing, _ := auth.LoadXVeraeToken()
caps := auth.GetCapabilities()
vlog.Printf("mac-key-setup: installApi=%v emailVerification=%v existing=%v", caps.InstallAPI, caps.EmailVerification, existing != "")
var opts []string
intro := "No Verae signing key was found for this Mac user. You need one to timestamp documents."
if existing != "" {
st, det := auth.ValidateXVeraeKey(existing)
vlog.Printf("mac-key-setup: existing key %s check=%s %s", auth.MaskKey(existing), st, det)
switch st {
case "valid":
intro = "A Verae signing key is already saved: " + auth.MaskKey(existing) + " (verified with Verae)."
opts = append(opts, optKeep)
case "invalid":
intro = "A saved key (" + auth.MaskKey(existing) + ") was REJECTED by Verae. Please sign in or paste a new key."
default:
intro = "A Verae signing key is saved: " + auth.MaskKey(existing) + " (could not verify right now)."
opts = append(opts, optKeep)
}
}
opts = append(opts, optLogin, optCreate, optPaste, optSkip)
if reason != "" {
intro = reason + "\n\n" + intro
}
prompt := intro + "\n\nYour password is sent only to customer.verae.ai over HTTPS and is never stored. Only the issued key is saved (Keychain + ~/.verae)."
for {
c, err := choose(prompt, opts)
if err != nil {
c = optSkip
}
var key string
switch c {
case optKeep:
return "kept " + auth.MaskKey(existing), false
case optLogin:
key, err = macLogin(caps)
case optCreate:
key, err = macCreate(caps)
case optPaste:
key, err = macPaste()
default:
btn, _, _ := ask("Continue without a Verae key?\n\nThe printer is installed, but documents cannot be timestamped until you add a key. You will be asked again on your first print (or open Verae Print Stamp from Applications).", "", false, false, []string{"Go back", "Skip"}, "Go back")
if btn == "Skip" {
return "skipped", false
}
continue
}
if errors.Is(err, errCancel) {
continue // back to the menu
}
if err != nil {
vlog.Errorf("mac-key-setup: %v", err)
say("Could not get a key: " + err.Error())
continue
}
if err := auth.SaveXVeraeToken(key); err != nil {
vlog.Errorf("mac-key-setup: save failed: %v", err)
say("The key could not be saved: " + err.Error())
continue
}
vlog.Printf("mac-key-setup: key saved %s", auth.MaskKey(key))
say("Your Verae signing key (" + auth.MaskKey(key) + ") is saved.\n\nPrint from any app and choose \"Verae Timestamping\".")
return "saved " + auth.MaskKey(key), true
}
}
func macLogin(caps auth.Capabilities) (string, error) {
email := ""
for {
btn, e, err := ask("Sign in to Verae\n\nEmail:", email, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
return "", errCancel
}
email = strings.TrimSpace(e)
if !auth.ValidEmail(email) {
say("Please enter a valid email address.")
continue
}
btn, pw, err := ask("Password for "+email+":", "", true, true, []string{"Back", "Sign in"}, "Sign in")
if err != nil || btn == "Back" {
continue
}
vlog.Printf("mac-key-setup: login %s", auth.MaskEmail(email))
key, err := auth.InstallLogin(caps, email, pw, "", tokenName())
if errors.Is(err, auth.ErrTOTPRequired) {
btn, code, cerr := ask("This account uses two-factor authentication.\n\nEnter the 6-digit code from your authenticator app:", "", false, true, []string{"Back", "Verify"}, "Verify")
if cerr != nil || btn == "Back" {
continue
}
key, err = auth.InstallLogin(caps, email, pw, strings.TrimSpace(code), tokenName())
}
pw = ""
if err != nil {
vlog.Errorf("mac-key-setup: login failed for %s: %v", auth.MaskEmail(email), err)
say("Sign-in failed: " + err.Error())
continue
}
return key, nil
}
}
func macCreate(caps auth.Capabilities) (string, error) {
var name, email string
for {
btn, n, err := ask("Create a Verae account\n\nYour name:", name, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
return "", errCancel
}
name = strings.TrimSpace(n)
if name == "" {
continue
}
btn, e, err := ask("Email address:", email, false, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
continue
}
email = strings.TrimSpace(e)
if !auth.ValidEmail(email) {
say("Please enter a valid email address.")
continue
}
policy := caps.Policy.Describe()
btn, pw, err := ask("Choose a password.\n"+policy, "", true, true, []string{"Back", "Next"}, "Next")
if err != nil || btn == "Back" {
continue
}
if perr := caps.Policy.Check(pw, email); perr != nil {
say(perr.Error())
continue
}
btn, pw2, err := ask("Confirm the password:", "", true, true, []string{"Back", "Create account"}, "Create account")
if err != nil || btn == "Back" {
continue
}
if pw != pw2 {
say("The passwords do not match.")
continue
}
pw2 = ""
vlog.Printf("mac-key-setup: register %s installApi=%v", auth.MaskEmail(email), caps.InstallAPI)
rr, err := auth.InstallRegister(caps, name, email, pw, tokenName())
pw = ""
if err != nil {
vlog.Errorf("mac-key-setup: register failed for %s: %v", auth.MaskEmail(email), err)
say("Could not create the account: " + err.Error())
continue
}
if rr.PendingID == "" {
return rr.Key, nil // server without email verification (legacy signup)
}
vlog.Printf("mac-key-setup: verification code sent to %s", auth.MaskEmail(email))
msg := "We emailed a 6-digit code to " + auth.MaskEmail(email) + ".\nIt may take a minute; check spam.\n\nCode:"
for {
btn, code, err := ask(msg, "", false, true, []string{"Cancel", "Send a new code", "Verify"}, "Verify")
if err != nil || btn == "Cancel" {
return "", errCancel
}
if btn == "Send a new code" {
if rerr := auth.InstallResend(rr.PendingID); rerr != nil {
say("Could not send a new code: " + rerr.Error())
} else {
say("A new code was sent.")
}
continue
}
key, verr := auth.InstallVerify(rr.PendingID, code, tokenName())
if verr != nil {
say(verr.Error())
continue
}
return key, nil
}
}
}
func macPaste() (string, error) {
for {
btn, k, err := ask("Paste your xVerae- key.\n\nNo key yet? Click \"Open portal\", sign in, open Security and create an xVerae- token (shown once).", "", true, true, []string{"Back", "Open portal", "Save"}, "Save")
if err != nil || btn == "Back" {
return "", errCancel
}
if btn == "Open portal" {
_ = exec.Command("open", auth.PortalSignupURL).Start()
continue
}
k = strings.TrimSpace(k)
st, det := auth.ValidateXVeraeKey(k)
vlog.Printf("mac-key-setup: pasted key %s: %s %s", auth.MaskKey(k), st, det)
switch st {
case "valid":
return k, nil
case "invalid":
say("Verae rejected this key (" + det + "). Please check it and paste again.")
case "badformat":
say("That does not look like a Verae key: " + det)
default:
b, _, _ := ask("Could not reach Verae to check the key ("+det+").\nSave it anyway?", "", false, false, []string{"No", "Save anyway"}, "Save anyway")
if b == "Save anyway" {
return k, nil
}
}
}
}
func askOpenOnComplete() string {
cur, _ := settings.Load()
def := map[string]string{settings.OpenFolder: "Show in Finder", settings.OpenPDF: "Open the PDF", settings.OpenNone: "Do nothing"}[cur.OpenOnComplete]

View file

@ -1,171 +0,0 @@
//go:build linux
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// Linux desktop sign-in dialogs (zenity, GNOME/Cinnamon/XFCE/KDE all ship or
// package it). Launched by the CUPS backend in the job user's session when a
// print needs a key, or by hand: verae-print key-dialog.
const (
dlgTitle = "Verae Print Stamp"
machineWord = "computer"
keyStoreDesc = "~/.verae, readable only by you"
)
func zenityPath() string {
p, err := exec.LookPath("zenity")
if err != nil {
return ""
}
return p
}
func guiAvailable() bool {
if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
return false
}
return zenityPath() != ""
}
func openURL(u string) {
cmd := exec.Command("xdg-open", u)
cmd.Stdout, cmd.Stderr = nil, nil
_ = cmd.Start()
go func() { _ = cmd.Wait() }()
}
// zenity renders --text as Pango markup.
func markupEscape(s string) string {
return strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;").Replace(s)
}
// zen runs zenity; returns stdout (trimmed) and exit code. Exit 1 = cancel or
// an extra button (whose label is printed), 5 = timeout, -1 = could not run.
func zen(args ...string) (string, int) {
base := []string{"--title=" + dlgTitle, "--width=520", "--timeout=900"}
cmd := exec.Command(zenityPath(), append(base, args...)...)
cmd.Stderr = nil
out, err := cmd.Output()
s := strings.TrimRight(string(out), "\n")
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
return s, ee.ExitCode()
}
return "", -1
}
return s, 0
}
// ask shows a dialog; returns (button, answer). The default button is OK, the
// first other button is Cancel, the rest are extra buttons.
func ask(msg, def string, hidden, withAnswer bool, buttons []string, defBtn string) (string, string, error) {
if !guiAvailable() {
return "", "", fmt.Errorf("no desktop session for dialogs")
}
var others []string
for _, b := range buttons {
if b != defBtn {
others = append(others, b)
}
}
var args []string
switch {
case withAnswer:
args = []string{"--entry", "--text=" + markupEscape(msg), "--entry-text=" + def}
if hidden {
args = append(args, "--hide-text")
}
case len(others) == 0:
args = []string{"--info", "--text=" + markupEscape(msg)}
default:
args = []string{"--question", "--text=" + markupEscape(msg)}
}
args = append(args, "--ok-label="+defBtn)
if len(others) > 0 {
args = append(args, "--cancel-label="+others[0])
for _, b := range others[1:] {
args = append(args, "--extra-button="+b)
}
}
out, code := zen(args...)
switch code {
case 0:
return defBtn, out, nil
case 1:
for _, b := range others[min(1, len(others)):] {
if out == b {
return b, "", nil
}
}
if len(others) > 0 {
return others[0], "", nil
}
return defBtn, "", nil // info dialog closed
case -1:
return "", "", fmt.Errorf("zenity could not start")
default:
return "", "", errCancel // timeout / window killed
}
}
func say(msg string) { _, _, _ = ask(msg, "", false, false, []string{"OK"}, "OK") }
func choose(msg string, opts []string) (string, error) {
if !guiAvailable() {
return "", fmt.Errorf("no desktop session for dialogs")
}
args := []string{"--list", "--radiolist", "--hide-header", "--height=470",
"--text=" + markupEscape(msg), "--column=", "--column=Option",
"--ok-label=Continue", "--cancel-label=Skip"}
for i, o := range opts {
sel := "FALSE"
if i == 0 {
sel = "TRUE"
}
args = append(args, sel, o)
}
out, code := zen(args...)
if code != 0 || out == "" {
return "", errCancel
}
return out, nil
}
// runKeyDialog: verae-print key-dialog — the desktop "get a signing key" flow.
func runKeyDialog() int {
vlog.Init("installer", false)
if !guiAvailable() {
fmt.Println("key-dialog needs a desktop session with zenity installed (sudo apt install zenity); or run: verae-print setup")
return 1
}
vlog.Printf("key-dialog start user=%s display=%s", os.Getenv("USER"), os.Getenv("DISPLAY"))
release, ok := promptLock()
if !ok {
vlog.Printf("key-dialog: another Verae sign-in window is already open")
return 1
}
reason := os.Getenv("VERAE_KEY_REASON")
result, saved := keyFlow(reason)
release()
vlog.Printf("key-dialog result: %s", result)
fmt.Println("key:", result)
if saved || strings.HasPrefix(result, "kept") {
if n := len(listPending()); n > 0 {
done, remaining, _ := retryPending(stampFile)
vlog.Printf("key-dialog: retried held print jobs: %d stamped, %d remaining", done, remaining)
}
return 0
}
return 1
}

View file

@ -14,7 +14,6 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
)
func main() {
@ -39,12 +38,6 @@ func main() {
os.Exit(runValidateKey(os.Args[2:]))
case "config":
os.Exit(runConfig(os.Args[2:]))
case "pending", "queue":
os.Exit(runPendingList(os.Args[2:]))
case "tray-status", "status":
os.Exit(runTrayStatus(os.Args[2:]))
case "menubar", "tray":
os.Exit(runMenubar(os.Args[2:]))
case "mac-key-setup":
os.Exit(runMacKeySetup(os.Args[2:]))
case "key-dialog":
@ -86,11 +79,6 @@ Usage:
verae-print ipp-selftest [--addr 127.0.0.1:8631] # probe the helper like Windows does
verae-print config [--open-on-complete folder|pdf|none] [--machine | --file PATH]
# after each print: reveal the certificate (folder, default), open it (pdf), or nothing
verae-print pending [--json|--html] # PT-04/W4 job queue (brand labels; hold id ≠ evidence)
verae-print tray-status [--json|--open] # PT-W3 tray substitute: held count, key, helper, queue URL
verae-print menubar [--addr host:port] [--install|--uninstall|--foreground]
# PT-W3/PT-06: Windows NotifyIcon / macOS menu bar (GET /status)
# --install: Win Startup shortcut or macOS LaunchAgent at login
verae-print mac-key-setup # macOS: native dialogs to keep/sign in/create account/paste a key (run as the user)
verae-print version
@ -226,37 +214,12 @@ func runStamp(args []string) int {
}
lj.Printf("stamp: key rejected (%v); job HELD as pending %s", err, m.ID)
fmt.Printf("Verae did not accept the signing key; the job is saved (pending %s) and will be timestamped after you sign in.\n", m.ID)
notify(statuslang.NotifyTitle("held"), notifyQueueHint(statuslang.NotifyBody("held", title, m.ID)))
handleAuthFailure(authReason(err), stampFile)
return 0
}
if err != nil && isBillingFailure(err) {
title := *name
if title == "" {
title = filepath.Base(pdfPath)
}
m, herr := holdJob(pdfPath, stampTarget{Title: title, OutDir: *outDir, OutPath: outPath}, "cli", err, false)
if herr != nil {
lj.Errorf("stamp failed (%v) and could not hold job: %v", err, herr)
fmt.Fprintf(os.Stderr, "stamp failed: %v\n", err)
fmt.Fprintf(os.Stderr, "Add funds: %s\n", auth.ZapBillingURL)
_ = openBrowser(auth.ZapBillingURL)
return 1
}
lj.Printf("stamp: out of credits (%v); job HELD as pending %s — opening Billing", err, m.ID)
fmt.Printf("Out of credits; the job is saved (pending %s). Opening Billing so you can add funds:\n %s\n", m.ID, auth.ZapBillingURL)
notify(statuslang.NotifyTitle("billing"), notifyQueueHint(statuslang.NotifyBody("billing", title, m.ID)))
handleBillingFailure(billingReason(err))
return 0
}
if err != nil {
lj.Errorf("stamp failed: %v", err)
fmt.Fprintf(os.Stderr, "stamp failed: %v\n", err)
failTitle := *name
if failTitle == "" {
failTitle = filepath.Base(pdfPath)
}
notify(statuslang.NotifyTitle("failed"), statuslang.NotifyBody("failed", failTitle, err.Error()))
return 1
}
lj.Printf("stamp ok receipt=%s sha256=%s cert=%q", res.ReceiptID, res.SHA256, res.CertificatePDF)
@ -272,11 +235,8 @@ func runStamp(args []string) int {
done, remaining, _ := retryPending(stampFile)
lj.Printf("pending: %d stamped, %d remaining", done, remaining)
}
docTitle := *name
if docTitle == "" {
docTitle = filepath.Base(pdfPath)
}
notify(statuslang.NotifyTitle("final"), statuslang.NotifyBody("final", docTitle, res.ReceiptID))
// macOS notification (best-effort)
notify(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
return 0
}
@ -306,40 +266,14 @@ func runInstallPrinter() int {
}
func repoRootHint() string {
// Prefer the go.mod tree that owns this binary (A4 packaging source of truth).
// No machine-specific absolute paths — walk cwd and exe parents.
var starts []string
if wd, err := os.Getwd(); err == nil {
starts = append(starts, wd)
// Prefer known research path; else cwd
candidates := []string{
"/Users/marchon/research/PrintToVeraeTimeStamp",
".",
}
starts = append(starts, exeDirHint())
if exe := exeDirHint(); exe != "." {
starts = append(starts, filepath.Join(exe, ".."), filepath.Join(exe, "..", ".."))
}
starts = append(starts, ".")
seen := map[string]bool{}
for _, start := range starts {
dir, err := filepath.Abs(start)
if err != nil {
continue
}
for i := 0; i < 8; i++ {
if seen[dir] {
break
}
seen[dir] = true
if fileExists(filepath.Join(dir, "go.mod")) {
// Any go.mod with platform/ is good enough for install scripts.
if fileExists(filepath.Join(dir, "platform")) ||
fileExists(filepath.Join(dir, "internal", "job", "job.go")) {
return dir
}
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
for _, c := range candidates {
if st, err := os.Stat(filepath.Join(c, "platform", "macos", "ipp", "install-ipp-printer.sh")); err == nil && !st.IsDir() {
return c
}
}
return "."

View file

@ -1,584 +0,0 @@
package main
// PT-W3 / PT-06 tray / menu-bar launcher.
// Ships a working presence without a multi-month native C++ tray:
// Windows → platform/windows/scripts/verae-tray.ps1 (NotifyIcon + /status poll)
// → Startup shortcut via menubar --install (or install-tray-startup.ps1)
// → Start Menu folder via build-tray-startmenu.ps1 (WiX/NSIS-free stub)
// macOS → platform/macos/menubar/run-menubar.sh (NSStatusItem via Swift)
// → durable copy under ~/Library/Application Support/Verae/menubar
// → LaunchAgent via menubar --install; optional .app via build-menubar-app.sh
// IPP POST stays pure; this only opens scripts that GET /status and /queue.
import (
"flag"
"fmt"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const (
menubarLaunchLabel = "com.verae.timestamping.menubar"
winStartupLnkName = "Verae Tray.lnk"
)
// runMenubar starts the platform tray/menu-bar helper, or prints install hints.
func runMenubar(args []string) int {
fs := flag.NewFlagSet("menubar", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
addr := fs.String("addr", defaultHelperAddr(), "helper loopback address for /status and /queue")
install := fs.Bool("install", false, "install at login (Windows Startup shortcut; macOS LaunchAgent)")
uninstall := fs.Bool("uninstall", false, "remove login install (Windows Startup shortcut; macOS LaunchAgent)")
foreground := fs.Bool("foreground", false, "run attached (default: detach on Windows/macOS)")
if err := fs.Parse(args); err != nil {
return 2
}
_ = os.Setenv("VERAE_IPP_ADDR", *addr)
if *uninstall {
return menubarUninstall()
}
if *install {
return menubarInstall(*addr)
}
switch runtime.GOOS {
case "windows":
return startWindowsTray(*addr, *foreground)
case "darwin":
return startDarwinMenubar(*addr, *foreground)
default:
fmt.Fprintln(os.Stderr, "menubar: no tray script on this OS — use: verae-print tray-status --open")
fmt.Fprintln(os.Stderr, " loopback: GET http://"+*addr+"/queue and GET http://"+*addr+"/status")
return runTrayStatus([]string{"--open", "--addr", *addr})
}
}
func startWindowsTray(addr string, foreground bool) int {
script, err := locateWindowsTrayScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar: verae-tray.ps1 not found (%v)\n", err)
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
return runTrayStatus([]string{"--open", "--addr", addr})
}
ps := windowsPowerShell()
args := []string{
"-NoProfile", "-ExecutionPolicy", "Bypass", "-STA",
"-File", script,
"-Addr", addr,
}
cmd := exec.Command(ps, args...)
if foreground {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: tray exited: %v\n", err)
return 1
}
return 0
}
hideWindow(cmd)
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: start tray: %v\n", err)
return 1
}
fmt.Printf("Verae tray started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
fmt.Println("Install at login: verae-print menubar --install")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func startDarwinMenubar(addr string, foreground bool) int {
script, err := locateDarwinMenubarScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar: run-menubar.sh not found (%v)\n", err)
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
return runTrayStatus([]string{"--open", "--addr", addr})
}
_ = os.Chmod(script, 0o755)
cmd := exec.Command("/bin/bash", script)
cmd.Env = append(os.Environ(), "VERAE_IPP_ADDR="+addr)
if foreground {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: %v\n", err)
return 1
}
return 0
}
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: start: %v\n", err)
return 1
}
fmt.Printf("Verae menu bar started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
fmt.Println("Install at login: verae-print menubar --install")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarInstall(addr string) int {
switch runtime.GOOS {
case "windows":
return menubarInstallWindows(addr)
case "darwin":
return menubarInstallDarwin(addr)
default:
fmt.Fprintln(os.Stderr, "menubar --install: no tray packaging on this OS")
fmt.Fprintln(os.Stderr, " use: verae-print tray-status --open")
return 2
}
}
func menubarUninstall() int {
switch runtime.GOOS {
case "windows":
return menubarUninstallWindows()
case "darwin":
return menubarUninstallDarwin()
default:
fmt.Fprintln(os.Stderr, "menubar --uninstall: no tray packaging on this OS")
return 2
}
}
// --- Windows Startup shortcut ------------------------------------------------
func menubarInstallWindows(addr string) int {
tray, err := locateWindowsTrayScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
installer, err := locateWindowsStartupInstaller()
if err == nil {
ps := windowsPowerShell()
cmd := exec.Command(ps,
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", installer,
"-Addr", addr,
"-TrayScript", tray,
)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
return 0
}
// Fallback without install-tray-startup.ps1: create .lnk via PowerShell COM.
if err := writeWindowsStartupShortcut(tray, addr); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
fmt.Println("Installed Startup shortcut:", windowsStartupShortcutPath())
fmt.Println("Tray starts at next login. Run now: verae-print menubar")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarUninstallWindows() int {
installer, err := locateWindowsStartupInstaller()
if err == nil {
ps := windowsPowerShell()
cmd := exec.Command(ps,
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", installer,
"-Uninstall",
)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar --uninstall: %v\n", err)
return 1
}
return 0
}
path := windowsStartupShortcutPath()
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println("Removed Startup shortcut:", path)
return 0
}
func windowsStartupShortcutPath() string {
appData := os.Getenv("APPDATA")
if appData == "" {
home, _ := os.UserHomeDir()
appData = filepath.Join(home, "AppData", "Roaming")
}
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", winStartupLnkName)
}
func writeWindowsStartupShortcut(trayScript, addr string) error {
ps := windowsPowerShell()
lnk := windowsStartupShortcutPath()
if err := os.MkdirAll(filepath.Dir(lnk), 0o755); err != nil {
return err
}
// Escape single quotes for PowerShell single-quoted strings.
esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
psExe := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
if _, err := os.Stat(psExe); err != nil {
psExe = "powershell.exe"
}
args := fmt.Sprintf(`-NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File "%s" -Addr %s`, trayScript, addr)
script := fmt.Sprintf(`
$ErrorActionPreference = 'Stop'
$wsh = New-Object -ComObject WScript.Shell
$sc = $wsh.CreateShortcut('%s')
$sc.TargetPath = '%s'
$sc.Arguments = '%s'
$sc.WorkingDirectory = '%s'
$sc.WindowStyle = 7
$sc.Description = 'Verae Timestamping tray — polls GET /status; hold id ≠ evidence.'
$sc.Save()
`, esc(lnk), esc(psExe), esc(args), esc(filepath.Dir(trayScript)))
cmd := exec.Command(ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
func windowsPowerShell() string {
ps := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
if _, err := os.Stat(ps); err != nil {
return "powershell.exe"
}
return ps
}
func locateWindowsTrayScript() (string, error) {
script, err := findRepoFile("platform", "windows", "scripts", "verae-tray.ps1")
if err == nil {
return script, nil
}
// Installer layout next to the exe.
for _, alt := range []string{
filepath.Join(exeDirHint(), "scripts", "verae-tray.ps1"),
filepath.Join(exeDirHint(), "..", "scripts", "verae-tray.ps1"),
filepath.Join(exeDirHint(), "verae-tray.ps1"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
func locateWindowsStartupInstaller() (string, error) {
script, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1")
if err == nil {
return script, nil
}
for _, alt := range []string{
filepath.Join(exeDirHint(), "scripts", "install-tray-startup.ps1"),
filepath.Join(exeDirHint(), "..", "scripts", "install-tray-startup.ps1"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
// --- macOS durable LaunchAgent -----------------------------------------------
func menubarSupportDir(home string) string {
return filepath.Join(home, "Library", "Application Support", "Verae", "menubar")
}
func menubarInstallDarwin(addr string) int {
srcScript, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
srcDir := filepath.Dir(srcScript)
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
support := menubarSupportDir(home)
if err := os.MkdirAll(support, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
// Copy durable assets so LaunchAgent does not depend on the checkout path.
for _, name := range []string{"run-menubar.sh", "verae-menubar.swift", "com.verae.timestamping.menubar.plist"} {
src := filepath.Join(srcDir, name)
if !fileExists(src) {
continue
}
dst := filepath.Join(support, name)
if err := copyFile(src, dst); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: copy %s: %v\n", name, err)
return 1
}
}
script := filepath.Join(support, "run-menubar.sh")
_ = os.Chmod(script, 0o755)
// Best-effort compile so LaunchAgent does not need swift at login.
bin := filepath.Join(support, "verae-menubar")
swiftSrc := filepath.Join(support, "verae-menubar.swift")
if fileExists(swiftSrc) {
if path, lookErr := exec.LookPath("swiftc"); lookErr == nil {
c := exec.Command(path, "-O", "-o", bin, swiftSrc)
if out, err := c.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: swiftc optional compile failed (will use swift at run): %v\n%s\n", err, strings.TrimSpace(string(out)))
} else {
_ = os.Chmod(bin, 0o755)
}
}
}
logDir := filepath.Join(home, "Library", "Logs")
_ = os.MkdirAll(logDir, 0o755)
agents := filepath.Join(home, "Library", "LaunchAgents")
_ = os.MkdirAll(agents, 0o755)
plistPath := filepath.Join(agents, menubarLaunchLabel+".plist")
body := menubarPlistBody(script, logDir, addr)
// Prefer rewriting the template if present.
if tpl := filepath.Join(support, "com.verae.timestamping.menubar.plist"); fileExists(tpl) {
raw, err := os.ReadFile(tpl)
if err == nil {
s := string(raw)
s = strings.ReplaceAll(s, "MENUBAR_SCRIPT_PLACEHOLDER", script)
s = strings.ReplaceAll(s, "LOG_DIR_PLACEHOLDER", logDir)
// Ensure VERAE_IPP_ADDR matches --addr.
if strings.Contains(s, "<key>VERAE_IPP_ADDR</key>") {
// Replace the next <string>…</string> after VERAE_IPP_ADDR.
s = replaceEnvString(s, "VERAE_IPP_ADDR", addr)
}
body = s
}
}
if err := os.WriteFile(plistPath, []byte(body), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := launchctlLoadMenubar(plistPath); err != nil {
fmt.Fprintf(os.Stderr, "menubar: wrote %s but launchctl load failed: %v\n", plistPath, err)
fmt.Fprintln(os.Stderr, " load manually after login, or: verae-print menubar")
fmt.Fprintln(os.Stderr, " assets:", support)
return 1
}
fmt.Println("Installed LaunchAgent:", plistPath)
fmt.Println("Durable assets:", support)
fmt.Println("Menu bar polls http://" + addr + "/status · Open queue → /queue")
fmt.Println("Optional .app: platform/macos/menubar/build-menubar-app.sh")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarUninstallDarwin() int {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
plistPath := filepath.Join(home, "Library", "LaunchAgents", menubarLaunchLabel+".plist")
_ = launchctlUnloadMenubar(plistPath)
if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, err)
return 1
}
// Leave Application Support assets; user may still run menubar manually.
fmt.Println("Removed LaunchAgent:", plistPath)
fmt.Println("Assets kept at:", menubarSupportDir(home), "(delete manually if desired)")
return 0
}
func menubarPlistBody(script, logDir, addr string) string {
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>%s</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>%s</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ProcessType</key>
<string>Interactive</string>
<key>StandardOutPath</key>
<string>%s/verae-menubar.launchd.out.log</string>
<key>StandardErrorPath</key>
<string>%s/verae-menubar.launchd.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>VERAE_IPP_ADDR</key>
<string>%s</string>
</dict>
</dict>
</plist>
`, menubarLaunchLabel, script, logDir, logDir, addr)
}
func launchctlLoadMenubar(plistPath string) error {
uid := os.Getuid()
domain := "gui/" + strconv.Itoa(uid)
label := menubarLaunchLabel
// Modern path first (macOS 10.13+); fall back to load -w.
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
_ = exec.Command("launchctl", "unload", plistPath).Run()
if err := exec.Command("launchctl", "bootstrap", domain, plistPath).Run(); err == nil {
_ = exec.Command("launchctl", "enable", domain+"/"+label).Run()
_ = exec.Command("launchctl", "kickstart", "-k", domain+"/"+label).Run()
return nil
}
if err := exec.Command("launchctl", "load", "-w", plistPath).Run(); err != nil {
return err
}
return nil
}
func launchctlUnloadMenubar(plistPath string) error {
uid := os.Getuid()
domain := "gui/" + strconv.Itoa(uid)
label := menubarLaunchLabel
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
_ = exec.Command("launchctl", "unload", "-w", plistPath).Run()
return nil
}
func locateDarwinMenubarScript() (string, error) {
// Prefer durable install copy if present.
if home, err := os.UserHomeDir(); err == nil {
p := filepath.Join(menubarSupportDir(home), "run-menubar.sh")
if fileExists(p) {
return p, nil
}
}
script, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
if err == nil {
return script, nil
}
for _, alt := range []string{
filepath.Join(exeDirHint(), "menubar", "run-menubar.sh"),
filepath.Join(exeDirHint(), "..", "menubar", "run-menubar.sh"),
filepath.Join(exeDirHint(), "run-menubar.sh"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
// replaceEnvString replaces the <string> value immediately following <key>NAME</key>.
func replaceEnvString(plist, key, value string) string {
needle := "<key>" + key + "</key>"
i := strings.Index(plist, needle)
if i < 0 {
return plist
}
rest := plist[i+len(needle):]
start := strings.Index(rest, "<string>")
end := strings.Index(rest, "</string>")
if start < 0 || end < 0 || end < start {
return plist
}
return plist[:i+len(needle)] + rest[:start] + "<string>" + value + rest[end:]
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
tmp := dst + ".tmp"
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
_ = os.Remove(tmp)
return copyErr
}
if closeErr != nil {
_ = os.Remove(tmp)
return closeErr
}
if st, err := os.Stat(src); err == nil {
_ = os.Chmod(tmp, st.Mode())
}
return os.Rename(tmp, dst)
}
func findRepoFile(parts ...string) (string, error) {
rel := filepath.Join(parts...)
// Prefer go.mod tree (repoRootHint) over bare relative/exe guesses so a
// sibling research checkout cannot shadow the Verae-Projects package tree.
candidates := []string{
filepath.Join(repoRootHint(), rel),
filepath.Join(".", rel),
filepath.Join(exeDirHint(), rel),
filepath.Join(exeDirHint(), "..", rel),
filepath.Join(exeDirHint(), "..", "..", rel),
}
// Walk up from cwd for go.mod (tests + nested cwd).
if wd, err := os.Getwd(); err == nil {
dir := wd
for i := 0; i < 8; i++ {
if fileExists(filepath.Join(dir, "go.mod")) {
candidates = append(candidates, filepath.Join(dir, rel))
break
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
}
var tried []string
seen := map[string]bool{}
for _, c := range candidates {
abs, err := filepath.Abs(c)
if err != nil {
continue
}
if seen[abs] {
continue
}
seen[abs] = true
tried = append(tried, abs)
if fileExists(abs) {
return abs, nil
}
}
return "", fmt.Errorf("not found: %s (tried %s)", rel, strings.Join(tried, ", "))
}
func exeDirHint() string {
exe, err := os.Executable()
if err != nil {
return "."
}
exe, _ = filepath.EvalSymlinks(exe)
return filepath.Dir(exe)
}

View file

@ -1,313 +0,0 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
)
func TestFindRepoFileMenubarAssets(t *testing.T) {
switch runtime.GOOS {
case "darwin":
p, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
if err != nil {
t.Fatal(err)
}
if filepath.Base(p) != "run-menubar.sh" {
t.Fatal(p)
}
if _, err := findRepoFile("platform", "macos", "menubar", "verae-menubar.swift"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "macos", "menubar", "build-menubar-app.sh"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "macos", "menubar", "com.verae.timestamping.menubar.plist"); err != nil {
t.Fatal(err)
}
case "windows":
if _, err := findRepoFile("platform", "windows", "scripts", "verae-tray.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "windows", "scripts", "build-tray-startmenu.ps1"); err != nil {
t.Fatal(err)
}
default:
// Scripts still exist in the tree for packaging even on Linux CI.
if _, err := findRepoFile("platform", "windows", "scripts", "verae-tray.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "windows", "scripts", "build-tray-startmenu.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "macos", "menubar", "verae-menubar.swift"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "macos", "menubar", "build-menubar-app.sh"); err != nil {
t.Fatal(err)
}
}
}
func TestMenubarPlistBody(t *testing.T) {
body := menubarPlistBody("/tmp/run-menubar.sh", "/tmp/logs", "127.0.0.1:8631")
for _, want := range []string{
menubarLaunchLabel,
"/tmp/run-menubar.sh",
"/tmp/logs/verae-menubar.launchd.out.log",
"<string>127.0.0.1:8631</string>",
"<key>KeepAlive</key>",
"<key>RunAtLoad</key>",
} {
if !strings.Contains(body, want) {
t.Fatalf("plist missing %q\n%s", want, body)
}
}
}
func TestReplaceEnvString(t *testing.T) {
in := `<key>VERAE_IPP_ADDR</key>
<string>127.0.0.1:8631</string>`
out := replaceEnvString(in, "VERAE_IPP_ADDR", "127.0.0.1:9999")
if !strings.Contains(out, "<string>127.0.0.1:9999</string>") {
t.Fatal(out)
}
if strings.Contains(out, "8631") {
t.Fatal(out)
}
}
func TestMenubarSupportDir(t *testing.T) {
p := menubarSupportDir("/Users/test")
want := filepath.Join("/Users/test", "Library", "Application Support", "Verae", "menubar")
if p != want {
t.Fatalf("got %q want %q", p, want)
}
}
func TestCopyFile(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "a.txt")
dst := filepath.Join(dir, "b.txt")
if err := os.WriteFile(src, []byte("hello-menubar"), 0o644); err != nil {
t.Fatal(err)
}
if err := copyFile(src, dst); err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(dst)
if err != nil {
t.Fatal(err)
}
if string(b) != "hello-menubar" {
t.Fatal(string(b))
}
}
func TestWindowsStartupShortcutPathShape(t *testing.T) {
p := windowsStartupShortcutPath()
if !strings.HasSuffix(p, winStartupLnkName) {
t.Fatal(p)
}
if !strings.Contains(strings.ToLower(p), "startup") {
t.Fatal(p)
}
}
func TestLocateScriptsPreferRepo(t *testing.T) {
// On every OS the packaging scripts should resolve from the go.mod tree.
if _, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "windows", "scripts", "build-tray-startmenu.ps1"); err != nil {
t.Fatal(err)
}
if _, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh"); err != nil {
t.Fatal(err)
}
}
func TestBuildTrayStartmenuScriptDocumentsWiXFree(t *testing.T) {
p, err := findRepoFile("platform", "windows", "scripts", "build-tray-startmenu.ps1")
if err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
body := string(b)
for _, want := range []string{
"Start Menu",
"Verae Tray",
"install-tray-startup.ps1",
"WiX",
"WScript.Shell",
"-Uninstall",
} {
if !strings.Contains(body, want) {
t.Fatalf("build-tray-startmenu.ps1 missing %q", want)
}
}
}
func TestMenubarAppBuilderHasNotarizationPointer(t *testing.T) {
p, err := findRepoFile("platform", "macos", "menubar", "build-menubar-app.sh")
if err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
body := string(b)
for _, want := range []string{
"LSUIElement",
"ai.verae.timestamping.menubar",
"entitlements.plist",
"notarize",
"PkgInfo",
"HelperVersion",
"packaging-metadata.txt",
} {
if !strings.Contains(body, want) {
t.Fatalf("build-menubar-app.sh missing %q", want)
}
}
readme, err := findRepoFile("platform", "macos", "menubar", "README.txt")
if err != nil {
t.Fatal(err)
}
rb, err := os.ReadFile(readme)
if err != nil {
t.Fatal(err)
}
rbody := string(rb)
for _, want := range []string{
"notarytool",
"stapler",
"Developer ID Application",
"codesign",
} {
if !strings.Contains(rbody, want) {
t.Fatalf("menubar README.txt missing notarization step %q", want)
}
}
}
func TestRepoRootHintPrefersGoModTree(t *testing.T) {
// Residual packaging: no machine-specific research absolute path; go.mod tree wins.
root := repoRootHint()
if root == "" || root == "." {
// cwd may already be the module; still require go.mod via findRepoFile.
} else {
if !fileExists(filepath.Join(root, "go.mod")) {
t.Fatalf("repoRootHint %q missing go.mod", root)
}
if !fileExists(filepath.Join(root, "internal", "job", "job.go")) {
t.Fatalf("repoRootHint %q missing internal/job/job.go", root)
}
}
jobPath, err := findRepoFile("internal", "job", "job.go")
if err != nil {
t.Fatal(err)
}
b, err := os.ReadFile(jobPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(b), `const HelperVersion = "`+job.HelperVersion+`"`) {
t.Fatalf("resolved job.go must declare HelperVersion=%s at %s", job.HelperVersion, jobPath)
}
// Source must not hardcode a sibling research checkout path.
mainPath, err := findRepoFile("cmd", "verae-print", "main.go")
if err != nil {
t.Fatal(err)
}
mb, err := os.ReadFile(mainPath)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(mb), "/Users/marchon/research/PrintToVeraeTimeStamp") {
t.Fatal("main.go must not hardcode research absolute path (use go.mod walk)")
}
}
func TestPackagingMetadataVersionStamp(t *testing.T) {
// A4: tray/menubar install scripts + READMEs stamp HelperVersion.
// job.HelperVersion is the single source of truth.
helper := job.HelperVersion
if helper == "" {
t.Fatal("job.HelperVersion empty")
}
checks := []struct {
parts []string
want []string
}{
{
parts: []string{"platform", "macos", "menubar", "build-menubar-app.sh"},
want: []string{"HelperVersion", "CFBundleShortVersionString", "packaging-metadata.txt", helper},
},
{
parts: []string{"platform", "macos", "menubar", "README.txt"},
want: []string{"HelperVersion", "Package version", helper},
},
{
parts: []string{"platform", "windows", "scripts", "common.ps1"},
want: []string{"Get-VeraePackageVersion", "HelperVersion", helper},
},
{
parts: []string{"platform", "windows", "scripts", "install-tray-startup.ps1"},
want: []string{"PackageVersion", "HelperVersion", "no elevated MSI"},
},
{
parts: []string{"platform", "windows", "scripts", "build-tray-startmenu.ps1"},
want: []string{"PackageVersion", "HelperVersion", "no elevated MSI"},
},
{
parts: []string{"platform", "windows", "scripts", "verae-tray.ps1"},
want: []string{"PackageVersion", "HelperVersion"},
},
{
parts: []string{"platform", "windows", "README.txt"},
want: []string{"HelperVersion", "Package version", "no elevated MSI"},
},
{
parts: []string{"platform", "windows", "README-WINDOWS.txt"},
want: []string{"HelperVersion", "Package version"},
},
{
parts: []string{"platform", "smoke-packaging-metadata.sh"},
want: []string{"HelperVersion", "no elevated MSI", "A4"},
},
}
for _, c := range checks {
path, err := findRepoFile(c.parts...)
if err != nil {
t.Fatalf("%v: %v", c.parts, err)
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
body := string(b)
for _, w := range c.want {
if !strings.Contains(body, w) {
t.Fatalf("%s missing %q", path, w)
}
}
}
}

View file

@ -13,7 +13,6 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
@ -26,7 +25,6 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
@ -37,9 +35,6 @@ var errNoKey = errors.New("no Verae signing key is set up")
// isAuthFailure: the job can succeed after the user signs in again.
func isAuthFailure(err error) bool { return errors.Is(err, errNoKey) || veraeapi.IsAuthError(err) }
// isBillingFailure: the job can succeed after the user adds funds on Zap Billing.
func isBillingFailure(err error) bool { return veraeapi.IsBillingError(err) }
type pendingMeta struct {
ID string `json:"id"`
Title string `json:"title"`
@ -165,18 +160,13 @@ func retryPending(stamp stampFn) (done, remaining int, authFail bool) {
lj.Errorf("pending: %s still not authorised: %v", m.ID, err)
return done, len(listPending()), true
}
if isBillingFailure(err) {
lj.Errorf("pending: %s still needs funds: %v", m.ID, err)
offerBillingCheckout(billingReason(err))
return done, len(listPending()), false
}
lj.Errorf("pending: %s failed (kept for next retry): %v", m.ID, err)
continue
}
removePending(m)
done++
lj.Printf("pending: %s OK receipt=%s cert=%q", m.ID, res.ReceiptID, res.CertificatePDF)
notifyUser(statuslang.NotifyTitle("final"), statuslang.NotifyBody("final", m.Title, res.ReceiptID))
notifyUser(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
openOnComplete(lj, res.CertificatePDF)
}
return done, len(listPending()), false
@ -204,35 +194,6 @@ func promptLock() (func(), bool) {
return func() {}, false
}
// billingOpenThrottle is how long we suppress re-opening the Billing tab
// after a credit failure (avoids a browser storm while jobs stay held).
const billingOpenThrottle = 10 * time.Minute
// offerBillingCheckout opens the Zap Billing reload page at most once per
// billingOpenThrottle. Safe across processes via a lock file in pendingDir.
func offerBillingCheckout(reason string) {
_ = os.MkdirAll(pendingDir(), 0o700)
marker := filepath.Join(pendingDir(), ".billing.open")
if st, err := os.Stat(marker); err == nil && time.Since(st.ModTime()) < billingOpenThrottle {
vlog.Printf("billing: checkout already offered %s ago; not re-opening (%d pending)",
time.Since(st.ModTime()).Round(time.Second), len(listPending()))
return
}
_ = os.WriteFile(marker, []byte(time.Now().Format(time.RFC3339)+"\n"+reason+"\n"), 0o600)
vlog.Printf("billing: opening %s (%d pending)", auth.ZapBillingURL, len(listPending()))
notifyUser(statuslang.NotifyTitle("billing"), notifyQueueHint(reason))
if err := openBrowser(auth.ZapBillingURL); err != nil {
vlog.Errorf("billing: could not open browser: %v — open %s", err, auth.ZapBillingURL)
fmt.Fprintf(os.Stderr, "Out of credits. Open Billing to add funds:\n %s\n", auth.ZapBillingURL)
}
}
// handleBillingFailure holds the print job narrative for the user and opens
// Zap Billing. Does not prompt for a new signing key.
func handleBillingFailure(reason string) {
offerBillingCheckout(reason)
}
// handleAuthFailure shows ONE login prompt (if none is open), then retries all
// pending jobs. Returns true when everything pending was stamped.
func handleAuthFailure(reason string, stamp stampFn) bool {
@ -255,8 +216,7 @@ func handleAuthFailure(reason string, stamp stampFn) bool {
}
if !acquire(msg) {
vlog.Printf("pending: sign-in cancelled; %d job(s) kept in %s", n, pendingDir())
ht, hb := statuslang.NotifyHeldJobs(n)
notifyUser(ht, notifyQueueHint(hb))
notifyUser("Verae: print jobs waiting", fmt.Sprintf("%d job(s) will be timestamped after you sign in.", n))
return false
}
markKeySeen()
@ -298,174 +258,3 @@ func keyChangedSinceSeen() bool {
}
return false
}
// runPendingList prints the held job queue (PT-04 / PT-W4 style) using brand status strings.
// Flags: -json (machine-readable), -html (self-contained local queue page).
func runPendingList(args []string) int {
fs := flag.NewFlagSet("pending", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
asJSON := fs.Bool("json", false, "machine-readable JSON (held jobs only; hold id is not evidence)")
asHTML := fs.Bool("html", false, "self-contained HTML job queue (PT-04/W4 labels)")
if err := fs.Parse(args); err != nil {
return 2
}
list := listPending()
if *asJSON {
b, err := encodePendingJSON(list, traySnapshot(""))
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
_, _ = os.Stdout.Write(b)
return 0
}
if *asHTML {
_, err := io.WriteString(os.Stdout, renderPendingHTML(list, traySnapshot("")))
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
fmt.Printf("Pending print jobs · %s\n", statuslang.Pending)
fmt.Printf("Folder: %s\n", pendingDir())
if len(list) == 0 {
fmt.Println("(empty) — nothing waiting for a signing key")
fmt.Println("Note:", statuslang.EvidenceNote)
return 0
}
fmt.Printf("%-22s %-28s %-16s %s\n", "HOLD_ID", "TITLE", "STATE", "CREATED")
for _, m := range list {
title := m.Title
if title == "" {
title = "(untitled)"
}
if len(title) > 28 {
title = title[:25] + "…"
}
fmt.Printf("%-22s %-28s %-16s %s\n", m.ID, title, statuslang.Pending, m.Created)
}
fmt.Printf("\n%d held · sign in / setup key to stamp\n", len(list))
fmt.Println(statuslang.EvidenceNote)
return 0
}
type pendingJSONRow struct {
HoldID string `json:"holdId"`
Title string `json:"title"`
Source string `json:"source,omitempty"`
State string `json:"state"`
Reason string `json:"reason,omitempty"`
Created string `json:"created"`
}
type pendingJSONOut struct {
Headline string `json:"headline"`
Detail string `json:"detail"`
State string `json:"state"`
Folder string `json:"folder"`
Count int `json:"count"`
KeyOK bool `json:"keyOk"`
HelperUp bool `json:"helperUp"`
QueueURL string `json:"queueUrl,omitempty"`
Note string `json:"note"`
Jobs []pendingJSONRow `json:"jobs"`
}
func encodePendingJSON(list []pendingMeta, snap traySnap) ([]byte, error) {
out := pendingJSONOut{
Headline: snap.Headline,
Detail: snap.Detail,
State: statuslang.Pending,
Folder: pendingDir(),
Count: len(list),
KeyOK: snap.KeyOK,
HelperUp: snap.HelperUp,
QueueURL: snap.QueueURL,
Note: statuslang.EvidenceNote,
Jobs: make([]pendingJSONRow, 0, len(list)),
}
if len(list) == 0 && snap.KeyOK {
out.State = statuslang.PrinterReady
}
for _, m := range list {
title := m.Title
if title == "" {
title = "(untitled)"
}
out.Jobs = append(out.Jobs, pendingJSONRow{
HoldID: m.ID, Title: title, Source: m.Source,
State: statuslang.Pending, Reason: m.Reason, Created: m.Created,
})
}
return json.MarshalIndent(out, "", " ")
}
func htmlEsc(s string) string {
return strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;").Replace(s)
}
// renderPendingHTML is the PT-04/W4 queue page (also served at helper GET /queue).
func renderPendingHTML(list []pendingMeta, snap traySnap) string {
var rows strings.Builder
if len(list) == 0 {
rows.WriteString(`<tr><td colspan="4" class="empty">No held jobs — print with Destination → Verae Timestamping</td></tr>`)
} else {
for _, m := range list {
title := m.Title
if title == "" {
title = "(untitled)"
}
rows.WriteString(fmt.Sprintf(
`<tr><td class="mono">%s</td><td>%s</td><td><span class="badge">%s</span></td><td class="muted">%s</td></tr>`,
htmlEsc(m.ID), htmlEsc(title), htmlEsc(statuslang.Pending), htmlEsc(m.Created),
))
}
}
meta := fmt.Sprintf("Folder: %s · %d held", htmlEsc(pendingDir()), len(list))
if snap.QueueURL != "" {
meta += fmt.Sprintf(` · live: <a class="mono" href="%s">%s</a>`, htmlEsc(snap.QueueURL), htmlEsc(snap.QueueURL))
} else {
meta += ` · refresh: <span class="mono">verae-print pending --html</span>`
}
meta += fmt.Sprintf(` · <span class="mono">verae-print tray-status</span>`)
return fmt.Sprintf(`<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta http-equiv="refresh" content="15"/>
<title>Print job queue · Verae Timestamping</title>
<style>
:root{--plum:#531B59;--gold:#C4A85A;--ink:#1D1534;--muted:#5E5470;--bg:#FBFAFD;--card:#fff;--line:#E3DEF0;--surface:#F4F1F8;--pend-bg:#EEF0F3;--pend:#475467;--ok:#0E6B3A;--ok-bg:#E3F5EA}
*{box-sizing:border-box}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:var(--bg);color:var(--ink)}
main{max-width:820px;margin:0 auto;padding:28px 16px}
h1{font-size:1.35rem;margin:0 0 6px} .lede{color:var(--muted);font-size:.95rem;margin:0 0 18px;max-width:40rem}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 18px;box-shadow:0 1px 2px rgba(29,21,52,.04)}
table{width:100%%;border-collapse:collapse;font-size:.9rem} th,td{text-align:left;padding:10px 8px;border-bottom:1px solid var(--line)}
th{font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600}
.mono{font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:.8rem}
.muted{color:var(--muted)}.empty{color:var(--muted);text-align:center;padding:28px 8px}
.badge{display:inline-block;background:var(--pend-bg);color:var(--pend);border-radius:999px;padding:2px 10px;font-size:.78rem;font-weight:600}
.pill{display:inline-block;border-radius:999px;padding:3px 12px;font-size:.8rem;font-weight:600;background:var(--ok-bg);color:var(--ok)}
.pill.warn{background:var(--pend-bg);color:var(--pend)}
.note{margin-top:14px;padding:12px 14px;background:var(--surface);border-left:3px solid var(--gold);border-radius:0 8px 8px 0;font-size:.85rem;color:var(--muted)}
.note b{color:var(--plum)} .meta{margin-top:12px;font-size:.8rem;color:var(--muted)}
a{color:var(--plum)} .status{margin:0 0 14px;display:flex;gap:10px;flex-wrap:wrap;align-items:center}
</style></head><body><main>
<h1>Print job queue</h1>
<p class="lede">PT-04 / PT-W4 · PT-W3 tray substitute · Held jobs wait for a valid xVerae- key. <b style="color:var(--plum)">%s</b></p>
<div class="status"><span class="pill%s">%s</span><span class="muted">%s</span></div>
<div class="card">
<table><thead><tr><th>Hold id</th><th>Document</th><th>State</th><th>Created</th></tr></thead>
<tbody>%s</tbody></table>
<div class="note"><b>Not proof:</b> %s Wait for <b>%s</b>.</div>
<p class="meta">%s</p>
</div></main></body></html>
`, htmlEsc(statuslang.EvidenceNote), pillClass(snap), htmlEsc(snap.Headline), htmlEsc(snap.Detail), rows.String(), htmlEsc(statuslang.EvidenceNote), htmlEsc(statuslang.FinalReceiptReady), meta)
}
func pillClass(snap traySnap) string {
if snap.Held > 0 || !snap.KeyOK {
return " warn"
}
return ""
}

View file

@ -91,17 +91,3 @@ func TestRetryKeepsNonAuthFailures(t *testing.T) {
t.Fatalf("done=%d remaining=%d authFail=%v", done, remaining, authFail)
}
}
func TestIsBillingFailure(t *testing.T) {
pendingEnv(t)
bill := &veraeapi.HTTPError{Service: "zapier stamp", Status: 402, Body: `{"error":"payment_required"}`}
if !isBillingFailure(bill) {
t.Fatal("expected billing failure")
}
if isAuthFailure(bill) {
t.Fatal("402 must not be auth failure")
}
if isBillingFailure(&veraeapi.HTTPError{Status: 401, Body: "no"}) {
t.Fatal("401 is not billing")
}
}

View file

@ -11,25 +11,11 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
// notify shows an OS banner. title should be statuslang.NotifyTitle; body NotifyBody.
func notify(title, body string) {
if os.Getenv("VERAE_NO_NOTIFY") == "1" {
func notify(title, subtitle string) {
if runtime.GOOS != "darwin" || os.Getenv("VERAE_NO_NOTIFY") == "1" {
return
}
if title == "" {
title = "Verae Timestamp"
}
if runtime.GOOS == "linux" {
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
_ = exec.Command("notify-send", "-a", "Verae Timestamp", title, body).Run()
}
return
}
if runtime.GOOS != "darwin" {
return
}
// Brand status string is the notification title (PT-06 / PT-W3).
script := fmt.Sprintf(`display notification %q with title %q`, body, title)
script := fmt.Sprintf(`display notification %q with title "Verae Timestamp" subtitle %q`, subtitle, title)
_ = exec.Command("osascript", "-e", script).Run()
}
@ -49,3 +35,8 @@ func openFolderPath(p string) {
func runInstallPrinterWindows() int { return 1 }
func debugFromRegistry() bool { return false }
func runKeyDialog() int {
fmt.Println("key-dialog is Windows-only; run: verae-print setup")
return 1
}

View file

@ -21,42 +21,14 @@ func exeDir() string {
}
// notifyUser: best-effort balloon via PowerShell (no extra deps). Always logs.
// BalloonTipClicked / DoubleClick open the live loopback /queue (PT-W3).
func notifyUser(title, body string) {
vlog.Printf("notify: %s — %s", title, body)
if os.Getenv("VERAE_NO_NOTIFY") == "1" {
return
}
esc := func(s string) string {
s = strings.ReplaceAll(s, "'", "''")
s = strings.ReplaceAll(s, "\r", " ")
s = strings.ReplaceAll(s, "\n", " ")
return s
}
queue := "http://127.0.0.1:8631/queue"
if serveListenAddr != "" && isLoopbackAddr(serveListenAddr) {
queue = queueURLFor(serveListenAddr)
} else if a := os.Getenv("VERAE_IPP_ADDR"); a != "" && isLoopbackAddr(a) {
queue = queueURLFor(a)
}
// STA + DoEvents so BalloonTipClicked can open /queue before dispose.
ps := fmt.Sprintf(`
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$q='%s'
$n=New-Object System.Windows.Forms.NotifyIcon
$n.Icon=[System.Drawing.SystemIcons]::Information
$n.Visible=$true
$n.Text='Verae Timestamping'
$open={ try { Start-Process $q } catch {} }
$n.add_BalloonTipClicked($open)
$n.add_DoubleClick($open)
$n.ShowBalloonTip(8000,'%s','%s',[System.Windows.Forms.ToolTipIcon]::Info)
$sw=[Diagnostics.Stopwatch]::StartNew()
while($sw.Elapsed.TotalSeconds -lt 9){ [System.Windows.Forms.Application]::DoEvents(); Start-Sleep -Milliseconds 120 }
$n.Visible=$false; $n.Dispose()
`, esc(queue), esc(title), esc(body))
cmd := exec.Command("powershell.exe", "-NoProfile", "-STA", "-WindowStyle", "Hidden", "-Command", ps)
esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
ps := fmt.Sprintf(`Add-Type -AssemblyName System.Windows.Forms;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;$n.ShowBalloonTip(8000,'%s','%s',[System.Windows.Forms.ToolTipIcon]::Info);Start-Sleep 9;$n.Dispose()`, esc(title), esc(body))
cmd := exec.Command("powershell.exe", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps)
hideWindow(cmd)
_ = cmd.Start()
}

View file

@ -16,7 +16,6 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/auth"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/settings"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/winprint"
)
@ -75,22 +74,8 @@ func runServe(args []string) int {
go pendingOnStart()
errc := make(chan error, 2)
if !*noIPP {
serveListenAddr = *addr
srv := &winprint.IPPServer{
Addr: *addr, Name: "Verae TimeStamp PDF",
SpoolDir: filepath.Join(userWorkDir(), "ipp"), Process: proc,
// PT-W3 tray substitute: GET-only status on the same loopback port as IPP.
// POST remains pure IPP — Windows print clients are unaffected.
ExtraHealth: helperExtraHealth,
ServeQueue: handleQueueHTTP,
ServeStatus: handleStatusHTTP,
}
srv := &winprint.IPPServer{Addr: *addr, Name: "Verae TimeStamp PDF", SpoolDir: filepath.Join(userWorkDir(), "ipp"), Process: proc}
go func() { errc <- srv.ListenAndServe() }()
vlog.Printf("status: queue http://%s/queue · status http://%s/status (GET only; hold id ≠ evidence)", *addr, *addr)
if n := len(listPending()); n > 0 {
ht, hb := statuslang.NotifyHeldJobs(n)
notifyUser(ht, notifyQueueHint(hb))
}
}
if !*noWatch {
w := &winprint.Watcher{Dir: fallbackDropDir(), WorkDir: filepath.Join(userWorkDir(), "fallback"), Process: proc}
@ -149,27 +134,14 @@ func makeProcessor(outDir string) winprint.Processor {
return err
}
lj.Printf("process: HELD as pending %s (%v); prompting for sign-in", m.ID, err)
notifyUser(statuslang.NotifyTitle("held"), notifyQueueHint(statuslang.NotifyBody("held", m.Title, m.ID)))
go handleAuthFailure(authReason(err), stampFile)
return nil // the job is safe; IPP reports success to Windows
}
if isBillingFailure(err) {
m, herr := holdJob(j.Path, target, j.Source, err, true)
if herr != nil {
lj.Errorf("process: could not hold job: %v (PDF left at %s)", herr, j.Path)
go offerBillingCheckout(billingReason(err))
return err
}
lj.Printf("process: HELD as pending %s (%v); opening Zap Billing", m.ID, err)
notifyUser(statuslang.NotifyTitle("billing"), notifyQueueHint(statuslang.NotifyBody("billing", m.Title, m.ID)))
go handleBillingFailure(billingReason(err))
return nil // job held; user adds funds then reprint / retry pending
}
notifyUser(statuslang.NotifyTitle("failed"), statuslang.NotifyBody("failed", j.Title, err.Error()))
notifyUser("Verae timestamp FAILED", err.Error())
return err
}
_ = os.Remove(j.Path)
notifyUser(statuslang.NotifyTitle("final"), statuslang.NotifyBody("final", j.Title, res.ReceiptID))
notifyUser(fmt.Sprintf("Verae timestamp %s", res.ReceiptID), res.CertificatePDF)
openOnComplete(lj, res.CertificatePDF)
// a successful stamp proves the key works: flush anything still pending
if len(listPending()) > 0 {

Some files were not shown because too many files have changed in this diff Show more