@nexart/ai-execution 0.4.0 → 0.4.2
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 +73 -7
- package/dist/index.cjs +208 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.mjs +204 -41
- package/dist/index.mjs.map +1 -1
- package/dist/providers/anthropic.cjs +1 -1
- package/dist/providers/anthropic.cjs.map +1 -1
- package/dist/providers/anthropic.d.cts +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.mjs +1 -1
- package/dist/providers/anthropic.mjs.map +1 -1
- package/dist/providers/openai.cjs +1 -1
- package/dist/providers/openai.cjs.map +1 -1
- package/dist/providers/openai.d.cts +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.mjs +1 -1
- package/dist/providers/openai.mjs.map +1 -1
- package/dist/providers/wrap.cjs +1 -1
- package/dist/providers/wrap.cjs.map +1 -1
- package/dist/providers/wrap.d.cts +1 -1
- package/dist/providers/wrap.d.ts +1 -1
- package/dist/providers/wrap.mjs +1 -1
- package/dist/providers/wrap.mjs.map +1 -1
- package/dist/{types-DF29BsH5.d.cts → types-Cnm2G_rg.d.cts} +26 -1
- package/dist/{types-DF29BsH5.d.ts → types-Cnm2G_rg.d.ts} +26 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# @nexart/ai-execution v0.4.
|
|
1
|
+
# @nexart/ai-execution v0.4.2
|
|
2
2
|
|
|
3
3
|
Tamper-evident records and Certified Execution Records (CER) for AI operations.
|
|
4
4
|
|
|
5
5
|
## Why Not Just Store Logs?
|
|
6
6
|
|
|
7
|
-
Logs tell you what happened. CERs prove
|
|
7
|
+
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.**
|
|
8
8
|
|
|
9
9
|
## What This Does
|
|
10
10
|
|
|
@@ -15,13 +15,13 @@ This package creates integrity records for AI executions. Every time you call an
|
|
|
15
15
|
- The exact parameters used (temperature, model, etc.)
|
|
16
16
|
- SHA-256 hashes of everything for tamper detection
|
|
17
17
|
|
|
18
|
-
These records can be verified offline to prove
|
|
18
|
+
These records can be verified offline to detect any post-hoc modification and prove integrity of the recorded execution.
|
|
19
19
|
|
|
20
|
-
**Important:** This does NOT promise that an AI model will produce the same output twice. LLMs are not deterministic. This package provides **integrity and auditability** — proof that
|
|
20
|
+
**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.
|
|
21
21
|
|
|
22
22
|
## Compatibility Guarantees
|
|
23
23
|
|
|
24
|
-
- **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.
|
|
24
|
+
- **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.
|
|
25
25
|
- **Hashing rules are frozen for `cer.ai.execution.v1`.** The canonicalization, SHA-256 computation, and certificate hash inputs (bundleType, version, createdAt, snapshot) are unchanged.
|
|
26
26
|
- **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.
|
|
27
27
|
- **Canonicalization is frozen for v1.** Number-to-string conversion uses `JSON.stringify()`, which is consistent across JavaScript engines but does not implement RFC 8785 (JCS) for edge cases like `-0`. If stricter canonicalization is required, it will ship as a new bundle type (`cer.ai.execution.v2`), never as a modification to v1.
|
|
@@ -139,7 +139,7 @@ const restored = importCer(json); // parse + verify (throws on tamper)
|
|
|
139
139
|
| `modelVersion` | Optional | `string \| null` | Defaults to `null` |
|
|
140
140
|
| `parameters.topP` | Optional | `number \| null` | Defaults to `null` |
|
|
141
141
|
| `parameters.seed` | Optional | `number \| null` | Defaults to `null` |
|
|
142
|
-
| `sdkVersion` | Optional | `string \| null` | Defaults to `"0.4.
|
|
142
|
+
| `sdkVersion` | Optional | `string \| null` | Defaults to `"0.4.2"` |
|
|
143
143
|
| `appId` | Optional | `string \| null` | Defaults to `null` |
|
|
144
144
|
| `runId` | Optional | `string \| null` | Workflow run ID |
|
|
145
145
|
| `stepId` | Optional | `string \| null` | Step identifier within a run |
|
|
@@ -180,6 +180,47 @@ Endpoint: `POST {nodeUrl}/api/attest`
|
|
|
180
180
|
|
|
181
181
|
Attestation verifies internal integrity only. It does not re-run the model or validate the correctness of the AI output.
|
|
182
182
|
|
|
183
|
+
### Attestation Receipt
|
|
184
|
+
|
|
185
|
+
After a successful attestation, you get a normalized `AttestationReceipt`:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
type AttestationReceipt = {
|
|
189
|
+
attestationId: string;
|
|
190
|
+
certificateHash: string; // sha256:...
|
|
191
|
+
nodeRuntimeHash: string; // sha256:...
|
|
192
|
+
protocolVersion: string;
|
|
193
|
+
nodeId?: string;
|
|
194
|
+
attestedAt?: string; // ISO 8601
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Recommended one-call integration:**
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { certifyAndAttestDecision } from '@nexart/ai-execution';
|
|
202
|
+
|
|
203
|
+
const { bundle, receipt } = await certifyAndAttestDecision(params, {
|
|
204
|
+
nodeUrl: 'https://my-node.example.com',
|
|
205
|
+
apiKey: process.env.NODE_API_KEY!,
|
|
206
|
+
});
|
|
207
|
+
// bundle is the sealed CER, receipt is the normalized attestation proof
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Skip re-attestation when already attested:**
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { attestIfNeeded, getAttestationReceipt } from '@nexart/ai-execution';
|
|
214
|
+
|
|
215
|
+
const { receipt } = await attestIfNeeded(bundle, options);
|
|
216
|
+
// or just read without network call:
|
|
217
|
+
const receipt = getAttestationReceipt(bundle); // null if not yet attested
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- `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
|
|
221
|
+
- `attestIfNeeded(bundle, options)` — skips the node call if a valid receipt is already present; prevents double-attestation
|
|
222
|
+
- `certifyAndAttestDecision(params, options)` — recommended one-call integration: `certifyDecision` + `attest` + normalized receipt
|
|
223
|
+
|
|
183
224
|
### Sanitization and Redaction
|
|
184
225
|
|
|
185
226
|
`sanitizeForAttestation(bundle)` returns a JSON-safe deep clone:
|
|
@@ -238,11 +279,34 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
|
|
|
238
279
|
| Function | Description |
|
|
239
280
|
|---|---|
|
|
240
281
|
| `attest(bundle, options)` | Post CER to canonical node (auto-sanitizes) |
|
|
282
|
+
| `certifyAndAttestDecision(params, options)` | One-call: certifyDecision + attest + receipt |
|
|
283
|
+
| `attestIfNeeded(bundle, options)` | Attest only if no receipt already present |
|
|
284
|
+
| `getAttestationReceipt(bundle)` | Extract normalized `AttestationReceipt` or `null` |
|
|
241
285
|
| `sanitizeForAttestation(bundle)` | Remove `undefined` keys, reject BigInt/functions/symbols |
|
|
242
286
|
| `hasAttestation(bundle)` | Check if bundle already has attestation fields |
|
|
243
287
|
| `exportCer(bundle)` | Serialize to canonical JSON string |
|
|
244
288
|
| `importCer(json)` | Parse + verify from JSON string |
|
|
245
289
|
|
|
290
|
+
### Reason Codes
|
|
291
|
+
|
|
292
|
+
`CerVerifyCode` — stable string-union constant exported from the package root:
|
|
293
|
+
|
|
294
|
+
| Code | When set |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `OK` | Verification passed |
|
|
297
|
+
| `CERTIFICATE_HASH_MISMATCH` | `certificateHash` doesn't match recomputed hash |
|
|
298
|
+
| `INPUT_HASH_MISMATCH` | `inputHash` doesn't match recomputed hash |
|
|
299
|
+
| `OUTPUT_HASH_MISMATCH` | `outputHash` doesn't match recomputed hash |
|
|
300
|
+
| `SNAPSHOT_HASH_MISMATCH` | Both `inputHash` and `outputHash` are wrong |
|
|
301
|
+
| `INVALID_SHA256_FORMAT` | A hash field doesn't start with `sha256:` |
|
|
302
|
+
| `SCHEMA_ERROR` | Wrong bundleType/version, missing snapshot, non-finite parameters, etc. |
|
|
303
|
+
| `CANONICALIZATION_ERROR` | `toCanonicalJson` threw during verification |
|
|
304
|
+
| `UNKNOWN_ERROR` | Catch-all for unclassified failures |
|
|
305
|
+
|
|
306
|
+
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`.
|
|
307
|
+
|
|
308
|
+
**These codes are stable across all future versions.** New codes may be added but existing codes will not be renamed or removed.
|
|
309
|
+
|
|
246
310
|
### Providers (sub-exports)
|
|
247
311
|
|
|
248
312
|
| Function | Export path |
|
|
@@ -258,7 +322,9 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
|
|
|
258
322
|
| v0.1.0 | Core snapshot + CER + verify + OpenAI adapter |
|
|
259
323
|
| v0.2.0 | certifyDecision, RunBuilder, attest, archive, Anthropic, wrapProvider, typed errors, workflow fields |
|
|
260
324
|
| v0.3.0 | Attestation hardening (hash validation, timeout), `verify` alias, `CerAttestationError.details`, release hygiene |
|
|
261
|
-
|
|
|
325
|
+
| v0.4.0 | Dual ESM/CJS build, `sanitizeForAttestation`, `hasAttestation`, auto-sanitize in `attest()`, fixed `ERR_PACKAGE_PATH_NOT_EXPORTED` |
|
|
326
|
+
| v0.4.1 | Verification reason codes (`CerVerifyCode`), `code` + `details` on `VerificationResult`, README provenance wording tightened |
|
|
327
|
+
| **v0.4.2** | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
|
|
262
328
|
| v1.0.0 | Planned: API stabilization, freeze public API surface |
|
|
263
329
|
|
|
264
330
|
## Releasing
|
package/dist/index.cjs
CHANGED
|
@@ -32,13 +32,17 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
CerAttestationError: () => CerAttestationError,
|
|
34
34
|
CerVerificationError: () => CerVerificationError,
|
|
35
|
+
CerVerifyCode: () => CerVerifyCode,
|
|
35
36
|
RunBuilder: () => RunBuilder,
|
|
36
37
|
attest: () => attest,
|
|
38
|
+
attestIfNeeded: () => attestIfNeeded,
|
|
39
|
+
certifyAndAttestDecision: () => certifyAndAttestDecision,
|
|
37
40
|
certifyDecision: () => certifyDecision,
|
|
38
41
|
computeInputHash: () => computeInputHash,
|
|
39
42
|
computeOutputHash: () => computeOutputHash,
|
|
40
43
|
createSnapshot: () => createSnapshot,
|
|
41
44
|
exportCer: () => exportCer,
|
|
45
|
+
getAttestationReceipt: () => getAttestationReceipt,
|
|
42
46
|
hasAttestation: () => hasAttestation,
|
|
43
47
|
hashCanonicalJson: () => hashCanonicalJson,
|
|
44
48
|
hashUtf8: () => hashUtf8,
|
|
@@ -54,6 +58,19 @@ __export(src_exports, {
|
|
|
54
58
|
});
|
|
55
59
|
module.exports = __toCommonJS(src_exports);
|
|
56
60
|
|
|
61
|
+
// src/types.ts
|
|
62
|
+
var CerVerifyCode = {
|
|
63
|
+
OK: "OK",
|
|
64
|
+
CERTIFICATE_HASH_MISMATCH: "CERTIFICATE_HASH_MISMATCH",
|
|
65
|
+
SNAPSHOT_HASH_MISMATCH: "SNAPSHOT_HASH_MISMATCH",
|
|
66
|
+
INPUT_HASH_MISMATCH: "INPUT_HASH_MISMATCH",
|
|
67
|
+
OUTPUT_HASH_MISMATCH: "OUTPUT_HASH_MISMATCH",
|
|
68
|
+
INVALID_SHA256_FORMAT: "INVALID_SHA256_FORMAT",
|
|
69
|
+
CANONICALIZATION_ERROR: "CANONICALIZATION_ERROR",
|
|
70
|
+
SCHEMA_ERROR: "SCHEMA_ERROR",
|
|
71
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
72
|
+
};
|
|
73
|
+
|
|
57
74
|
// src/errors.ts
|
|
58
75
|
var CerVerificationError = class extends Error {
|
|
59
76
|
errors;
|
|
@@ -147,7 +164,7 @@ function computeOutputHash(output) {
|
|
|
147
164
|
}
|
|
148
165
|
|
|
149
166
|
// src/snapshot.ts
|
|
150
|
-
var PACKAGE_VERSION = "0.4.
|
|
167
|
+
var PACKAGE_VERSION = "0.4.2";
|
|
151
168
|
function validateParameters(params) {
|
|
152
169
|
const errors = [];
|
|
153
170
|
if (typeof params.temperature !== "number" || !Number.isFinite(params.temperature)) {
|
|
@@ -203,54 +220,84 @@ function createSnapshot(params) {
|
|
|
203
220
|
return snapshot;
|
|
204
221
|
}
|
|
205
222
|
function verifySnapshot(snapshot) {
|
|
206
|
-
const
|
|
223
|
+
const schemaErrors = [];
|
|
224
|
+
const formatErrors = [];
|
|
225
|
+
const inputHashErrors = [];
|
|
226
|
+
const outputHashErrors = [];
|
|
207
227
|
if (snapshot.type !== "ai.execution.v1") {
|
|
208
|
-
|
|
228
|
+
schemaErrors.push(`Expected type "ai.execution.v1", got "${snapshot.type}"`);
|
|
209
229
|
}
|
|
210
230
|
if (snapshot.protocolVersion !== "1.2.0") {
|
|
211
|
-
|
|
231
|
+
schemaErrors.push(`Expected protocolVersion "1.2.0", got "${snapshot.protocolVersion}"`);
|
|
212
232
|
}
|
|
213
233
|
if (snapshot.executionSurface !== "ai") {
|
|
214
|
-
|
|
234
|
+
schemaErrors.push(`Expected executionSurface "ai", got "${snapshot.executionSurface}"`);
|
|
215
235
|
}
|
|
216
236
|
if (!snapshot.executionId || typeof snapshot.executionId !== "string") {
|
|
217
|
-
|
|
237
|
+
schemaErrors.push("executionId must be a non-empty string");
|
|
218
238
|
}
|
|
219
239
|
if (!snapshot.timestamp || typeof snapshot.timestamp !== "string") {
|
|
220
|
-
|
|
240
|
+
schemaErrors.push("timestamp must be a non-empty string");
|
|
221
241
|
}
|
|
222
242
|
if (!snapshot.provider || typeof snapshot.provider !== "string") {
|
|
223
|
-
|
|
243
|
+
schemaErrors.push("provider must be a non-empty string");
|
|
224
244
|
}
|
|
225
245
|
if (!snapshot.model || typeof snapshot.model !== "string") {
|
|
226
|
-
|
|
246
|
+
schemaErrors.push("model must be a non-empty string");
|
|
227
247
|
}
|
|
228
248
|
if (!snapshot.prompt || typeof snapshot.prompt !== "string") {
|
|
229
|
-
|
|
249
|
+
schemaErrors.push("prompt must be a non-empty string");
|
|
230
250
|
}
|
|
231
251
|
if (snapshot.input === void 0 || snapshot.input === null) {
|
|
232
|
-
|
|
252
|
+
schemaErrors.push("input must be a string or object");
|
|
233
253
|
}
|
|
234
254
|
if (snapshot.output === void 0 || snapshot.output === null) {
|
|
235
|
-
|
|
255
|
+
schemaErrors.push("output must be a string or object");
|
|
236
256
|
}
|
|
237
257
|
const paramErrors = validateParameters(snapshot.parameters);
|
|
238
|
-
|
|
258
|
+
schemaErrors.push(...paramErrors);
|
|
239
259
|
if (!snapshot.inputHash || !snapshot.inputHash.startsWith("sha256:")) {
|
|
240
|
-
|
|
260
|
+
formatErrors.push(`inputHash must start with "sha256:", got "${snapshot.inputHash}"`);
|
|
241
261
|
}
|
|
242
262
|
if (!snapshot.outputHash || !snapshot.outputHash.startsWith("sha256:")) {
|
|
243
|
-
|
|
263
|
+
formatErrors.push(`outputHash must start with "sha256:", got "${snapshot.outputHash}"`);
|
|
244
264
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
265
|
+
if (formatErrors.length === 0) {
|
|
266
|
+
const expectedInputHash = computeInputHash(snapshot.input);
|
|
267
|
+
if (snapshot.inputHash !== expectedInputHash) {
|
|
268
|
+
inputHashErrors.push(`inputHash mismatch: expected ${expectedInputHash}, got ${snapshot.inputHash}`);
|
|
269
|
+
}
|
|
270
|
+
const expectedOutputHash = computeOutputHash(snapshot.output);
|
|
271
|
+
if (snapshot.outputHash !== expectedOutputHash) {
|
|
272
|
+
outputHashErrors.push(`outputHash mismatch: expected ${expectedOutputHash}, got ${snapshot.outputHash}`);
|
|
273
|
+
}
|
|
248
274
|
}
|
|
249
|
-
const
|
|
250
|
-
if (
|
|
251
|
-
|
|
275
|
+
const errors = [...schemaErrors, ...formatErrors, ...inputHashErrors, ...outputHashErrors];
|
|
276
|
+
if (errors.length === 0) {
|
|
277
|
+
return { ok: true, errors: [], code: CerVerifyCode.OK };
|
|
278
|
+
}
|
|
279
|
+
let code;
|
|
280
|
+
let details;
|
|
281
|
+
if (schemaErrors.length > 0) {
|
|
282
|
+
code = CerVerifyCode.SCHEMA_ERROR;
|
|
283
|
+
details = schemaErrors;
|
|
284
|
+
} else if (formatErrors.length > 0) {
|
|
285
|
+
code = CerVerifyCode.INVALID_SHA256_FORMAT;
|
|
286
|
+
details = formatErrors;
|
|
287
|
+
} else if (inputHashErrors.length > 0 && outputHashErrors.length > 0) {
|
|
288
|
+
code = CerVerifyCode.SNAPSHOT_HASH_MISMATCH;
|
|
289
|
+
details = [...inputHashErrors, ...outputHashErrors];
|
|
290
|
+
} else if (inputHashErrors.length > 0) {
|
|
291
|
+
code = CerVerifyCode.INPUT_HASH_MISMATCH;
|
|
292
|
+
details = inputHashErrors;
|
|
293
|
+
} else if (outputHashErrors.length > 0) {
|
|
294
|
+
code = CerVerifyCode.OUTPUT_HASH_MISMATCH;
|
|
295
|
+
details = outputHashErrors;
|
|
296
|
+
} else {
|
|
297
|
+
code = CerVerifyCode.UNKNOWN_ERROR;
|
|
298
|
+
details = errors;
|
|
252
299
|
}
|
|
253
|
-
return { ok: errors
|
|
300
|
+
return { ok: false, errors, code, details };
|
|
254
301
|
}
|
|
255
302
|
|
|
256
303
|
// src/cer.ts
|
|
@@ -280,36 +327,80 @@ function sealCer(snapshot, options) {
|
|
|
280
327
|
return bundle;
|
|
281
328
|
}
|
|
282
329
|
function verifyCer(bundle) {
|
|
283
|
-
const
|
|
330
|
+
const schemaErrors = [];
|
|
331
|
+
const formatErrors = [];
|
|
284
332
|
if (bundle.bundleType !== "cer.ai.execution.v1") {
|
|
285
|
-
|
|
333
|
+
schemaErrors.push(`Expected bundleType "cer.ai.execution.v1", got "${bundle.bundleType}"`);
|
|
286
334
|
}
|
|
287
335
|
if (bundle.version !== "0.1") {
|
|
288
|
-
|
|
336
|
+
schemaErrors.push(`Expected version "0.1", got "${bundle.version}"`);
|
|
289
337
|
}
|
|
290
338
|
if (!bundle.createdAt || typeof bundle.createdAt !== "string") {
|
|
291
|
-
|
|
339
|
+
schemaErrors.push("createdAt must be a non-empty string");
|
|
292
340
|
}
|
|
293
341
|
if (!bundle.certificateHash || !bundle.certificateHash.startsWith("sha256:")) {
|
|
294
|
-
|
|
342
|
+
formatErrors.push(`certificateHash must start with "sha256:", got "${bundle.certificateHash}"`);
|
|
295
343
|
}
|
|
296
344
|
if (!bundle.snapshot) {
|
|
297
|
-
|
|
298
|
-
|
|
345
|
+
schemaErrors.push("snapshot is required");
|
|
346
|
+
const allErrors = [...schemaErrors, ...formatErrors];
|
|
347
|
+
return { ok: false, errors: allErrors, code: CerVerifyCode.SCHEMA_ERROR, details: schemaErrors };
|
|
299
348
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
349
|
+
let canonicalizationError = null;
|
|
350
|
+
let snapshotResult = null;
|
|
351
|
+
try {
|
|
352
|
+
snapshotResult = verifySnapshot(bundle.snapshot);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
canonicalizationError = err instanceof Error ? err.message : String(err);
|
|
355
|
+
}
|
|
356
|
+
if (canonicalizationError !== null) {
|
|
357
|
+
const errors2 = [...schemaErrors, ...formatErrors, canonicalizationError];
|
|
358
|
+
return { ok: false, errors: errors2, code: CerVerifyCode.CANONICALIZATION_ERROR, details: [canonicalizationError] };
|
|
359
|
+
}
|
|
360
|
+
const snapshotErrors = snapshotResult.errors;
|
|
361
|
+
const certHashErrors = [];
|
|
362
|
+
try {
|
|
363
|
+
const payload = {
|
|
364
|
+
bundleType: "cer.ai.execution.v1",
|
|
365
|
+
createdAt: bundle.createdAt,
|
|
366
|
+
snapshot: bundle.snapshot,
|
|
367
|
+
version: "0.1"
|
|
368
|
+
};
|
|
369
|
+
const expectedHash = computeCertificateHash(payload);
|
|
370
|
+
if (bundle.certificateHash !== expectedHash) {
|
|
371
|
+
certHashErrors.push(`certificateHash mismatch: expected ${expectedHash}, got ${bundle.certificateHash}`);
|
|
372
|
+
}
|
|
373
|
+
} catch (err) {
|
|
374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
375
|
+
const errors2 = [...schemaErrors, ...formatErrors, ...snapshotErrors, msg];
|
|
376
|
+
return { ok: false, errors: errors2, code: CerVerifyCode.CANONICALIZATION_ERROR, details: [msg] };
|
|
377
|
+
}
|
|
378
|
+
const errors = [...schemaErrors, ...formatErrors, ...snapshotErrors, ...certHashErrors];
|
|
379
|
+
if (errors.length === 0) {
|
|
380
|
+
return { ok: true, errors: [], code: CerVerifyCode.OK };
|
|
381
|
+
}
|
|
382
|
+
let code;
|
|
383
|
+
let details;
|
|
384
|
+
if (schemaErrors.length > 0) {
|
|
385
|
+
code = CerVerifyCode.SCHEMA_ERROR;
|
|
386
|
+
details = schemaErrors;
|
|
387
|
+
} else if (formatErrors.length > 0) {
|
|
388
|
+
code = CerVerifyCode.INVALID_SHA256_FORMAT;
|
|
389
|
+
details = formatErrors;
|
|
390
|
+
} else if (certHashErrors.length > 0 && snapshotErrors.length === 0) {
|
|
391
|
+
code = CerVerifyCode.CERTIFICATE_HASH_MISMATCH;
|
|
392
|
+
details = certHashErrors;
|
|
393
|
+
} else if (snapshotResult && snapshotResult.code !== CerVerifyCode.OK) {
|
|
394
|
+
code = snapshotResult.code;
|
|
395
|
+
details = snapshotResult.details ?? snapshotErrors;
|
|
396
|
+
} else if (certHashErrors.length > 0) {
|
|
397
|
+
code = CerVerifyCode.CERTIFICATE_HASH_MISMATCH;
|
|
398
|
+
details = certHashErrors;
|
|
399
|
+
} else {
|
|
400
|
+
code = CerVerifyCode.UNKNOWN_ERROR;
|
|
401
|
+
details = errors;
|
|
311
402
|
}
|
|
312
|
-
return { ok: errors
|
|
403
|
+
return { ok: false, errors, code, details };
|
|
313
404
|
}
|
|
314
405
|
|
|
315
406
|
// src/certify.ts
|
|
@@ -580,17 +671,93 @@ function wrapProvider(config) {
|
|
|
580
671
|
}
|
|
581
672
|
};
|
|
582
673
|
}
|
|
674
|
+
|
|
675
|
+
// src/receipt.ts
|
|
676
|
+
function extractStr(obj, key) {
|
|
677
|
+
const v = obj[key];
|
|
678
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
679
|
+
}
|
|
680
|
+
function buildReceipt(obj) {
|
|
681
|
+
const attestationId = extractStr(obj, "attestationId");
|
|
682
|
+
const certificateHash = extractStr(obj, "certificateHash");
|
|
683
|
+
const nodeRuntimeHash = extractStr(obj, "nodeRuntimeHash");
|
|
684
|
+
const protocolVersion = extractStr(obj, "protocolVersion");
|
|
685
|
+
if (!attestationId || !certificateHash || !nodeRuntimeHash || !protocolVersion) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const receipt = {
|
|
689
|
+
attestationId,
|
|
690
|
+
certificateHash,
|
|
691
|
+
nodeRuntimeHash,
|
|
692
|
+
protocolVersion
|
|
693
|
+
};
|
|
694
|
+
const nodeId = extractStr(obj, "nodeId");
|
|
695
|
+
if (nodeId) receipt.nodeId = nodeId;
|
|
696
|
+
const attestedAt = extractStr(obj, "attestedAt");
|
|
697
|
+
if (attestedAt) receipt.attestedAt = attestedAt;
|
|
698
|
+
return receipt;
|
|
699
|
+
}
|
|
700
|
+
function getAttestationReceipt(bundle) {
|
|
701
|
+
if (typeof bundle !== "object" || bundle === null) return null;
|
|
702
|
+
const b = bundle;
|
|
703
|
+
const topLevel = buildReceipt(b);
|
|
704
|
+
if (topLevel) return topLevel;
|
|
705
|
+
if (typeof b.meta === "object" && b.meta !== null) {
|
|
706
|
+
const meta = b.meta;
|
|
707
|
+
if (typeof meta.attestation === "object" && meta.attestation !== null) {
|
|
708
|
+
const nested = buildReceipt(meta.attestation);
|
|
709
|
+
if (nested) return nested;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/wrappers.ts
|
|
716
|
+
function receiptFromProof(proof, bundle) {
|
|
717
|
+
if (!proof.attestationId || !proof.nodeRuntimeHash || !proof.protocolVersion) {
|
|
718
|
+
throw new CerAttestationError(
|
|
719
|
+
"Attestation proof is missing required fields (attestationId, nodeRuntimeHash, protocolVersion)"
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
attestationId: proof.attestationId,
|
|
724
|
+
certificateHash: proof.certificateHash ?? bundle.certificateHash,
|
|
725
|
+
nodeRuntimeHash: proof.nodeRuntimeHash,
|
|
726
|
+
protocolVersion: proof.protocolVersion
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async function certifyAndAttestDecision(params, options) {
|
|
730
|
+
const bundle = certifyDecision(params);
|
|
731
|
+
const proof = await attest(bundle, options);
|
|
732
|
+
const receipt = receiptFromProof(proof, bundle);
|
|
733
|
+
return { bundle, receipt };
|
|
734
|
+
}
|
|
735
|
+
async function attestIfNeeded(bundle, options) {
|
|
736
|
+
if (hasAttestation(bundle)) {
|
|
737
|
+
const existing = getAttestationReceipt(bundle);
|
|
738
|
+
if (existing) {
|
|
739
|
+
return { bundle, receipt: existing };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const proof = await attest(bundle, options);
|
|
743
|
+
const receipt = receiptFromProof(proof, bundle);
|
|
744
|
+
return { bundle, receipt };
|
|
745
|
+
}
|
|
583
746
|
// Annotate the CommonJS export names for ESM import in node:
|
|
584
747
|
0 && (module.exports = {
|
|
585
748
|
CerAttestationError,
|
|
586
749
|
CerVerificationError,
|
|
750
|
+
CerVerifyCode,
|
|
587
751
|
RunBuilder,
|
|
588
752
|
attest,
|
|
753
|
+
attestIfNeeded,
|
|
754
|
+
certifyAndAttestDecision,
|
|
589
755
|
certifyDecision,
|
|
590
756
|
computeInputHash,
|
|
591
757
|
computeOutputHash,
|
|
592
758
|
createSnapshot,
|
|
593
759
|
exportCer,
|
|
760
|
+
getAttestationReceipt,
|
|
594
761
|
hasAttestation,
|
|
595
762
|
hashCanonicalJson,
|
|
596
763
|
hashUtf8,
|