diff --git a/.forgejo/workflows/test-and-deploy.yml b/.forgejo/workflows/test-and-deploy.yml new file mode 100644 index 0000000..d38656b --- /dev/null +++ b/.forgejo/workflows/test-and-deploy.yml @@ -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 diff --git a/.github/workflows/windows-msi-sign.yml.example b/.github/workflows/windows-msi-sign.yml.example deleted file mode 100644 index 566d37d..0000000 --- a/.github/workflows/windows-msi-sign.yml.example +++ /dev/null @@ -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" diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml deleted file mode 100644 index b5d70a3..0000000 --- a/.github/workflows/windows-msi.yml +++ /dev/null @@ -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 diff --git a/.gitignore b/.gitignore index 1c0a9fb..d17d79e 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md index 376c857..b5175d9 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/verae-print/acquire_other.go b/cmd/verae-print/acquire_other.go index e684390..5872a56 100644 --- a/cmd/verae-print/acquire_other.go +++ b/cmd/verae-print/acquire_other.go @@ -1,4 +1,4 @@ -//go:build !windows && !darwin && !linux +//go:build !windows && !darwin package main diff --git a/cmd/verae-print/hide_other.go b/cmd/verae-print/hide_other.go deleted file mode 100644 index 15788a4..0000000 --- a/cmd/verae-print/hide_other.go +++ /dev/null @@ -1,7 +0,0 @@ -//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 deleted file mode 100644 index 306afd1..0000000 --- a/cmd/verae-print/keydialog_stub.go +++ /dev/null @@ -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 -} diff --git a/cmd/verae-print/keydialog_windows.go b/cmd/verae-print/keydialog_windows.go index 7cbddfd..4578f97 100644 --- a/cmd/verae-print/keydialog_windows.go +++ b/cmd/verae-print/keydialog_windows.go @@ -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." diff --git a/cmd/verae-print/keyflow_gui.go b/cmd/verae-print/keyflow_gui.go deleted file mode 100644 index 40da49b..0000000 --- a/cmd/verae-print/keyflow_gui.go +++ /dev/null @@ -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 - } - } - } -} diff --git a/cmd/verae-print/keysetup_darwin.go b/cmd/verae-print/keysetup_darwin.go index 96db150..10c4b4f 100644 --- a/cmd/verae-print/keysetup_darwin.go +++ b/cmd/verae-print/keysetup_darwin.go @@ -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: "