@nexart/ai-execution
v0.5.0
Published
AI Execution Integrity — tamper-evident records and Certified Execution Records (CER) for AI operations
Downloads
627
Maintainers
Readme
@nexart/ai-execution v0.5.0
Tamper-evident records and Certified Execution Records (CER) for AI operations.
Why Not Just Store Logs?
Logs tell you what happened. CERs prove integrity. A log entry can be edited, truncated, or fabricated after the fact with no way to detect it. A CER bundle is cryptographically sealed: any modification — to the input, output, parameters, or ordering — invalidates the certificate hash. If you need to demonstrate to an auditor, regulator, or downstream system that a recorded execution has not been modified post-hoc, logs are insufficient. CERs provide the tamper-evident chain of custody that logs cannot. CERs certify records, not model determinism or provider execution.
What This Does
This package creates integrity records for AI executions. Every time you call an AI model, it captures:
- What you sent (input + prompt)
- What you got back (output)
- The exact parameters used (temperature, model, etc.)
- SHA-256 hashes of everything for tamper detection
These records can be verified offline to detect any post-hoc modification and prove integrity of the recorded execution.
Important: This does NOT promise that an AI model will produce the same output twice, and it does not verify provider or model identity. LLMs are not deterministic. This package provides integrity and auditability — proof that the recorded input, output, and parameters have not been modified, and chain-of-custody for the execution record.
Compatibility Guarantees
- v0.1.0, v0.2.0, and v0.3.0 bundles verify forever. Any CER bundle produced by any prior version will pass
verify()in v0.4.2 and all future versions. - Hashing rules are frozen for
cer.ai.execution.v1. The canonicalization, SHA-256 computation, and certificate hash inputs (bundleType, version, createdAt, snapshot) are unchanged. - New optional snapshot fields (runId, stepId, stepIndex, etc.) default to undefined and are excluded from legacy snapshots. They participate in the certificate hash only when present.
- Canonicalization is frozen for v1. Number-to-string conversion uses
JSON.stringify(), which is consistent across JavaScript engines but does not implement RFC 8785 (JCS) for edge cases like-0. If stricter canonicalization is required, it will ship as a new bundle type (cer.ai.execution.v2), never as a modification to v1.
Installation
npm install @nexart/ai-executionQuick Start
Single Decision (3 lines)
import { certifyDecision } from '@nexart/ai-execution';
const cer = certifyDecision({
provider: 'openai',
model: 'gpt-4o',
prompt: 'Summarize.',
input: userQuery,
output: llmResponse,
parameters: { temperature: 0.7, maxTokens: 1024, topP: null, seed: null },
});
console.log(cer.certificateHash); // "sha256:..."Manual Snapshot + Seal
import { createSnapshot, sealCer, verify } from '@nexart/ai-execution';
const snapshot = createSnapshot({
executionId: 'exec-001',
provider: 'openai',
model: 'gpt-4o',
prompt: 'You are a helpful assistant.',
input: 'What is 2+2?',
parameters: { temperature: 0.7, maxTokens: 1024, topP: null, seed: null },
output: 'The answer is 4.',
});
const bundle = sealCer(snapshot);
const result = verify(bundle);
console.log(result.ok); // trueAgentic Multi-Step Workflow
import { RunBuilder } from '@nexart/ai-execution';
const run = new RunBuilder({ runId: 'analysis-run', workflowId: 'data-pipeline' });
run.step({
provider: 'openai', model: 'gpt-4o',
prompt: 'Plan the analysis.',
input: 'Analyze Q1 sales data.',
output: 'I will: 1) load data, 2) compute totals, 3) summarize.',
parameters: { temperature: 0.3, maxTokens: 512, topP: null, seed: null },
});
run.step({
provider: 'openai', model: 'gpt-4o',
prompt: 'Execute step 1.',
input: 'Load and total Q1 data.',
output: 'Total revenue: $1.2M.',
parameters: { temperature: 0.3, maxTokens: 512, topP: null, seed: null },
});
const summary = run.finalize();
// { runId, stepCount: 2, steps: [...], finalStepHash: "sha256:..." }Attest to Canonical Node
import { certifyDecision, attest } from '@nexart/ai-execution';
const cer = certifyDecision({ /* ... */ });
const proof = await attest(cer, {
nodeUrl: 'https://node.nexart.io',
apiKey: process.env.NEXART_API_KEY!,
});
console.log(proof.attestationId);Attestation verifies internal integrity only. It does not re-run the model. The node confirms the bundle's hashes are consistent and records it in the proof ledger.
Archive (Export / Import)
import { exportCer, importCer } from '@nexart/ai-execution';
const json = exportCer(bundle); // canonical JSON string
const restored = importCer(json); // parse + verify (throws on tamper)Snapshot Format (ai.execution.v1)
Required vs Optional Fields
| Field | Required | Type | Notes |
|---|---|---|---|
| executionId | Yes | string | Caller-supplied unique ID |
| provider | Yes | string | e.g. "openai", "anthropic" |
| model | Yes | string | e.g. "gpt-4o" |
| prompt | Yes | string | System prompt |
| input | Yes | string \| object | User input (text or structured) |
| output | Yes | string \| object | Model output (text or structured) |
| parameters.temperature | Yes | number | Must be finite |
| parameters.maxTokens | Yes | number | Must be finite |
| timestamp | Optional | string | ISO 8601; defaults to new Date().toISOString() |
| modelVersion | Optional | string \| null | Defaults to null |
| parameters.topP | Optional | number \| null | Defaults to null |
| parameters.seed | Optional | number \| null | Defaults to null |
| sdkVersion | Optional | string \| null | Defaults to "0.4.2" |
| appId | Optional | string \| null | Defaults to null |
| runId | Optional | string \| null | Workflow run ID |
| stepId | Optional | string \| null | Step identifier within a run |
| stepIndex | Optional | number \| null | 0-based step position |
| workflowId | Optional | string \| null | Workflow template ID |
| conversationId | Optional | string \| null | Conversation/session ID |
| prevStepHash | Optional | string \| null | certificateHash of previous step |
Auto-generated fields (set by createSnapshot, do not set manually): type, protocolVersion, executionSurface, inputHash, outputHash.
CER Bundle Format
{
"bundleType": "cer.ai.execution.v1",
"certificateHash": "sha256:...",
"createdAt": "2026-02-12T00:00:00.000Z",
"version": "0.1",
"snapshot": { ... },
"meta": { "source": "my-app", "tags": ["production"] }
}Certificate Hash Computation
The certificateHash is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: { bundleType, version, createdAt, snapshot }. meta is excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
Attestation
Endpoint: POST {nodeUrl}/api/attest
- Authorization:
Bearer {apiKey} - Body: the full CER bundle as JSON (auto-sanitized via
sanitizeForAttestationin v0.4.0+) - Returns:
AttestationResultwithattestationId,nodeRuntimeHash,certificateHash,protocolVersion - Default timeout: 10 seconds (configurable via
timeoutMs) - Validates: response
certificateHashmatches submitted bundle; all hashes insha256:<64hex>format - Throws:
CerAttestationErroron mismatch, network error, timeout, or HTTP error
Attestation verifies internal integrity only. It does not re-run the model or validate the correctness of the AI output.
Attestation Receipt
After a successful attestation, you get a normalized AttestationReceipt:
type AttestationReceipt = {
attestationId: string;
certificateHash: string; // sha256:...
nodeRuntimeHash: string; // sha256:...
protocolVersion: string;
nodeId?: string;
attestedAt?: string; // ISO 8601
attestorKeyId?: string; // kid of the signing key (v0.5.0+)
signatureB64Url?: string; // base64url Ed25519 signature (v0.5.0+)
};Recommended one-call integration:
import { certifyAndAttestDecision } from '@nexart/ai-execution';
const { bundle, receipt } = await certifyAndAttestDecision(params, {
nodeUrl: 'https://my-node.example.com',
apiKey: process.env.NODE_API_KEY!,
});
// bundle is the sealed CER, receipt is the normalized attestation proofSkip re-attestation when already attested:
import { attestIfNeeded, getAttestationReceipt } from '@nexart/ai-execution';
const { receipt } = await attestIfNeeded(bundle, options);
// or just read without network call:
const receipt = getAttestationReceipt(bundle); // null if not yet attestedgetAttestationReceipt(bundle)— extracts a normalized receipt from any supported shape (top-level fields orbundle.meta.attestation); returnsnullif required fields are missing, never throwsattestIfNeeded(bundle, options)— skips the node call if a valid receipt is already present; prevents double-attestationcertifyAndAttestDecision(params, options)— recommended one-call integration:certifyDecision+attest+ normalized receipt
Signed Receipt Verification (v0.5.0+)
After a node signs the receipt, verify it offline without a round-trip:
import {
verifyNodeReceiptSignature,
verifyBundleAttestation,
fetchNodeKeys,
selectNodeKey,
} from '@nexart/ai-execution';
// One-call: fetches node keys, selects correct key, verifies Ed25519 signature
const result = await verifyBundleAttestation(bundle, {
nodeUrl: 'https://my-node.example.com',
});
// result.ok === true → signature is valid
// result.code → CerVerifyCode enum value
// result.details → string[] with failure explanation (when ok=false)Verify against a specific key directly:
const result = await verifyNodeReceiptSignature({
receipt: { attestationId: '...', certificateHash: 'sha256:...', ... },
signatureB64Url: 'aDEKyu...Q',
key: { jwk: { kty: 'OKP', crv: 'Ed25519', x: '<base64url pubkey>' } },
// or: key: { rawB64Url: '<base64url raw 32 bytes>' }
});Failure codes:
| Code | Meaning |
|---|---|
| ATTESTATION_MISSING | No signed receipt in bundle |
| ATTESTATION_KEY_NOT_FOUND | kid not found in node keys document |
| ATTESTATION_INVALID_SIGNATURE | Ed25519 signature did not verify |
| ATTESTATION_KEY_FORMAT_UNSUPPORTED | Key cannot be decoded (wrong crv, no fields, etc.) |
Node keys document is fetched from {nodeUrl}/.well-known/nexart-node.json. See SPEC.md for the full shape.
Sanitization and Redaction
sanitizeForAttestation(bundle) returns a JSON-safe deep clone:
- Removes keys with
undefinedvalues at all nesting levels - Rejects
BigInt, functions, and symbols (throws) - Safe to serialize with
JSON.stringifyor canonical JSON
Recommended redaction pattern: delete keys or set them to null — never set to undefined, which is not valid JSON. Call sanitizeForAttestation before archiving or attesting if your bundle may contain undefined values.
Skip re-attestation: use hasAttestation(bundle) to check if a bundle already includes attestation fields before calling attest() again.
Canonical JSON Constraints
- Object keys sorted lexicographically (Unicode codepoint order) at every nesting level.
- No whitespace between tokens.
- Array order preserved.
nullserialized asnull.- Numbers must be finite.
NaN,Infinity,-Infinityrejected (throw). undefinedvalues in object properties omitted (key dropped).BigInt, functions,Symbolrejected (throw).- Strings JSON-escaped.
Canonicalization is frozen for v1. Number formatting uses JSON.stringify() (V8-consistent). This does not normalize -0 to 0 and does not implement RFC 8785 exponential notation rules. These are documented known behaviors, not bugs. Any future stricter canonicalization will ship under a new bundle type.
Error Types
| Error | When thrown | Structured data |
|---|---|---|
| CerVerificationError | importCer() on invalid/tampered data | .errors: string[] |
| CerAttestationError | attest() on failure | .statusCode, .responseBody, .details: string[] |
Interoperability (Test Vectors)
Fixtures at fixtures/vectors/ and fixtures/golden/. Cross-language implementations must match committed hash values exactly. Golden fixtures (v0.1.0 semantics) must verify with every future version.
API Reference
Core
| Function | Description |
|---|---|
| createSnapshot(params) | Create snapshot with computed hashes |
| verifySnapshot(snapshot) | Verify snapshot hashes and structure |
| sealCer(snapshot, options?) | Seal snapshot into CER bundle |
| verify(bundle) / verifyCer(bundle) | Verify CER bundle |
| certifyDecision(params) | One-call: createSnapshot + sealCer |
Workflow
| Export | Description |
|---|---|
| RunBuilder | Multi-step workflow builder with prevStepHash chaining |
Attestation & Archive
| Function | Description |
|---|---|
| attest(bundle, options) | Post CER to canonical node (auto-sanitizes) |
| certifyAndAttestDecision(params, options) | One-call: certifyDecision + attest + receipt |
| attestIfNeeded(bundle, options) | Attest only if no receipt already present |
| getAttestationReceipt(bundle) | Extract normalized AttestationReceipt or null |
| verifyNodeReceiptSignature(params) | Verify an Ed25519-signed receipt offline (v0.5.0+) |
| fetchNodeKeys(nodeUrl) | Fetch NodeKeysDocument from /.well-known/nexart-node.json (v0.5.0+) |
| selectNodeKey(doc, kid?) | Select a key from a NodeKeysDocument by kid or activeKid (v0.5.0+) |
| verifyBundleAttestation(bundle, options) | One-call offline attestation verification (v0.5.0+) |
| sanitizeForAttestation(bundle) | Remove undefined keys, reject BigInt/functions/symbols |
| hasAttestation(bundle) | Check if bundle already has attestation fields |
| exportCer(bundle) | Serialize to canonical JSON string |
| importCer(json) | Parse + verify from JSON string |
Reason Codes
CerVerifyCode — stable string-union constant exported from the package root:
| Code | When set |
|---|---|
| OK | Verification passed |
| CERTIFICATE_HASH_MISMATCH | certificateHash doesn't match recomputed hash |
| INPUT_HASH_MISMATCH | inputHash doesn't match recomputed hash |
| OUTPUT_HASH_MISMATCH | outputHash doesn't match recomputed hash |
| SNAPSHOT_HASH_MISMATCH | Both inputHash and outputHash are wrong |
| INVALID_SHA256_FORMAT | A hash field doesn't start with sha256: |
| SCHEMA_ERROR | Wrong bundleType/version, missing snapshot, non-finite parameters, etc. |
| CANONICALIZATION_ERROR | toCanonicalJson threw during verification |
| UNKNOWN_ERROR | Catch-all for unclassified failures |
| ATTESTATION_MISSING | No signed receipt found in bundle (v0.5.0+) |
| ATTESTATION_KEY_NOT_FOUND | kid not found in node keys document (v0.5.0+) |
| ATTESTATION_INVALID_SIGNATURE | Ed25519 signature did not verify (v0.5.0+) |
| ATTESTATION_KEY_FORMAT_UNSUPPORTED | Key cannot be decoded (v0.5.0+) |
Priority when multiple failures exist: CANONICALIZATION_ERROR > SCHEMA_ERROR > INVALID_SHA256_FORMAT > CERTIFICATE_HASH_MISMATCH > INPUT_HASH_MISMATCH > OUTPUT_HASH_MISMATCH > SNAPSHOT_HASH_MISMATCH > UNKNOWN_ERROR.
These codes are stable across all future versions. New codes may be added but existing codes will not be renamed or removed.
Providers (sub-exports)
| Function | Export path |
|---|---|
| runOpenAIChatExecution | @nexart/ai-execution/providers/openai |
| runAnthropicExecution | @nexart/ai-execution/providers/anthropic |
| wrapProvider | @nexart/ai-execution/providers/wrap |
Version History
| Version | Description |
|---|---|
| v0.1.0 | Core snapshot + CER + verify + OpenAI adapter |
| v0.2.0 | certifyDecision, RunBuilder, attest, archive, Anthropic, wrapProvider, typed errors, workflow fields |
| v0.3.0 | Attestation hardening (hash validation, timeout), verify alias, CerAttestationError.details, release hygiene |
| v0.4.0 | Dual ESM/CJS build, sanitizeForAttestation, hasAttestation, auto-sanitize in attest(), fixed ERR_PACKAGE_PATH_NOT_EXPORTED |
| v0.4.1 | Verification reason codes (CerVerifyCode), code + details on VerificationResult, README provenance wording tightened |
| v0.4.2 | AttestationReceipt, getAttestationReceipt, certifyAndAttestDecision, attestIfNeeded |
| v0.5.0 | Ed25519 signed receipt verification: verifyNodeReceiptSignature, verifyBundleAttestation, fetchNodeKeys, selectNodeKey; new attestation CerVerifyCode entries; SPEC.md; NodeKeysDocument, SignedAttestationReceipt, NodeReceiptVerifyResult types |
| v1.0.0 | Planned: API stabilization, freeze public API surface |
Releasing
cd packages/ai-execution
npm run build
npm test
npm publish --access publicThe release script automates build, test, version bump, and publish:
npm run releaseLicense
MIT
