Technical
Activation envelope wire format.
The activation envelope is the wire-format contract between useloupe.tools and Loupe.app. The website signs it; the desktop app verifies the signature against an embedded public key, then unlocks the tier the envelope claims. No network calls from the app — the bytes carry their own trust.
Wire format
A single base64url string carrying the signature concatenated with the JSON payload.
wire = base64url( signature_64bytes || payload_json_bytes )
The website hands the wire blob to Loupe.app via a custom URL scheme:
loupe://activate?envelope=<wire>
macOS routes loupe://URLs to the registered Loupe.app, which handles the URL on launch and validates the envelope before showing any “activated” UI.
Payload schema
The signed payload is canonical JSON — keys in declaration order, UTF-8, no insignificant whitespace. The verifier signs the exact bytes it received, so reordering keys would invalidate the signature in transit.
{
"v": 1,
"tier": "single",
"seatId": "",
"orgId": "",
"userId": "u_8a4c7f12b938ef38",
"issuedAt": "2026-04-27T14:11:08Z",
"expiresAt": null,
"envelopeId": "9f1b4e7c-2a83-4c91-bd56-7e02af19c3d4",
"productVersion": 1
}- v
- 1
- Schema version. The verifier rejects any value other than 1.
- tier
- enum
- "single" | "team" | "site" | "trial" | "upgrade-single" | "upgrade-team" | "upgrade-site". Determines which features Loupe.app unlocks after verification.
- seatId
- string
- Per-seat identifier within an org (Team/Site tiers). Empty string for Single and Trial.
- orgId
- string
- Org identifier for Team/Site tiers. Empty string for Single and Trial.
- userId
- string
- OAuth-subject-derived stable user ID from useloupe.tools. Survives email changes.
- issuedAt
- ISO-8601 UTC
- When useloupe.tools signed the envelope. Verifier rejects if older than 24h or more than 5 minutes in the future (clock-skew tolerance).
- expiresAt
- ISO-8601 UTC | null
- When the activation expires. null = perpetual (paid tiers). Trial envelopes set this 14 days out.
- envelopeId
- UUID v4
- Unique identifier per envelope. Loupe.app may use it to detect replay (rare; mostly relevant in audit forensics).
- productVersion
- 1
- Loupe major-version line this envelope unlocks. v1 envelopes do not unlock v2 — rebuying the major upgrade re-issues with productVersion=2.
Signature scheme
- Algorithm
- Ed25519 (RFC 8032)
- Key size
- 32-byte private, 32-byte public
- Signature size
- 64 bytes (fixed)
- Hash
- SHA-512 (built into Ed25519, not separate)
- Encoding
- base64url, no padding, RFC 4648 §5
- Public key embed
- Compiled into Loupe.app as a constant in EnvelopeVerifier.swift
The private key lives only in useloupe.tools' signing environment as the ED25519_SIGNING_KEY_B64 environment variable. Rotating it requires generating a new keypair, embedding the new public key in Loupe.app, and shipping a new notarized DMG — by design, a deliberate cycle, not a routine.
Verification flow (Loupe.app side)
Loupe.app receives the URL, extracts the envelope blob, and performs five checks in order. Any failing check rejects the envelope without partial state.
- 1Decode
base64url→ bytes. Reject if total length > 8 KB. - 2Split into
signature[0..64]+payload[64..]. - 3Verify the Ed25519 signature against the embedded public key. Reject on signature failure.
- 4Parse the payload as JSON. Reject if
v ≠ 1or any required field is missing. - 5Check freshness — reject if
issuedAtis > 24h old or > 5 minutes in the future.
All five passing — Loupe.app stores the verified envelope in the user's Keychain (under studio.shyguy.loupe) and transitions to the activated state.
Reject cases
If you see Loupe.app refuse an activation, the failure is one of these five.
Signature invalid
The 64-byte Ed25519 signature does not validate against the public key embedded in Loupe.app. Cause: forgery, transit corruption, or wrong-key envelope.
Schema version mismatch
payload.v ≠ 1. Cause: malformed envelope, or a v2-line envelope offered to a v1 build.
Stale envelope
issuedAt is more than 24h ago. Bound to limit the activation-link replay window — the issued blob is good only for first-handoff.
Future-dated envelope
issuedAt is more than 5 minutes ahead of wall-clock. Tolerates legitimate clock skew while rejecting blatant tampering.
Oversize blob
Total wire blob exceeds 8 KB. Defense-in-depth against pathological payloads. Real envelopes are ~600 bytes.
Verifying an envelope yourself
For security review or audit purposes, you can verify any envelope with a few lines of code. The public key is embedded in Loupe.app's EnvelopeVerifier.swift and is reproduced in the bundle's license tab so you can cross-reference.
# Node.js
import { ed25519 } from "@noble/curves/ed25519";
const wire = Buffer.from(blob_base64url, "base64url");
const sig = wire.subarray(0, 64);
const payload = wire.subarray(64);
const publicKey = Buffer.from(LOUPE_PUBLIC_KEY_BASE64, "base64");
const ok = ed25519.verify(sig, payload, publicKey);
console.log(ok ? "VALID" : "INVALID", JSON.parse(payload.toString("utf8")));