0.3.1: never overwrite outputs (seconds + -N suffix); OpenOnComplete (folder|pdf|none) on Windows+macOS; hold jobs on missing/invalid key (401/403) with one native sign-in prompt and auto-retry; macOS pkg in-installer key flow (mac-key-setup); Windows release installer (no debug naming, errors-only log, options page); VERAE_LOG_LEVEL=debug switch

This commit is contained in:
Grok Bot (for George) 2026-10-03 19:40:08 -04:00
commit e960883a0a
7 changed files with 1804 additions and 378 deletions

View file

@ -15,7 +15,7 @@ import (
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
)
const HelperVersion = "0.1.2"
const HelperVersion = "0.3.1"
// Result paths written for one print job.
type Result struct {
@ -111,14 +111,17 @@ func Run(opt Options) (Result, error) {
if origBase == "" || origBase == "." {
origBase = stem + ".pdf"
}
origPath := filepath.Join(outDir, origBase)
srcBytes, err := os.ReadFile(opt.SourcePDF)
if err != nil {
return Result{}, err
}
if err := os.WriteFile(origPath, srcBytes, 0o600); err != nil {
// Reserve a name that collides with nothing already there: the original,
// its .verae.json sidecar. Never overwrite; add -2, -3, ... instead.
origPath, stem, err := reserveOriginal(outDir, origBase, stem, srcBytes)
if err != nil {
return Result{}, err
}
origBase = filepath.Base(origPath)
digest, err := hash.FileSHA256(origPath)
if err != nil {
@ -136,7 +139,7 @@ func Run(opt Options) (Result, error) {
}
safeID := SanitizeReceiptID(rec.ReceiptID)
certPath := filepath.Join(outDir, fmt.Sprintf("%s-verae-timestamp-%s.pdf", stem, safeID))
certStem := fmt.Sprintf("%s-verae-timestamp-%s", stem, safeID)
// Always prefer the branded Registration Document (digests-only wording,
// no mcp.verae.ai). Edge attachPdf is optional and may still say MCP.
@ -154,7 +157,8 @@ func Run(opt Options) (Result, error) {
return Result{}, err
}
_ = rec.PDF // upstream PDF ignored in favor of branded local certificate
if err := os.WriteFile(certPath, pdfBytes, 0o600); err != nil {
certPath, err := writeExclusive(outDir, certStem, ".pdf", pdfBytes)
if err != nil {
return Result{}, err
}
@ -173,12 +177,12 @@ func Run(opt Options) (Result, error) {
"status": "ok",
"saved_at": time.Now().UTC().Format(time.RFC3339),
}
jsonPath := filepath.Join(outDir, stem+".verae.json")
jb, err := json.MarshalIndent(sidecar, "", " ")
if err != nil {
return Result{}, err
}
if err := os.WriteFile(jsonPath, jb, 0o600); err != nil {
jsonPath, err := writeExclusive(outDir, stem, ".verae.json", jb)
if err != nil {
return Result{}, err
}
@ -191,3 +195,72 @@ func Run(opt Options) (Result, error) {
Channel: opt.Channel,
}, nil
}
const maxSuffix = 10000
func suffixed(base string, n int) string {
if n <= 1 {
return base
}
return fmt.Sprintf("%s-%d", base, n)
}
func exists(p string) bool {
_, err := os.Lstat(p)
return err == nil
}
// createNew writes data to path only if path does not exist yet (O_EXCL).
func createNew(path string, data []byte) error {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
f.Close()
os.Remove(path)
return err
}
return f.Close()
}
// writeExclusive writes dir/<stem><ext>, or <stem>-2<ext>, -3 ... if taken.
func writeExclusive(dir, stem, ext string, data []byte) (string, error) {
for n := 1; n < maxSuffix; n++ {
p := filepath.Join(dir, suffixed(stem, n)+ext)
err := createNew(p, data)
if err == nil {
return p, nil
}
if !os.IsExist(err) {
return "", err
}
}
return "", fmt.Errorf("no free file name for %s%s in %s", stem, ext, dir)
}
// reserveOriginal saves the original under origBase (or origBase-N) such that
// neither it nor the matching <stem>.verae.json already exists. Returns the
// saved path and the stem to use for the certificate and sidecar.
func reserveOriginal(dir, origBase, stem string, data []byte) (string, string, error) {
ext := filepath.Ext(origBase)
base := strings.TrimSuffix(origBase, ext)
if base == "" {
base = stem
}
for n := 1; n < maxSuffix; n++ {
st := suffixed(stem, n)
if exists(filepath.Join(dir, st+".verae.json")) {
continue
}
p := filepath.Join(dir, suffixed(base, n)+ext)
err := createNew(p, data)
if err == nil {
return p, st, nil
}
if !os.IsExist(err) {
return "", "", err
}
}
return "", "", fmt.Errorf("no free file name for %s in %s", origBase, dir)
}

View file

@ -103,3 +103,46 @@ func TestRunVeraeRendersLocalCert(t *testing.T) {
t.Fatal("not a pdf")
}
}
func TestRunNeverOverwritesSameName(t *testing.T) {
dir := t.TempDir()
out := filepath.Join(dir, "out")
n := 0
client := &veraeapi.Client{
Channel: auth.ChannelZapier,
ZapierBase: "https://zap.test",
XVeraeToken: "xVerae-x",
HTTPDo: func(req *http.Request) (*http.Response, error) {
n++
// same receipt id every time, to also exercise cert-name collisions
body := `{"receipt_id":"bafySame"}`
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
},
}
var results []Result
for i, content := range []string{"%PDF-1.4 first", "%PDF-1.4 second", "%PDF-1.4 third"} {
src := filepath.Join(dir, "src"+string(rune('a'+i))+".pdf")
_ = os.WriteFile(src, []byte(content), 0o600)
res, err := Run(Options{SourcePDF: src, OutDir: out, DocumentName: "Printed 2026-10-03 192901.pdf", Channel: auth.ChannelZapier, Client: client})
if err != nil {
t.Fatal(err)
}
results = append(results, res)
}
wantOrig := []string{"Printed 2026-10-03 192901.pdf", "Printed 2026-10-03 192901-2.pdf", "Printed 2026-10-03 192901-3.pdf"}
wantJSON := []string{"Printed-2026-10-03-192901.verae.json", "Printed-2026-10-03-192901-2.verae.json", "Printed-2026-10-03-192901-3.verae.json"}
wantCert := []string{"Printed-2026-10-03-192901-verae-timestamp-bafySame.pdf", "Printed-2026-10-03-192901-2-verae-timestamp-bafySame.pdf", "Printed-2026-10-03-192901-3-verae-timestamp-bafySame.pdf"}
for i, r := range results {
if filepath.Base(r.OriginalPDF) != wantOrig[i] || filepath.Base(r.ReceiptJSON) != wantJSON[i] || filepath.Base(r.CertificatePDF) != wantCert[i] {
t.Fatalf("job %d: %s | %s | %s", i, filepath.Base(r.OriginalPDF), filepath.Base(r.ReceiptJSON), filepath.Base(r.CertificatePDF))
}
}
if b, _ := os.ReadFile(results[0].OriginalPDF); string(b) != "%PDF-1.4 first" {
t.Fatalf("first original overwritten: %q", b)
}
// a pre-existing cert with the target name is never overwritten
p, err := writeExclusive(out, "Printed-2026-10-03-192901-verae-timestamp-bafySame", ".pdf", []byte("x"))
if err != nil || filepath.Base(p) != "Printed-2026-10-03-192901-verae-timestamp-bafySame-2.pdf" {
t.Fatal(p, err)
}
}

View file

@ -0,0 +1,128 @@
// Package settings holds user preferences for the print helper.
//
// Lookup order (first hit wins per field):
// 1. env VERAE_OPEN_ON_COMPLETE
// 2. user file ~/.verae/config.json (written by the installers for the console/logged-in user)
// 3. machine file %ProgramData%\Verae\PrintStamp\config.json (Windows)
// or /Library/Application Support/Verae/PrintStamp/config.json (macOS)
// 4. defaults (OpenOnComplete = "folder", LogLevel = info)
package settings
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// OpenOnComplete values.
const (
OpenFolder = "folder" // reveal the certificate PDF in Explorer/Finder
OpenPDF = "pdf" // open the certificate PDF in the default viewer
OpenNone = "none"
)
// Settings is the on-disk JSON shape.
type Settings struct {
OpenOnComplete string `json:"openOnComplete,omitempty"`
// LogLevel "debug" enables verbose helper logs (same as VERAE_LOG_LEVEL=debug).
LogLevel string `json:"logLevel,omitempty"`
}
// Default settings.
var Default = Settings{OpenOnComplete: OpenFolder}
// NormalizeOpen maps user input to a valid value ("" if invalid).
func NormalizeOpen(v string) string {
switch strings.ToLower(strings.TrimSpace(v)) {
case "folder", "reveal", "finder", "explorer", "show":
return OpenFolder
case "pdf", "open", "file":
return OpenPDF
case "none", "off", "no", "false", "0":
return OpenNone
}
return ""
}
// UserPath is ~/.verae/config.json for the given home ("" = current user).
func UserPath(home string) string {
if home == "" {
home, _ = os.UserHomeDir()
}
return filepath.Join(home, ".verae", "config.json")
}
// MachinePath is the all-users default file.
func MachinePath() string {
if runtime.GOOS == "windows" {
pd := os.Getenv("ProgramData")
if pd == "" {
pd = `C:\ProgramData`
}
return filepath.Join(pd, "Verae", "PrintStamp", "config.json")
}
return "/Library/Application Support/Verae/PrintStamp/config.json"
}
func readFile(p string) (Settings, bool) {
var s Settings
b, err := os.ReadFile(p)
if err != nil {
return s, false
}
b = []byte(strings.TrimPrefix(string(b), "\uFEFF"))
if json.Unmarshal(b, &s) != nil {
return Settings{}, false
}
return s, true
}
// Load resolves the effective settings and a short description of where each came from.
func Load() (Settings, string) {
eff := Default
src := "default"
if s, ok := readFile(MachinePath()); ok {
if v := NormalizeOpen(s.OpenOnComplete); v != "" {
eff.OpenOnComplete, src = v, MachinePath()
}
}
if s, ok := readFile(UserPath("")); ok {
if v := NormalizeOpen(s.OpenOnComplete); v != "" {
eff.OpenOnComplete, src = v, UserPath("")
}
}
for _, p := range []string{MachinePath(), UserPath("")} {
if s, ok := readFile(p); ok && s.LogLevel != "" {
eff.LogLevel = strings.ToLower(strings.TrimSpace(s.LogLevel))
}
}
if v := NormalizeOpen(os.Getenv("VERAE_OPEN_ON_COMPLETE")); v != "" {
eff.OpenOnComplete, src = v, "env VERAE_OPEN_ON_COMPLETE"
}
return eff, src
}
// SetOpenOnComplete updates (merges) the JSON file at path, preserving unknown keys.
func SetOpenOnComplete(path, value string) error {
v := NormalizeOpen(value)
if v == "" {
return fmt.Errorf("openOnComplete must be folder, pdf or none (got %q)", value)
}
m := map[string]any{}
if b, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal([]byte(strings.TrimPrefix(string(b), "\uFEFF")), &m)
}
m["openOnComplete"] = v
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return err
}
b, _ := json.MarshalIndent(m, "", " ")
tmp := path + ".tmp"
if err := os.WriteFile(tmp, append(b, '\n'), 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}

View file

@ -0,0 +1,48 @@
package settings
import (
"os"
"path/filepath"
"testing"
)
func TestLoadOrderAndMerge(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("USERPROFILE", home)
t.Setenv("VERAE_OPEN_ON_COMPLETE", "")
if s, src := Load(); s.OpenOnComplete != OpenFolder || (src != "default" && src != MachinePath()) {
t.Fatalf("default: %v %s", s, src)
}
p := UserPath("")
_ = os.MkdirAll(filepath.Dir(p), 0o700)
_ = os.WriteFile(p, []byte(`{"other":1}`), 0o600)
if err := SetOpenOnComplete(p, "PDF"); err != nil {
t.Fatal(err)
}
b, _ := os.ReadFile(p)
if s, _ := Load(); s.OpenOnComplete != OpenPDF {
t.Fatalf("user: %s", b)
}
if string(b) == "" || !contains(string(b), `"other": 1`) {
t.Fatalf("unknown key lost: %s", b)
}
t.Setenv("VERAE_OPEN_ON_COMPLETE", "none")
if s, src := Load(); s.OpenOnComplete != OpenNone || src != "env VERAE_OPEN_ON_COMPLETE" {
t.Fatal(s, src)
}
if SetOpenOnComplete(p, "bogus") == nil {
t.Fatal("want error")
}
}
func contains(s, sub string) bool { return indexOf(s, sub) >= 0 }
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -105,7 +106,7 @@ func (c *Client) stampVerae(digest string) (Receipt, error) {
_ = json.Unmarshal(raw, &parsed)
if res.StatusCode >= 300 {
return Receipt{}, fmt.Errorf("verae stamp HTTP %d: %s", res.StatusCode, truncate(raw, 400))
return Receipt{}, &HTTPError{Service: "verae stamp", Status: res.StatusCode, Body: truncate(raw, 400)}
}
cid, created, merkle := extractVeraeReceipt(parsed)
@ -311,7 +312,7 @@ func (c *Client) stampZapier(digest, documentName string) (Receipt, error) {
return Receipt{}, err
}
if res.StatusCode >= 300 {
return Receipt{}, fmt.Errorf("zapier stamp HTTP %d: %s", res.StatusCode, truncate(raw, 400))
return Receipt{}, &HTTPError{Service: "zapier stamp", Status: res.StatusCode, Body: truncate(raw, 400)}
}
parsed := map[string]any{}
_ = json.Unmarshal(raw, &parsed)
@ -377,3 +378,18 @@ func truncate(b []byte, n int) string {
func decodeBase64(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}
// HTTPError is a non-2xx answer from a stamp endpoint.
type HTTPError struct {
Service string
Status int
Body string
}
func (e *HTTPError) Error() string { return fmt.Sprintf("%s HTTP %d: %s", e.Service, e.Status, e.Body) }
// IsAuthError reports whether err is an HTTP 401/403 (invalid, revoked or expired key).
func IsAuthError(err error) bool {
var he *HTTPError
return errors.As(err, &he) && (he.Status == 401 || he.Status == 403)
}

View file

@ -1,4 +1,4 @@
// Package vlog is the verbose debug logger used by the CLI, the print helper
// Package vlog is the logger used by the CLI, the print helper
// and the IPP/watch servers. On Windows logs go to %ProgramData%\Verae\logs
// (shared, ACL'd by the installer so Users can write); elsewhere to the user
// cache dir. Every print job carries a job ID so one job can be followed from
@ -24,9 +24,15 @@ var (
file *os.File
comp = "verae"
jobSeq uint64
debugOn = os.Getenv("VERAE_DEBUG") == "1"
debugOn = envDebug()
)
// envDebug: DEBUG level is off by default; VERAE_LOG_LEVEL=debug (or the
// legacy VERAE_DEBUG=1) turns it on. INFO/ERROR are always written.
func envDebug() bool {
return strings.EqualFold(strings.TrimSpace(os.Getenv("VERAE_LOG_LEVEL")), "debug") || os.Getenv("VERAE_DEBUG") == "1"
}
// Dir returns the log directory for this platform.
func Dir() string {
if runtime.GOOS == "windows" {

View file

@ -90,7 +90,7 @@ func (w *Watcher) claim(p string, size int64) {
}
}
lj.Printf("watch: claimed %s (%d bytes, magic=%q) → %s", p, size, sniff(dst), dst)
j := Job{ID: id, Source: "watch", Title: "Printed " + time.Now().Format("2006-01-02 1504"), Format: "application/pdf", Path: dst, Bytes: size, Received: time.Now()}
j := Job{ID: id, Source: "watch", Title: "Printed " + time.Now().Format("2006-01-02 150405"), Format: "application/pdf", Path: dst, Bytes: size, Received: time.Now()}
start := time.Now()
err := func() (err error) {
defer func() {