@nexart/ai-execution
v1.4.0
Published
AI Execution Integrity — tamper-evident records and Certified Execution Records (CER) for AI operations
Maintainers
Readme
@nexart/ai-execution
Tamper-evident records and Certified Execution Records (CER) for AI operations, project bundles for grouping multiple certified executions into portable, self-verifiable evidence artifacts, browser-safe async verification for all verification functions, and an opt-in Confidential Execution Mode that commits sensitive fields before sealing so an attestation node never sees plaintext.
Version Information
These version axes are distinct and not interchangeable:
| Axis | Value |
|---|---|
| npm package (release candidate, not yet published) | @nexart/[email protected] |
| Record schemas | cer.ai.execution.v1 (ai.execution.v1) · cer.ai.execution.v2 (ai.execution.v2) |
| Canonicalisation protocol | 1.2.0 (default, nexart-v1) · 1.3.0 (jcs-v1) · 1.3.1 (jcs-v1, confidential first-class) |
| Canonicalisation mode | nexart-v1 or jcs-v1 (RFC 8785), selected by protocolVersion |
| AIEF interoperability target | AIEF v0.2.5 (Section 9 projection) |
| CER envelope version | 0.1 |
The npm package version is a release-versioning axis only. There is no protocol 1.4.0 — the canonicalisation protocol remains 1.3.1, and AI Execution V2 is a record schema version, not a protocol version.
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 hashed fields — including input, output, parameters, and any recorded chain/tool evidence — 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
- Optionally: upstream context signals from other systems, sealed into the same certificate hash (v0.10.0+)
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 versioned by
protocolVersion, and1.2.0is frozen. The1.2.0mode (nexart-v1) usesJSON.stringify()number formatting (consistent across JavaScript engines) and is byte-frozen — never modified in place. Stricter RFC 8785 (JCS) canonicalization ships additively asprotocolVersion1.3.0(jcs-v1), opt-in and backward-compatible, rather than as a change to1.2.0. See "protocolVersion: 1.2.0 vs 1.3.0" below andSPEC.md.
protocolVersion: 1.2.0 vs 1.3.0
Every certified record carries a protocolVersion that selects how its bytes are
canonicalised before hashing — this is what makes a hash reproducible by someone else.
- 1.2.0 (legacy, default): the original NexArt canonicalisation (
nexart-v1). Fully supported and frozen — existing records keep verifying byte-for-byte. - 1.3.0 (stranger-verifiable): RFC 8785 (JSON Canonicalization Scheme,
jcs-v1), opt-in viaprotocolVersion: '1.3.0'. The forward standard for any new record that must be verified independently by a third party who does not run our exact code. 1.3.0 is required for independent / external verification. - 1.3.1 (confidential, first-class): the same
jcs-v1canonicalisation as1.3.0(byte-for-byte identical bytes, hashing, and signed payload), but makes commitment-based Confidential Execution a first-class protocol. Seal a snapshot created withprotocolVersion: '1.3.1'whoseinput/outputare confidential commitment envelopes;verifyCerthen strictly validates the envelope shape and thesnapshot.confidentialblock, and reports averdict(VERIFIEDfor full snapshots,VERIFIED_CONFIDENTIALwhen fields are committed). See "Confidential Execution Mode" below.
Opting in (v0.22.0+). protocolVersion is accepted by both the low-level
createSnapshot({ protocolVersion: '1.3.0' }) path and every high-level convenience
wrapper, so a single call selects the protocol:
certifyDecision({ ...params, protocolVersion: '1.3.0' });
certifyDecisionFromProviderCall({ provider, request, response, protocolVersion: '1.3.0' });
new RunBuilder().step({ ...stepParams, protocolVersion: '1.3.0' });
createLangChainCer({ ...input, protocolVersion: '1.3.0' });
// Opt an entire client in once; per-call params still override.
const client = createClient({ protocolVersion: '1.3.0' });
client.certifyDecision({ ...params });Omitting protocolVersion keeps the frozen 1.2.0 default — existing callers are
byte-for-byte unchanged.
Both versions verify consistently. An unknown or unsupported protocolVersion is
rejected (fail-closed) — createSnapshot throws and verifyCer returns SCHEMA_ERROR
(reason code SCHEMA_VERSION_UNSUPPORTED), never a silent default. For real-world payloads 1.3.0 produces the same bytes as 1.2.0,
so adoption is safe and additive. The verifying node and the SDK mirrors ship the same
version-aware module (node lockstep); see SPEC.md.
Legacy Compatibility (protocol 1.2.0)
Historical 1.2.0 CER records were written by earlier SDKs with a looser schema than the
current one. verifyCer / verifyCerAsync verify 100% of those historical records through
a compatibility layer that is scoped exclusively to protocolVersion === "1.2.0", applied
in memory only, and non-evidentiary — it never changes canonicalization, the
certificateHash, the signed payload, or the input bundle.
Why this is hash-safe. Every tolerated field below is part of the snapshot and therefore part
of the certificateHash. The verifier recomputes the hash over the record exactly as received
(no fields are added, removed, or rewritten — the sole exception being parameters: null → {},
see below), so the recomputed hash still matches the value sealed at creation time. Any post-hoc
tampering is still caught by the certificate-hash check. Relaxing the shape validation of a field
that is already bound into the hash does not weaken integrity.
Tolerated for 1.2.0 only (best-effort schema):
| Field | Legacy tolerance |
| --- | --- |
| parameters | null or {} accepted. A null is normalized to {} in memory for the hash recompute (the canonical form was always {}). A missing / undefined parameters key still FAILS. |
| prompt | May be missing. Not injected, not added to canonicalization. |
| type | May be missing or carry a legacy value (e.g. ai.execution). |
| executionSurface | May be missing or carry a legacy value. |
| version and other unknown/extra fields | Not validated; flow into the canonical payload untouched, so the recomputed hash still matches. |
Always strict on every version (including 1.2.0): executionId, timestamp, provider,
model, input, output, inputHash, outputHash (format and match), certificateHash
(format and match), and protocolVersion (the version gate that selects canonicalization).
1.3.0 stays strict and fail-closed. A 1.3.0 record with parameters: null, a missing
prompt, a missing/wrong type, or any other schema deviation is rejected with SCHEMA_ERROR.
The entire compatibility layer is gated on protocolVersion === "1.2.0" and never affects 1.3.0.
AI Execution V2 (cer.ai.execution.v2)
AI Execution V2 is the new explicit record schema for newly integrated AI execution evidence. It is a schema version within the same NexArt AI Execution Evidence record family — not a new protocol, not AIEF v2, and not a separate product.
V2 is strictly additive. Historical V1 CERs (cer.ai.execution.v1) and every
V1 API remain fully supported and unchanged. Existing V1 records do not need
migration and keep verifying byte-for-byte forever. V2 records canonicalise under
the existing protocol 1.3.1 (jcs-v1 / RFC 8785).
V2 core capabilities
executionRole—advisory|operative|unspecified. A producer declaration of how the execution's output was used.- Optional
instruction+instructionKind— an explicit, hash-protected system/developer instruction field. V2 has no heuristic instruction detection. decisionRefs— references binding the execution to external decisions (e.g. invoice or case identifiers), individually hash-protected.- Requested model vs response-declared evidence —
model(the requested / producer-declared model) is a separate hash-bound claim from what the provider response declared. modelEvidence—responseDeclaredModel(model identifier copied from the provider response),transactionRef(provider response/transaction reference where available), andproviderEvidence(producer-transcribed provider response metadata).- SDK-derived protected set — the list of hash-protected fields is derived and sealed by the SDK itself, never caller-supplied.
- Namespaced
ext— extension data under namespaced keys, sealed into the certificate hash. - Confidential commitments — opt-in keyed commitments for
input,output,instruction, and selecteddecisionRefs(see below). - Selective reveal / openings — committed fields can be individually opened and verified later without disclosing the others.
- Project Bundles — V1-only, V2-only, and mixed V1/V2 Project Bundles are all
supported (see Project Bundles; the Project Bundle
itself remains
cer.project.bundle.v1).
Evidence claim boundaries
NexArt cryptographically protects these claims — any post-hoc modification breaks the certificate hash. It does not independently establish that:
executionRoleis factually true (it is a producer declaration);modelEvidence.responseDeclaredModelproves the actual underlying model revision (it is producer-transcribed from the provider response, not independently attested);providerEvidenceis provider-signed;modelEvidence.transactionRefis independently authentic.
Integrity and chain of custody of the record, not ground truth of the provider's behavior.
V2 public API
| Function | Purpose |
|---|---|
| createSnapshotV2(params) | Build an ai.execution.v2 snapshot |
| sealCerV2(snapshot, options?) | Validate (fail-closed) and seal into a cer.ai.execution.v2 bundle |
| certifyDecisionV2(params) | One-call snapshot + seal convenience |
| verifyCerV2(bundle) | Offline verification (sync) |
| verifyCerV2Async(bundle) | Offline verification (browser-safe async) |
| runOpenAIChatExecutionV2(params) | OpenAI provider integration (@nexart/ai-execution/providers/openaiV2) |
| runAnthropicExecutionV2(params) | Anthropic provider integration (@nexart/ai-execution/providers/anthropicV2) |
| certifyDecisionFromProviderCallV2(params) | Generic provider-call certification |
| createLangChainCerV2(...) | LangChain V2 integration (exported from the package root) |
Minimal V2 example
Local, offline — no node or network is required to create or verify the artifact:
import { createSnapshotV2, sealCerV2, verifyCerV2 } from '@nexart/ai-execution';
const snapshot = createSnapshotV2({
provider: 'openai',
model: 'gpt-4o', // requested model (producer-declared)
input: 'Should invoice INV-2026-0042 be approved?',
output: 'Yes — all checks passed.',
parameters: { temperature: 0, maxTokens: 256, topP: null, seed: null },
executionRole: 'advisory',
});
const bundle = sealCerV2(snapshot); // cer.ai.execution.v2
const result = verifyCerV2(bundle);
console.log(result.ok); // trueConfidential Execution Mode in V2
V2 supports opt-in commitments for input, output, instruction, and
selected decisionRefs: the committed value is replaced by a keyed, salted
commitment before sealing. These values are committed / redacted — not
encrypted. Key points:
- Explicit opt-in only; nothing is committed by default.
- Base integrity verification (
verifyCerV2) does not require openings. - Openings are separate disclosure material for selective reveal; keep them out of the record itself.
- CER packages and export-verification artifacts do not automatically disclose openings.
Installation
npm install @nexart/ai-executionQuick Start
End-to-end with @nexart/signals (the canonical flow)
import { createContext } from '@nexart/signals';
import { verifyCer, getContextInfo } from '@nexart/ai-execution';
const ctx = createContext();
ctx.input(userQuery);
ctx.tool('crm_lookup', { accountId: 123 });
ctx.decision('risk_check', { result: 'pass' });
ctx.output(response);
const bundle = await ctx.certify({
provider: 'openai',
model: 'gpt-4o-mini',
prompt,
input: userQuery,
output: response,
});
console.log(bundle.certificateHash); // sha256:... — covers the whole record
console.log(bundle.context.contextHash); // sha256:... — covers the signal context
console.log(getContextInfo(bundle)); // { hasContext, contextHash, signalCount, summary }
console.log(verifyCer(bundle).ok); // truectx.certify() injects the signal context into the bundle automatically. ctx.contextHash (in @nexart/signals v0.8.1+) returns the same value as bundle.context.contextHash — one canonical name across both packages.
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 NexArt Attestation Node (optional)
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 returns an independently verifiable signed receipt (when the node is configured for signing).
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.8.0" |
| 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 |
| toolCalls | Optional | ToolEvent[] | v0.7.0+ tool/dependency evidence records. Included in certificateHash when present. See Level 4 section. |
| identity | Optional | AiExecutionIdentity | v0.23.0+ externally verified identity assertion { provider, sub, assertionHash, emailHash?, verified }. Included in certificateHash when present. See Identity Binding section. |
Auto-generated fields (set by createSnapshot, do not set manually): type, protocolVersion, executionSurface, inputHash, outputHash.
Identity Binding (v0.23.0+)
The optional snapshot.identity field seals an externally verified identity
assertion into the CER. Because it lives on the snapshot, it is bound by
certificateHash with no change to canonicalization, the signed payload, or any
golden vector — bundles without it hash byte-for-byte as before.
import { createSnapshot, sealCer, type AiExecutionIdentity } from '@nexart/ai-execution';
// Your app verifies the IdP token (JWKS), then normalizes + hashes PII first.
const identity: AiExecutionIdentity = {
provider: 'google', // IdP that issued the assertion
sub: '1234567890', // stable subject anchor (never PII)
assertionHash: 'sha256:...', // one-way hash of the raw token
emailHash: 'sha256:...', // optional one-way hash of the email
verified: true, // your app confirmed the assertion
};
const snapshot = createSnapshot({ /* ...params... */, identity });
const bundle = sealCer(snapshot);Boundaries. The SDK never verifies a JWT, fetches IdP keys (JWKS), or reads
request headers/cookies — it stays deterministic and offline. Your application
performs verification and PII hashing, then assigns identity before sealing.
The field is PII-safe by design: only the stable provider/sub anchor and
one-way hashes are carried — never a raw email or other PII.
CER Bundle Format
{
"bundleType": "cer.ai.execution.v1",
"certificateHash": "sha256:...",
"createdAt": "2026-02-12T00:00:00.000Z",
"version": "0.1",
"snapshot": { "..." : "..." },
"context": {
"signals": [
{ "type": "approval", "source": "github-actions", "step": 0,
"timestamp": "2026-02-12T00:00:00.000Z", "actor": "alice",
"status": "ok", "payload": { "pr": 42 } }
]
},
"meta": { "source": "my-app", "tags": ["production"] },
"declaration": {
"stabilitySchemeId": "nexart-cer-v1",
"protectedSetId": "ai.execution.v1.full",
"protectedFields": ["snapshot", "bundleType", "version", "createdAt"],
"notes": "optional free text"
}
}context is optional and only appears when signals are supplied. Bundles without context are structurally identical to all prior versions.
Certificate Hash Computation
The certificateHash is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: { bundleType, version, createdAt, snapshot } — or, when context signals are present: { bundleType, version, createdAt, snapshot, context }. meta and declaration are always excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
Context and backward compatibility: when no signals are provided (or an empty array is passed), context is omitted from the hash payload entirely. The resulting certificateHash is byte-for-byte identical to a pre-v0.10.0 bundle with the same snapshot. Passing signals is strictly additive — it can only extend the hash, never break it for existing callers.
Context Visibility Layer (v0.16.0+)
When a bundle is sealed with signals, v0.16.0 surfaces context as a first-class, queryable layer on top of the existing evidence. These fields are populated after certificateHash is computed and are excluded from the hash payload — they are purely informational.
bundle.context = {
signals: [...], // existing — INCLUDED in certificateHash
hasContext: true, // v0.16.0 — excluded from certificateHash
contextHash: 'sha256:...', // v0.16.0 — excluded from certificateHash
signalCount: 3, // v0.16.0 — excluded from certificateHash
};
bundle.contextSummary = { // v0.16.0 — excluded from certificateHash
count: 3,
types: { approval: 2, review: 1 },
stepRange: { min: 0, max: 2 },
};certificateHash vs contextHash
These are two independent integrity digests with different purposes:
certificateHash— covers the entire execution evidence (bundleType,version,createdAt,snapshot, andcontext.signalswhen present). Verifies the execution record as a whole. Computed unchanged across all SDK versions.contextHash— covers ONLY the signal collection. Lets UIs and verifiers display context integrity independently of the wider certificate. Algorithm: stable sort bystepascending → canonical JSON (sorted keys, undefined dropped) →sha256:<64-hex>. Identical byte-for-byte to@nexart/signals.hashSignals(no runtime dependency — algorithm shared by convention).
Tampering with hasContext, contextHash, signalCount, or contextSummary does not invalidate certificateHash. They are advisory display fields — verifiers MUST treat the underlying signals array as the source of truth.
Helpers
import {
getContextInfo,
computeContextHash,
summarizeContext,
buildContextInfo,
} from '@nexart/ai-execution';
const info = getContextInfo(bundle);
// { hasContext: true, contextHash: 'sha256:...', signalCount: 3, summary: { ... } }
// or { hasContext: false } for context-less bundles.
const hash = computeContextHash(signals); // 'sha256:<64-hex>'
const summary = summarizeContext(signals); // ContextSummary
const compact = buildContextInfo(signals); // CerContextInfogetContextInfo() is safe for any bundle — it returns { hasContext: false } when no signals are present, and recomputes contextHash / summary on the fly if the bundle was produced by an older SDK that did not emit them.
Declaration Block (v0.7.0+)
The optional declaration field is a self-describing metadata block for AIEF-02 conformance. It carries stabilitySchemeId, protectedSetId, and protectedFields so that verifiers can confirm which fields are covered by the certificate hash without side-channel knowledge.
declaration is excluded from certificateHash by design. It is purely informational — mutating it does not invalidate the bundle. Pass it via sealCer(snapshot, { declaration: { ... } }).
Verifiers MUST treat declaration as advisory. For cer.ai.execution.v1, the protected set is defined by the bundleType semantics: { bundleType, version, createdAt, snapshot } is what is hashed.
const bundle = sealCer(snapshot, {
declaration: {
stabilitySchemeId: 'nexart-cer-v1',
protectedSetId: 'ai.execution.v1.full',
protectedFields: ['snapshot', 'bundleType', 'version', 'createdAt'],
},
});
// verifyCer(bundle).ok === true — declaration does not affect the resultCER Packages (v0.12.0+)
A CER package is a transport/export envelope that wraps a sealed AI Execution CER — cer.ai.execution.v1 or cer.ai.execution.v2 — with optional receipt, signature, and attestation metadata. The inner CER is carried verbatim: packaging (and export-verification) never reseals or re-certifies it. Verification layers are separate concerns: artifact integrity (offline hash/protected-set verification), node authenticity (receipt/signature against node keys), and RFC 3161 external time (optional trusted timestamp) — each can pass or be absent independently.
{
"cer": { ...sealed cer.ai.execution.v1 bundle... },
"receipt": { ...optional attestation receipt... },
"signature": "base64url...",
"attestation": { ...optional attestation summary... },
"verificationEnvelope": { ...optional envelope metadata... },
"verificationEnvelopeSignature": "base64url..."
}When to use package helpers vs raw bundle helpers
| Use case | Recommended API |
|---|---|
| Creating, verifying, or archiving a CER locally | certifyDecision, verifyCer, exportCer / importCer (raw bundle) |
| Wrapping a CER for transport or external storage with optional metadata | createCerPackage, exportCerPackage, importCerPackage |
| Detecting whether an object is a package or a raw bundle | isCerPackage |
| Extracting the CER from a received package | getCerFromPackage |
| Verifying integrity of a received package's inner CER | verifyCerPackage |
The cer field inside a package is authoritative. Package helpers never mutate it, never re-hash it, and never inject package-level fields into the CER.
Creating and exporting a package
import { certifyDecision, createCerPackage, exportCerPackage } 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 },
});
const pkg = createCerPackage({
cer,
receipt: myAttestationReceipt, // optional
signature: 'base64url...', // optional
});
const json = exportCerPackage(pkg); // stable canonical JSONImporting and verifying a package
import { importCerPackage, verifyCerPackage, getCerFromPackage } from '@nexart/ai-execution';
// importCerPackage parses + validates shape + verifies inner CER hash
const pkg = importCerPackage(receivedJsonString); // throws CerVerificationError on failure
// Alternatively: parse yourself then verify
const result = verifyCerPackage(parsedObj);
if (!result.ok) throw new Error(result.errors.join('; '));
const cer = getCerFromPackage(parsedObj); // throws if not a valid packageDesign constraints of package helpers:
- Additive only — no changes to CER hashing, canonicalization, or
verifyCer()semantics verifyCerPackageonly verifies the innercerbundle; receipt/signature/envelope fields are opaque transport metadata not verified here- All existing raw bundle flows (
importCer,exportCer,verifyCer) are unchanged
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.
Enterprise Trust Layers (Anchoring · Trusted Timestamps · Key Lifecycle · External Verification)
These four capabilities are strictly additive and NON-EVIDENTIARY: they are never part of the
certificateHash, the canonicalization, the signed receipt payload, or protocolVersion. Bundles
created with 1.2.0 and 1.3.0 verify byte-for-byte unchanged whether or not these layers are
present. Each layer is opt-in and reported as a separate secondary-proof status — it can never
change the base verifyCer result (ok/code) or the CLI exit code. See SPEC.md for the full
contract.
Anchoring Layer
Anchors attach external proof that a certificateHash existed at a point in time (transparency log,
TSA, or blockchain). They are excluded from the hashed/signed payload and verified independently.
import { attachAnchor, verifyAnchors, verifyCer } from '@nexart/ai-execution';
const anchored = attachAnchor(bundle, anchor); // pure: returns a new bundle, append-only
const status = verifyAnchors(anchored); // { checked, valid, errors? }
// or fold it into a single verification pass (base outcome is unaffected):
const result = verifyCer(anchored, { verifyAnchors: true }); // result.anchorStatusA bundle with no anchors returns { checked: false, valid: true }. The SDK does not generate
anchors; they are produced by external systems and merely carried.
Trusted Timestamp Layer
Trusted timestamps (RFC 3161) attest to existence-in-time and follow the same non-evidentiary rules.
import { attachTrustedTimestamp, verifyTrustedTimestamps, verifyCer } from '@nexart/ai-execution';
const stamped = attachTrustedTimestamp(bundle, {
type: 'rfc3161', tsa: 'https://freetsa.org/tsr',
timestamp: '2026-04-26T00:06:00.000Z', token: '<base64 RFC 3161 token>',
});
const status = verifyTrustedTimestamps(stamped); // offline: structure + ordering only
const result = verifyCer(stamped, { verifyTimestamps: true }); // result.timestampStatusVerification is fully offline: it validates the timestamp structure and that each timestamp
is at or after bundle.createdAt (a missing/unparseable createdAt is reported as invalid because
ordering cannot be established). It does not contact a TSA or decode the token.
Cryptographic RFC 3161 Verification (v1.3.0+, optional)
For full cryptographic proof, the separate subpath export @nexart/ai-execution/rfc3161
verifies the token itself — CMS signature, TSA certificate chain, message-imprint binding to
the bundle's certificateHash, certificate validity window, and genTime >= createdAt:
import { verifyRfc3161Token } from '@nexart/ai-execution/rfc3161';
import { readFileSync } from 'node:fs';
const res = await verifyRfc3161Token(bundle, {
trustAnchorsPem: readFileSync('digicert-assured-id-root.pem', 'utf-8'),
});
// res.status: 'verified' | 'invalid' | 'untrusted' | 'unsupported' | 'NOT_PRESENT'
// res.entries[i]: { verdict, tsa, genTime, reasons }- Per-entry verdicts:
verified(all checks pass under a supplied trust anchor),invalid(any cryptographic check fails),untrusted(crypto sound but the chain does not reach a supplied anchor),unsupported(unknowntokenFormat— skipped but always reported, never silently dropped). Bundle status is the worst entry verdict, orNOT_PRESENTwhentrustedTimestampsis absent. - Optional peer dependencies: requires
pkijs+asn1js(npm install pkijs asn1js), lazy-loaded only when a supported token is actually present. The core package keeps zero runtime dependencies; importing@nexart/ai-executionnever touches them. - Strictly additive: a parallel verdict dimension — never affects
verifyCer, structuralverifyTrustedTimestamps, hashes, or any existing result. - Verifies tokens with
tokenFormat: "rfc3161-timestampresp-der-base64"(fullTimeStampResp; bareTimeStampTokens are also accepted). Both message-imprint bindings are accepted (v1.3.1+, parity with the CLI): the raw cert-hash digest binding used by NexArt Canonical Node tokens (imprint hex == the<hex>of"sha256:<hex>"— equivalent toopenssl ts -verify -digest <certhash-hex> -in resp.tsr -CAfile <root.pem>), or the legacy full-string binding (imprint = sha256 of the FULL"sha256:<hex>"certificateHash string).
Key Lifecycle Validation
A node signing key may carry optional lifecycle fields in the published NodeKeysDocument:
notBefore, notAfter, and revoked. validateKeyLifecycle(key, attestedAt) gates whether a key
may be used to trust a signature:
revoked: true→KEY_REVOKEDattestedAtbeforenotBeforeor afternotAfter→SCHEMA_KEY_INVALID- missing lifecycle fields → valid (legacy keys remain accepted)
import { validateKeyLifecycle } from '@nexart/ai-execution';
const check = validateKeyLifecycle(key, bundle.snapshot.timestamp); // { valid, code? }Because verifyCer is offline and keyless, lifecycle enforcement runs in the attestation/receipt
verification path (verifyBundleAttestation) — the only place key material is available — after
the signing key is selected and before its signature is trusted.
External Verifier Readiness
exportVerificationPackage(bundle, options?) produces a self-contained, read-only package a third
party can verify without the NexArt node or this SDK. It re-hashes/re-signs nothing.
import { exportVerificationPackage } from '@nexart/ai-execution';
const pkg = exportVerificationPackage(bundle, { nodeKeys });
// → { bundle, publicKey, verificationInstructions }
// verificationInstructions: { spec, protocolVersion, canonicalMode, certificateHash,
// signatureAlgorithm, layers, steps[], notes[] }The resolved publicKey comes from options.publicKey, else the selected key from options.nodeKeys,
else null. The instructions explicitly mark anchors and timestamps as non-evidentiary secondary
layers.
AIEF Interop (v0.7.0+)
verifyAief(bundle) is an adapter over the existing verifyCer() / verify(). It produces an AIEF v0.2.5 Section 9 projection for cross-vendor verifier interoperability, and supports both V1 and V2 AI Execution CERs. The §9 result shape is: result, reason, and a checks object containing schemaSupported, integrityValid, protectedSetValid, chainValid. (There is no "AIEF v2" — the interoperability target remains AIEF v0.2.5; this is a projection, not a compliance certification.) The internal verify() return value is unchanged — verifyAief is purely additive.
import { verifyAief } from '@nexart/ai-execution';
const result = verifyAief(bundle);
// {
// result: 'PASS' | 'FAIL',
// reason: string | null, // null on PASS; AIEF §9.2 reason string on FAIL
// checks: {
// schemaSupported: boolean, // false if SCHEMA_ERROR or CANONICALIZATION_ERROR
// integrityValid: boolean, // false if any hash mismatch
// protectedSetValid: boolean, // false if any hash mismatch (mirrors integrityValid for v1)
// chainValid: boolean, // false only if CHAIN_BREAK_DETECTED
// },
// notes?: string[], // failure detail strings when available
// }What we map / what we don't rename:
| NexArt CerVerifyCode | AIEF reason |
|---|---|
| OK | ok (reason is null) |
| CERTIFICATE_HASH_MISMATCH | integrityProofMismatch |
| INPUT_HASH_MISMATCH | integrityProofMismatch |
| OUTPUT_HASH_MISMATCH | integrityProofMismatch |
| TOOL_OUTPUT_HASH_MISMATCH | integrityProofMismatch |
| SCHEMA_ERROR | unsupportedSchema |
| CANONICALIZATION_ERROR | malformedArtifact |
| INVALID_SHA256_FORMAT | malformedArtifact |
| UNKNOWN_ERROR | malformedArtifact |
| INCOMPLETE_ARTIFACT | incompleteArtifact |
| TOOL_EVIDENCE_MISSING | incompleteArtifact |
| CHAIN_BREAK_DETECTED | chainBreakDetected |
| VERIFICATION_MATERIAL_UNAVAILABLE | verificationMaterialUnavailable |
| ATTESTATION_MISSING | verificationMaterialUnavailable |
| ATTESTATION_KEY_NOT_FOUND | verificationMaterialUnavailable |
| ATTESTATION_KEY_FORMAT_UNSUPPORTED | verificationMaterialUnavailable |
| ATTESTATION_INVALID_SIGNATURE | signatureInvalid |
The AIEF reason strings are not renamed or mapped to NexArt-specific vocabulary — they are passed through verbatim for AIEF conformance. Per AIEF §9.0 rule #7, chainValid is true when chain fields are absent from the snapshot.
mapToAiefReason(code: string): string is also exported if you need to convert a CerVerifyCode to an AIEF reason string directly. Unknown codes fall back to "malformedArtifact".
Level 4 (Optional): Chain + Tool Evidence (v0.7.0+)
If you do nothing, nothing changes. All Level 4 features are additive and opt-in. Existing bundles without
toolCallsverify identically.
Tool Calls
Add a toolCalls array to the snapshot to record evidence of external tool or dependency invocations. When present, toolCalls is included in the certificateHash computation — tool evidence is part of the sealed record.
toolCalls provide tamper-evident evidence of what was recorded about a tool/dependency call. They do not prove the external tool actually executed unless the tool itself provides independent verifiable proof (e.g., its own signed receipt).
import { makeToolEvent, createSnapshot, sealCer } from '@nexart/ai-execution';
const webResult = await fetch('https://api.example.com/data');
const data = await webResult.json();
const toolEvent = makeToolEvent({
toolId: 'web-search',
output: data, // hashed automatically
input: { query: 'Q1 revenue' }, // optional; hashed if provided
evidenceRef: 'https://api.example.com/data', // optional URL/ID
});
const snapshot = createSnapshot({
// ...
toolCalls: [toolEvent],
});
const bundle = sealCer(snapshot);ToolEvent shape:
| Field | Required | Description |
|---|---|---|
| toolId | Yes | Identifier of the tool/dependency called |
| at | Yes | ISO 8601 timestamp (defaults to new Date() in makeToolEvent) |
| outputHash | Yes | sha256:<64hex> of the tool output |
| inputHash | No | sha256:<64hex> of the tool input (optional) |
| evidenceRef | No | URL or external ID pointing to the raw evidence |
| error | No | Error message if the tool call failed |
hashToolOutput(value) hashes a tool output value: strings → SHA-256 of UTF-8 bytes; anything else → SHA-256 of canonical JSON bytes.
Chain Verification
verifyRunSummary(summary, bundles, opts?) validates that a RunBuilder multi-step run forms an unbroken cryptographic chain. It detects insertion, deletion, and reordering of steps.
import { RunBuilder, verifyRunSummary } from '@nexart/ai-execution';
const run = new RunBuilder({ runId: 'my-run' });
run.step({ /* step 0 */ });
run.step({ /* step 1 */ });
const summary = run.finalize();
const bundles = run.getBundles(); // or retrieve from storage
const result = verifyRunSummary(summary, bundles);
// { ok: boolean, code: CerVerifyCode, errors: string[], breakAt?: number }verifyRunSummary returns RunSummaryVerifyResult:
ok: true— full chain is validok: falsewithINCOMPLETE_ARTIFACT— step count mismatchok: falsewithCHAIN_BREAK_DETECTED— stepIndex/prevStepHash/certificateHash mismatch;breakAtis the index of the first broken link
Profiles (Opt-in Strictness)
validateProfile(target, profile) applies extra field-presence checks at creation time. It never affects certificateHash or verifyCer().
| Profile | What it enforces |
|---|---|
| 'flexible' | No extra validation (default SDK behaviour) |
| 'AIEF_L2' | AIEF-01 required fields: executionId, timestamp, provider, model, input, output, inputHash, outputHash |
| 'AIEF_L3' | Same as AIEF_L2 |
| 'AIEF_L4' | AIEF_L3 + validates each ToolEvent in toolCalls; requires prevStepHash when stepIndex > 0 |
import { validateProfile } from '@nexart/ai-execution';
const result = validateProfile(bundle, 'AIEF_L4');
// { ok: boolean, errors: string[] }Opinionated Run Helper
certifyAndAttestRun(steps, options?) combines RunBuilder step creation, optional per-step attestation, and finalization into a single call. It does not change RunBuilder semantics — it creates an internal RunBuilder and returns all artifacts together.
import { certifyAndAttestRun, verifyRunSummary, attest } from '@nexart/ai-execution';
const { runSummary, stepBundles, receipts, finalStepHash } =
await certifyAndAttestRun(
[step0Params, step1Params, step2Params],
{
runId: 'analysis-run',
workflowId: 'data-pipeline',
// Optional: attest each step immediately after sealing
attestStep: (bundle) => attest(bundle, { nodeUrl, apiKey }),
},
);
verifyRunSummary(runSummary, stepBundles); // { ok: true }Return shape:
| Field | Type | Description |
|---|---|---|
| runSummary | RunSummary | From RunBuilder.finalize() — stepCount, steps, finalStepHash |
| stepBundles | CerAiExecutionBundle[] | Sealed bundles in step order (index 0 = step 0) |
| receipts | (AttestationReceipt \| null)[] | Attestation receipts in step order; null if attestStep was not provided |
| finalStepHash | string \| null | Alias for runSummary.finalStepHash |
Testing without network: inject a mock attestStep to test the full flow without hitting a node:
const { runSummary, stepBundles, receipts } = await certifyAndAttestRun(steps, {
attestStep: async (bundle) => ({
attestationId: 'mock-' + bundle.certificateHash.slice(7, 15),
certificateHash: bundle.certificateHash,
nodeRuntimeHash: 'sha256:' + 'a'.repeat(64),
protocolVersion: '1.2.0',
}),
});Redaction Semantics (v0.7.0+)
Pre-seal verifiable redaction
redactBeforeSeal(snapshot, policy) replaces sensitive snapshot fields with stable envelopes before sealing. Because the certificateHash is computed over the already-redacted snapshot, the resulting bundle passes verifyCer() unchanged.
import { redactBeforeSeal, sealCer, verify } from '@nexart/ai-execution';
const redacted = redactBeforeSeal(snapshot, { paths: ['input', 'output'] });
const bundle = sealCer(redacted);
verify(bundle).ok; // true — hash matches the redacted snapshotEach redacted field becomes { _redacted: true, hash: "sha256:..." } where hash is the SHA-256 of the original value. This lets authorized reviewers confirm what was there without accessing the raw content.
Supported fields for pre-seal redaction:
| Field | Supported | Notes |
|---|---|---|
| input | Yes | inputHash is recomputed from the envelope |
| output | Yes | outputHash is recomputed from the envelope |
| toolCalls[n].output (via ToolEvent.outputHash) | Yes | The outputHash already stores only the hash — the raw tool output need not be in the bundle at all |
| prompt and other schema-validated strings | No | verifySnapshot validates these as non-empty strings. Replacing with an object envelope causes verifyCer() to return SCHEMA_ERROR. |
Verifiable redacted export (post-seal, new bundle)
exportVerifiableRedacted(bundle, policy, options?) is the right choice when you already have a sealed bundle and want to share a sanitized version that is still independently verifiable. It:
- Applies
redactBeforeSeal()to the original snapshot - Re-seals the redacted snapshot into a new bundle with a new
certificateHash - Stores the original
certificateHashinmeta.provenance.originalCertificateHashas an informational cross-reference
import { certifyDecision, exportVerifiableRedacted, verify } from '@nexart/ai-execution';
const original = certifyDecision({ ... });
const { bundle, originalCertificateHash } = exportVerifiableRedacted(
original,
{ paths: ['input', 'output'] },
);
verify(bundle).ok; // true — new bundle verifies
bundle.certificateHash !== original.certificateHash; // true — different hash
bundle.meta.provenance.originalCertificateHash; // 'sha256:...' — reference only
bundle.snapshot.input; // { _redacted: true, hash: 'sha256:...' }Provenance semantics: meta.provenance.originalCertificateHash is reference metadata only. It is not part of the new certificateHash computation. Recipients of the new bundle cannot verify the original bundle's integrity from it — they can only confirm what hash the original had. If you need to prove the original's integrity, keep the original bundle available alongside the redacted one.
Rule of thumb for choosing a redaction approach:
| Approach | verify() passes | Original hash preserved | Notes |
|---|---|---|---|
| redactBeforeSeal(snapshot, policy) + sealCer() | ✅ | N/A — no original exists yet | Use when building a redacted bundle from scratch |
| exportVerifiableRedacted(bundle, policy) | ✅ | Reference only in meta.provenance | Use when you have a sealed bundle and need a shareable sanitized copy |
| sanitizeForStorage(bundle, options) | ❌ | N/A — hash broken by design | Use only for storage; do not pass result to verify() |
Post-hoc redaction breaks integrity — by design
sanitizeForStorage(bundle, options) can replace arbitrary paths with a string placeholder, but it does not recompute any content hashes. This means verifyCer() will return CERTIFICATE_HASH_MISMATCH on the result. This is intentional: post-hoc redaction is a lossy, storage-only operation. If you need a verifiable record after storage-side redaction, use exportVerifiableRedacted instead.
Confidential Execution Mode (v1.1.0+)
Confidential Execution Mode lets you commit sensitive fields before sealing, so the CER carries only a cryptographic commitment — never the plaintext. An attestation node (or anyone else holding the bundle) can still verify the record's authenticity, but cannot read the committed content. Later, whoever holds the plaintext and its salt can prove exactly what was committed.
This is opt-in and additive. Nothing changes for bundles that don't use it — sealCer, verifyCer, canonicalization, hashing, and every golden vector are byte-for-byte unchanged. A non-confidential bundle has no confidential block and hashes identically to v1.0.0.
How it works
For each committed field, the SDK computes:
commitment = "hmac-sha256:" + hex( HMAC-SHA256( key = salt, message = domainSep ‖ content ) )
domainSep = canonicalJson({ scheme, field, protocolVersion }) ‖ 0x00- Hiding rests on the salt being secret and high-entropy (256-bit CSPRNG).
- Binding rests on HMAC-SHA256 (salt secrecy not required for binding).
- Domain separation binds every commitment to its scheme, field name, and
protocolVersion, so a commitment minted for one field/profile cannot be replayed as another.
The committed field is replaced in the snapshot by an envelope, and the set of committed fields is recorded in an additive snapshot.confidential block that is sealed into certificateHash (so it is tamper-evident):
// the field's place in the sealed snapshot:
{ "_redacted": true, "mode": "confidential", "commitmentScheme": "hmac-sha256-v1", "commitment": "hmac-sha256:<64 hex>" }
// plus, on the snapshot:
"confidential": { "scheme": "hmac-sha256-v1", "fields": ["input", "output"] }The salts are returned to you as openings and are never written into the CER. The data owner keeps them; they are required to later prove a field's content.
Seal confidentially
import { createSnapshot, sealConfidential, verifyConfidential } from '@nexart/ai-execution';
const snapshot = createSnapshot({
executionId: 'exec-confidential-001',
provider: 'openai',
model: 'gpt-4o',
prompt: 'Summarize the patient note.',
input: 'PHI: patient record ...',
output: 'De-identified summary ...',
parameters: { temperature: 0.2, maxTokens: 512, topP: null, seed: null },
});
// Commit input + output (the default fields). Feature gate is mandatory.
const { cer, openings } = sealConfidential(snapshot, { confidential: true });
// `cer` is a normal, fully-sealed CER — but input/output hold only commitments.
// `openings` is [{ field, salt, scheme }, ...]. Store the salts securely; they
// are NOT in the CER and are required to prove content later.- Fields: defaults to
['input', 'output']. Passfields: ['input', 'output', 'prompt']to also commit the prompt — butpromptis only allowed on a legacy1.2.0protocolVersion (the1.3.0schema requirespromptto stay a string), and fails closed otherwise. - Deterministic salts: for testing you may supply
salts: { input: '<64 hex>' }; otherwise a fresh 256-bit salt is generated per field. - Async: use
sealConfidentialAsyncin browsers / edge runtimes (WebCrypto). It is byte-for-byte identical to the sync path.
Verify (authenticity, and optionally content)
// 1) Anyone can verify authenticity WITHOUT plaintext:
const r = verifyConfidential(cer);
r.state; // 'AUTHENTIC' — the record is genuine; content was not checked
r.authentic; // true
// 2) The data owner proves content by supplying plaintext + the matching salt:
const inputOpening = openings.find((o) => o.field === 'input')!;
const proven = verifyConfidential(cer, [
{ field: 'input', salt: inputOpening.salt, plaintext: 'PHI: patient record ...' },
]);
proven.state; // 'VERIFIED' — authentic AND every supplied opening matchedverifyConfidential returns a ConfidentialVerifyResult with an aggregate state (priority INVALID > UNVERIFIABLE > VERIFIED > AUTHENTIC), the underlying base (verifyCer) result, and per-field fields[]:
| state | Meaning |
|---|---|
| AUTHENTIC | Record is authentic; no plaintext supplied, so content was not checked. |
| VERIFIED | Record is authentic and every supplied opening matched its commitment. |
| UNVERIFIABLE | An opening was supplied without a usable (256-bit hex) salt. |
| INVALID | Base verification failed, a commitment mismatched, or the bundle/envelope/scheme was malformed (fail-closed). |
Use verifyConfidentialAsync for the WebCrypto path.
Fail-closed guarantees
Unknown commitment scheme, malformed commitment string, unknown protocolVersion (for both string and object content), weak/missing salt, an unknown field name, and committing prompt on a non-legacy protocolVersion all fail closed. There is never a fallback to an unkeyed sha256(content).
Low-level commitment primitive
If you need to compute or check commitments yourself:
import { generateSalt, computeCommitment } from '@nexart/ai-execution';
const salt = generateSalt(); // 256-bit, 64 lowercase hex chars
const commitment = computeCommitment({
scheme: 'hmac-sha256-v1',
field: 'input',
protocolVersion: '1.2.0',
content: 'What is 2+2?', // string → UTF-8; object → canonical JSON for the protocolVersion
salt,
});
// "hmac-sha256:<64 hex>" — use computeCommitmentAsync for WebCryptoCanonical 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.
1.2.0 canonicalization is frozen. 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 of 1.2.0, not bugs. Stricter RFC 8785 (JCS) canonicalization ships additively as protocolVersion 1.3.0 (jcs-v1), never as a change to 1.2.0.
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 (Node 18+, uses crypto.createHash) |
| sealCer(snapshot, options?) | Seal snapshot into CER bundle |
| verify(bundle) / verifyCer(bundle) | Verify CER bundle (Node 18+, uses crypto.createHash) |
| certifyDecision(params) | One-call: createSnapshot + sealCer. Accepts optional signals?: CerContextSignal[] — included in certificateHash when present (v0.10.0+) |
Async Verification — Browser-safe (v0.14.0+)
Use these functions whenever Node's built-in crypto module is unavailable: browsers, edge workers (Cloudflare Workers, Vercel Edge), Deno, React Native, or any context where import 'crypto' fails. They use globalThis.crypto.subtle.digest (Web Crypto API) and produce byte-for-byte identical verification outcomes to their sync counterparts.
If you are running on Node 18+ only, use the sync functions — they are simpler and equally correct. Use the async functions when you need the same SDK-owned verification to work in browser environments without maintaining a separate reimplementation.
| Function | Description |
|---|---|
| verifyCerAsync(bundle) | Async mirror of verifyCer(). Returns Promise<VerificationResult>. Use in browser apps to verify a received CER bundle without Node's crypto module. |
| verifyProjectBundleAsync(bundle) | Async mirror of verifyProjectBundle(). Returns Promise<ProjectBundleVerifyResult>. Use in browser apps to verify a full project bundle. |
| verifySnapshotAsync(snapshot) | Async mirror of verifySnapshot(). Returns Promise<VerificationResult>. Primarily consumed internally by verifyCerAsync. |
| sha256HexAsync(data) | WebCrypto SHA-256 primitive. Returns Promise<string> (64-char hex). Accepts string or Uint8Array. |
| hashUtf8Async(value) | sha256: + SHA-256 of a UTF-8 string (async). |
| hashCanonicalJsonAsync(value) | sha256: + SHA-256 of canonical JSON of value (async). |
| computeInputHashAsync(input) | Async mirror of computeInputHash(). |
| computeOutputHashAsync(output) | Async mirror of computeOutputHash(). |
Confidential Execution Mode (v1.1.0+)
See "Confidential Execution Mode" for the full guide.
| Function | Description |
|---|---|
| sealConfidential(snapshot, options) | Commit chosen fields then seal via the existing sealCer. Requires { confidential: true } (fail-closed). Returns { cer, openings }; salts (openings) are never written into the CER. |
| sealConfidentialAsync(snapshot, options) | WebCrypto mirror of sealConfidential(). Byte-for-byte identical. |
| verifyConfidential(bundle, openings?) | Verify authenticity via verifyCer, plus optional content proof from supplied { field, salt, plaintext } openings. Returns ConfidentialVerifyResult (state: AUTHENTIC / VERIFIED / UNVERIFIABLE / INVALID). |
| verifyConfidentialAsync(bundle, openings?) | WebCrypto mirror of verifyConfidential(). |
| computeCommitment(params) | Low-level commitment primitive: hmac-sha256:<hex> = HMAC-SHA256(salt, domainSep ‖ content). Fail-closed on unknown scheme / weak salt / unknown protocolVersion. |
| computeCommitmentAsync(params) | WebCrypto mirror of computeCommitment(). |
| generateSalt() | Fresh 256-bit salt (64 lowercase hex chars) from a CSPRNG. |
New types: ConfidentialEnvelope, ConfidentialBlock, SealConfidentialOptions, SealConfidentialResult, Opening, ConfidentialOpeningInput, ConfidentialVerifyResult, ConfidentialFieldResult, ConfidentialVerifyState, ConfidentialField, CommitmentParams, CommitmentScheme. Optional snapshot.confidential?: ConfidentialBlock.
Workflow
| Export | Description |
|---|---|
| RunBuilder | Multi-step workflow builder with prevStepHash chaining |
| verifyRunSummary(summary, bundles, opts?) | Verify that a RunSummary + step bundles form an unbroken cryptographic chain (v0.7.0+) |
| certifyAndAttestRun(steps, options?) | One-call: create RunBuilder internally, certify all steps, optionally attest each bundle, return { runSummary, stepBundles, receipts, finalStepHash }. Injectable attestStep for mocking. |
AIEF Interop (v0.7.0+)
| Function | Description |
|---|---|
| verifyAief(bundle) | Verify a CER bundle and return the exact AIEF §9.1 output shape |
| mapToAiefReason(code) | Convert a CerVerifyCode string to an AIEF §9.2 reason string |
| hashToolOutput(value) | Hash a tool output value: string → UTF-8 SHA-256; other → canonical JSON SHA-256 |
| makeToolEvent(params) | Build a ToolEvent record for inclusion in snapshot.toolCalls |
| redactBeforeSeal(snapshot, policy) | Pre-seal redaction: replace input/output with verifiable envelopes before sealing |
| exportVerifiableRedacted(bundle, policy, options?) | Post-seal: produce a new sealed bundle with redacted snapshot + meta.provenance.originalCertificateHash. verify() passes on the new bundle. Original is unchanged. |
| validateProfile(target, profile) | Validate a snapshot or bundle against an AIEF strictness profile (does not affect hashing) |
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 |
| sanitizeForStorage(bundle, options?) | Sanitize for DB storage with optional path-based redaction; does not recompute hashes (v0.6.0+) |
| sanitizeForStamp(bundle) | Extract attestable core (no meta); does not recompute hashes (v0.6.0+) |
| hasAttestation(bundle) | Check if bundle already has attestation fields |
| exportCer(bundle) | Serialize to canonical JSON string |
| importCer(json) | Parse + verify from JSON string |
CER Package Helpers (v0.12.0+)
| Function | Description |
|---|---|
| isCerPackage(value) | Type guard: returns true if value is a CER package shape ({ cer: { bundleType: 'cer.ai.execution.v1', ... }, ... }). Structural check only — does not verify the inner CER hash. |
| createCerPackage(params) | Assemble a CER package from a sealed CER and optional transport fields (receipt, signature, attestation, verificationEnvelope, verificationEnvelopeSignature). Assembly only — does not re-hash or re-sign. |
| getCerFromPackage(pkg) | Extract and return the inner CER bundle. Throws CerVerificationError if pkg is not a valid package shape. |
| exportCerPackage(pkg) | S
