diff --git a/.forgejo/workflows/test-and-deploy.yml b/.forgejo/workflows/test-and-deploy.yml deleted file mode 100644 index d38656b..0000000 --- a/.forgejo/workflows/test-and-deploy.yml +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/.github/workflows/windows-msi-sign.yml.example b/.github/workflows/windows-msi-sign.yml.example new file mode 100644 index 0000000..566d37d --- /dev/null +++ b/.github/workflows/windows-msi-sign.yml.example @@ -0,0 +1,164 @@ +# 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" diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml new file mode 100644 index 0000000..b5d70a3 --- /dev/null +++ b/.github/workflows/windows-msi.yml @@ -0,0 +1,164 @@ +# 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 diff --git a/.gitignore b/.gitignore index d17d79e..1c0a9fb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ .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/ diff --git a/README.md b/README.md index b5175d9..376c857 100644 --- a/README.md +++ b/README.md @@ -1,136 +1,66 @@ # PrintToVeraeTimeStamp -Print / CLI helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`. +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) -Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine. +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)). ## Install -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 +```sh +go build ./... ``` ## Usage -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 +```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 ``` -Default output directory: `~/Documents/Verae Timestamps/` +## Packaging -| File | Content | -|------|---------| -| `{name}.pdf` | Exact printed bytes (proven artifact) | -| `{name}-verae-timestamp-{RECEIPTID}.pdf` | Registration / certificate PDF | -| `{name}.verae.json` | Receipt sidecar | +Tray, menu bar, CERT-01 digests, version stamps (`HelperVersion` **0.3.6**), and IPP GET vs POST split: -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/). +- [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). ## Contributing -Open questions and design changes in this repository. Pull requests are accepted for review against the spec. +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. ## License -UNLICENSED © Verae / George Lambert +UNLICENSED © Verae Inc. (verae.com) diff --git a/cmd/verae-print/acquire_other.go b/cmd/verae-print/acquire_other.go index 5872a56..e684390 100644 --- a/cmd/verae-print/acquire_other.go +++ b/cmd/verae-print/acquire_other.go @@ -1,4 +1,4 @@ -//go:build !windows && !darwin +//go:build !windows && !darwin && !linux package main diff --git a/cmd/verae-print/hide_other.go b/cmd/verae-print/hide_other.go new file mode 100644 index 0000000..15788a4 --- /dev/null +++ b/cmd/verae-print/hide_other.go @@ -0,0 +1,7 @@ +//go:build !windows + +package main + +import "os/exec" + +func hideWindow(c *exec.Cmd) {} diff --git a/cmd/verae-print/keydialog_stub.go b/cmd/verae-print/keydialog_stub.go new file mode 100644 index 0000000..306afd1 --- /dev/null +++ b/cmd/verae-print/keydialog_stub.go @@ -0,0 +1,10 @@ +//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 +} diff --git a/cmd/verae-print/keydialog_windows.go b/cmd/verae-print/keydialog_windows.go index 4578f97..7cbddfd 100644 --- a/cmd/verae-print/keydialog_windows.go +++ b/cmd/verae-print/keydialog_windows.go @@ -45,6 +45,8 @@ 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 @@ -53,7 +55,35 @@ 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)" } else { $script:result } + 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' } 'create' { $n = Field 'Your name' $false ''; $e = Field 'Email' $false $pre; $p = Field 'Password' $true ''; $p2 = Field 'Confirm password' $true '' @@ -143,9 +173,21 @@ 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() + msg = "Sign-in failed: " + err.Error() + "\r\nForgot your password? Click \"Forgot password?\" below." 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 { @@ -231,6 +273,95 @@ 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." diff --git a/cmd/verae-print/keyflow_gui.go b/cmd/verae-print/keyflow_gui.go new file mode 100644 index 0000000..40da49b --- /dev/null +++ b/cmd/verae-print/keyflow_gui.go @@ -0,0 +1,365 @@ +//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 + } + } + } +} diff --git a/cmd/verae-print/keysetup_darwin.go b/cmd/verae-print/keysetup_darwin.go index 10c4b4f..96db150 100644 --- a/cmd/verae-print/keysetup_darwin.go +++ b/cmd/verae-print/keysetup_darwin.go @@ -3,14 +3,12 @@ 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" ) @@ -22,6 +20,15 @@ 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: "