@nexart/ai-execution 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,15 @@
1
- # @nexart/ai-execution v0.5.0
1
+ # @nexart/ai-execution v0.6.0
2
2
 
3
3
  Tamper-evident records and Certified Execution Records (CER) for AI operations.
4
4
 
5
+ ## Version Information
6
+
7
+ | Component | Version |
8
+ |---|---|
9
+ | Service | — |
10
+ | SDK | 0.6.0 |
11
+ | Protocol | 1.2.0 |
12
+
5
13
  ## Why Not Just Store Logs?
6
14
 
7
15
  Logs tell you what happened. CERs prove integrity. A log entry can be edited, truncated, or fabricated after the fact with no way to detect it. A CER bundle is cryptographically sealed: any modification — to the input, output, parameters, or ordering — invalidates the certificate hash. If you need to demonstrate to an auditor, regulator, or downstream system that a recorded execution has not been modified post-hoc, logs are insufficient. CERs provide the tamper-evident chain of custody that logs cannot. **CERs certify records, not model determinism or provider execution.**
@@ -332,10 +340,24 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
332
340
  | `selectNodeKey(doc, kid?)` | Select a key from a `NodeKeysDocument` by kid or activeKid (v0.5.0+) |
333
341
  | `verifyBundleAttestation(bundle, options)` | One-call offline attestation verification (v0.5.0+) |
334
342
  | `sanitizeForAttestation(bundle)` | Remove `undefined` keys, reject BigInt/functions/symbols |
343
+ | `sanitizeForStorage(bundle, options?)` | Sanitize for DB storage with optional path-based redaction; does not recompute hashes (v0.6.0+) |
344
+ | `sanitizeForStamp(bundle)` | Extract attestable core (no meta); does not recompute hashes (v0.6.0+) |
335
345
  | `hasAttestation(bundle)` | Check if bundle already has attestation fields |
336
346
  | `exportCer(bundle)` | Serialize to canonical JSON string |
337
347
  | `importCer(json)` | Parse + verify from JSON string |
338
348
 
349
+ ### Provider Drop-in (v0.6.0+)
350
+
351
+ | Function | Description |
352
+ |---|---|
353
+ | `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. |
354
+
355
+ ### Opinionated Client (v0.6.0+)
356
+
357
+ | Export | Description |
358
+ |---|---|
359
+ | `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. |
360
+
339
361
  ### Reason Codes
340
362
 
341
363
  `CerVerifyCode` — stable string-union constant exported from the package root:
@@ -378,7 +400,8 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
378
400
  | v0.4.0 | Dual ESM/CJS build, `sanitizeForAttestation`, `hasAttestation`, auto-sanitize in `attest()`, fixed `ERR_PACKAGE_PATH_NOT_EXPORTED` |
379
401
  | v0.4.1 | Verification reason codes (`CerVerifyCode`), `code` + `details` on `VerificationResult`, README provenance wording tightened |
380
402
  | v0.4.2 | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
381
- | **v0.5.0** | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
403
+ | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
404
+ | **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 |
382
405
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
383
406
 
384
407
  ## Releasing
package/dist/index.cjs CHANGED
@@ -38,8 +38,10 @@ __export(src_exports, {
38
38
  attestIfNeeded: () => attestIfNeeded,
39
39
  certifyAndAttestDecision: () => certifyAndAttestDecision,
40
40
  certifyDecision: () => certifyDecision,
41
+ certifyDecisionFromProviderCall: () => certifyDecisionFromProviderCall,
41
42
  computeInputHash: () => computeInputHash,
42
43
  computeOutputHash: () => computeOutputHash,
44
+ createClient: () => createClient,
43
45
  createSnapshot: () => createSnapshot,
44
46
  exportCer: () => exportCer,
45
47
  fetchNodeKeys: () => fetchNodeKeys,
@@ -49,6 +51,8 @@ __export(src_exports, {
49
51
  hashUtf8: () => hashUtf8,
50
52
  importCer: () => importCer,
51
53
  sanitizeForAttestation: () => sanitizeForAttestation,
54
+ sanitizeForStamp: () => sanitizeForStamp,
55
+ sanitizeForStorage: () => sanitizeForStorage,
52
56
  sealCer: () => sealCer,
53
57
  selectNodeKey: () => selectNodeKey,
54
58
  sha256Hex: () => sha256Hex,
@@ -172,7 +176,7 @@ function computeOutputHash(output) {
172
176
  }
173
177
 
174
178
  // src/snapshot.ts
175
- var PACKAGE_VERSION = "0.5.0";
179
+ var PACKAGE_VERSION = "0.6.0";
176
180
  function validateParameters(params) {
177
181
  const errors = [];
178
182
  if (typeof params.temperature !== "number" || !Number.isFinite(params.temperature)) {
@@ -523,9 +527,45 @@ function deepRemoveUndefined(value) {
523
527
  }
524
528
  return value;
525
529
  }
530
+ function redactPath(obj, path, replacement) {
531
+ const parts = path.split(".");
532
+ const clone = { ...obj };
533
+ let cursor = clone;
534
+ for (let i = 0; i < parts.length - 1; i++) {
535
+ const key = parts[i];
536
+ if (typeof cursor[key] !== "object" || cursor[key] === null) return clone;
537
+ cursor[key] = { ...cursor[key] };
538
+ cursor = cursor[key];
539
+ }
540
+ const last = parts[parts.length - 1];
541
+ if (last in cursor) cursor[last] = replacement;
542
+ return clone;
543
+ }
526
544
  function sanitizeForAttestation(bundle) {
527
545
  return deepRemoveUndefined(bundle);
528
546
  }
547
+ function sanitizeForStorage(bundle, options) {
548
+ let cleaned = deepRemoveUndefined(bundle);
549
+ if (options?.redactPaths && options.redactPaths.length > 0) {
550
+ const replacement = options.redactWith ?? "[REDACTED]";
551
+ let obj = cleaned;
552
+ for (const path of options.redactPaths) {
553
+ obj = redactPath(obj, path, replacement);
554
+ }
555
+ cleaned = obj;
556
+ }
557
+ return cleaned;
558
+ }
559
+ function sanitizeForStamp(bundle) {
560
+ const core = {
561
+ bundleType: bundle.bundleType,
562
+ certificateHash: bundle.certificateHash,
563
+ createdAt: bundle.createdAt,
564
+ version: bundle.version,
565
+ snapshot: bundle.snapshot
566
+ };
567
+ return deepRemoveUndefined(core);
568
+ }
529
569
  function hasAttestation(bundle) {
530
570
  if (typeof bundle !== "object" || bundle === null) return false;
531
571
  const b = bundle;
@@ -844,10 +884,10 @@ var M = (a, b = P) => {
844
884
  return r >= 0n ? r : b + r;
845
885
  };
846
886
  var modN = (a) => M(a, N);
847
- var invert = (num, md) => {
848
- if (num === 0n || md <= 0n)
849
- err("no inverse n=" + num + " mod=" + md);
850
- let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
887
+ var invert = (num2, md) => {
888
+ if (num2 === 0n || md <= 0n)
889
+ err("no inverse n=" + num2 + " mod=" + md);
890
+ let a = M(num2, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
851
891
  while (a !== 0n) {
852
892
  const q = b / a, r = b % a;
853
893
  const m = x - u * q, n = y - v * q;
@@ -1064,7 +1104,7 @@ var G = new Point(Gx, Gy, 1n, M(Gx * Gy));
1064
1104
  var I = new Point(0n, 1n, 1n, 0n);
1065
1105
  Point.BASE = G;
1066
1106
  Point.ZERO = I;
1067
- var numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();
1107
+ var numTo32bLE = (num2) => hexToBytes(padh(assertRange(num2, 0n, B256), L2)).reverse();
1068
1108
  var bytesToNumLE = (b) => big("0x" + bytesToHex(u8fr(abytes(b)).reverse()));
1069
1109
  var pow2 = (x, power) => {
1070
1110
  let r = x;
@@ -1237,10 +1277,10 @@ function clean(...arrays) {
1237
1277
  function createView(arr) {
1238
1278
  return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
1239
1279
  }
1240
- function utf8ToBytes(str) {
1241
- if (typeof str !== "string")
1280
+ function utf8ToBytes(str2) {
1281
+ if (typeof str2 !== "string")
1242
1282
  throw new Error("string expected");
1243
- return new Uint8Array(new TextEncoder().encode(str));
1283
+ return new Uint8Array(new TextEncoder().encode(str2));
1244
1284
  }
1245
1285
  function toBytes(data) {
1246
1286
  if (typeof data === "string")
@@ -1846,6 +1886,270 @@ async function verifyBundleAttestation(bundle, options) {
1846
1886
  if (contextLines.length === 0) return sigResult;
1847
1887
  return sigResult.ok ? { ok: true, code: CerVerifyCode.OK, details: contextLines } : { ...sigResult, details: [...contextLines, ...sigResult.details ?? []] };
1848
1888
  }
1889
+
1890
+ // src/certifyFromProvider.ts
1891
+ var crypto5 = __toESM(require("crypto"), 1);
1892
+
1893
+ // src/providerExtract.ts
1894
+ function str(v) {
1895
+ return typeof v === "string" && v.length > 0 ? v : null;
1896
+ }
1897
+ function num(v, fallback) {
1898
+ return typeof v === "number" && Number.isFinite(v) ? v : fallback;
1899
+ }
1900
+ function numOrNull(v) {
1901
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
1902
+ }
1903
+ function extractChoicesOutput(response) {
1904
+ const choices = response.choices;
1905
+ if (!Array.isArray(choices) || choices.length === 0) return null;
1906
+ const first = choices[0];
1907
+ if (!first || typeof first !== "object") return null;
1908
+ const msg = first.message;
1909
+ if (!msg || typeof msg !== "object") return null;
1910
+ if (typeof msg.content === "string") return msg.content;
1911
+ if (Array.isArray(msg.content)) {
1912
+ const texts = msg.content.filter((p) => typeof p === "object" && p !== null).map((p) => str(p.text)).filter((t) => t !== null);
1913
+ return texts.length > 0 ? texts.join("\n") : null;
1914
+ }
1915
+ return null;
1916
+ }
1917
+ function extractAnthropicOutput(response) {
1918
+ const content = response.content;
1919
+ if (!Array.isArray(content) || content.length === 0) return null;
1920
+ const first = content[0];
1921
+ if (!first || typeof first !== "object") return null;
1922
+ return str(first.text);
1923
+ }
1924
+ function extractGeminiOutput(response) {
1925
+ const candidates = response.candidates;
1926
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
1927
+ const first = candidates[0];
1928
+ if (!first || typeof first !== "object") return null;
1929
+ const contentObj = first.content;
1930
+ if (!contentObj || typeof contentObj !== "object") return null;
1931
+ const parts = contentObj.parts;
1932
+ if (!Array.isArray(parts) || parts.length === 0) return null;
1933
+ const part = parts[0];
1934
+ if (!part || typeof part !== "object") return null;
1935
+ return str(part.text);
1936
+ }
1937
+ function extractGenericOutput(response) {
1938
+ return str(response.text) ?? str(response.output_text) ?? (typeof response.output === "string" ? response.output : null) ?? str(response.result) ?? null;
1939
+ }
1940
+ function extractOutput(response) {
1941
+ return extractChoicesOutput(response) ?? extractAnthropicOutput(response) ?? extractGeminiOutput(response) ?? extractGenericOutput(response);
1942
+ }
1943
+ function extractInput(request) {
1944
+ if (Array.isArray(request.messages) && request.messages.length > 0) {
1945
+ return { messages: request.messages };
1946
+ }
1947
+ if (Array.isArray(request.contents) && request.contents.length > 0) {
1948
+ return { contents: request.contents };
1949
+ }
1950
+ if (typeof request.prompt === "string") return request.prompt;
1951
+ if (typeof request.input === "string") return request.input;
1952
+ if (typeof request.input === "object" && request.input !== null) {
1953
+ return request.input;
1954
+ }
1955
+ return null;
1956
+ }
1957
+ function derivePrompt(request) {
1958
+ if (typeof request.prompt === "string") return request.prompt;
1959
+ if (Array.isArray(request.messages)) {
1960
+ const msgs = request.messages;
1961
+ for (let i = msgs.length - 1; i >= 0; i--) {
1962
+ const msg = msgs[i];
1963
+ if (msg.role === "user" || msg.role === "human") {
1964
+ const content = msg.content;
1965
+ if (typeof content === "string") return content;
1966
+ if (Array.isArray(content)) {
1967
+ const text = content.filter((p) => typeof p === "object" && p !== null).map((p) => str(p.text)).filter((t) => t !== null).join("\n");
1968
+ if (text) return text;
1969
+ }
1970
+ }
1971
+ }
1972
+ }
1973
+ if (Array.isArray(request.contents)) {
1974
+ const contents = request.contents;
1975
+ for (let i = contents.length - 1; i >= 0; i--) {
1976
+ const c = contents[i];
1977
+ if (c.role === "user") {
1978
+ const parts = c.parts;
1979
+ if (Array.isArray(parts)) {
1980
+ const text = parts.map((p) => str(p.text)).filter((t) => t !== null).join("\n");
1981
+ if (text) return text;
1982
+ }
1983
+ }
1984
+ }
1985
+ }
1986
+ return null;
1987
+ }
1988
+ function extractParams(request) {
1989
+ const cfg = typeof request.generationConfig === "object" && request.generationConfig !== null ? request.generationConfig : request;
1990
+ return {
1991
+ temperature: num(cfg.temperature, 1),
1992
+ maxTokens: num(cfg.max_tokens ?? cfg.maxTokens ?? cfg.maxOutputTokens ?? cfg.max_output_tokens, 1024),
1993
+ topP: numOrNull(cfg.top_p ?? cfg.topP),
1994
+ seed: numOrNull(cfg.seed)
1995
+ };
1996
+ }
1997
+ function extractModel(providerHint, request, response, override) {
1998
+ const raw = override ?? str(request.model) ?? str(request.modelId) ?? // Bedrock
1999
+ str(response.model) ?? str(response.modelId) ?? null;
2000
+ if (!raw) return null;
2001
+ const parts = raw.split("/");
2002
+ const modelName = parts[parts.length - 1];
2003
+ const modelVersion = str(response.model_version ?? response.modelVersion) ?? null;
2004
+ return { model: modelName, modelVersion };
2005
+ }
2006
+ function tryUnwrapBedrock(request) {
2007
+ const modelId = str(request.modelId);
2008
+ if (!modelId) return null;
2009
+ let body = {};
2010
+ if (typeof request.body === "string") {
2011
+ try {
2012
+ body = JSON.parse(request.body);
2013
+ } catch {
2014
+ body = {};
2015
+ }
2016
+ } else if (typeof request.body === "object" && request.body !== null) {
2017
+ body = request.body;
2018
+ }
2019
+ return { req: { ...body, modelId }, modelId };
2020
+ }
2021
+ function extractFromProviderCall(provider, modelOverride, rawRequest, rawResponse) {
2022
+ let request = rawRequest;
2023
+ if (provider === "bedrock" || str(rawRequest.modelId)) {
2024
+ const unwrapped = tryUnwrapBedrock(rawRequest);
2025
+ if (unwrapped) request = unwrapped.req;
2026
+ }
2027
+ const modelResult = extractModel(provider, request, rawResponse, modelOverride);
2028
+ if (!modelResult) {
2029
+ return {
2030
+ ok: false,
2031
+ reason: "Could not determine model name. Provide it via the `model` field, or ensure request.model / request.modelId is present."
2032
+ };
2033
+ }
2034
+ const input = extractInput(request);
2035
+ if (input === null) {
2036
+ return {
2037
+ ok: false,
2038
+ reason: "Could not extract input. Expected request.messages (OpenAI/Anthropic/Mistral), request.contents (Gemini), or request.prompt/request.input (generic)."
2039
+ };
2040
+ }
2041
+ const prompt = derivePrompt(request) ?? (typeof input === "string" ? input : "[structured input]");
2042
+ const output = extractOutput(rawResponse);
2043
+ if (output === null) {
2044
+ return {
2045
+ ok: false,
2046
+ reason: "Could not extract output text. Expected response.choices[0].message.content (OpenAI/Mistral), response.content[0].text (Anthropic), response.candidates[0].content.parts[0].text (Gemini), or response.text / response.output_text (generic)."
2047
+ };
2048
+ }
2049
+ const parameters = extractParams(request);
2050
+ return {
2051
+ ok: true,
2052
+ data: {
2053
+ model: modelResult.model,
2054
+ modelVersion: modelResult.modelVersion,
2055
+ prompt,
2056
+ input,
2057
+ output,
2058
+ parameters
2059
+ }
2060
+ };
2061
+ }
2062
+
2063
+ // src/certifyFromProvider.ts
2064
+ function certifyDecisionFromProviderCall(params) {
2065
+ const extracted = extractFromProviderCall(
2066
+ params.provider,
2067
+ params.model,
2068
+ params.request,
2069
+ params.response
2070
+ );
2071
+ if (!extracted.ok) {
2072
+ return { ok: false, code: CerVerifyCode.SCHEMA_ERROR, reason: extracted.reason };
2073
+ }
2074
+ const { model, modelVersion, prompt, input, output, parameters } = extracted.data;
2075
+ try {
2076
+ const snapshot = createSnapshot({
2077
+ executionId: params.executionId ?? crypto5.randomUUID(),
2078
+ timestamp: params.timestamp,
2079
+ provider: params.provider,
2080
+ model,
2081
+ modelVersion,
2082
+ prompt,
2083
+ input,
2084
+ parameters,
2085
+ output,
2086
+ appId: params.appId ?? null,
2087
+ workflowId: params.workflowId ?? null,
2088
+ conversationId: params.conversationId ?? null
2089
+ });
2090
+ const bundle = sealCer(snapshot, { meta: params.meta, createdAt: params.createdAt });
2091
+ return { ok: true, bundle };
2092
+ } catch (err2) {
2093
+ return {
2094
+ ok: false,
2095
+ code: CerVerifyCode.SCHEMA_ERROR,
2096
+ reason: err2 instanceof Error ? err2.message : String(err2)
2097
+ };
2098
+ }
2099
+ }
2100
+
2101
+ // src/client.ts
2102
+ async function resolveApiKey(key) {
2103
+ if (typeof key === "function") return key();
2104
+ return key ?? "";
2105
+ }
2106
+ function mergeDefaultMeta(defaults, meta) {
2107
+ const base = {};
2108
+ if (defaults.tags && defaults.tags.length > 0) base.tags = defaults.tags;
2109
+ if (defaults.source) base.source = defaults.source;
2110
+ if (!meta && Object.keys(base).length === 0) return void 0;
2111
+ return { ...base, ...meta };
2112
+ }
2113
+ function createClient(defaults = {}) {
2114
+ return {
2115
+ certifyDecision(params) {
2116
+ return certifyDecision({
2117
+ appId: defaults.appId ?? null,
2118
+ workflowId: defaults.workflowId ?? null,
2119
+ ...params,
2120
+ meta: mergeDefaultMeta(defaults, params.meta)
2121
+ });
2122
+ },
2123
+ async certifyAndAttestDecision(params, options) {
2124
+ const nodeUrl = options?.nodeUrl ?? defaults.nodeUrl;
2125
+ if (!nodeUrl) {
2126
+ throw new Error("certifyAndAttestDecision requires nodeUrl (set in defaults or options)");
2127
+ }
2128
+ const apiKey = options?.apiKey ?? await resolveApiKey(defaults.apiKey);
2129
+ const mergedParams = {
2130
+ appId: defaults.appId ?? null,
2131
+ workflowId: defaults.workflowId ?? null,
2132
+ ...params,
2133
+ meta: mergeDefaultMeta(defaults, params.meta)
2134
+ };
2135
+ return certifyAndAttestDecision(mergedParams, {
2136
+ nodeUrl,
2137
+ apiKey,
2138
+ timeoutMs: options?.timeoutMs
2139
+ });
2140
+ },
2141
+ verify(bundle) {
2142
+ return verifyCer(bundle);
2143
+ },
2144
+ async verifyBundleAttestation(bundle, options) {
2145
+ const nodeUrl = options?.nodeUrl ?? defaults.nodeUrl;
2146
+ if (!nodeUrl) {
2147
+ throw new Error("verifyBundleAttestation requires nodeUrl (set in defaults or options)");
2148
+ }
2149
+ return verifyBundleAttestation(bundle, { nodeUrl, kid: options?.kid });
2150
+ }
2151
+ };
2152
+ }
1849
2153
  // Annotate the CommonJS export names for ESM import in node:
1850
2154
  0 && (module.exports = {
1851
2155
  CerAttestationError,
@@ -1856,8 +2160,10 @@ async function verifyBundleAttestation(bundle, options) {
1856
2160
  attestIfNeeded,
1857
2161
  certifyAndAttestDecision,
1858
2162
  certifyDecision,
2163
+ certifyDecisionFromProviderCall,
1859
2164
  computeInputHash,
1860
2165
  computeOutputHash,
2166
+ createClient,
1861
2167
  createSnapshot,
1862
2168
  exportCer,
1863
2169
  fetchNodeKeys,
@@ -1867,6 +2173,8 @@ async function verifyBundleAttestation(bundle, options) {
1867
2173
  hashUtf8,
1868
2174
  importCer,
1869
2175
  sanitizeForAttestation,
2176
+ sanitizeForStamp,
2177
+ sanitizeForStorage,
1870
2178
  sealCer,
1871
2179
  selectNodeKey,
1872
2180
  sha256Hex,