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
150 changed files with 967 additions and 18819 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,96 +0,0 @@
name: installers
on:
push:
branches: ["**"]
pull_request:
jobs:
netprint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.1"
- name: Toolchain
run: sudo apt-get update && sudo apt-get install -y nsis cpio g++ git
- name: Build driver and network-printer installers
run: VERSION=0.3.4 ./scripts/build-netprint-installers.sh
- uses: actions/upload-artifact@v4
with:
name: verae-netprint-linux
path: |
build/netprint/*.deb
build/netprint/*.exe
build/netprint/macos/**
if-no-files-found: error
macos-pkg:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26.1"
- name: Build arm64 binary, AirDrop helper, and package
run: |
set -euo pipefail
VERSION=0.3.4
OUT="$PWD/build/netprint/macos"
mkdir -p "$OUT"
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o "$OUT/verae-print" ./cmd/verae-print
chmod +x platform/macos/pkg/build-netprint-pkg.sh platform/macos/pkg/check-pkg.sh
VERSION="$VERSION" BIN="$OUT/verae-print" PKG_ARCH=arm64 OUT="$OUT" ./platform/macos/pkg/build-netprint-pkg.sh
cp -f "$OUT/verae-print" "$OUT/verae-print-arm64"
APP="$OUT/payload/Applications/Verae NetPrint.app/Contents/MacOS"
test -x "$APP/verae-airdrop"
test -x "$APP/verae-menubar"
test -x "$APP/verae-print"
test -f "$OUT/payload/Applications/Verae NetPrint.app/Contents/Info.plist"
test -f "$OUT/VeraeNetPrint-macos-arm64-${VERSION}.pkg"
"$APP/verae-menubar" --smoke | tee "$OUT/menubar-smoke.txt"
"$APP/verae-print" tray --smoke | tee "$OUT/tray-smoke.txt"
grep -E 'smoke ok|no window server' "$OUT/menubar-smoke.txt"
grep -E 'smoke ok|no window server' "$OUT/tray-smoke.txt"
- name: pkgutil --expand, pkgutil --check-signature, and installer -pkginfo
run: bash platform/macos/pkg/check-pkg.sh build/netprint/macos/VeraeNetPrint-macos-arm64-0.3.4.pkg
- uses: actions/upload-artifact@v4
with:
name: verae-netprint-macos
path: |
build/netprint/macos/*.pkg
build/netprint/macos/verae-print
build/netprint/macos/verae-print-arm64
if-no-files-found: error
publish:
needs: [netprint, macos-pkg]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: verae-netprint-linux
path: dist
- uses: actions/download-artifact@v4
with:
name: verae-netprint-macos
path: macpkg
- name: SHA256SUMS
run: |
set -euo pipefail
rm -f dist/macos/*.pkg dist/SHA256SUMS
mkdir -p dist/macos
cp -f macpkg/*.pkg macpkg/verae-print macpkg/verae-print-arm64 dist/macos/
cd dist
sha256sum *.deb *.exe macos/*.pkg macos/*.tar.gz macos/verae-print macos/verae-print-amd64 macos/verae-print-arm64 macos/distribution.xml macos/postinstall-netprint.sh | tee SHA256SUMS
test -f macos/VeraeNetPrint-macos-arm64-0.3.4.pkg
- uses: actions/upload-artifact@v4
with:
name: verae-netprint-installers
path: |
dist/*.deb
dist/*.exe
dist/SHA256SUMS
dist/macos/**
if-no-files-found: error

View file

@ -1,13 +0,0 @@
.PHONY: test netprint-debs netprint-windows netprint-macos installers
VERSION ?= 0.3.4
test:
go test ./...
netprint-debs:
VERSION=$(VERSION) ./platform/linux/deb/build-deb.sh amd64
VERSION=$(VERSION) ./platform/linux/deb/build-netprint-deb.sh amd64
netprint-windows netprint-macos installers:
VERSION=$(VERSION) ./scripts/build-netprint-installers.sh

View file

@ -26,14 +26,6 @@ verae-print setup
verae-print stamp document.pdf
```
**Network printer (optional, separate from the driver):** an IPP Everywhere queue named **Verae Timestamp** for other devices on the LAN. Install the standalone package, or leave the driver installer’s option off (Windows checkbox, macOS Customize, Debian debconf). Both use one service and one tray / menu-bar control. On Windows the service advertises through DNS-SD (`DnsServiceRegister`) because the DNS Client owns UDP 5353, writes `%ProgramData%\Verae\logs\netprint.log`, and the setup starts the tray for the logged-in user. Public networks and a local Windows queue stay off unless you opt in. The standalone Windows folder is `C:\Program Files\Verae\NetPrint` (the driver stays in `PrintStamp`). The LAN fallback port is **8632**, so it does not collide with the driver helper on `127.0.0.1:8631`. See [docs/network-printer.md](docs/network-printer.md).
| OS | Standalone | With the driver (off unless you opt in) |
|----|------------|------------------------------------------|
| Windows | `VeraeNetPrint-windows-amd64-setup.exe` | Checkbox on the driver setup’s options page |
| macOS | `VeraeNetPrint-macos.pkg` | Customize → Network timestamp printer |
| Debian | `sudo apt install ./verae-netprint_amd64.deb` | `sudo apt install ./verae-print_amd64.deb` and answer the prompt, or install `verae-netprint` later |
**Linux (tarball, any distro):**
```bash
@ -72,13 +64,6 @@ printenv XVERAE_TOKEN > ~/.verae/xverae-token # or paste once
chmod 600 ~/.verae/xverae-token
```
Build the network-printer installers (Debian packages, Windows NSIS setups, macOS binaries and a `.pkg` when `pkgbuild` is available):
```bash
make installers
# artifacts: build/netprint/
```
## Usage
In any app: **File → Print → Destination → Verae Timestamping → Print**.

View file

@ -1,19 +0,0 @@
// Command verae-netprint-tray is the Windows notification-area build of the
// network-printer menu. It is the same control code as `verae-print tray`,
// linked as a GUI-subsystem binary so it does not open a console.
package main
import (
"context"
"fmt"
"os"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/netprint"
)
func main() {
if err := netprint.RunTray(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View file

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

View file

@ -3,52 +3,10 @@ package main
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"unicode/utf16"
)
func TestNetworkStampUsesDesktopPath(t *testing.T) {
netprintSrc, err := os.ReadFile("netprint_cmd.go")
if err != nil {
t.Fatal(err)
}
core, err := os.ReadFile("stampcore.go")
if err != nil {
t.Fatal(err)
}
text := string(netprintSrc)
if !strings.Contains(text, "stampWithClient(") || strings.Contains(text, "mock-cert") || strings.Contains(text, `DocumentName: "document.pdf"`) {
t.Fatal("netprint stamp is not the desktop stamp path")
}
if !strings.Contains(string(core), "func stampWithClient(") || !strings.Contains(string(core), "stampWithClient(pdf, t.Title") {
t.Fatal("stampFile does not use stampWithClient")
}
}
func TestNetprintUnknownCommandDoesNotStartServer(t *testing.T) {
if code := runNetprint([]string{"launch-server"}); code != 2 {
t.Fatalf("unknown command exit %d, want 2", code)
}
if runtime.GOOS == "windows" {
return
}
// Linux has no tray launcher. The command must fail before any listen.
if code := runNetprint([]string{"launch-tray"}); code != 1 {
t.Fatalf("launch-tray exit %d, want 1", code)
}
}
func TestTrayUnknownArgument(t *testing.T) {
old := os.Args
t.Cleanup(func() { os.Args = old })
os.Args = []string{"verae-print", "tray", "--nope"}
if code := runTrayCmd(); code != 2 {
t.Fatalf("tray exit %d, want 2", code)
}
}
func TestReadTextFileUTF16(t *testing.T) {
s := "Email=a@b.co\r\nPassword=p=\"w\\x\r\n"
u := utf16.Encode([]rune(s))

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

@ -21,9 +21,6 @@ func main() {
if len(os.Args) < 2 && isHelperExe() {
os.Args = append(os.Args, "serve")
}
if len(os.Args) < 2 && (isTrayExe() || isMenuBarApp()) {
os.Args = append(os.Args, "tray")
}
if len(os.Args) < 2 {
usage()
os.Exit(2)
@ -33,10 +30,6 @@ func main() {
os.Exit(runStamp(os.Args[2:]))
case "serve":
os.Exit(runServe(os.Args[2:]))
case "netprint":
os.Exit(runNetprint(os.Args[2:]))
case "tray":
os.Exit(runTrayCmd())
case "ipp-selftest":
os.Exit(runIPPSelftest(os.Args[2:]))
case "installer-account":
@ -83,13 +76,6 @@ Usage:
verae-print install-macos # also install PDF Services menu items (legacy)
verae-print serve [--addr 127.0.0.1:8631] [--no-ipp] [--no-watch] [--debug]
# Windows print helper: IPP loopback printer + fallback watcher
verae-print netprint [--config FILE] [--name NAME] [--port N] [--data-dir DIR] [--forward TARGET] [--no-mdns]
# LAN IPP Everywhere printer
verae-print netprint install --owner driver|standalone [--allow-public true|false] [--local-queue]
verae-print netprint uninstall --owner driver|standalone
verae-print netprint launch-tray # Windows: start the tray as the logged-in user
verae-print netprint enable-receipts-share # macOS: save shareReceiptsSMB and publish the folder
verae-print tray # menu-bar / system-tray control for the network printer
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

View file

@ -1,132 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/job"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/netprint"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/vlog"
)
func runNetprint(args []string) int {
if len(args) > 0 {
switch args[0] {
case "install", "uninstall", "launch-tray", "enable-receipts-share":
return runNetprintInstall(args)
}
// An unknown word must not fall through into the server. That is how
// `netprint launch-tray` on an older binary started a second listener.
if !strings.HasPrefix(args[0], "-") {
fmt.Fprintf(os.Stderr, "unknown netprint command %q (not starting the server)\n", args[0])
return 2
}
}
fs := flag.NewFlagSet("netprint", flag.ContinueOnError)
config := fs.String("config", "", "config file (default: system netprint.json)")
name := fs.String("name", "", "printer name on the network")
port := fs.Int("port", 0, "IPP port (631, or the fallback when 631 is taken)")
dataDir := fs.String("data-dir", "", "where receipts are stored")
forward := fs.String("forward", "", "queue name or ipp:// URI to forward the PDF to")
noMDNS := fs.Bool("no-mdns", false, "do not advertise with mDNS")
fs.SetOutput(os.Stderr)
if err := fs.Parse(args); err != nil {
return 2
}
flags := netprint.Config{Name: *name, Port: *port, DataDir: *dataDir, ForwardPrinter: *forward}
if *noMDNS {
off := false
flags.Advertise = &off
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := netprint.RunCommand(ctx, *config, flags, stampNetwork); err != nil && ctx.Err() == nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
func runNetprintInstall(args []string) int {
fs := flag.NewFlagSet(args[0], flag.ContinueOnError)
owner := fs.String("owner", "standalone", "driver or standalone")
token := fs.String("token-file", "", "optional xVerae- token file to copy for the service")
allowPublic := fs.String("allow-public", "", "true or false; empty leaves the saved choice")
localQueue := fs.Bool("local-queue", false, "add a local Windows IPP queue named Verae Timestamp")
fs.SetOutput(os.Stderr)
if err := fs.Parse(args[1:]); err != nil {
return 2
}
var err error
switch args[0] {
case "launch-tray":
err = netprint.LaunchTrayNow()
case "enable-receipts-share":
err = netprint.EnableReceiptsShare()
case "install":
pub, perr := parseAllowPublic(*allowPublic)
if perr != nil {
fmt.Fprintln(os.Stderr, perr)
return 2
}
err = netprint.Install(*owner, *token, netprint.InstallOptions{AllowPublic: pub, LocalQueue: *localQueue})
default:
err = netprint.Uninstall(*owner)
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println(args[0], "ok (", *owner, ")")
return 0
}
func parseAllowPublic(s string) (*bool, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "":
return nil, nil
case "true", "1", "yes":
v := true
return &v, nil
case "false", "0", "no":
v := false
return &v, nil
default:
return nil, fmt.Errorf("allow-public must be true or false")
}
}
func runTrayCmd() (code int) {
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "tray panic: %v\n", r)
code = 1
}
}()
for _, a := range os.Args[2:] {
switch a {
case "--smoke", "-smoke":
_ = os.Setenv("VERAE_TRAY_SMOKE", "1")
default:
fmt.Fprintf(os.Stderr, "unknown tray argument %q\n", a)
return 2
}
}
if err := netprint.RunTray(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
return 0
}
func stampNetwork(pdf, title, user string) (job.Result, error) {
_ = user
// Same client, document name, and job.Run call as a desktop Print-Job.
return stampWithClient(pdf, title, filepath.Dir(pdf), "", vlog.Job(vlog.NewJobID("net")))
}

View file

@ -12,16 +12,7 @@ import (
)
func notify(title, subtitle string) {
if os.Getenv("VERAE_NO_NOTIFY") == "1" {
return
}
if runtime.GOOS == "linux" {
if os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != "" {
_ = exec.Command("notify-send", "-a", "Verae Timestamp", "Verae: "+title, subtitle).Run()
}
return
}
if runtime.GOOS != "darwin" {
if runtime.GOOS != "darwin" || os.Getenv("VERAE_NO_NOTIFY") == "1" {
return
}
script := fmt.Sprintf(`display notification %q with title "Verae Timestamp" subtitle %q`, subtitle, title)
@ -44,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

@ -28,24 +28,6 @@ func isHelperExe() bool {
return strings.Contains(strings.ToLower(filepath.Base(exe)), "printhelper")
}
func isTrayExe() bool {
exe, err := os.Executable()
if err != nil {
return false
}
return strings.Contains(strings.ToLower(filepath.Base(exe)), "netprint-tray")
}
// isMenuBarApp reports whether this process is the macOS menu-bar bundle.
// Opening Verae NetPrint.app with no arguments starts the tray.
func isMenuBarApp() bool {
exe, err := os.Executable()
if err != nil {
return false
}
return strings.Contains(filepath.Clean(exe), "Verae NetPrint.app"+string(filepath.Separator))
}
// fallbackDropDir is the folder the "Verae TimeStamp PDF (Local Port)" queue writes into.
func fallbackDropDir() string {
if v := os.Getenv("VERAE_FALLBACK_DIR"); v != "" {

View file

@ -36,29 +36,15 @@ func clientForChannel(ch auth.Channel, lj vlog.Job) (*veraeapi.Client, auth.Chan
return client, ch, nil
}
// stampWithClient is the real stamp used by the desktop driver, Print-Job, and
// POST /api/stamp. It hashes the file and calls the Verae client. It does not
// invent a receipt.
func stampWithClient(pdf, title, outDir, outPath string, lj vlog.Job) (job.Result, error) {
// stampFile stamps one PDF with the current key (no holding / prompting).
func stampFile(pdf string, t stampTarget, lj vlog.Job) (job.Result, error) {
client, ch, err := clientForChannel(auth.PreferredChannel(), lj)
if err != nil {
return job.Result{}, err
}
client.HTTPDo = loggingDo(lj)
return job.Run(job.Options{
SourcePDF: pdf,
OutDir: outDir,
OutPath: outPath,
DocumentName: safeStem(title) + ".pdf",
Channel: ch,
Client: client,
})
}
// stampFile stamps one PDF with the current key (no holding / prompting).
func stampFile(pdf string, t stampTarget, lj vlog.Job) (job.Result, error) {
start := time.Now()
res, err := stampWithClient(pdf, t.Title, t.OutDir, t.OutPath, lj)
res, err := job.Run(job.Options{SourcePDF: pdf, OutDir: t.OutDir, OutPath: t.OutPath, DocumentName: safeStem(t.Title) + ".pdf", Channel: ch, Client: client})
if err != nil {
return res, err
}

View file

@ -1,11 +0,0 @@
//go:build darwin
package main
import "runtime"
func init() {
// -[NSApplication run] returns immediately when it is not on the process
// main thread. Lock before the scheduler can move this goroutine.
runtime.LockOSThread()
}

View file

@ -1,36 +0,0 @@
# Verae Timestamp iPhone shortcut
Build this in the Shortcuts app. It shows up in the Share sheet, sends the PDF or photo to the Verae printer on your Wi-Fi, and saves the stamped certificate. A signed `.shortcut` file is not included: Apple only accepts shortcuts signed on a Mac or iPhone, and this tree is built on Linux.
Open the receipts page on a computer (`http://<printer>:631/receipts`, or 8632 if CUPS is using 631). The QR code opens `http://<printer>:<port>/ios-shortcut` on the phone, which repeats these steps and shows the stamp address for this machine.
## Build it
1. Open **Shortcuts** and tap **+**. Name the shortcut **Verae Timestamp**.
2. Tap the shortcut’s **(i)** or settings and turn on **Show in Share Sheet**.
3. Under share sheet types, leave **PDFs** and **Images** selected. You can clear the other types.
4. Add **Get Contents of URL**.
- URL: `http://<printer>:<port>/api/stamp`
If the receipts page has a PIN, use `http://<printer>:<port>/api/stamp?access=YOURPIN`.
- Method: **POST**.
- Request Body: **File**.
- File: **Shortcut Input**.
- Add a header `X-Filename` if you want the job title to be the file name. The server also accepts `Content-Disposition: attachment; filename="Contract.pdf"`. PDF and JPEG are recognized from the file bytes, so a photo from the share sheet works.
5. Add **Quick Look** and pass the contents of the URL. That is the stamped certificate PDF.
6. Add **Save File**. Leave **Ask Where to Save** on and pick iCloud Drive or On My iPhone when it asks.
7. Show the receipt id:
- Add **Get Dictionary Value**.
- Dictionary: the **Headers** of the Get Contents of URL result.
- Key: `X-Verae-Receipt-Id`.
- Add **Show Result** (or **Show Notification**) with that value.
- `X-Verae-SHA256` is the document hash. `X-Verae-Receipt-JSON` is the URL of the `.verae.json` summary on the printer.
To get a zip instead, change the URL to end in `?format=zip` (use `&access=` if you already have `?access=`). **Save File** then receives `original.pdf`, `certificate.pdf`, and `receipt.json`.
## Use it
In Files or Photos, open a PDF or picture, tap **Share**, and choose **Verae Timestamp**. If it is missing, tap **More** and enable it.
The printer records the request as a job, lists it on the receipts page, and emails the stamped PDF when **Email stamped PDFs** is on. The shortcut does not wait for the email. The PDF in the shortcut result is the certificate.
The stamp address is only for Private and Domain networks, same as the printer. A wrong or missing PIN returns `401` and no PDF.

View file

@ -1,47 +0,0 @@
# Share extension for the Verae Timestamping iOS app
The app (`com.flightlookup.verae-timestamping`) is not in this repository. This note is the design for a **Verae Timestamp** row in the iOS Share sheet that stamps a PDF or photo on the LAN print server and keeps the result in Files and in the app.
The shortcut in `README.md` is the path that works before that extension ships. The server side it needs is already here: `POST /api/stamp` and a `_verae._tcp` Bonjour record.
## Extension
- Extension point: `com.apple.share-services`.
- Display name: **Verae Timestamp**.
- Activation rule: the first attachment conforms to `com.adobe.pdf` or `public.image` (which covers JPEG and HEIC). Ignore other share types.
- Process the attachment in the extension, then call `completeRequestReturningItems` with an item provider for the stamped PDF so the system share sheet can hand it back. Also write a copy into the app group so the main app can list it without waiting for Files.
Suggested app group: `group.com.flightlookup.verae-timestamping`. Put the certificate, the receipt id, the SHA-256, and the receipt JSON in that container. The main app reads the same folder on launch.
## Finding the printer
Browse Bonjour in `local.` with `NWBrowser`:
1. Prefer `_verae._tcp`. The TXT record is `txtvers=1`, `path=/api/stamp`, `ver=1`, and `url=http://<host>:<port>/api/stamp`. The SRV record is the port (631, or 8632 when CUPS owns 631). Resolve the host and POST to `http://<host>:<port>` plus `path`.
2. If that browse is empty, browse `_ipp._tcp` and keep the instance whose TXT `ty` or `product` is **Verae Timestamp**. Use the host and port from that service and POST to `/api/stamp`. The AirPrint `adminurl` ends in `/receipts`; the stamp URL is the same origin with `/api/stamp`.
This repository advertises `_verae._tcp` from the print server’s mDNS socket. It is a separate service name from `<name>._ipp._tcp.local`, so it does not create a second IPP advertisement. On Windows, `DnsServiceRegister` still owns only the IPP name. The shared UDP 5353 socket answers AirPrint subtype PTRs for IPP and, separately, the `_verae._tcp` PTR, SRV, and TXT. It does not add IPP SRV, TXT, or address records while DNS-SD is up.
Stop the browse when the extension finishes. Cache the last host and port in the app group so a later share can try it first and fall back to a browse if the POST fails.
## Upload
`POST /api/stamp` with the file bytes as the body, or as multipart field `file`.
- Header `X-Filename`: the document title.
- Header `X-Verae-User`: optional, shown on the receipts page. Default is `stamp`.
- The response body is `application/pdf` (the certificate).
- Headers: `X-Verae-Receipt-Id`, `X-Verae-SHA256`, `X-Verae-Receipt-JSON`.
- `?format=zip` returns `original.pdf`, `certificate.pdf`, and `receipt.json`.
The job is stored like a print, appears on `/receipts`, and is emailed when email is enabled. The extension should treat the HTTP response as the copy to save, not wait for the email.
## PIN and network
The route uses the same Private/Domain check and optional receipts PIN as `/receipts`. Send the PIN as `X-Verae-Access`, `Authorization: Bearer <pin>`, or `?access=`. Store the PIN in the Keychain (`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`), not in the app group. A `401` means the PIN is missing or wrong. A `403` means the computer is on a Public network and Public access is off. Show that text; do not save an empty file.
## Saving
- Write the certificate into the app group and refresh the main app’s receipt list (a Darwin notification on the app group is enough).
- Offer **Save to Files** with a document picker, or by returning the PDF from the extension so the user can pick a folder.
- Keep the receipt id and SHA-256 next to the file. The JSON URL in `X-Verae-Receipt-JSON` is on the LAN and needs the same PIN.

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