SDK reference
Verification
canonicalise, verifyEnvelope and verifyAgainstIssuer — and why canonicalisation is the first thing to port.
verifyAgainstIssuer(envelope, issuerBaseUrl, fetch?)
The one you usually want. Fetches the issuer's keys, finds the one matching the envelope's kid,
verifies.
import { verifyAgainstIssuer } from '@pathprotocol/sdk';
const { valid, reason } = await verifyAgainstIssuer(receipt, 'https://api.other-member.com');
if (!valid) throw new Error(reason);Possible reasons:
reason | Meaning |
|---|---|
Could not fetch keys (404) | The issuer publishes no key document at that base URL |
Issuer publishes no key with kid "…" | Likely a rotation you have not picked up — refetch |
Signature does not verify | The envelope does not match the key |
The middle one is not an attack. Treat it as a cache-invalidation event.
verifyEnvelope(envelope, publicKeyHex)
Synchronous, offline, no network.
import { verifyEnvelope } from '@pathprotocol/sdk';
const valid = verifyEnvelope(receipt, cachedKey);Use it when you already hold the key — verifying a scanned receipt on a device with no connectivity, or checking a batch without a fetch per item.
canonicalise(value)
RFC 8785 canonical JSON. What gets signed.
import { canonicalise } from '@pathprotocol/sdk';
canonicalise({ b: 1, a: { d: 4, c: 3 } });
// '{"a":{"c":3,"d":4},"b":1}'Rules: keys sorted by UTF-16 code unit, no insignificant whitespace, undefined members dropped,
-0 normalised to 0, numbers in shortest round-trip form.
Why this exists
JSON.stringify preserves insertion order. Two implementations sending the same object produce
different bytes, therefore different signatures, and neither verifies the other.
Worse, it passes every test — because in testing there is only one implementation, talking to itself. It fails the day a second one appears, and the error points at the key rather than at the serialiser.
If you port PATH to another language, port canonicalisation first and test it against vectors from an independent implementation before writing anything else. A version skew here is silent: all signatures fail at once, and nothing in the error message says why.
What the envelope covers
{
"kind": "resolver_answer",
"address": "path:4a91c2f7e8d3",
"accepts": [{ "asset": "USDC", "chain": "base" }],
"protocol_version": "0.1.0",
"signed_at": "2026-09-09T10:00:00Z",
"kid": "op_example_2026_01",
"signature": "…"
}The signature covers the payload and protocol_version, signed_at and kid. Everything except
signature itself.
Leaving those three outside would let anyone restate a genuine answer under a different version or timestamp: the bytes would still verify, and the meaning would have changed.
Why asymmetric
Ed25519, not an HMAC.
A shared secret proves that someone holding the secret produced the value — and the recipient holds it too, so the recipient could have produced it. Fine for a session cookie, useless for a statement a third party has to rely on.
If a payer needs to show an auditor what it was told before sending, a MAC the payer could have forged proves nothing at all.