wip: registration PDF, auth setup, CLI, Windows/Linux installers
This commit is contained in:
parent
c075478783
commit
4f1d90cf64
26 changed files with 1257 additions and 42 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,3 +6,4 @@
|
|||
*.msi
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/build/
|
||||
|
|
|
|||
56
README.md
56
README.md
|
|
@ -1,20 +1,53 @@
|
|||
# PrintToVeraeTimeStamp
|
||||
|
||||
macOS Print → PDF helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`.
|
||||
Print / CLI helper that saves `{name}.pdf`, stamps the digest with Verae, and writes `{name}-verae-timestamp-{RECEIPTID}.pdf`.
|
||||
|
||||
Only the SHA-256 digest is sent to Verae. Document bytes never leave the machine.
|
||||
|
||||
## Install
|
||||
|
||||
Downloads: [printit.verae.ai](https://printit.verae.ai/) (macOS `.pkg`, Windows `.zip`, Linux `.tar.gz`).
|
||||
|
||||
**macOS (recommended):** open `VeraePrintStamp-macos.pkg`. First launch opens **customer.verae.ai** for an **xVerae-** signing key.
|
||||
|
||||
**Windows (GUI installer):**
|
||||
|
||||
```text
|
||||
Download VeraePrintStamp-windows-amd64-setup.exe from printit.verae.ai
|
||||
Run the installer → Finish with “Run setup” → paste your xVerae- key
|
||||
verae-print stamp .\document.pdf
|
||||
```
|
||||
|
||||
**Debian / Ubuntu:**
|
||||
|
||||
```bash
|
||||
sudo apt install ./verae-print_amd64.deb
|
||||
verae-print setup
|
||||
verae-print stamp document.pdf
|
||||
```
|
||||
|
||||
**Linux (tarball, any distro):**
|
||||
|
||||
```bash
|
||||
tar xzf VeraePrintStamp-linux-amd64.tar.gz
|
||||
cd VeraePrintStamp-linux-amd64-*
|
||||
sudo ./install.sh # CLI + optional CUPS queue
|
||||
verae-print setup
|
||||
verae-print stamp document.pdf
|
||||
```
|
||||
|
||||
From source (macOS):
|
||||
|
||||
```bash
|
||||
cd /Users/marchon/research/PrintToVeraeTimeStamp
|
||||
go build -o "$HOME/bin/verae-print" ./cmd/verae-print
|
||||
verae-print setup # create account + save xVerae- signing key
|
||||
verae-print install-printer # Print → Destination → Verae Timestamping
|
||||
```
|
||||
|
||||
Optional (buried PDF submenu): `verae-print install-macos`
|
||||
|
||||
Ensure `$HOME/bin` is on your `PATH`. Refresh Verae Time login when the JWT expires:
|
||||
Ensure `$HOME/bin` is on your `PATH`. For the legacy Verae Time JWT channel:
|
||||
|
||||
```bash
|
||||
uvx verae-mcp-timestamping-server setup
|
||||
|
|
@ -53,8 +86,8 @@ Default output directory: `~/Documents/Verae Timestamps/`
|
|||
|
||||
Channels:
|
||||
|
||||
- `verae` (default) — `api.veraetime.net` + JWT in `~/.verae/auth.properties`
|
||||
- `zapier` — `zap.verae.ai` + `xVerae-` + `attachPdf:true`
|
||||
- `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)
|
||||
|
||||
|
|
@ -64,7 +97,7 @@ Same FlightLookup Developer ID + notarization path as Verae Timestamping.app (`~
|
|||
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.0.pkg
|
||||
# → build/macos-pkg/dist/VeraePrintStamp-0.1.2.pkg
|
||||
# → build/macos-pkg/dist/VeraePrintStamp-macos.pkg
|
||||
```
|
||||
|
||||
|
|
@ -79,9 +112,20 @@ The pkg installs:
|
|||
|
||||
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 + branded registration PDF work locally. Signed `.pkg` build script is under `platform/macos/pkg/`. Linux and Windows remain per the [spec](docs/superpowers/specs/2026-10-03-print-to-verae-timestamp.md) and [plan](docs/superpowers/plans/2026-10-03-print-to-verae-timestamp.md).
|
||||
macOS Destination printer (notarized `.pkg`), Windows/Linux CLI packages, and branded registration PDF are published at [printit.verae.ai](https://printit.verae.ai/).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ func main() {
|
|||
switch os.Args[1] {
|
||||
case "stamp":
|
||||
os.Exit(runStamp(os.Args[2:]))
|
||||
case "setup", "login":
|
||||
os.Exit(runSetup())
|
||||
case "install-macos":
|
||||
os.Exit(runInstallMacOS(os.Args[2:]))
|
||||
case "install-printer":
|
||||
|
|
@ -30,9 +32,9 @@ func main() {
|
|||
case "help", "-h", "--help":
|
||||
usage()
|
||||
default:
|
||||
// PDF Services may invoke the binary with the PDF path as argv[1]
|
||||
if strings.HasSuffix(strings.ToLower(os.Args[1]), ".pdf") && fileExists(os.Args[1]) {
|
||||
os.Exit(runStamp([]string{"-channel", envOr("VERAE_PRINT_CHANNEL", "verae"), os.Args[1]}))
|
||||
// PDF Services / drop-target may invoke the binary with the file as argv[1]
|
||||
if fileExists(os.Args[1]) {
|
||||
os.Exit(runStamp([]string{"-channel", string(auth.PreferredChannel()), os.Args[1]}))
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n", os.Args[1])
|
||||
usage()
|
||||
|
|
@ -44,14 +46,18 @@ func usage() {
|
|||
fmt.Fprintf(os.Stderr, `verae-print %s — stamp a printed PDF with Verae (digests only)
|
||||
|
||||
Usage:
|
||||
verae-print setup # first run: create customer account + save xVerae- signing key
|
||||
verae-print stamp [--channel=verae|zapier] [--out-dir DIR] [--name STEM] [--open] <file.pdf>
|
||||
verae-print install-printer # Destination list: Verae Timestamping (recommended)
|
||||
verae-print install-macos # also install PDF Services menu items (legacy)
|
||||
verae-print version
|
||||
|
||||
First install opens https://customer.verae.ai/portal/ so you can create an account
|
||||
and mint an xVerae- signing key (Security tab). The key is stored in Keychain.
|
||||
|
||||
Channels:
|
||||
verae (default) api.veraetime.net + ~/.verae/auth.properties JWT
|
||||
zapier zap.verae.ai + xVerae- token (XVERAE_TOKEN / ~/.verae/xverae-token / Keychain)
|
||||
zapier (default) zap.verae.ai + customer xVerae- signing key
|
||||
verae api.veraetime.net + ~/.verae/auth.properties JWT
|
||||
|
||||
Outputs in --out-dir (default ~/Documents/Verae Timestamps):
|
||||
{name}.pdf
|
||||
|
|
@ -60,12 +66,27 @@ Outputs in --out-dir (default ~/Documents/Verae Timestamps):
|
|||
`, job.HelperVersion)
|
||||
}
|
||||
|
||||
func runSetup() int {
|
||||
if auth.HasXVeraeToken() {
|
||||
fmt.Println("Signing key already saved (xVerae-).")
|
||||
fmt.Println("Portal:", auth.PortalSignupURL)
|
||||
return 0
|
||||
}
|
||||
if err := auth.RunFirstRunSetup(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
return 1
|
||||
}
|
||||
fmt.Println("Signing key saved. Print → Destination → Verae Timestamping is ready.")
|
||||
return 0
|
||||
}
|
||||
|
||||
func runStamp(args []string) int {
|
||||
fs := flag.NewFlagSet("stamp", flag.ExitOnError)
|
||||
channel := fs.String("channel", envOr("VERAE_PRINT_CHANNEL", "verae"), "verae | zapier")
|
||||
channel := fs.String("channel", string(auth.PreferredChannel()), "verae | zapier")
|
||||
outDir := fs.String("out-dir", "", "output directory")
|
||||
name := fs.String("name", "", "document stem override")
|
||||
openFolder := fs.Bool("open", false, "open output folder on success (macOS)")
|
||||
skipSetup := fs.Bool("skip-setup", false, "do not open first-run account dialog")
|
||||
_ = fs.Parse(args)
|
||||
if fs.NArg() < 1 {
|
||||
fmt.Fprintln(os.Stderr, "stamp: PDF path required")
|
||||
|
|
@ -79,6 +100,19 @@ func runStamp(args []string) int {
|
|||
return 2
|
||||
}
|
||||
|
||||
// First-run: customer account + xVerae- signing key (preferred path)
|
||||
if ch == auth.ChannelZapier && !auth.HasXVeraeToken() {
|
||||
if *skipSetup {
|
||||
fmt.Fprintf(os.Stderr, "no xVerae- signing key — run: verae-print setup\nPortal: %s\n", auth.PortalSignupURL)
|
||||
return 1
|
||||
}
|
||||
if err := auth.EnsureReady(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
notify("Create a Verae customer account to get your signing key", auth.PortalSignupURL)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
client := &veraeapi.Client{Channel: ch}
|
||||
switch ch {
|
||||
case auth.ChannelZapier:
|
||||
|
|
@ -91,7 +125,7 @@ func runStamp(args []string) int {
|
|||
default:
|
||||
tok, err := auth.LoadVeraeJWT()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintf(os.Stderr, "%v\nTip: run verae-print setup to create a customer account and xVerae- key instead.\n", err)
|
||||
return 1
|
||||
}
|
||||
client.VeraeToken = tok
|
||||
|
|
|
|||
|
|
@ -92,3 +92,72 @@ func LoadXVeraeToken() (string, error) {
|
|||
}
|
||||
return "", fmt.Errorf("no xVerae- token (set XVERAE_TOKEN, ~/.verae/xverae-token, or Keychain verae.xVeraeToken)")
|
||||
}
|
||||
|
||||
// HasXVeraeToken reports whether a customer signing key is already stored.
|
||||
func HasXVeraeToken() bool {
|
||||
_, err := LoadXVeraeToken()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// HasVeraeJWT reports whether a Verae Time JWT is available.
|
||||
func HasVeraeJWT() bool {
|
||||
_, err := LoadVeraeJWT()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// PreferredChannel picks the print product path: customer xVerae- on zap.verae.ai.
|
||||
// Override with VERAE_PRINT_CHANNEL=verae for the legacy JWT channel.
|
||||
func PreferredChannel() Channel {
|
||||
if env := strings.ToLower(strings.TrimSpace(os.Getenv("VERAE_PRINT_CHANNEL"))); env != "" {
|
||||
if c, err := ParseChannel(env); err == nil {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return ChannelZapier
|
||||
}
|
||||
|
||||
const (
|
||||
KeychainService = "verae.xVeraeToken"
|
||||
PortalSignupURL = "https://customer.verae.ai/portal/"
|
||||
)
|
||||
|
||||
// SaveXVeraeToken stores the customer signing key in ~/.verae/xverae-token (0600)
|
||||
// and macOS Keychain service verae.xVeraeToken.
|
||||
func SaveXVeraeToken(token string) error {
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return fmt.Errorf("empty token")
|
||||
}
|
||||
if !strings.HasPrefix(token, "xVerae-") && !strings.HasPrefix(token, "key-") {
|
||||
return fmt.Errorf("token should start with xVerae- (or legacy key-)")
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Join(home, ".verae")
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(dir, "xverae-token")
|
||||
if err := os.WriteFile(path, []byte(token+"\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
// Keychain (best-effort): delete old, add new
|
||||
account := os.Getenv("USER")
|
||||
if account == "" {
|
||||
account = "verae"
|
||||
}
|
||||
_ = exec.Command("security", "delete-generic-password", "-s", KeychainService, "-a", account).Run()
|
||||
if err := exec.Command("security", "add-generic-password",
|
||||
"-a", account,
|
||||
"-s", KeychainService,
|
||||
"-w", token,
|
||||
"-T", "", // allow all apps that know the service; print helper needs it
|
||||
"-U",
|
||||
).Run(); err != nil {
|
||||
// Non-fatal if file write succeeded
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,18 @@ func TestParseChannel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPreferredChannelDefaultsToZapier(t *testing.T) {
|
||||
t.Setenv("VERAE_PRINT_CHANNEL", "")
|
||||
t.Setenv("XVERAE_TOKEN", "")
|
||||
if c := PreferredChannel(); c != ChannelZapier {
|
||||
t.Fatalf("got %v want zapier (customer signing key path)", c)
|
||||
}
|
||||
t.Setenv("VERAE_PRINT_CHANNEL", "verae")
|
||||
if c := PreferredChannel(); c != ChannelVerae {
|
||||
t.Fatalf("got %v want verae from env", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVeraeJWT(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
|
|
|
|||
106
internal/auth/setup_darwin.go
Normal file
106
internal/auth/setup_darwin.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
//go:build darwin
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RunFirstRunSetup guides the user through customer.verae.ai signup and
|
||||
// xVerae- signing-key paste. Interactive (osascript dialogs).
|
||||
func RunFirstRunSetup() error {
|
||||
if HasXVeraeToken() {
|
||||
return nil
|
||||
}
|
||||
|
||||
btn, err := osaButton(
|
||||
"Verae Print Stamp needs a customer signing key before it can timestamp documents.\n\nCreate a free account on customer.verae.ai, open Security, and create an xVerae- token.",
|
||||
"Verae customer account",
|
||||
[]string{"Create account…", "I already have a key", "Cancel"},
|
||||
"Create account…",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch btn {
|
||||
case "Cancel":
|
||||
return fmt.Errorf("setup cancelled")
|
||||
case "Create account…":
|
||||
_ = exec.Command("open", PortalSignupURL).Start()
|
||||
btn2, err2 := osaButton(
|
||||
"In the browser:\n1. Create your account (or sign in)\n2. Open the Security tab\n3. Create an xVerae- token\n4. Copy it once — it is shown only once\n\nClick Continue when you have copied the key.",
|
||||
"Create your signing key",
|
||||
[]string{"Continue", "Cancel"},
|
||||
"Continue",
|
||||
)
|
||||
if err2 != nil || btn2 == "Cancel" {
|
||||
return fmt.Errorf("setup cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
token, err := osaPrompt(
|
||||
"Paste your xVerae- signing key from customer.verae.ai → Security.\n\nIt starts with xVerae- and is shown only once when created.",
|
||||
"Paste signing key",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token = strings.TrimSpace(token)
|
||||
if token == "" {
|
||||
return fmt.Errorf("no key pasted")
|
||||
}
|
||||
if err := SaveXVeraeToken(token); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = osaButton(
|
||||
"Signing key saved.\n\nYou can Print → Destination → Verae Timestamping from any app.\nKeys are stored in Keychain (verae.xVeraeToken) and ~/.verae/xverae-token.",
|
||||
"Verae Print Stamp",
|
||||
[]string{"OK"},
|
||||
"OK",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func osaButton(message, title string, buttons []string, defaultBtn string) (string, error) {
|
||||
quoted := make([]string, len(buttons))
|
||||
for i, b := range buttons {
|
||||
quoted[i] = fmt.Sprintf("%q", b)
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
`button returned of (display dialog %q with title %q buttons {%s} default button %q)`,
|
||||
message, title, strings.Join(quoted, ", "), defaultBtn,
|
||||
)
|
||||
out, err := exec.Command("osascript", "-e", script).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dialog cancelled")
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
func osaPrompt(message, title, defaultAnswer string) (string, error) {
|
||||
script := fmt.Sprintf(
|
||||
`text returned of (display dialog %q with title %q default answer %q with hidden answer buttons {"Cancel", "Save key"} default button "Save key")`,
|
||||
message, title, defaultAnswer,
|
||||
)
|
||||
out, err := exec.Command("osascript", "-e", script).Output()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("prompt cancelled")
|
||||
}
|
||||
return strings.TrimSpace(string(out)), nil
|
||||
}
|
||||
|
||||
// EnsureReady runs first-run setup when no signing key is present.
|
||||
func EnsureReady() error {
|
||||
if HasXVeraeToken() {
|
||||
return nil
|
||||
}
|
||||
// Headless (no GUI session): fail with instructions
|
||||
if os.Getenv("SSH_CONNECTION") != "" && os.Getenv("DISPLAY") == "" {
|
||||
return fmt.Errorf("no xVerae- signing key — run: verae-print setup\nOr open %s , create an account, mint a key under Security, then:\n verae-print setup", PortalSignupURL)
|
||||
}
|
||||
return RunFirstRunSetup()
|
||||
}
|
||||
79
internal/auth/setup_other.go
Normal file
79
internal/auth/setup_other.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//go:build !darwin
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RunFirstRunSetup opens the customer portal and prompts for an xVerae- signing key on stdin.
|
||||
func RunFirstRunSetup() error {
|
||||
fmt.Println("Verae Print Stamp — first-run setup")
|
||||
fmt.Println()
|
||||
fmt.Println("1. Create a free account (or sign in):")
|
||||
fmt.Println(" ", PortalSignupURL)
|
||||
fmt.Println("2. Open Security → create an xVerae- token")
|
||||
fmt.Println("3. Paste the token below (starts with xVerae-)")
|
||||
fmt.Println()
|
||||
_ = openBrowser(PortalSignupURL)
|
||||
|
||||
fmt.Print("Paste xVerae- signing key: ")
|
||||
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("read token: %w", err)
|
||||
}
|
||||
token := strings.TrimSpace(line)
|
||||
if token == "" {
|
||||
return fmt.Errorf("empty token — create one at %s then re-run: verae-print setup", PortalSignupURL)
|
||||
}
|
||||
if err := SaveXVeraeToken(token); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Signing key saved to ~/.verae/xverae-token")
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureReady requires an xVerae- token on non-macOS.
|
||||
func EnsureReady() error {
|
||||
if HasXVeraeToken() {
|
||||
return nil
|
||||
}
|
||||
if !isInteractive() {
|
||||
return fmt.Errorf("no xVerae- signing key — run: verae-print setup\nPortal: %s", PortalSignupURL)
|
||||
}
|
||||
return RunFirstRunSetup()
|
||||
}
|
||||
|
||||
func isInteractive() bool {
|
||||
fi, err := os.Stdin.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
|
||||
func openBrowser(url string) error {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
cmd = exec.Command("cmd", "/c", "start", "", url)
|
||||
case "linux":
|
||||
for _, bin := range []string{"xdg-open", "gio", "sensible-browser"} {
|
||||
if _, err := exec.LookPath(bin); err == nil {
|
||||
cmd = exec.Command(bin, url)
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported OS")
|
||||
}
|
||||
if cmd == nil {
|
||||
return fmt.Errorf("no browser opener found")
|
||||
}
|
||||
return cmd.Start()
|
||||
}
|
||||
28
internal/auth/token_save_test.go
Normal file
28
internal/auth/token_save_test.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSaveXVeraeToken(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XVERAE_TOKEN", "") // force file path
|
||||
if err := SaveXVeraeToken("xVerae-testtoken123"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := filepath.Join(home, ".verae", "xverae-token")
|
||||
b, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(b) != "xVerae-testtoken123\n" {
|
||||
t.Fatalf("%q", b)
|
||||
}
|
||||
st, _ := os.Stat(p)
|
||||
if st.Mode().Perm()&0o077 != 0 {
|
||||
t.Fatalf("perms %v", st.Mode())
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import (
|
|||
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/veraeapi"
|
||||
)
|
||||
|
||||
const HelperVersion = "0.1.0"
|
||||
const HelperVersion = "0.1.2"
|
||||
|
||||
// Result paths written for one print job.
|
||||
type Result struct {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/phpdave11/gofpdf"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
|
|
@ -69,6 +70,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
}, fields[1:]...)
|
||||
}
|
||||
|
||||
isHTTP := func(s string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "http://") ||
|
||||
strings.HasPrefix(strings.ToLower(strings.TrimSpace(s)), "https://")
|
||||
}
|
||||
|
||||
y := 168.0
|
||||
for _, fv := range fields {
|
||||
pdf.SetFont("Helvetica", "B", 10)
|
||||
|
|
@ -77,10 +83,20 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
pdf.Cell(pageW-margin*2, 12, fv[0])
|
||||
y += 14
|
||||
pdf.SetFont("Helvetica", "", 10)
|
||||
pdf.SetTextColor(40, 40, 50)
|
||||
linkURL := ""
|
||||
if isHTTP(fv[1]) {
|
||||
linkURL = strings.TrimSpace(fv[1])
|
||||
pdf.SetTextColor(74, 46, 140)
|
||||
} else {
|
||||
pdf.SetTextColor(40, 40, 50)
|
||||
}
|
||||
for _, ln := range pdf.SplitLines([]byte(fv[1]), pageW-margin*2-10) {
|
||||
pdf.SetXY(margin, y)
|
||||
pdf.Cell(pageW-margin*2-10, 12, string(ln))
|
||||
if linkURL != "" {
|
||||
pdf.CellFormat(pageW-margin*2-10, 12, string(ln), "", 0, "L", false, 0, linkURL)
|
||||
} else {
|
||||
pdf.Cell(pageW-margin*2-10, 12, string(ln))
|
||||
}
|
||||
y += 12
|
||||
}
|
||||
y += 10
|
||||
|
|
@ -95,10 +111,11 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
qrX := pageW - margin - qrSize
|
||||
qrY := pageH - margin - qrSize - 36
|
||||
pdf.ImageOptions(qrFile, qrX, qrY, qrSize, qrSize, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
|
||||
pdf.LinkString(qrX, qrY, qrSize, qrSize, lookup)
|
||||
pdf.SetFont("Helvetica", "", 8)
|
||||
pdf.SetTextColor(90, 80, 100)
|
||||
pdf.SetTextColor(74, 46, 140)
|
||||
pdf.SetXY(qrX, qrY+qrSize+4)
|
||||
pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, "")
|
||||
pdf.CellFormat(qrSize, 10, "Scan to look up this registration", "", 0, "C", false, 0, lookup)
|
||||
if qclean != nil {
|
||||
defer qclean()
|
||||
}
|
||||
|
|
@ -112,7 +129,13 @@ func buildFallbackGofpdf(in Input, docName, created, scope, merkle, lookup strin
|
|||
pdf.SetFont("Helvetica", "B", 9)
|
||||
pdf.SetTextColor(42, 24, 56)
|
||||
pdf.SetXY(margin, pageH-36)
|
||||
pdf.Cell(pageW-margin*2, 12, "Verae | verae.com | explorer.verae.ai")
|
||||
pdf.Cell(40, 12, "Verae | ")
|
||||
pdf.SetTextColor(74, 46, 140)
|
||||
pdf.CellFormat(55, 12, "verae.com", "", 0, "L", false, 0, "https://verae.com")
|
||||
pdf.SetTextColor(42, 24, 56)
|
||||
pdf.Cell(12, 12, " | ")
|
||||
pdf.SetTextColor(74, 46, 140)
|
||||
pdf.CellFormat(90, 12, "explorer.verae.ai", "", 0, "L", false, 0, "https://explorer.verae.ai/")
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pdf.Output(&buf); err != nil {
|
||||
|
|
|
|||
|
|
@ -28,13 +28,21 @@ type Input struct {
|
|||
ByteLength int64
|
||||
}
|
||||
|
||||
// LookupURL builds a public lookup link without mcp.verae.ai.
|
||||
// LookupURL builds a public VeraeTime verify deep link (same as explorer
|
||||
// scan-bar → /time, which auto-fills receipt/sha256 and runs Lookup).
|
||||
// https://explorer.verae.ai/time?receipt=…&sha256=…
|
||||
func LookupURL(sha256, receiptID string) string {
|
||||
sha256 = strings.TrimSpace(sha256)
|
||||
sha256 = strings.TrimPrefix(strings.ToLower(sha256), "0x")
|
||||
receiptID = strings.TrimSpace(receiptID)
|
||||
u := "https://explorer.verae.ai/?sha256=" + sha256
|
||||
u := "https://explorer.verae.ai/time"
|
||||
sep := "?"
|
||||
if receiptID != "" {
|
||||
u += "&receipt=" + receiptID
|
||||
u += sep + "receipt=" + receiptID
|
||||
sep = "&"
|
||||
}
|
||||
if sha256 != "" {
|
||||
u += sep + "sha256=" + sha256
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
|
@ -113,6 +121,7 @@ func buildWithNode(row map[string]any) ([]byte, error) {
|
|||
|
||||
func findBuildScript() string {
|
||||
candidates := []string{
|
||||
"/Library/Application Support/Verae/PrintStamp/registration-pdf/build.mjs",
|
||||
"/Users/marchon/research/PrintToVeraeTimeStamp/platform/registration-pdf/build.mjs",
|
||||
filepath.Join("platform", "registration-pdf", "build.mjs"),
|
||||
}
|
||||
|
|
@ -120,6 +129,8 @@ func findBuildScript() string {
|
|||
if exe, err := os.Executable(); err == nil {
|
||||
dir := filepath.Dir(exe)
|
||||
candidates = append(candidates,
|
||||
filepath.Join(dir, "registration-pdf", "build.mjs"),
|
||||
filepath.Join(dir, "..", "registration-pdf", "build.mjs"),
|
||||
filepath.Join(dir, "platform", "registration-pdf", "build.mjs"),
|
||||
filepath.Join(dir, "..", "platform", "registration-pdf", "build.mjs"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -41,14 +41,30 @@ func TestBuildBrandedRegistration(t *testing.T) {
|
|||
if !strings.Contains(s, "explorer.verae.ai") {
|
||||
t.Fatal("expected explorer lookup")
|
||||
}
|
||||
// Clickable link annotations (jsPDF textWithLink / link, or gofpdf LinkString)
|
||||
if !strings.Contains(s, "/URI") && !strings.Contains(s, "/URI(") {
|
||||
if !strings.Contains(s, "URI") || !strings.Contains(s, "explorer.verae.ai/time") {
|
||||
t.Fatal("expected clickable URI annotation for public lookup URL")
|
||||
}
|
||||
}
|
||||
if !strings.Contains(s, "/time?") || !strings.Contains(s, "QmTestReceipt123") {
|
||||
t.Fatal("expected /time? deep link with receipt in PDF")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupURLNoMCP(t *testing.T) {
|
||||
u := LookupURL("abc", "rid")
|
||||
u := LookupURL("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "QmTestReceipt123")
|
||||
if strings.Contains(u, "mcp.verae.ai") {
|
||||
t.Fatal(u)
|
||||
}
|
||||
if !strings.Contains(u, "explorer.verae.ai") {
|
||||
if !strings.HasPrefix(u, "https://explorer.verae.ai/time?") {
|
||||
t.Fatalf("want /time? deep link, got %s", u)
|
||||
}
|
||||
if !strings.Contains(u, "receipt=QmTestReceipt123") || !strings.Contains(u, "sha256=0123456789abcdef") {
|
||||
t.Fatal(u)
|
||||
}
|
||||
// Must not use the old root query form that the scan bar does not honor
|
||||
if strings.Contains(u, "explorer.verae.ai/?sha256=") {
|
||||
t.Fatalf("old root URL form: %s", u)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
39
platform/linux/README.txt
Normal file
39
platform/linux/README.txt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
Verae Print Stamp — Linux (CLI + optional CUPS)
|
||||
===============================================
|
||||
|
||||
Digests-only timestamping. Document bytes stay on your machine.
|
||||
|
||||
Install
|
||||
-------
|
||||
tar xzf VeraePrintStamp-linux-*.tar.gz
|
||||
cd VeraePrintStamp-linux-*
|
||||
sudo ./install.sh # CLI + CUPS queue when cups is present
|
||||
# or: PREFIX=$HOME/.local ./install.sh # user install; WITH_CUPS=0
|
||||
|
||||
Setup
|
||||
-----
|
||||
verae-print setup
|
||||
# opens https://customer.verae.ai/portal/ — mint xVerae- → paste
|
||||
|
||||
Stamp
|
||||
-----
|
||||
verae-print stamp ./Contract.pdf
|
||||
|
||||
Outputs (default)
|
||||
-----------------
|
||||
~/Documents/Verae Timestamps/
|
||||
Contract.pdf
|
||||
Contract-verae-timestamp-{RECEIPTID}.pdf
|
||||
Contract.verae.json
|
||||
|
||||
CUPS destination
|
||||
----------------
|
||||
install.sh installs backend `verae` and queue Verae_Timestamping when
|
||||
`lpadmin` is available. Use your desktop’s print dialog → Verae Timestamping.
|
||||
If the queue is missing, stamp via the CLI above.
|
||||
|
||||
Links
|
||||
-----
|
||||
Downloads: https://printit.verae.ai/
|
||||
Portal: https://customer.verae.ai/portal/
|
||||
Explorer: https://explorer.verae.ai/time
|
||||
76
platform/linux/cups/backend-verae.sh
Executable file
76
platform/linux/cups/backend-verae.sh
Executable file
|
|
@ -0,0 +1,76 @@
|
|||
#!/bin/bash
|
||||
# CUPS backend: verae — virtual printer "Verae Timestamping" (Linux)
|
||||
# Discovery (no args): advertise device URI verae:/
|
||||
# Job: spool PDF and run verae-print as the job user (digests only).
|
||||
set -euo pipefail
|
||||
|
||||
LOG_ROOT="${CUPS_SERVERROOT:-/var/log/cups}"
|
||||
BACKEND_LOG="${LOG_ROOT}/verae-backend.log"
|
||||
|
||||
log() {
|
||||
echo "$(date -u +%Y-%m-%dT%H:%MZ) $*" >>"$BACKEND_LOG" 2>/dev/null || true
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
echo 'direct verae:/ "Verae Timestamping" "Verae Timestamping — digests-only stamp"'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
JOB_ID="${1:-}"
|
||||
JOB_USER="${2:-}"
|
||||
JOB_TITLE="${3:-document}"
|
||||
JOB_FILE="${6:-}"
|
||||
|
||||
VERAE_PRINT="${VERAE_PRINT_BIN:-/usr/local/bin/verae-print}"
|
||||
CHANNEL="${VERAE_PRINT_CHANNEL:-zapier}"
|
||||
|
||||
if [[ -n "$JOB_USER" && -d "/home/$JOB_USER" ]]; then
|
||||
SPOOL_BASE="/home/$JOB_USER/.local/share/verae/print-spool"
|
||||
OUT_DIR="/home/$JOB_USER/Documents/Verae Timestamps"
|
||||
else
|
||||
SPOOL_BASE="/var/spool/verae/${JOB_USER:-unknown}"
|
||||
OUT_DIR="/tmp/VeraeTimestamps"
|
||||
fi
|
||||
|
||||
mkdir -p "$SPOOL_BASE"
|
||||
chmod 700 "$SPOOL_BASE" 2>/dev/null || true
|
||||
|
||||
SAFE_TITLE=$(echo "$JOB_TITLE" | tr -cd 'A-Za-z0-9._ -' | head -c 80)
|
||||
SAFE_TITLE=${SAFE_TITLE:-document}
|
||||
WORK="$SPOOL_BASE/job-${JOB_ID}-$$"
|
||||
mkdir -p "$WORK"
|
||||
|
||||
INPUT="$WORK/input.dat"
|
||||
if [[ -n "$JOB_FILE" && -f "$JOB_FILE" ]]; then
|
||||
cp "$JOB_FILE" "$INPUT"
|
||||
else
|
||||
cat >"$INPUT"
|
||||
fi
|
||||
|
||||
PDF="$WORK/${SAFE_TITLE}.pdf"
|
||||
if [[ "$(head -c 4 "$INPUT")" == "%PDF" ]]; then
|
||||
cp "$INPUT" "$PDF"
|
||||
else
|
||||
# Best-effort: keep bytes; stamp accepts PDF/HTML
|
||||
cp "$INPUT" "$PDF"
|
||||
fi
|
||||
|
||||
log "job=$JOB_ID user=$JOB_USER title=$JOB_TITLE pdf=$PDF channel=$CHANNEL"
|
||||
|
||||
run_as_user() {
|
||||
if [[ -n "$JOB_USER" && "$JOB_USER" != "root" ]] && command -v runuser >/dev/null 2>&1; then
|
||||
runuser -u "$JOB_USER" -- "$@"
|
||||
elif [[ -n "$JOB_USER" && "$JOB_USER" != "root" ]] && command -v su >/dev/null 2>&1; then
|
||||
su - "$JOB_USER" -c "$(printf '%q ' "$@")"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "$OUT_DIR" 2>/dev/null || true
|
||||
if run_as_user "$VERAE_PRINT" stamp -channel "$CHANNEL" -out-dir "$OUT_DIR" -skip-setup "$PDF"; then
|
||||
log "job $JOB_ID ok → $OUT_DIR"
|
||||
exit 0
|
||||
fi
|
||||
log "job $JOB_ID stamp failed"
|
||||
exit 1
|
||||
210
platform/linux/deb/build-deb.sh
Executable file
210
platform/linux/deb/build-deb.sh
Executable file
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build Debian/Ubuntu .deb packages for verae-print.
|
||||
# Run on a Debian/Ubuntu host (or any system with dpkg-deb + fakeroot).
|
||||
#
|
||||
# Usage:
|
||||
# ./platform/linux/deb/build-deb.sh [amd64|arm64|all]
|
||||
# Env:
|
||||
# VERSION=0.1.2
|
||||
# BIN_AMD64=path/to/verae-print BIN_ARM64=path/to/verae-print
|
||||
# OUT=build/deb
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
VERSION="${VERSION:-0.1.2}"
|
||||
OUT="${OUT:-$ROOT/build/deb}"
|
||||
ARCHS="${1:-all}"
|
||||
PKG_NAME="verae-print"
|
||||
|
||||
mkdir -p "$OUT"
|
||||
|
||||
if ! command -v dpkg-deb >/dev/null 2>&1; then
|
||||
echo "dpkg-deb required (run on Debian/Ubuntu, e.g. host 138)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build_arch() {
|
||||
local arch="$1"
|
||||
local bin="$2"
|
||||
if [[ ! -f "$bin" ]]; then
|
||||
echo "missing binary for $arch: $bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local stage="$OUT/stage-${arch}"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage/DEBIAN" \
|
||||
"$stage/usr/bin" \
|
||||
"$stage/usr/lib/cups/backend" \
|
||||
"$stage/usr/share/doc/$PKG_NAME" \
|
||||
"$stage/usr/share/man/man1"
|
||||
|
||||
install -m 755 "$bin" "$stage/usr/bin/verae-print"
|
||||
if [[ -f "$ROOT/platform/linux/cups/backend-verae.sh" ]]; then
|
||||
install -m 755 "$ROOT/platform/linux/cups/backend-verae.sh" "$stage/usr/lib/cups/backend/verae"
|
||||
fi
|
||||
|
||||
cat >"$stage/usr/share/doc/$PKG_NAME/copyright" <<EOF
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: PrintToVeraeTimeStamp
|
||||
Source: https://printit.verae.ai/
|
||||
|
||||
Files: *
|
||||
Copyright: Verae / George Lambert
|
||||
License: UNLICENSED
|
||||
EOF
|
||||
|
||||
cat >"$stage/usr/share/doc/$PKG_NAME/README.Debian" <<EOF
|
||||
verae-print for Debian/Ubuntu
|
||||
=============================
|
||||
|
||||
After install:
|
||||
verae-print setup
|
||||
verae-print stamp document.pdf
|
||||
|
||||
Optional CUPS queue (if cups-client is installed):
|
||||
sudo lpadmin -p Verae_Timestamping -E -v verae:/ -m raw -D "Verae Timestamping" || true
|
||||
sudo cupsenable Verae_Timestamping; sudo cupsaccept Verae_Timestamping
|
||||
|
||||
Portal: https://customer.verae.ai/portal/
|
||||
Downloads: https://printit.verae.ai/
|
||||
EOF
|
||||
gzip -9n -c "$stage/usr/share/doc/$PKG_NAME/README.Debian" >"$stage/usr/share/doc/$PKG_NAME/README.Debian.gz" 2>/dev/null || true
|
||||
|
||||
# Simple man page
|
||||
cat >"$stage/usr/share/man/man1/verae-print.1" <<EOF
|
||||
.TH VERAE-PRINT 1 "$VERSION" "Verae" "User Commands"
|
||||
.SH NAME
|
||||
verae-print \\- stamp a PDF digest with Verae Time
|
||||
.SH SYNOPSIS
|
||||
.B verae-print
|
||||
setup
|
||||
.br
|
||||
.B verae-print
|
||||
stamp [options] file.pdf
|
||||
.SH DESCRIPTION
|
||||
Digests-only timestamping. Document bytes stay local.
|
||||
.SH SEE ALSO
|
||||
https://printit.verae.ai/
|
||||
EOF
|
||||
gzip -9n -f "$stage/usr/share/man/man1/verae-print.1"
|
||||
|
||||
local size
|
||||
size=$(du -sk "$stage" | awk '{print $1}')
|
||||
|
||||
cat >"$stage/DEBIAN/control" <<EOF
|
||||
Package: $PKG_NAME
|
||||
Version: $VERSION
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: $arch
|
||||
Maintainer: Verae <support@verae.ai>
|
||||
Depends: libc6
|
||||
Recommends: cups-client
|
||||
Installed-Size: $size
|
||||
Homepage: https://printit.verae.ai/
|
||||
Description: Digests-only Verae timestamping from Print / CLI
|
||||
Verae Print Stamp hashes a document locally and registers only the
|
||||
SHA-256 digest with Verae. Includes an optional CUPS backend for a
|
||||
"Verae Timestamping" print destination.
|
||||
EOF
|
||||
|
||||
cat >"$stage/DEBIAN/postinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if command -v lpadmin >/dev/null 2>&1 && [ -x /usr/lib/cups/backend/verae ]; then
|
||||
lpadmin -x Verae_Timestamping 2>/dev/null || true
|
||||
lpadmin -p Verae_Timestamping -E -v verae:/ -m raw \
|
||||
-D "Verae Timestamping" -L "Digests-only Verae stamp" 2>/dev/null || \
|
||||
lpadmin -p Verae_Timestamping -E -v verae:/ -m everywhere \
|
||||
-D "Verae Timestamping" 2>/dev/null || true
|
||||
cupsenable Verae_Timestamping 2>/dev/null || true
|
||||
cupsaccept Verae_Timestamping 2>/dev/null || true
|
||||
fi
|
||||
echo "verae-print installed. Next: verae-print setup"
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$stage/DEBIAN/postinst"
|
||||
|
||||
cat >"$stage/DEBIAN/prerm" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if command -v lpadmin >/dev/null 2>&1; then
|
||||
lpadmin -x Verae_Timestamping 2>/dev/null || true
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$stage/DEBIAN/prerm"
|
||||
|
||||
local deb="$OUT/${PKG_NAME}_${VERSION}_${arch}.deb"
|
||||
if command -v fakeroot >/dev/null 2>&1; then
|
||||
fakeroot dpkg-deb --build "$stage" "$deb"
|
||||
else
|
||||
dpkg-deb --build "$stage" "$deb"
|
||||
fi
|
||||
# Stable name
|
||||
cp -f "$deb" "$OUT/${PKG_NAME}_${arch}.deb"
|
||||
echo "built $deb"
|
||||
dpkg-deb -I "$deb" | head -20
|
||||
}
|
||||
|
||||
resolve_bin() {
|
||||
local arch="$1"
|
||||
local val=""
|
||||
case "$arch" in
|
||||
amd64) val="${BIN_AMD64:-}" ;;
|
||||
arm64) val="${BIN_ARM64:-}" ;;
|
||||
esac
|
||||
if [[ -n "$val" && -f "$val" ]]; then
|
||||
echo "$val"
|
||||
return
|
||||
fi
|
||||
local label="linux-amd64"
|
||||
[[ "$arch" == "arm64" ]] && label="linux-arm64"
|
||||
local cand
|
||||
for cand in \
|
||||
"$ROOT/build/win-linux/VeraePrintStamp-${label}-${VERSION}/verae-print" \
|
||||
"$ROOT/build/win-linux/VeraePrintStamp-${label}-0.1.2/verae-print" \
|
||||
"$ROOT/build/linux-bins/verae-print-${arch}"
|
||||
do
|
||||
if [[ -f "$cand" ]]; then
|
||||
echo "$cand"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
}
|
||||
|
||||
do_one() {
|
||||
local arch="$1"
|
||||
local bin
|
||||
bin="$(resolve_bin "$arch")"
|
||||
if [[ -z "$bin" ]]; then
|
||||
echo "No binary for $arch — set BIN_AMD64 / BIN_ARM64 or run scripts/build-win-linux.sh first" >&2
|
||||
exit 1
|
||||
fi
|
||||
build_arch "$arch" "$bin"
|
||||
}
|
||||
|
||||
case "$ARCHS" in
|
||||
all)
|
||||
do_one amd64
|
||||
# arm64 deb can be built on amd64 host (just packages the arm64 binary)
|
||||
if [[ -n "$(resolve_bin arm64)" ]]; then
|
||||
do_one arm64
|
||||
else
|
||||
echo "(skip arm64 — no binary)"
|
||||
fi
|
||||
;;
|
||||
amd64|arm64)
|
||||
do_one "$ARCHS"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [amd64|arm64|all]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
(cd "$OUT" && shasum -a 256 *.deb 2>/dev/null | tee SHA256SUMS) || \
|
||||
(cd "$OUT" && sha256sum *.deb | tee SHA256SUMS)
|
||||
ls -lh "$OUT"/*.deb
|
||||
48
platform/linux/install.sh
Executable file
48
platform/linux/install.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/usr/bin/env bash
|
||||
# Install verae-print CLI (+ optional CUPS "Verae Timestamping" queue).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
PREFIX="${PREFIX:-/usr/local}"
|
||||
BIN_DST="$PREFIX/bin/verae-print"
|
||||
WITH_CUPS="${WITH_CUPS:-1}"
|
||||
|
||||
if [[ ! -x "$ROOT/verae-print" ]]; then
|
||||
echo "verae-print binary missing next to install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "→ Installing $BIN_DST"
|
||||
install -d "$PREFIX/bin"
|
||||
install -m 755 "$ROOT/verae-print" "$BIN_DST"
|
||||
|
||||
if [[ "$WITH_CUPS" == "1" ]] && command -v lpadmin >/dev/null 2>&1; then
|
||||
BACKEND_SRC="$ROOT/cups/backend-verae.sh"
|
||||
BACKEND_DST="/usr/lib/cups/backend/verae"
|
||||
if [[ ! -d /usr/lib/cups/backend && -d /usr/libexec/cups/backend ]]; then
|
||||
BACKEND_DST="/usr/libexec/cups/backend/verae"
|
||||
fi
|
||||
if [[ -f "$BACKEND_SRC" ]]; then
|
||||
echo "→ CUPS backend $BACKEND_DST"
|
||||
install -m 755 "$BACKEND_SRC" "$BACKEND_DST"
|
||||
# Point backend at installed binary
|
||||
if grep -q 'VERAE_PRINT_BIN:-/usr/local/bin/verae-print' "$BACKEND_DST" 2>/dev/null; then
|
||||
:
|
||||
fi
|
||||
QUEUE="Verae_Timestamping"
|
||||
lpadmin -x "$QUEUE" 2>/dev/null || true
|
||||
lpadmin -p "$QUEUE" -E -v verae:/ -m raw -D "Verae Timestamping" -L "Digests-only Verae stamp" || \
|
||||
lpadmin -p "$QUEUE" -E -v verae:/ -m everywhere -D "Verae Timestamping" || true
|
||||
cupsenable "$QUEUE" 2>/dev/null || true
|
||||
cupsaccept "$QUEUE" 2>/dev/null || true
|
||||
echo "CUPS queue: $QUEUE (Print → Verae Timestamping when your DE lists it)"
|
||||
fi
|
||||
else
|
||||
echo "(Skipping CUPS — set WITH_CUPS=1 and install cups-client to enable)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Next:"
|
||||
echo " verae-print setup # paste xVerae- key from https://customer.verae.ai/portal/"
|
||||
echo " verae-print stamp file.pdf"
|
||||
echo "Outputs: ~/Documents/Verae Timestamps/"
|
||||
|
|
@ -8,10 +8,17 @@ exec >>"$LOG" 2>&1
|
|||
echo "$(date -u +%Y-%m-%dT%H:%MZ) IPP job start env=$(env | grep -E '^IPP_|^CONTENT_|^USER=' | tr '\n' ' ')"
|
||||
|
||||
VERAE_PRINT="${VERAE_PRINT_BIN:-$HOME/bin/verae-print}"
|
||||
CHANNEL="${VERAE_PRINT_CHANNEL:-verae}"
|
||||
OUT_DIR="${HOME}/Documents/Verae Timestamps"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
# First-run: require customer xVerae- signing key (opens portal dialogs)
|
||||
if ! "$VERAE_PRINT" setup; then
|
||||
echo "ERROR: signing key setup required" >&2
|
||||
osascript -e 'display notification "Create a Verae customer account to get your signing key" with title "Verae Timestamping"' || true
|
||||
open "https://customer.verae.ai/portal/" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TITLE="${IPP_JOB_NAME:-${IPP_DOCUMENT_NAME:-document}}"
|
||||
# Strip path / extension for stem
|
||||
BASE="$(basename "$TITLE")"
|
||||
|
|
@ -48,8 +55,8 @@ else
|
|||
cp "$RAW" "$PDF"
|
||||
fi
|
||||
|
||||
echo "stamping stem=$STEM channel=$CHANNEL pdf=$PDF"
|
||||
"$VERAE_PRINT" stamp -channel "$CHANNEL" -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF"
|
||||
echo "stamping stem=$STEM pdf=$PDF"
|
||||
"$VERAE_PRINT" stamp -out-dir "$OUT_DIR" -name "$STEM" -open "$PDF"
|
||||
echo "ATTR: job-impressions-completed=1" >&2
|
||||
rm -rf "$WORK"
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ SIGNING_ENV="${SIGNING_ENV:-$HOME/fie/installers/macos/signing.env}"
|
|||
# shellcheck source=/dev/null
|
||||
source "$SIGNING_ENV"
|
||||
|
||||
VERSION="${VERSION:-0.1.0}"
|
||||
VERSION="${VERSION:-0.1.2}"
|
||||
BUILD_ROOT="$ROOT/build/macos-pkg"
|
||||
STAGE="$BUILD_ROOT/stage"
|
||||
SCRIPTS="$BUILD_ROOT/scripts"
|
||||
|
|
@ -98,18 +98,46 @@ PLIST
|
|||
cat > "$APP/Contents/MacOS/Verae Print Stamp" <<'RUN'
|
||||
#!/bin/bash
|
||||
BIN="/Library/Application Support/Verae/PrintStamp/bin/verae-print"
|
||||
CHANNEL="${VERAE_PRINT_CHANNEL:-verae}"
|
||||
# First launch: create customer account + save xVerae- signing key
|
||||
SETUP_OK=0
|
||||
if "$BIN" setup >/tmp/verae-print-setup.log 2>&1; then
|
||||
SETUP_OK=1
|
||||
else
|
||||
osascript -e 'display notification "Create a Verae customer account to get your signing key" with title "Verae Print Stamp"' || true
|
||||
fi
|
||||
if [[ $# -eq 0 ]]; then
|
||||
osascript <<'OSA'
|
||||
display dialog "Drop a PDF on this app, use Open With, or Print → Destination → Verae Timestamping." buttons {"OK"} default button 1 with title "Verae Print Stamp"
|
||||
if [[ "$SETUP_OK" -eq 1 ]]; then
|
||||
osascript <<'OSA'
|
||||
display dialog "Signing key saved. You are ready to timestamp.
|
||||
|
||||
• Drop a PDF on this app, or Open With
|
||||
• Or Print → Destination → Verae Timestamping
|
||||
|
||||
Manage keys: https://customer.verae.ai/portal/" buttons {"Open portal", "OK"} default button "OK" with title "Verae Print Stamp"
|
||||
if button returned of result is "Open portal" then
|
||||
open location "https://customer.verae.ai/portal/"
|
||||
end if
|
||||
OSA
|
||||
open -R "/Library/Application Support/Verae/PrintStamp" 2>/dev/null || true
|
||||
else
|
||||
osascript <<'OSA'
|
||||
display dialog "Verae Print Stamp needs a customer signing key.
|
||||
|
||||
1. Create a free account on customer.verae.ai
|
||||
2. Open Security and create an xVerae- token
|
||||
3. Run this app again (or: verae-print setup) and paste the key
|
||||
|
||||
Print → Destination → Verae Timestamping works after the key is saved." buttons {"Open portal", "OK"} default button "Open portal" with title "Verae Print Stamp"
|
||||
if button returned of result is "Open portal" then
|
||||
open location "https://customer.verae.ai/portal/"
|
||||
end if
|
||||
OSA
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
for f in "$@"; do
|
||||
case "$f" in
|
||||
*.pdf|*.PDF|*.html|*.HTML|*.htm|*.HTM)
|
||||
"$BIN" stamp -channel "$CHANNEL" -open "$f" \
|
||||
"$BIN" stamp -open "$f" \
|
||||
|| osascript -e "display notification \"Stamp failed for $(basename "$f")\" with title \"Verae Print Stamp\""
|
||||
;;
|
||||
*)
|
||||
|
|
|
|||
|
|
@ -62,5 +62,11 @@ if [[ -n "${CONSOLE_USER:-}" && "$CONSOLE_USER" != "root" ]]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# First-run: open the app so the user creates a customer account + signing key
|
||||
if [[ -n "${CONSOLE_USER:-}" && "$CONSOLE_USER" != "root" ]]; then
|
||||
echo "Launching Verae Print Stamp for account setup (user=$CONSOLE_USER)"
|
||||
sudo -u "$CONSOLE_USER" open "$APP" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "$(date -u +%Y-%m-%dT%H:%MZ) postinstall done"
|
||||
exit 0
|
||||
|
|
|
|||
|
|
@ -34,10 +34,15 @@ async function loadQR() {
|
|||
return QRCode.default || QRCode;
|
||||
}
|
||||
|
||||
/** Same destination as explorer scan-bar → /time (auto Lookup). */
|
||||
function lookupURL(sha, receipt) {
|
||||
let u = `https://explorer.verae.ai/?sha256=${encodeURIComponent(sha || "")}`;
|
||||
if (receipt) u += `&receipt=${encodeURIComponent(receipt)}`;
|
||||
return u;
|
||||
const params = new URLSearchParams();
|
||||
const rid = String(receipt || "").trim();
|
||||
let digest = String(sha || "").trim().toLowerCase().replace(/^0x/, "");
|
||||
if (rid) params.set("receipt", rid);
|
||||
if (digest) params.set("sha256", digest);
|
||||
const q = params.toString();
|
||||
return q ? `https://explorer.verae.ai/time?${q}` : "https://explorer.verae.ai/time";
|
||||
}
|
||||
|
||||
function findLogo() {
|
||||
|
|
@ -120,6 +125,9 @@ async function buildPdf(row) {
|
|||
fields.splice(1, 0, ["Document size", `${row.size_bytes} bytes`]);
|
||||
}
|
||||
|
||||
const linkColor = [74, 46, 140]; // Verae purple — visible as a link
|
||||
const isHttpURL = (s) => /^https?:\/\//i.test(String(s || "").trim());
|
||||
|
||||
let y = 168;
|
||||
doc.setFontSize(10);
|
||||
for (const [label, value] of fields) {
|
||||
|
|
@ -127,9 +135,21 @@ async function buildPdf(row) {
|
|||
doc.setTextColor(42, 24, 56);
|
||||
doc.text(label, margin, y);
|
||||
doc.setFont("helvetica", "normal");
|
||||
doc.setTextColor(40, 40, 50);
|
||||
const lines = doc.splitTextToSize(String(value ?? ""), pageW - margin * 2 - 10);
|
||||
doc.text(lines, margin, y + 14);
|
||||
const text = String(value ?? "");
|
||||
const maxW = pageW - margin * 2 - 10;
|
||||
const lines = doc.splitTextToSize(text, maxW);
|
||||
const valueY = y + 14;
|
||||
if (isHttpURL(text)) {
|
||||
doc.setTextColor(...linkColor);
|
||||
let ly = valueY;
|
||||
for (const line of lines) {
|
||||
doc.textWithLink(line, margin, ly, { url: text.trim() });
|
||||
ly += 12;
|
||||
}
|
||||
} else {
|
||||
doc.setTextColor(40, 40, 50);
|
||||
doc.text(lines, margin, valueY);
|
||||
}
|
||||
y += 14 + lines.length * 12 + 10;
|
||||
if (y > pageH - 220) break;
|
||||
}
|
||||
|
|
@ -144,13 +164,17 @@ async function buildPdf(row) {
|
|||
const qrX = pageW - margin - qrSize;
|
||||
const qrY = pageH - margin - qrSize - 36;
|
||||
doc.addImage(qr, "PNG", qrX, qrY, qrSize, qrSize);
|
||||
// Clickable hotspot over the QR (same destination as the public lookup URL)
|
||||
doc.link(qrX, qrY, qrSize, qrSize, { url: lookup });
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(90, 80, 100);
|
||||
doc.text("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, {
|
||||
doc.setTextColor(...linkColor);
|
||||
doc.textWithLink("Scan to look up this registration", qrX + qrSize / 2, qrY + qrSize + 14, {
|
||||
align: "center",
|
||||
url: lookup,
|
||||
});
|
||||
|
||||
doc.setFontSize(8);
|
||||
doc.setTextColor(90, 80, 100);
|
||||
doc.text(
|
||||
"This document certifies that the SHA-256 digest above was registered with Verae. File bytes were not uploaded; only the digest was submitted.",
|
||||
margin,
|
||||
|
|
@ -159,7 +183,17 @@ async function buildPdf(row) {
|
|||
);
|
||||
doc.setFont("helvetica", "bold");
|
||||
doc.setTextColor(42, 24, 56);
|
||||
doc.text("Verae · verae.com · explorer.verae.ai", margin, pageH - 36);
|
||||
const footY = pageH - 36;
|
||||
doc.text("Verae · ", margin, footY);
|
||||
let fx = margin + doc.getTextWidth("Verae · ");
|
||||
doc.setTextColor(...linkColor);
|
||||
doc.textWithLink("verae.com", fx, footY, { url: "https://verae.com" });
|
||||
fx += doc.getTextWidth("verae.com");
|
||||
doc.setTextColor(42, 24, 56);
|
||||
doc.text(" · ", fx, footY);
|
||||
fx += doc.getTextWidth(" · ");
|
||||
doc.setTextColor(...linkColor);
|
||||
doc.textWithLink("explorer.verae.ai", fx, footY, { url: "https://explorer.verae.ai/" });
|
||||
|
||||
return Buffer.from(doc.output("arraybuffer"));
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue