Minimum Integration, 3 Steps
- Extract receipt, Use
getAttestationReceipt(bundle)to get the normalized receipt. - Fetch node keys, Call
fetchNodeKeys(nodeUrl)to get the public key document. - Verify signature, Use
verifyBundleAttestation(bundle, { nodeUrl })for the full check.
Receipt Fields
After a successful attestation, the bundle includes a normalized AttestationReceipt:
| Field | Type | Description |
|---|---|---|
attestationId | string | Unique ID of this attestation event |
certificateHash | string | Must match bundle.certificateHash |
nodeRuntimeHash | string | Hash of the node's runtime state at attestation time |
protocolVersion | string | Protocol version the node used for verification |
nodeId | string? | Identifier of the attestation node |
attestedAt | string? | ISO 8601 timestamp of attestation |
attestorKeyId | string? | kid of the Ed25519 signing key (v0.5.0+) |
signatureB64Url | string? | Base64url-encoded Ed25519 signature (v0.5.0+) |
import { getAttestationReceipt } from '@nexart/ai-execution';
const receipt = getAttestationReceipt(bundle);
if (receipt) {
console.log(receipt.attestationId);
console.log(receipt.attestorKeyId); // "key-2025-01"
console.log(receipt.signatureB64Url); // "aDEKyu...Q"
}Multiple layouts supported: getAttestationReceipt() extracts receipts from both top-level bundle fields and meta.attestation, depending on the producer. The returned object is always normalized to the same AttestationReceipt shape.
Node Keys Endpoint
Every attestation node publishes its public keys at a well-known URL:
GET {nodeUrl}/.well-known/nexart-node.jsonLive endpoint: nexart-canonical-renderer-production.up.railway.app/.well-known/nexart-node.json
The document contains keys in three formats:
| Format | Field | Use Case |
|---|---|---|
| JWK | key.publicKeyJwk | Web Crypto API (crypto.subtle.importKey) |
| SPKI (Base64) | key.publicKeySpkiB64 | Node.js crypto.createPublicKey |
| Raw (Base64url) | key.publicKey | Direct 32-byte Ed25519 public key |
import { fetchNodeKeys, selectNodeKey } from '@nexart/ai-execution';
const doc = await fetchNodeKeys(
'https://nexart-canonical-renderer-production.up.railway.app'
);
// Select key by kid (from receipt.attestorKeyId)
const { key } = selectNodeKey(doc, receipt.attestorKeyId);
console.log(key.publicKeyJwk);
// { kty: "OKP", crv: "Ed25519", x: "<base64url>" }
console.log(key.publicKey);
// "<base64url-encoded 32-byte Ed25519 public key>"selectNodeKey selection order: explicit kid argument → activeKid from the node document → first key in the array. Always pass receipt.attestorKeyId when verifying a specific receipt.
Key Rotation
The node keys document includes an activeKid field indicating which key is currently used for new attestations. Older keys remain in the document so historical receipts can still be verified.
activeKid, Thekidof the key used for new attestations- Historical keys remain listed with their
kidfor backward-compatible verification selectNodeKey(doc)defaults toactiveKidwhen nokidis specified- When verifying an old receipt, pass its
attestorKeyIdtoselectNodeKey(doc, kid)
Troubleshooting
| Symptom | Code | Cause | Fix |
|---|---|---|---|
| No receipt in bundle | ATTESTATION_MISSING | Bundle was never attested, or attested before v0.5.0 | Re-attest with current SDK, or accept local-only verification |
| Signature fails | ATTESTATION_INVALID_SIGNATURE | Receipt was modified after signing | Obtain a fresh copy of the bundle |
| Key not found | ATTESTATION_KEY_NOT_FOUND | The kid in the receipt doesn't match any key in the node document | Verify you are querying the same node that issued the receipt (nodeUrl must match the attesting node). Historical keys should remain published; if missing, contact the node operator |
| Key format unsupported | ATTESTATION_KEY_FORMAT_UNSUPPORTED | Key has wrong crv or missing fields | Report to node operator, key document is malformed |
"Stamp incomplete" is not an error. It means the bundle has legacy attestation fields (pre-v0.5.0) without a signed receipt, these may appear at the top level or under meta.attestation. Local integrity verification still works; only the node signature check is unavailable.