@zkp2p/zkp2p-attestation
v1.3.1
Published
Browser, React Native, and Node verifier/encrypter for ZKP2P Nitro attested uploads
Keywords
Readme
@zkp2p/zkp2p-attestation
Client-side verifier and encryptor for ZKP2P Nitro seller credential uploads and buyer TEE session-material requests.
The package runs in Chrome MV3 service workers, browser DOM contexts, React Native with a Web Crypto polyfill, and Node >= 20. Library entrypoints use fetch, SubtleCrypto, crypto.getRandomValues, and Uint8Array; they do not import node:* modules or use Buffer.
Install
yarn add @zkp2p/zkp2p-attestationDuring monorepo development, point clients at attestation-service/package/zkp2p-attestation.
Environment Init
Use the client factory when the host owns an environment variable:
import { createNitroAttestationClient } from "@zkp2p/zkp2p-attestation";
const nitro = createNitroAttestationClient({
environment: process.env.ZKP2P_ATTESTATION_ENV === "production" ? "production" : "staging",
});Environment defaults:
| Environment | Service URL | PCR8 source |
|---|---|---|
| staging | https://attestation-service-staging.zkp2p.xyz | bundled staging pin |
| production | https://attestation-service-preprod.zkp2p.xyz | bundled production/preprod pin |
Callers can still pass attestationServiceUrl and trust.expectedPcr8Hex directly. Explicit trust pins always win.
Fast Path
Wise uploads use the server-derived payee id path:
const encryptedUpload = await nitro.createEncryptedSellerCredentialUpload({
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
// Optional. Omit for single-profile PATs; include after a
// WISE_PROFILE_SELECTION_REQUIRED response for multi-profile PATs.
profileId: "41246868",
},
});
await fetch(`${attestationServiceUrl}/seller/credentials/wise`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ encryptedUpload }),
});This fetches GET /attestation?nonce=..., verifies the Nitro document to the pinned AWS root and PCR8, extracts the attested seller-upload RSA SPKI, and returns a compact JWE for POST /seller/credentials/:platform.
For the same flow without hand-rolling the POST:
const credentialBundle = await nitro.uploadSellerCredential({
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
},
});Venmo, Cash App, and PayPal still require a top-level payeeId; existing callers can keep using the same call shape:
const encryptedUpload = await nitro.createEncryptedSellerCredentialUpload({
payeeId: "1130030979",
sessionMaterial: venmoSessionMaterial,
});Buyer TEE requests use the same attested upload key and compact JWE envelope. The typed helper encrypts the captured
session material, posts { encryptedSessionMaterial, params, chainId, intent } to
POST /buyer/verify/:platform/:actionType, unwraps the service envelope, and returns a typed AttestationOutput.
The service does not enforce capture-age or one-use replay limits for buyer TEE session material; verification depends
on the upstream session still being active. A leaked encrypted JWE is therefore valid for the upstream session
lifetime — treat any accidental disclosure as equivalent to leaking the underlying upstream credential (cookies, PAT,
etc.) and rotate the upstream session.
const attestation = await nitro.verifyBuyerTeePayment({
platform: "venmo",
actionType: "transfer_venmo",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
"User-Agent": "<captured-user-agent>",
},
params: { senderId: "<venmo-account-id>", index: 0 },
chainId,
intent,
});The helper is a thin wrapper over the existing wire format; callers can still hand-roll the final POST:
const encryptedSessionMaterial = await nitro.createEncryptedBuyerTeeSessionMaterial({
platform: "wise",
actionType: "transfer_wise",
sessionMaterial: { "X-Access-Token": "<captured-token>" },
});
await fetch(`${attestationServiceUrl}/buyer/verify/wise/transfer_wise`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
encryptedSessionMaterial,
params: { profileId: "41246868", transactionId: "123456789" },
chainId,
intent,
}),
});Buyer TEE platform matrix:
| Platform | actionType | Required encrypted session material | Public params fields |
|---|---|---|---|
| venmo | transfer_venmo | Cookie | senderId, index |
| cashapp | transfer_cashapp | Cookie, x-csrf-token, x-device-name, x-request-signature, x-request-uuid, cash-web-request, x-web-device-info, x-web-context, x-bt-id | senderId, index |
| luxon | transfer_luxon | X-Auth-Token | transferId |
| monzo | transfer_monzo | Authorization | txId |
| n26 | transfer_n26 | Cookie, csrf-token, body | {} |
| wise | transfer_wise | Cookie or X-Access-Token | profileId, transactionId |
| revolut | transfer_revolut | Cookie, x-device-id | index |
| idfc | transfer_idfc | Cookie | senderId, index |
| citi | transfer_zelle | Cookie | index |
| chime | transfer_chime | Cookie, body | {} |
| chase | transfer_zelle | Cookie, x-jpmc-channel, x-jpmc-csrf-token, Referer, Origin | index |
| bankofamerica | transfer_zelle | Cookie | index |
| mercadopago | transfer_mercadopago | Cookie | paymentId, urlParamsFrom |
| paypal | transfer_paypal | Cookie | index |
| alipay | transfer_alipay | Cookie | tradeNo |
Per-platform session-material types require these headers using the canonical names shown above while still allowing additional captured headers. Captured request bodies are session material because they can contain sensitive data and are encrypted before being sent to the service.
Cache-Friendly Path
const verified = await nitro.fetchAndVerifyAttestation();
const encryptedUpload = await nitro.encryptSellerCredentialUpload({
key: verified.attestedSellerUploadKey,
platform: "wise",
sessionMaterial: {
apiToken: "<wise-api-token>",
},
});For buyer TEE flows with a cached attestation:
const encryptedSessionMaterial = await nitro.encryptBuyerTeeSessionMaterial({
key: verified.attestedSellerUploadKey,
platform: "venmo",
actionType: "transfer_venmo",
sessionMaterial: {
Cookie: "<captured-cookie-header>",
},
});The attested SPKI is stable within one enclave process. If the enclave restarts, cached keys become stale and upload decrypt will fail; fetch a fresh attestation for a new session.
React Native Wiring
React Native must provide Web Crypto-compatible primitives when they are not on globalThis.crypto:
const nitro = createNitroAttestationClient({
environment: "staging",
subtle: webCrypto.subtle,
getRandomValues: (out) => webCrypto.getRandomValues(out),
fetch,
});Any conforming SubtleCrypto works. The package does not require Buffer.
Direct Entrypoints
import {
createEncryptedBuyerTeeSessionMaterial,
createEncryptedSellerCredentialUpload,
encryptBuyerTeeSessionMaterial,
encryptSellerCredentialUpload,
fetchAndVerifyAttestation,
uploadSellerCredential,
verifyBuyerTeePayment,
} from "@zkp2p/zkp2p-attestation";Direct functions accept attestationServiceUrl, trust, fetch, subtle, getRandomValues, now, onWarning, and timeoutMs overrides. If no caller or bundled PCR8 pin exists and trust.strictPin !== true, the verifier falls back to the service-advertised PCR8 and emits TRUST_PIN_NOT_PROVIDED; production callers should pin.
PCR8 Rotation Runbook
- Update
src/trust/pins.tsfrom the deploymentSTATE.mdpublished by the attestation-service team. - Refresh
tests/golden/staging-2026-05-05.raw.jsonand expected metadata if staging rotates. - Run
yarn test:coverageandSTAGING_E2E=true yarn test:integration. - Release a patch version for PCR8-only rotations. Use a major version for wire-format or algorithm changes.
CLI
The existing verifier is preserved on top of the library:
yarn verify --service https://attestation-service-staging.zkp2p.xyzOptional EIP-712 signer cross-check remains available through --verify-signature.
