JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 258
  • Score
    100M100P100Q101563F
  • License MIT

AI Execution Integrity — tamper-evident records and Certified Execution Records (CER) for AI operations

Package Exports

  • @nexart/ai-execution
  • @nexart/ai-execution/langchain
  • @nexart/ai-execution/package.json
  • @nexart/ai-execution/providers/anthropic
  • @nexart/ai-execution/providers/openai
  • @nexart/ai-execution/providers/wrap

Readme

@nexart/ai-execution v1.2.0

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

Component Version
Service
SDK 1.2.0
Protocol 1.2.0 (default) · 1.3.0 (supported) · 1.3.1 (confidential, first-class)

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, and 1.2.0 is frozen. The 1.2.0 mode (nexart-v1) uses JSON.stringify() number formatting (consistent across JavaScript engines) and is byte-frozen — never modified in place. Stricter RFC 8785 (JCS) canonicalization ships additively as protocolVersion 1.3.0 (jcs-v1), opt-in and backward-compatible, rather than as a change to 1.2.0. See "protocolVersion: 1.2.0 vs 1.3.0" below and SPEC.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 via protocolVersion: '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-v1 canonicalisation as 1.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 with protocolVersion: '1.3.1' whose input / output are confidential commitment envelopes; verifyCer then strictly validates the envelope shape and the snapshot.confidential block, and reports a verdict (VERIFIED for full snapshots, VERIFIED_CONFIDENTIAL when 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.

Installation

npm install @nexart/ai-execution

Quick 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);            // true

ctx.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); // true

Agentic 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, and context.signals when 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 by step ascending → 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);         // CerContextInfo

getContextInfo() 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 result

CER Packages (v0.12.0+)

A CER package is a transport/export envelope that wraps a sealed cer.ai.execution.v1 bundle with optional receipt, signature, and attestation metadata.

{
  "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 JSON

Importing 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 package

Design constraints of package helpers:

  • Additive only — no changes to CER hashing, canonicalization, or verifyCer() semantics
  • verifyCerPackage only verifies the inner cer bundle; 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 sanitizeForAttestation in v0.4.0+)
  • Returns: AttestationResult with attestationId, nodeRuntimeHash, certificateHash, protocolVersion
  • Default timeout: 10 seconds (configurable via timeoutMs)
  • Validates: response certificateHash matches submitted bundle; all hashes in sha256:<64hex> format
  • Throws: CerAttestationError on 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 proof

Skip 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 attested
  • getAttestationReceipt(bundle) — extracts a normalized receipt from any supported shape (top-level fields or bundle.meta.attestation); returns null if required fields are missing, never throws
  • attestIfNeeded(bundle, options) — skips the node call if a valid receipt is already present; prevents double-attestation
  • certifyAndAttestDecision(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 undefined values at all nesting levels
  • Rejects BigInt, functions, and symbols (throws)
  • Safe to serialize with JSON.stringify or 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.anchorStatus

A 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.timestampStatus

Verification 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.

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: trueKEY_REVOKED
  • attestedAt before notBefore or after notAfterSCHEMA_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 returns the exact output shape required by AIEF §9.1 for cross-vendor verifier interoperability. 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 toolCalls verify 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 valid
  • ok: false with INCOMPLETE_ARTIFACT — step count mismatch
  • ok: false with CHAIN_BREAK_DETECTED — stepIndex/prevStepHash/certificateHash mismatch; breakAt is 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 snapshot

Each 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:

  1. Applies redactBeforeSeal() to the original snapshot
  2. Re-seals the redacted snapshot into a new bundle with a new certificateHash
  3. Stores the original certificateHash in meta.provenance.originalCertificateHash as 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']. Pass fields: ['input', 'output', 'prompt'] to also commit the prompt — but prompt is only allowed on a legacy 1.2.0 protocolVersion (the 1.3.0 schema requires prompt to 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 sealConfidentialAsync in 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 matched

verifyConfidential 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 WebCrypto

Canonical JSON Constraints

  1. Object keys sorted lexicographically (Unicode codepoint order) at every nesting level.
  2. No whitespace between tokens.
  3. Array order preserved.
  4. null serialized as null.
  5. Numbers must be finite. NaN, Infinity, -Infinity rejected (throw).
  6. undefined values in object properties omitted (key dropped).
  7. BigInt, functions, Symbol rejected (throw).
  8. 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) Serialize a CER package to a stable canonical JSON string.
importCerPackage(json) Parse a CER package JSON string, validate its shape, and verify the inner CER with verifyCer(). Throws CerVerificationError on any failure.
verifyCerPackage(pkg) Verify the inner CER of a package using verifyCer(). Returns a VerificationResult. Conservative: only verifies the inner cer — does not verify receipt/signature/envelope.

Exported types: AiCerPackage, CreateCerPackageParams

Project Bundle Helpers (v0.13.0+)

Function Description
createProjectBundle(params) Assemble and finalize a project bundle from project metadata and a list of already-certified CER bundles. Derives totalSteps, step registry entries (with certificateHash + executionId per step), model identity, and integrity.projectHash deterministically. stepId and projectBundleId are auto-generated (UUID v4) when omitted. Input array order is authoritative; no reshuffling.
verifyProjectBundle(bundle) Fully verify a project bundle. Sync — uses Node's crypto.createHash. Use in Node 18+ server and CLI contexts. Returns ProjectBundleVerifyResult.
verifyProjectBundleAsync(bundle) Async mirror of verifyProjectBundle(). Uses Web Crypto (globalThis.crypto.subtle). Use this in browser apps, edge workers, or any non-Node runtime. Same verification model, same result shape, same error messages. Returns Promise<ProjectBundleVerifyResult>. (v0.14.0+)

Exported types: ProjectBundle, ProjectBundleIntegrity, ProjectBundleStepEntry, ProjectBundleStepType, ProjectBundleStepModelIdentity, ProjectBundleVerifyResult, ProjectBundleStepVerifyResult, CreateProjectBundleParams, CreateProjectBundleStepParams

CerVerifyCode values: OK, SCHEMA_ERROR, CERTIFICATE_HASH_MISMATCH, INPUT_HASH_MISMATCH, OUTPUT_HASH_MISMATCH, PROJECT_HASH_MISMATCH, STEP_REGISTRY_MISMATCH

Project Bundle Registration (v0.15.0+)

Register a finalized project bundle with a NexArt node over HTTP.

Function Description
registerProjectBundle(options) Pre-validate the bundle with verifyProjectBundle(), then POST it to {nodeUrl}/api/register/project-bundle with Bearer auth. Validates that the node response echoes projectBundleId and projectHash back correctly before resolving. Returns ProjectBundleRegistrationResult. Throws ProjectBundleRegistrationError on any failure.

RegisterProjectBundleOptions:

Field Type Required Description
projectBundle ProjectBundle The finalized project bundle to register
nodeUrl string Base URL of the NexArt node (e.g. https://node.nexart.io)
apiKey string Bearer token for node authentication
timeoutMs number? Request timeout in ms. Default: 30 000

ProjectBundleRegistrationResult:

Field Type Description
ok true Always true on success
registrationId string Node-assigned registration identifier
projectBundleId string Echoed from submitted bundle (validated to match)
projectHash string Echoed from submitted bundle (validated to match)
registeredAt string ISO 8601 timestamp from the node
nodeRuntimeHash string? Optional sha256: hash of the node's runtime version

ProjectBundleRegistrationError (extends Error):

Property Type Description
statusCode number? HTTP status code, if the failure was a non-200 response
details string[] One or more error messages describing the failure(s)

Design properties:

  • Pre-validates the bundle locally with verifyProjectBundle() before making any network call. An invalid bundle never reaches the network.
  • HTTP 200 alone is not treated as success — the response must echo projectBundleId and projectHash correctly to confirm the node persisted the exact submitted bundle.
  • The submitted bundle is never mutated.

Example:

import { registerProjectBundle, ProjectBundleRegistrationError } from '@nexart/ai-execution';

try {
  const result = await registerProjectBundle({
    projectBundle: myBundle,
    nodeUrl:       'https://node.nexart.io',
    apiKey:        process.env.NEXART_API_KEY!,
    timeoutMs:     15_000,
  });

  console.log('Registered:', result.registrationId);
  console.log('At:',        result.registeredAt);
} catch (err) {
  if (err instanceof ProjectBundleRegistrationError) {
    console.error('Registration failed:', err.message);
    console.error('Details:', err.details);
    console.error('HTTP status:', err.statusCode);
  }
}

Provider Drop-in (v0.6.0+)

Function Description
certifyDecisionFromProviderCall(params) One-function wrapper: extracts prompt/input/output/params from raw provider request+response and returns { ok, bundle } or { ok: false, code: 'SCHEMA_ERROR', reason }. Supports OpenAI, Anthropic, Gemini, Mistral, Bedrock, and generic shapes.

Opinionated Client (v0.6.0+)

Export Description
createClient(defaults) Returns a NexArtClient with bound defaults (appId, workflowId, nodeUrl, apiKey, tags, source). Methods: certifyDecision, certifyAndAttestDecision, verify, verifyBundleAttestation. Defaults do not affect bundle hashing.

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+)
CHAIN_BREAK_DETECTED verifyRunSummary detected a broken prevStepHash link or reordered step (v0.7.0+)
INCOMPLETE_ARTIFACT Step count mismatch between RunSummary and provided bundles (v0.7.0+)
VERIFICATION_MATERIAL_UNAVAILABLE Required verification material (keys, receipt) is absent (v0.7.0+)
TOOL_EVIDENCE_MISSING Required tool call evidence absent in an AIEF_L4 context (v0.7.0+)
TOOL_OUTPUT_HASH_MISMATCH A recorded outputHash in toolCalls does not match the provided tool output (v0.7.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

LangChain Integration

@nexart/ai-execution includes a minimal LangChain helper surface (introduced v0.10.0, current v0.13.0). Certify the final input and output of any LangChain chain, agent, or runnable as a tamper-evident CER — no LangChain package dependency required.

certifyLangChainRun operates in two modes depending on whether nodeUrl and apiKey are supplied:

Mode How to call Returns
Local (default) No nodeUrl / apiKey LangChainCerResult (sync)
Node-attested nodeUrl + apiKey on input Promise<LangChainAttestedResult>

Mode 1 — Local CER creation (synchronous, no network)

createLangChainCer is always local. certifyLangChainRun without nodeUrl/apiKey is identical.

import { createLangChainCer, certifyLangChainRun } from '@nexart/ai-execution';
// or: import { ... } from '@nexart/ai-execution/langchain';

// Using createLangChainCer — always explicit about local-only behaviour
const { bundle, certificateHash, executionId } = createLangChainCer({
  executionId: 'run-001',           // optional — UUID generated if omitted
  provider: 'openai',
  model: 'gpt-4o-mini',
  input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
  output: { text: 'Paris.' },
  metadata: { appId: 'my-app', projectId: 'docs-bot' },
  parameters: { temperature: 0, maxTokens: 200, topP: null, seed: null },
  createdAt: new Date().toISOString(),  // pin for deterministic hash
});

console.log(certificateHash); // sha256:...

// certifyLangChainRun without nodeUrl/apiKey: identical, sync
const { bundle: b2 } = certifyLangChainRun({
  provider: 'openai', model: 'gpt-4o-mini',
  input: { messages: [{ role: 'user', content: 'Summarise this.' }] },
  output: { text: 'Summary...' },
});

Mode 2 — Node-attested certification (async)

Add nodeUrl and apiKey to the same input to route through the existing certifyAndAttestDecision() path. The function returns a Promise<LangChainAttestedResult> with the receipt from the NexArt node.

import { certifyLangChainRun } from '@nexart/ai-execution';

const result = await certifyLangChainRun({
  executionId: 'run-001',
  provider: 'openai',
  model: 'gpt-4o-mini',
  input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
  output: { text: 'Paris.' },
  metadata: { appId: 'my-app', projectId: 'docs-bot' },
  createdAt: new Date().toISOString(),
  nodeUrl: 'https://node.nexart.io',   // ← triggers attested mode
  apiKey: process.env.NEXART_API_KEY!, // ← required with nodeUrl
});

console.log(result.certificateHash);   // sha256:... (same semantics as local)
console.log(result.attested);          // true
console.log(result.receipt);
// {
//   attestationId:   "nxa_attest_...",
//   certificateHash: "sha256:...",
//   nodeRuntimeHash: "sha256:...",
//   protocolVersion: "1.2.0"
// }

result.bundle passes verifyCer() identically to local mode — the certificateHash covers only the CER content, not the receipt fields.

Three helpers

All three helpers accept an optional signals?: CerContextSignal[] field on their input. When provided, signals are sealed into bundle.context and included in certificateHash. Omitting signals produces a hash identical to a bundle without signals.

Helper Returns Network
createLangChainCer(input) LangChainCerResult (sync) Never
certifyLangChainRun(input) without nodeUrl/apiKey LangChainCerResult (sync) Never
certifyLangChainRun({ ...input, nodeUrl, apiKey }) Promise<LangChainAttestedResult> Yes — NexArt node

Result types

interface LangChainCerResult {
  bundle:          CerAiExecutionBundle;
  certificateHash: string;
  executionId:     string;
}

interface LangChainAttestedResult extends LangChainCerResult {
  receipt:  AttestationReceipt;
  attested: true;
}

Input normalization

Input shape Stored in CER as
string string (pass-through)
Plain object {} Record<string, unknown> (pass-through)
Array [] { items: [...] }
Anything else String(value)

Prompt extraction

Resolved in this order: metadata.promptinput.messages[0].contentinput.promptinput.text"[LangChain run]".

Metadata mapping

Metadata key Mapped to
appId snapshot.appId
runId snapshot.runId
workflowId snapshot.workflowId
conversationId snapshot.conversationId
prompt Used as the CER prompt field
All other keys bundle.meta.tags (as "key:value" strings)

Verification

Bundles from all three helpers are fully protocol-aligned:

import { verifyCer, verifyAiCerBundleDetailed } from '@nexart/ai-execution';

const basic    = verifyCer(bundle);
// { ok: true, code: 'OK' }

const detailed = verifyAiCerBundleDetailed(bundle);
// { status: 'VERIFIED', checks: { bundleIntegrity: 'PASS', nodeSignature: 'SKIPPED' }, ... }

Testing the attested path without a network

Use the injectable _attestFn test option (same pattern as certifyAndAttestRun's attestStep):

import type { AttestDecisionFn } from '@nexart/ai-execution';

const mockAttest: AttestDecisionFn = async (params, _opts) => {
  const { certifyDecision } = await import('@nexart/ai-execution');
  const bundle = certifyDecision(params);
  return { bundle, receipt: { attestationId: 'mock-123', certificateHash: bundle.certificateHash,
    nodeRuntimeHash: 'sha256:' + 'beef'.repeat(16), protocolVersion: '1.2.0' } };
};

const result = await certifyLangChainRun(
  { ...input, nodeUrl: 'https://node.nexart.io', apiKey: 'nxa_test' },
  { _attestFn: mockAttest },
);

V3 roadmap (not yet implemented)

  • BaseCallbackHandler integration for automatic mid-chain CER capture at onLLMEnd
  • LangSmith trace correlation via metadata.runIdsnapshot.conversationId
  • Separate @nexart/langchain package once the callback surface is proven in production

Context Signals (v0.10.0+)

CERs can optionally include context signals — structured, protocol-agnostic records of upstream events that were present when the AI execution ran. Signals are evidence-only: they have no effect on how the AI call is made, what parameters are used, or how the output is processed. Their only role is to be bound into the certificateHash alongside the execution record so that any post-hoc modification to either the signals or the snapshot is detectable.

What signals are:

  • Records of things that happened before or alongside the AI call (approvals, deploys, audits, tool invocations, review outcomes, etc.)
  • Protocol-agnostic — type, source, actor, and payload are all free-form strings and objects
  • Included in certificateHash only when present — omitting signals produces a hash identical to pre-v0.10.0

What signals are not:

  • Governance enforcement — NexArt does not interpret signal content or enforce policy based on it
  • Execution dependencies — signals do not gate or modify the AI call in any way
  • A replacement for toolCallstoolCalls records tool invocations within a run; signals record upstream context around a run

Quick example

import { createSignalCollector } from '@nexart/signals';
import { certifyDecision, verifyCer } from '@nexart/ai-execution';

// 1. Collect upstream signals during your pipeline run
const collector = createSignalCollector({ defaultSource: 'github-actions' });
collector.add({ type: 'approval', actor: 'alice', status: 'ok', payload: { pr: 42 } });
collector.add({ type: 'deploy',   actor: 'ci-bot', status: 'ok', payload: { env: 'prod' } });
const { signals } = collector.export();

// 2. Certify the AI execution with signals bound as context evidence
const bundle = certifyDecision({
  provider: 'openai',
  model: 'gpt-4o-mini',
  prompt: 'Summarise.',
  input: userQuery,
  output: llmResponse,
  parameters: { temperature: 0, maxTokens: 512, topP: null, seed: null },
  signals,  // ← included in certificateHash; omit to get identical hash as before
});

verifyCer(bundle).ok;               // true
console.log(bundle.context?.signals.length); // 2 — signals are sealed in the bundle

NexArtSignal[] from @nexart/signals is structurally identical to CerContextSignal[] — no casting or conversion needed. @nexart/ai-execution has no hard dependency on @nexart/signals; any object matching the CerContextSignal shape works.

Signals also work on the LangChain path

import { certifyLangChainRun } from '@nexart/ai-execution';

const { bundle } = certifyLangChainRun({
  provider: 'openai',
  model: 'gpt-4o-mini',
  input: { messages: [...] },
  output: { text: '...' },
  signals,  // ← same field, same semantics
});

CerContextSignal shape

Every CerContextSignal has the following fields. All are always present in the stored bundle — no undefined values:

Field Type Default Description
type string required Signal category — free-form (e.g. "approval", "deploy", "audit")
source string required Upstream system — free-form (e.g. "github-actions", "linear")
step number 0 Position in sequence — used for ordering within the context.signals array
timestamp string current time ISO 8601
actor string "unknown" Who produced the signal — free-form
status string "ok" Outcome — free-form (e.g. "ok", "error", "pending")
payload Record<string, unknown> {} Opaque upstream data — NexArt does not interpret this

Signal ordering within the array is part of the sealed hash. Reordering signals produces a different certificateHash.

Tamper detection

verifyCer() always reconstructs context from the stored bundle.context.signals when recomputing the hash. Any mutation — changing a signal field, adding or removing signals, or injecting a context block into a no-signals bundle — produces a CERTIFICATE_HASH_MISMATCH result:

const tampered = { ...bundle, context: { signals: [...altered] } };
verifyCer(tampered).ok;   // false
verifyCer(tampered).code; // 'CERTIFICATE_HASH_MISMATCH'

Exported types

import type { CerContextSignal, CerContext } from '@nexart/ai-execution';

CerContext is the shape of bundle.context{ signals: CerContextSignal[] }. Both are exported from the package root.


Project Bundles (v0.13.0)

A project bundle (bundleType: 'cer.project.bundle.v1') is a project-scoped evidence artifact that groups multiple already-certified CerAiExecutionBundles under a single projectHash. Each step in the bundle preserves its own executionId and certificateHash from when it was individually certified. The bundle does not re-seal or mutate the embedded CERs.

Project bundles support:

  • Tightly connected agentic multi-step workflow traces (step A → step B → step C)
  • Partially connected steps where only some steps declare parent step relationships
  • Looser project-scoped collections of certified executions — product development runs, client implementation evidence, grouped audit/compliance records — where steps have no parent links at all

parentStepIds are always optional. A bundle with no parent links on any step is valid and fully verifiable.

Creating a project bundle

Certify each individual step first, then assemble:

import { certifyDecision, createProjectBundle } from '@nexart/ai-execution';

const b1 = certifyDecision({ prompt: 'Classify intent', input: userQuery, output: intent,    model: 'gpt-4o',                     provider: 'openai'    });
const b2 = certifyDecision({ prompt: 'Retrieve docs',   input: intent,    output: documents, model: 'gpt-4o',                     provider: 'openai'    });
const b3 = certifyDecision({ prompt: 'Draft response',  input: documents, output: response,  model: 'claude-3-5-sonnet-20241022', provider: 'anthropic' });

const project = createProjectBundle({
  // projectBundleId is auto-generated if omitted
  projectTitle:   'Support resolution — 2026-04-06',
  projectGoal:    'Resolve billing query end-to-end',
  projectSummary: 'Classified, retrieved context, and drafted reply.',
  appName:        'support-triage',
  frameworkName:  'LangChain',
  tags:           ['billing', 'production'],
  startedAt:      '2026-04-06T08:00:00.000Z',
  completedAt:    '2026-04-06T08:00:04.321Z',
  steps: [
    { stepId: 'step-classify', stepLabel: 'Classify intent', bundle: b1 },
    { stepId: 'step-retrieve', stepLabel: 'Retrieve docs',   bundle: b2, parentStepIds: ['step-classify'] },
    { stepId: 'step-draft',    stepLabel: 'Draft response',  bundle: b3, parentStepIds: ['step-retrieve'] },
  ],
});

console.log(project.bundleType);            // 'cer.project.bundle.v1'
console.log(project.integrity.projectHash); // 'sha256:...'
console.log(project.totalSteps);            // 3
console.log(project.stepRegistry[0].certificateHash); // 'sha256:...' (from b1)

Loose collection — no parent links needed:

const project = createProjectBundle({
  projectTitle: 'Q1 implementation runs',
  startedAt:    '2026-01-01T00:00:00.000Z',
  completedAt:  '2026-03-31T23:59:59.000Z',
  steps: [
    { stepLabel: 'Run A', bundle: bundleA },
    { stepLabel: 'Run B', bundle: bundleB },
    { stepLabel: 'Run C', bundle: bundleC },
    // No parentStepIds — loose grouping is fully valid.
  ],
});

When stepId or projectBundleId are omitted they are auto-generated as UUID v4 values.

Verifying a project bundle

Two verification functions are available. They run identical checks and return identical result shapes — the only difference is which crypto API they use:

verifyProjectBundle verifyProjectBundleAsync
Return type ProjectBundleVerifyResult (sync) Promise<ProjectBundleVerifyResult>
Crypto API Node.js crypto.createHash globalThis.crypto.subtle (Web Crypto)
Use when Node 18+ servers, CLI tools, scripts Browsers, edge workers, Deno, non-Node runtimes
Available since v0.13.0 v0.14.0

On Node 18+ (server / CLI):

import { verifyProjectBundle } from '@nexart/ai-execution';

const result = verifyProjectBundle(project);

if (result.ok) {
  console.log(`All ${result.totalSteps} steps verified`);
} else {
  console.log(`${result.failedSteps}/${result.totalSteps} steps failed`);
  for (const step of result.steps) {
    if (!step.ok) console.log(`  ${step.stepId}: ${step.code}`);
  }
}

In browser apps (React, Vue, Svelte, Next.js client components, etc.):

import { verifyProjectBundleAsync } from '@nexart/ai-execution';

// Works in any browser that supports the Web Crypto API (all modern browsers).
// No separate reimplementation needed — this is the same SDK-owned verification
// model as the server path, just using a different crypto primitive.
const result = await verifyProjectBundleAsync(project);

if (result.ok) {
  console.log(`All ${result.totalSteps} steps verified`);
} else {
  console.log(`${result.failedSteps}/${result.totalSteps} steps failed`);
  for (const step of result.steps) {
    if (!step.ok) console.log(`  ${step.stepId}: ${step.code}`);
  }
}

Verifying a single CER bundle in the browser:

import { verifyCerAsync } from '@nexart/ai-execution';

const result = await verifyCerAsync(bundle);
// Identical outcome to verifyCer(bundle) — same checks, same codes, same error messages.
console.log(result.ok, result.code);

verifyProjectBundle / verifyProjectBundleAsync check (in order):

  1. Discriminant — bundleType === 'cer.project.bundle.v1'
  2. Required top-level fields
  3. totalSteps consistency
  4. stepId uniqueness
  5. executionId uniqueness
  6. No duplicate sequence values
  7. Parent reference resolution (all parentStepIds must reference a known stepId)
  8. Per-step cross-check — executionId + certificateHash in the registry match the embedded CER
  9. Inner verifyCer() on every embedded CER bundle
  10. projectHash recomputation over all material project metadata

Result shape:

type ProjectBundleVerifyResult = {
  ok:               boolean;
  code:             string;          // CerVerifyCode
  errors:           string[];
  projectHashValid?: boolean;
  structuralValid?:  boolean;
  totalSteps?:       number;
  passedSteps?:      number;
  failedSteps?:      number;
  steps:             ProjectBundleStepVerifyResult[];
};

type ProjectBundleStepVerifyResult = {
  stepId:           string;
  sequence:         number;
  executionId:      string;
  certificateHash?: string;
  ok:               boolean;
  code:             string;
  errors:           string[];
};

Project hash scope

The integrity.projectHash is a SHA-256 over a deterministic canonical JSON payload. It covers:

Field Included
bundleType, version, protocolVersion Yes
projectBundleId, projectTitle Yes
projectGoal, projectSummary Yes (when set)
startedAt, completedAt Yes
appName, frameworkName, tags Yes (when set)
totalSteps, stepRegistry (full, including per-step certificateHash) Yes
integrity.projectHash No — self-referential
finalOutputSummary No — human-readable, may be updated post-facto

Because the full stepRegistry (including each step's certificateHash) is covered, any change to any embedded CER propagates into the projectHash.

Sequence rules

Each step in stepRegistry has a sequence: number field:

  • Not provided → auto-assigned as the 0-based array index (0, 1, 2, …)
  • Explicitly provided → preserved exactly as-is
  • Sequence values must be unique; duplicates cause verifyProjectBundle to fail with a descriptive error
  • Values need not be contiguous — 5, 10, 15 is valid
  • Step registry is written in input array order; no reshuffling occurs

What is NOT in this package

The following are deliberately out of scope for @nexart/ai-execution:

  • Project-level node attestation — the integrity.projectHash is offline-verifiable only; no node endpoint changes
  • Project-level receipt or verification envelope — no network submission of the project bundle as a whole
  • High-level session lifecycle abstraction — incremental step addition, session start/end, project export/import helpers, rich graph visualization

Higher-level developer ergonomics built on createProjectBundle and verifyProjectBundle are planned in @nexart/agent-kit.


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
v0.6.0 Frictionless integration: certifyDecisionFromProviderCall (OpenAI/Anthropic/Gemini/Mistral/Bedrock drop-in); sanitizeForStorage + sanitizeForStamp redaction helpers; createClient(defaults) factory; regression fixture suite; all backward-compatible, no hash changes
v0.7.0 AIEF alignment: verifyAief() (AIEF §9.1 adapter); verifyRunSummary() chain verifier; makeToolEvent() + hashToolOutput() + snapshot.toolCalls (AIEF-06); BundleDeclaration block (stabilitySchemeId, protectedSetId, protectedFields) excluded from certificateHash; redactBeforeSeal() pre-seal verifiable redaction; validateProfile() (flexible/AIEF_L2/L3/L4); 5 new CerVerifyCode entries; backward-compatible, no hash changes
v0.8.0 Helper APIs: exportVerifiableRedacted() (post-seal re-seal with redacted snapshot + provenance metadata); certifyAndAttestRun() (one-call multi-step certify + optional per-step attestation with injectable mock); test determinism fix; all v0.1–v0.7 bundles verify identically
v0.9.0 CER Protocol types: CerVerificationResult, ReasonCode, CheckStatus; verifyAiCerBundleDetailed(); CertifyDecisionParams.createdAt wired through; determinism bug fix
v0.10.0 LangChain integration: createLangChainCer() (sync/local); certifyLangChainRun() dual-mode — local (sync) or node-attested (Promise<LangChainAttestedResult> when nodeUrl+apiKey supplied); LangChainAttestedResult, AttestDecisionFn; injectable _attestFn for test mocking. Context signals: optional signals?: CerContextSignal[] on certifyDecision, certifyLangChainRun, and createLangChainCer — sealed into bundle.context and included in certificateHash when non-empty; absent or empty = hash unchanged from pre-v0.10.0. New types: CerContextSignal, CerContext. New example: examples/signals-cer.ts. 413 total tests; all prior bundles verify identically
v0.11.0 Version release. Ships all v0.10.0 features (LangChain integration + context signals) as the published package version. cerSignals.test.js added to the npm test script. No API, hash, or canonicalization changes
v0.12.0 CER package helpers: isCerPackage, createCerPackage, getCerFromPackage, exportCerPackage, importCerPackage, verifyCerPackage. New types: AiCerPackage, CreateCerPackageParams. Additive only — no changes to CER hashing, canonicalization, or verification semantics. 466 total tests; all prior bundles verify identically
v0.13.0 Project Bundle support: createProjectBundle() + verifyProjectBundle(). New artifact type cer.project.bundle.v1 — groups multiple certified CERs into one portable, self-verifiable record. Supports tightly connected agentic workflows, partial chains, and loose project-scoped collections. New types: ProjectBundle, ProjectBundleIntegrity, ProjectBundleStepEntry, ProjectBundleStepType, ProjectBundleStepModelIdentity, ProjectBundleVerifyResult, ProjectBundleStepVerifyResult, CreateProjectBundleParams, CreateProjectBundleStepParams. Two new CerVerifyCode values: PROJECT_HASH_MISMATCH, STEP_REGISTRY_MISMATCH. Strictly additive — 530 total tests; all prior bundles verify identically
v0.14.0 Browser-safe async verification. New async mirrors of all verification functions using globalThis.crypto.subtle (Web Crypto API) instead of Node's crypto.createHash. Browser apps and edge workers can now use the SDK's own verification model without maintaining a separate reimplementation. New exports: verifyCerAsync(), verifyProjectBundleAsync(), verifySnapshotAsync(), sha256HexAsync(), hashUtf8Async(), hashCanonicalJsonAsync(), computeInputHashAsync(), computeOutputHashAsync(). Produces byte-for-byte identical outcomes to the sync counterparts for any well-formed bundle. Compatible with Node 18+, browsers, Cloudflare Workers, Vercel Edge, Deno. Strictly additive — 558 total tests (28 new Group F tests including cross-runtime behavioral alignment); all existing sync functions unchanged
v0.15.0 Project Bundle registration: registerProjectBundle() — a client-side helper for submitting Project Bundles to a NexArt node (/api/register/project-bundle). Whether bundles are durably persisted depends on the target node implementing that route; the helper covers only the client side. Strictly additive — all existing APIs unchanged, no hash or canonicalization changes
v0.16.0 Context Visibility Layer: exposes hasContext, contextHash, signalCount, and a structured contextSummary alongside certificateHash for bundles sealed with signals. These fields are populated after certificateHash is computed and excluded from the hash payload. Strictly additive; certificateHash byte-for-byte unchanged. (Superseded by v0.16.1, which fixes a post-hash mutation so the shipped bundle no longer diverges from the hashed payload — use v0.16.1+ in production.)
v0.17.0 Versioned canonicalisation: adds protocolVersion 1.3.0 (RFC 8785 / jcs-v1), opt-in and backward-compatible; 1.2.0 (nexart-v1) stays frozen. Verification is fail-closed on an unsupported protocolVersion (SCHEMA_ERROR / SCHEMA_VERSION_UNSUPPORTED) before any hash recompute. Also ships four additive, non-evidentiary enterprise trust layers (anchoring, trusted timestamps, key lifecycle validation, external-verifier export) — all excluded from the hashed/signed payload; 1.2.0/1.3.0 bundles verify byte-for-byte unchanged
v0.17.1 Fix — legacy 1.2.0 records with parameters: null. Early 1.2.0 records serialize snapshot.parameters as null even though the canonical (hashed) form was {}; these now verify. verifyCer/verifyCerAsync normalize null{} in memory only (non-mutating) for 1.2.0. Scope is strictly null or {}: 1.3.0 stays fail-closed on null, and 1.2.0 with a missing/undefined parameters key still fails (SCHEMA_ERROR). No change to canonicalization, certificateHash, or the signed payload — all golden vectors byte-identical
v0.18.0 Extended legacy 1.2.0 compatibility. verifyCer/verifyCerAsync now verify 100% of historical 1.2.0 records: for 1.2.0 only, a missing prompt, a missing/legacy type, a missing executionSurface, and extra/legacy fields (e.g. version) are tolerated (best-effort schema, in memory, non-evidentiary). No fields are injected — the hash recomputes over the record as received, so certificateHash still matches and tampering still FAILS. Integrity fields stay strict on every version; 1.3.0 stays strict/fail-closed. No change to canonicalization, certificateHash, or the signed payload — all golden vectors byte-identical
v0.19.0 Legacy backward-compatibility fix — verify over the snapshot exactly as stored. Removed the in-memory parameters: null → {} coercion (normalizeSnapshotForLegacy); verifyCer/verifyCerAsync now feed the snapshot unchanged into both schema verification and the certificate-hash payload — no coercion, no field injection, no default-filling. For 1.2.0 only, parameters may be null, absent, or {}, and input/output may be omitted (the content-hash recompute is guarded so an omitted field no longer throws CANONICALIZATION_ERROR; the retained inputHash/outputHash keep integrity bound to the cert hash). The legacy gate keys off snapshot.protocolVersion. 1.3.0 stays strict/fail-closed. No change to canonicalization, certificateHash, or sealing — all golden vectors byte-identical (746 tests)
v0.20.0 Legacy backward-compatibility (Round 2). Two more historical shapes now verify. (1) Sampling fields temperature/topP are optional when legacy and absent — a present value is still fully validated; maxTokens/seed unchanged. (2) Pre-protocolVersion vintage (snapshot.protocolVersion absent): all legacy relaxations now apply to it, the bundle version check tolerates "1.0.0" for this vintage only, the recompute payload uses version as stored, and the cert-hash recompute resolves to nexart-v1. "Legacy" = protocolVersion === "1.2.0" or absent. A present-but-unknown protocolVersion still FAILS; 1.3.0 stays strict/fail-closed; verify never mutates the bundle. No change to canonicalization, certificateHash, or sealing — all golden vectors byte-identical (759 tests)
v0.21.0 Verification-provenance layer + full legacy parameters pass-through. Every VerificationResult is now self-describing via additive fields: assurance: 'strict' | 'legacy-relaxed' (required), optional verifiedUnder { canonicalization, protocolVersion }, and optional relaxations[] (the exact legacy tolerances exercised, threaded from the real validation branches — never heuristically). This does not change pass/fail or hashes; interpretation of legacy-relaxed is left to the node layer (no unverifiable status). Also completes the legacy parameters relaxation: for legacy records, maxTokens and seed are validated only when present and already the modern type — absent or legacy-shaped values (e.g. a string seed) are tolerated (parameters-legacy-shape), never coerced. 1.3.0 stays strict/fail-closed; present-but-unknown protocolVersion still FAILS and omits verifiedUnder. No change to canonicalization, certificateHash, or sealing — all golden vectors byte-identical (781 tests)
v0.22.0 protocolVersion passthrough in the high-level wrappers. The opt-in to 1.3.0 (jcs-v1 / RFC 8785) no longer requires the low-level createSnapshot path: certifyDecision, certifyDecisionFromProviderCall, RunBuilder.step, and createLangChainCer all accept protocolVersion and forward it verbatim to createSnapshot, and createClient accepts a default protocolVersion that per-call params override. Additive and backward-compatible — omitting the field yields the frozen 1.2.0 default, so existing callers are byte-for-byte unchanged. Fail-closed is preserved: an unsupported value still throws at snapshot creation. No change to canonicalization, certificateHash, or sealing — all golden vectors byte-identical (787 tests)
v0.23.0 Identity binding (snapshot.identity). New optional AiExecutionIdentity field { provider, sub, assertionHash, emailHash?, verified } (exported from root; CreateSnapshotParams.identity threaded by createSnapshot) that seals an externally verified identity assertion into the CER. Because it rides on the already-hashed snapshot, it is bound by certificateHash with no change to sealCer, canonicalization, or the signed payload. PII-safe — only the provider/sub anchor and one-way hashes are carried, never raw email/PII. The SDK never verifies JWTs, fetches JWKS, or reads headers; the app verifies + hashes before sealing and the node stays a passive signer. Strictly additive — bundles without identity hash byte-for-byte unchanged; tampering with snapshot.identity FAILS (CERTIFICATE_HASH_MISMATCH). All golden vectors byte-identical (795 tests)
v1.0.0 API stabilization — public API surface frozen under semantic versioning; no breaking changes from v0.23.0
v1.1.0 Confidential Execution Mode (P1: commitment-only). Opt-in, additive: commit sensitive fields before sealing so the attestation node never sees plaintext. New exports: sealConfidential / sealConfidentialAsync, verifyConfidential / verifyConfidentialAsync, computeCommitment / computeCommitmentAsync, generateSalt. Commitments are HMAC-SHA256(salt, domainSep ‖ content) (hmac-sha256-v1, 256-bit salts); salts are returned as openings and never stored in the CER. The committed-field set is sealed into certificateHash via an additive snapshot.confidential block (tamper-evident). Fail-closed on unknown scheme / weak salt / unknown protocolVersion / unknown field — never a fallback to sha256(content). No new runtime dependencies. No change to canonicalization, the signed payload, hashing, or any golden vector — non-confidential bundles hash byte-for-byte as v1.0.0 (824 tests)
v1.2.0 Confidential Execution as a first-class protocol (protocolVersion 1.3.1). Additive protocol extension: registers 1.3.1 against the existing jcs-v1 (RFC 8785) mode — byte-for-byte identical canonicalization, hashing, and signed payload as 1.3.0. For 1.3.1 only, verifyCer / verifyCerAsync strictly validate the confidential envelope shape on input / output and enforce the snapshot.confidential block ({ scheme, fields }), and populate an additive optional verdict on success (VERIFIED for full snapshots, VERIFIED_CONFIDENTIAL when fields are committed). New export: CerVerdict type. 1.2.0 / 1.3.0 validation, outcomes, and result shapes are unchanged (no verdict); sealConfidential envelope is unchanged. Fail-closed on unknown protocolVersion / malformed envelope. No canonicalization, hash, or signature changes (833 tests)

Releasing

cd packages/ai-execution
npm run build
npm test
npm publish --access public

The release script automates build, test, version bump, and publish:

npm run release

License

MIT