tsp-verify 0.1.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.
@@ -0,0 +1,192 @@
1
+ const PRIVATE_JWK_PARAMETERS = ['d', 'p', 'q', 'dp', 'dq', 'qi', 'oth'];
2
+ const manifestFields = [
3
+ 'tsp',
4
+ 'organization',
5
+ 'rootKey',
6
+ 'instances',
7
+ 'revoked',
8
+ 'sequence',
9
+ 'issuedAt',
10
+ 'acceptableAge',
11
+ 'rootSignatureOverManifest',
12
+ ];
13
+ const organizationFields = ['name', 'domain'];
14
+ const acceptableAgeFields = ['seconds'];
15
+ const instanceFields = ['id', 'publicKey', 'validFrom', 'validUntil', 'rootSignature'];
16
+ const revokedFields = ['id', 'revokedAt', 'reason'];
17
+ const publicJwkFields = ['kty', 'crv', 'x', 'alg', 'use', 'kid', 'ext', 'key_ops'];
18
+
19
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ const isString = (value) => typeof value === 'string';
21
+ const isoDateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
22
+
23
+ const hasOnly = (value, path, allowed, errors) => {
24
+ const allowedSet = new Set(allowed);
25
+
26
+ for (const key of Object.keys(value)) {
27
+ if (!allowedSet.has(key)) {
28
+ errors.push(`${path}.${key} is not allowed`);
29
+ }
30
+ }
31
+ };
32
+
33
+ const requireRecord = (parent, key, path, errors) => {
34
+ const value = parent[key];
35
+
36
+ if (!isRecord(value)) {
37
+ errors.push(`${path}.${key} must be an object`);
38
+ return undefined;
39
+ }
40
+
41
+ return value;
42
+ };
43
+
44
+ const requireString = (parent, key, path, errors) => {
45
+ const value = parent[key];
46
+
47
+ if (!isString(value) || value.length === 0) {
48
+ errors.push(`${path}.${key} must be a non-empty string`);
49
+ return undefined;
50
+ }
51
+
52
+ return value;
53
+ };
54
+
55
+ const requireIsoDateTime = (parent, key, path, errors) => {
56
+ const value = requireString(parent, key, path, errors);
57
+
58
+ if (value !== undefined && (!isoDateTimePattern.test(value) || Number.isNaN(Date.parse(value)))) {
59
+ errors.push(`${path}.${key} must be an ISO-8601 date-time string`);
60
+ }
61
+ };
62
+
63
+ const validatePublicJwk = (jwk, path, errors) => {
64
+ if (!isRecord(jwk)) {
65
+ errors.push(`${path} must be an object`);
66
+ return;
67
+ }
68
+
69
+ hasOnly(jwk, path, publicJwkFields, errors);
70
+
71
+ const privateParameters = PRIVATE_JWK_PARAMETERS.filter((parameter) => Object.hasOwn(jwk, parameter));
72
+ if (privateParameters.length > 0) {
73
+ errors.push(`${path} must not contain private JWK parameter(s): ${privateParameters.join(', ')}`);
74
+ }
75
+
76
+ if (jwk.kty === 'oct' || Object.hasOwn(jwk, 'k')) {
77
+ errors.push(`${path} must not contain symmetric key material`);
78
+ }
79
+
80
+ if (jwk.kty !== 'OKP') {
81
+ errors.push(`${path}.kty must be OKP for Ed25519 public keys`);
82
+ }
83
+
84
+ if (jwk.crv !== 'Ed25519') {
85
+ errors.push(`${path}.crv must be Ed25519`);
86
+ }
87
+
88
+ if (!isString(jwk.x) || jwk.x.length === 0) {
89
+ errors.push(`${path}.x must be a non-empty public key value`);
90
+ }
91
+
92
+ if (jwk.alg !== undefined && jwk.alg !== 'Ed25519' && jwk.alg !== 'EdDSA') {
93
+ errors.push(`${path}.alg must be Ed25519 or EdDSA when present`);
94
+ }
95
+ };
96
+
97
+ const validateOrganization = (organization, errors) => {
98
+ if (!organization) return;
99
+
100
+ hasOnly(organization, 'manifest.organization', organizationFields, errors);
101
+ requireString(organization, 'name', 'manifest.organization', errors);
102
+ requireString(organization, 'domain', 'manifest.organization', errors);
103
+ };
104
+
105
+ const validateAcceptableAge = (acceptableAge, errors) => {
106
+ if (!acceptableAge) return;
107
+
108
+ hasOnly(acceptableAge, 'manifest.acceptableAge', acceptableAgeFields, errors);
109
+
110
+ if (!Number.isFinite(acceptableAge.seconds) || acceptableAge.seconds <= 0) {
111
+ errors.push('manifest.acceptableAge.seconds must be a positive number');
112
+ }
113
+ };
114
+
115
+ const validateInstance = (instance, index, errors) => {
116
+ const path = `manifest.instances[${index}]`;
117
+
118
+ if (!isRecord(instance)) {
119
+ errors.push(`${path} must be an object`);
120
+ return;
121
+ }
122
+
123
+ hasOnly(instance, path, instanceFields, errors);
124
+ requireString(instance, 'id', path, errors);
125
+ validatePublicJwk(instance.publicKey, `${path}.publicKey`, errors);
126
+ requireIsoDateTime(instance, 'validFrom', path, errors);
127
+ requireIsoDateTime(instance, 'validUntil', path, errors);
128
+ requireString(instance, 'rootSignature', path, errors);
129
+ };
130
+
131
+ const validateRevoked = (entry, index, errors) => {
132
+ const path = `manifest.revoked[${index}]`;
133
+
134
+ if (!isRecord(entry)) {
135
+ errors.push(`${path} must be an object`);
136
+ return;
137
+ }
138
+
139
+ hasOnly(entry, path, revokedFields, errors);
140
+ requireString(entry, 'id', path, errors);
141
+ requireIsoDateTime(entry, 'revokedAt', path, errors);
142
+ requireString(entry, 'reason', path, errors);
143
+ };
144
+
145
+ export const validateTrustManifest = (manifest) => {
146
+ const errors = [];
147
+
148
+ if (!isRecord(manifest)) {
149
+ return { errors: ['manifest must be a JSON object'], ok: false };
150
+ }
151
+
152
+ hasOnly(manifest, 'manifest', manifestFields, errors);
153
+
154
+ if (manifest.tsp !== '3.0') {
155
+ errors.push('manifest.tsp must be "3.0"');
156
+ }
157
+
158
+ validateOrganization(requireRecord(manifest, 'organization', 'manifest', errors), errors);
159
+ validatePublicJwk(manifest.rootKey, 'manifest.rootKey', errors);
160
+
161
+ if (!Array.isArray(manifest.instances) || manifest.instances.length === 0) {
162
+ errors.push('manifest.instances must be a non-empty array');
163
+ } else {
164
+ const instanceIds = new Set();
165
+ manifest.instances.forEach((instance, index) => {
166
+ validateInstance(instance, index, errors);
167
+
168
+ if (isRecord(instance) && isString(instance.id)) {
169
+ if (instanceIds.has(instance.id)) {
170
+ errors.push(`manifest.instances contains duplicate instance id "${instance.id}"`);
171
+ }
172
+ instanceIds.add(instance.id);
173
+ }
174
+ });
175
+ }
176
+
177
+ if (!Array.isArray(manifest.revoked)) {
178
+ errors.push('manifest.revoked must be an array');
179
+ } else {
180
+ manifest.revoked.forEach((entry, index) => validateRevoked(entry, index, errors));
181
+ }
182
+
183
+ if (!Number.isInteger(manifest.sequence) || manifest.sequence < 0) {
184
+ errors.push('manifest.sequence must be a non-negative integer');
185
+ }
186
+
187
+ requireIsoDateTime(manifest, 'issuedAt', 'manifest', errors);
188
+ validateAcceptableAge(requireRecord(manifest, 'acceptableAge', 'manifest', errors), errors);
189
+ requireString(manifest, 'rootSignatureOverManifest', 'manifest', errors);
190
+
191
+ return { errors, ok: errors.length === 0 };
192
+ };
package/src/schema.js ADDED
@@ -0,0 +1,375 @@
1
+ const TSP_V3_VERSION = '3.0';
2
+ const sha256Pattern = /^[a-f0-9]{64}$/;
3
+ const dateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
4
+ const lowercaseHexPattern = /^[a-f0-9]+$/;
5
+ const sourceTypes = new Set([
6
+ 'legal-database',
7
+ 'government-website',
8
+ 'official-document',
9
+ 'academic-paper',
10
+ 'verified-website',
11
+ 'model-knowledge',
12
+ 'user-input',
13
+ 'unknown',
14
+ ]);
15
+ const contentTypes = new Set(['text', 'document', 'structured']);
16
+ const severities = new Set(['low', 'med', 'high']);
17
+ const signatureRoles = new Set(['instance', 'human-reviewer']);
18
+
19
+ const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
20
+ const isString = (value) => typeof value === 'string';
21
+
22
+ const hasOnly = (value, path, allowed, errors) => {
23
+ const allowedSet = new Set(allowed);
24
+
25
+ for (const key of Object.keys(value)) {
26
+ if (!allowedSet.has(key)) {
27
+ errors.push(`${path}.${key} is not allowed`);
28
+ }
29
+ }
30
+ };
31
+
32
+ const recordAt = (parent, key, path, errors) => {
33
+ const value = parent[key];
34
+
35
+ if (!isRecord(value)) {
36
+ errors.push(`${path}.${key} must be an object`);
37
+ return undefined;
38
+ }
39
+
40
+ return value;
41
+ };
42
+
43
+ const arrayAt = (parent, key, path, errors) => {
44
+ const value = parent[key];
45
+
46
+ if (!Array.isArray(value)) {
47
+ errors.push(`${path}.${key} must be an array`);
48
+ return undefined;
49
+ }
50
+
51
+ return value;
52
+ };
53
+
54
+ const stringAt = (parent, key, path, errors) => {
55
+ const value = parent[key];
56
+
57
+ if (!isString(value)) {
58
+ errors.push(`${path}.${key} must be a string`);
59
+ return undefined;
60
+ }
61
+
62
+ return value;
63
+ };
64
+
65
+ const optionalStringAt = (parent, key, path, errors) => {
66
+ if (parent[key] !== undefined && !isString(parent[key])) {
67
+ errors.push(`${path}.${key} must be a string`);
68
+ }
69
+ };
70
+
71
+ const booleanAt = (parent, key, path, errors) => {
72
+ if (typeof parent[key] !== 'boolean') {
73
+ errors.push(`${path}.${key} must be a boolean`);
74
+ }
75
+ };
76
+
77
+ const numberAt = (parent, key, path, errors) => {
78
+ if (typeof parent[key] !== 'number' || !Number.isFinite(parent[key])) {
79
+ errors.push(`${path}.${key} must be a finite number`);
80
+ }
81
+ };
82
+
83
+ const integerAt = (parent, key, path, errors) => {
84
+ if (!Number.isInteger(parent[key])) {
85
+ errors.push(`${path}.${key} must be an integer`);
86
+ }
87
+ };
88
+
89
+ const sha256At = (parent, key, path, errors) => {
90
+ const value = stringAt(parent, key, path, errors);
91
+
92
+ if (value !== undefined && !sha256Pattern.test(value)) {
93
+ errors.push(`${path}.${key} must be a lowercase sha256 hex string`);
94
+ }
95
+ };
96
+
97
+ const dateTimeAt = (parent, key, path, errors) => {
98
+ const value = stringAt(parent, key, path, errors);
99
+
100
+ if (value !== undefined && (!dateTimePattern.test(value) || Number.isNaN(Date.parse(value)))) {
101
+ errors.push(`${path}.${key} must be an ISO-8601 date-time string`);
102
+ }
103
+ };
104
+
105
+ const lowercaseHexAt = (parent, key, path, errors) => {
106
+ const value = stringAt(parent, key, path, errors);
107
+
108
+ if (value !== undefined && !lowercaseHexPattern.test(value)) {
109
+ errors.push(`${path}.${key} must be lowercase hexadecimal`);
110
+ }
111
+ };
112
+
113
+ const uriAt = (parent, key, path, errors) => {
114
+ const value = stringAt(parent, key, path, errors);
115
+
116
+ if (value === undefined) {
117
+ return;
118
+ }
119
+
120
+ try {
121
+ new URL(value);
122
+ } catch {
123
+ errors.push(`${path}.${key} must be a URI`);
124
+ }
125
+ };
126
+
127
+ export const validateTrustEnvelopeShape = (value) => {
128
+ const errors = [];
129
+
130
+ if (!isRecord(value)) {
131
+ return ['envelope must be an object'];
132
+ }
133
+
134
+ hasOnly(
135
+ value,
136
+ 'envelope',
137
+ [
138
+ 'tsp',
139
+ 'content',
140
+ 'declaration',
141
+ 'process',
142
+ 'alignment',
143
+ 'timestamp',
144
+ 'ledger',
145
+ 'signatures',
146
+ 'executionProvenance',
147
+ ],
148
+ errors,
149
+ );
150
+
151
+ const tsp = stringAt(value, 'tsp', 'envelope', errors);
152
+ if (tsp !== undefined && tsp !== TSP_V3_VERSION) {
153
+ errors.push(`envelope.tsp must be "${TSP_V3_VERSION}"`);
154
+ }
155
+
156
+ validateContent(recordAt(value, 'content', 'envelope', errors), errors);
157
+ validateDeclaration(recordAt(value, 'declaration', 'envelope', errors), errors);
158
+ validateProcess(recordAt(value, 'process', 'envelope', errors), errors);
159
+ validateAlignment(recordAt(value, 'alignment', 'envelope', errors), errors);
160
+ validateTimestamp(recordAt(value, 'timestamp', 'envelope', errors), errors);
161
+ validateLedger(recordAt(value, 'ledger', 'envelope', errors), errors);
162
+
163
+ const signatures = arrayAt(value, 'signatures', 'envelope', errors);
164
+ if (signatures !== undefined) {
165
+ if (signatures.length === 0) {
166
+ errors.push('envelope.signatures must contain at least one entry');
167
+ }
168
+
169
+ signatures.forEach((entry, index) => validateSignature(entry, `envelope.signatures[${index}]`, errors));
170
+ }
171
+
172
+ if (value.executionProvenance !== undefined) {
173
+ validateExecutionProvenance(recordAt(value, 'executionProvenance', 'envelope', errors), errors);
174
+ }
175
+
176
+ return errors;
177
+ };
178
+
179
+ const validateContent = (value, errors) => {
180
+ if (!value) return;
181
+
182
+ hasOnly(value, 'content', ['type', 'value', 'hash'], errors);
183
+ const type = stringAt(value, 'type', 'content', errors);
184
+ if (type !== undefined && !contentTypes.has(type)) {
185
+ errors.push('content.type must be text, document, or structured');
186
+ }
187
+ stringAt(value, 'value', 'content', errors);
188
+ sha256At(value, 'hash', 'content', errors);
189
+ };
190
+
191
+ const validateDeclaration = (value, errors) => {
192
+ if (!value) return;
193
+
194
+ hasOnly(value, 'declaration', ['primarySource', 'citations'], errors);
195
+ const primarySource = recordAt(value, 'primarySource', 'declaration', errors);
196
+ if (primarySource) {
197
+ hasOnly(primarySource, 'declaration.primarySource', ['type', 'url', 'title', 'retrieved'], errors);
198
+ const type = stringAt(primarySource, 'type', 'declaration.primarySource', errors);
199
+ if (type !== undefined && !sourceTypes.has(type)) {
200
+ errors.push('declaration.primarySource.type is not a v3 source type');
201
+ }
202
+ optionalStringAt(primarySource, 'url', 'declaration.primarySource', errors);
203
+ stringAt(primarySource, 'title', 'declaration.primarySource', errors);
204
+ if (primarySource.retrieved !== undefined) {
205
+ dateTimeAt(primarySource, 'retrieved', 'declaration.primarySource', errors);
206
+ }
207
+ }
208
+
209
+ arrayAt(value, 'citations', 'declaration', errors)?.forEach((entry, index) => {
210
+ const path = `declaration.citations[${index}]`;
211
+ if (!isRecord(entry)) {
212
+ errors.push(`${path} must be an object`);
213
+ return;
214
+ }
215
+ hasOnly(entry, path, ['url', 'paragraph', 'quote', 'retrieved'], errors);
216
+ stringAt(entry, 'url', path, errors);
217
+ stringAt(entry, 'paragraph', path, errors);
218
+ stringAt(entry, 'quote', path, errors);
219
+ dateTimeAt(entry, 'retrieved', path, errors);
220
+ });
221
+ };
222
+
223
+ const validateProcess = (value, errors) => {
224
+ if (!value) return;
225
+
226
+ hasOnly(value, 'process', ['model', 'systemPrompt', 'pipeline'], errors);
227
+ const model = recordAt(value, 'model', 'process', errors);
228
+ if (model) {
229
+ hasOnly(model, 'process.model', ['provider', 'name', 'version', 'temperature', 'contextWindow'], errors);
230
+ stringAt(model, 'provider', 'process.model', errors);
231
+ stringAt(model, 'name', 'process.model', errors);
232
+ stringAt(model, 'version', 'process.model', errors);
233
+ numberAt(model, 'temperature', 'process.model', errors);
234
+ integerAt(model, 'contextWindow', 'process.model', errors);
235
+ if (typeof model.contextWindow === 'number' && model.contextWindow < 0) {
236
+ errors.push('process.model.contextWindow must be non-negative');
237
+ }
238
+ }
239
+
240
+ validateSystemPrompt(recordAt(value, 'systemPrompt', 'process', errors), errors);
241
+ };
242
+
243
+ const validateSystemPrompt = (value, errors) => {
244
+ if (!value) return;
245
+
246
+ sha256At(value, 'hash', 'process.systemPrompt', errors);
247
+ if ('text' in value) {
248
+ hasOnly(value, 'process.systemPrompt', ['hash', 'text'], errors);
249
+ stringAt(value, 'text', 'process.systemPrompt', errors);
250
+ return;
251
+ }
252
+
253
+ hasOnly(value, 'process.systemPrompt', ['hash', 'redacted', 'reason'], errors);
254
+ if (value.redacted !== true) {
255
+ errors.push('process.systemPrompt.redacted must be true');
256
+ }
257
+ stringAt(value, 'reason', 'process.systemPrompt', errors);
258
+ };
259
+
260
+ const validateAlignment = (value, errors) => {
261
+ if (!value) return;
262
+
263
+ hasOnly(value, 'alignment', ['uncertainty', 'flags', 'humanReviewRequired', 'policy', 'refusal'], errors);
264
+ arrayAt(value, 'uncertainty', 'alignment', errors)?.forEach((entry, index) => {
265
+ const path = `alignment.uncertainty[${index}]`;
266
+ if (!isRecord(entry)) {
267
+ errors.push(`${path} must be an object`);
268
+ return;
269
+ }
270
+ hasOnly(entry, path, ['field', 'reason', 'severity'], errors);
271
+ stringAt(entry, 'field', path, errors);
272
+ stringAt(entry, 'reason', path, errors);
273
+ const severity = stringAt(entry, 'severity', path, errors);
274
+ if (severity !== undefined && !severities.has(severity)) {
275
+ errors.push(`${path}.severity must be low, med, or high`);
276
+ }
277
+ });
278
+
279
+ booleanAt(value, 'humanReviewRequired', 'alignment', errors);
280
+ const policy = recordAt(value, 'policy', 'alignment', errors);
281
+ if (policy) {
282
+ hasOnly(policy, 'alignment.policy', ['id', 'version'], errors);
283
+ stringAt(policy, 'id', 'alignment.policy', errors);
284
+ stringAt(policy, 'version', 'alignment.policy', errors);
285
+ }
286
+ };
287
+
288
+ const validateTimestamp = (value, errors) => {
289
+ if (!value) return;
290
+
291
+ hasOnly(value, 'timestamp', ['claimed', 'tsaToken', 'tsaUrl'], errors);
292
+ dateTimeAt(value, 'claimed', 'timestamp', errors);
293
+ stringAt(value, 'tsaToken', 'timestamp', errors);
294
+ uriAt(value, 'tsaUrl', 'timestamp', errors);
295
+ };
296
+
297
+ const validateLedger = (value, errors) => {
298
+ if (!value) return;
299
+
300
+ hasOnly(value, 'ledger', ['id', 'prevHash', 'hash'], errors);
301
+ stringAt(value, 'id', 'ledger', errors);
302
+ sha256At(value, 'prevHash', 'ledger', errors);
303
+ sha256At(value, 'hash', 'ledger', errors);
304
+ };
305
+
306
+ const validateSignature = (value, path, errors) => {
307
+ if (!isRecord(value)) {
308
+ errors.push(`${path} must be an object`);
309
+ return;
310
+ }
311
+
312
+ hasOnly(value, path, ['role', 'algorithm', 'keyRef', 'signature', 'certChain'], errors);
313
+ const role = stringAt(value, 'role', path, errors);
314
+ if (role !== undefined && !signatureRoles.has(role)) {
315
+ errors.push(`${path}.role must be instance or human-reviewer`);
316
+ }
317
+ const algorithm = stringAt(value, 'algorithm', path, errors);
318
+ if (algorithm !== undefined && algorithm !== 'ed25519') {
319
+ errors.push(`${path}.algorithm must be ed25519`);
320
+ }
321
+ uriAt(value, 'keyRef', path, errors);
322
+ stringAt(value, 'signature', path, errors);
323
+ arrayAt(value, 'certChain', path, errors)?.forEach((entry, index) => {
324
+ if (!isString(entry)) {
325
+ errors.push(`${path}.certChain[${index}] must be a string`);
326
+ }
327
+ });
328
+ };
329
+
330
+ const validateExecutionProvenance = (value, errors) => {
331
+ if (!value) return;
332
+
333
+ hasOnly(value, 'executionProvenance', ['spatialBoundary', 'temporalBoundary', 'deterministicOutput'], errors);
334
+ const spatialBoundary = recordAt(value, 'spatialBoundary', 'executionProvenance', errors);
335
+ if (spatialBoundary) {
336
+ hasOnly(
337
+ spatialBoundary,
338
+ 'executionProvenance.spatialBoundary',
339
+ ['gateway', 'toolsMounted', 'toolsIsolated', 'o1ConstraintMet'],
340
+ errors,
341
+ );
342
+ stringAt(spatialBoundary, 'gateway', 'executionProvenance.spatialBoundary', errors);
343
+ arrayAt(spatialBoundary, 'toolsMounted', 'executionProvenance.spatialBoundary', errors)?.forEach((entry, index) => {
344
+ if (!isString(entry)) {
345
+ errors.push(`executionProvenance.spatialBoundary.toolsMounted[${index}] must be a string`);
346
+ }
347
+ });
348
+ booleanAt(spatialBoundary, 'toolsIsolated', 'executionProvenance.spatialBoundary', errors);
349
+ booleanAt(spatialBoundary, 'o1ConstraintMet', 'executionProvenance.spatialBoundary', errors);
350
+ }
351
+
352
+ const temporalBoundary = recordAt(value, 'temporalBoundary', 'executionProvenance', errors);
353
+ if (temporalBoundary) {
354
+ hasOnly(
355
+ temporalBoundary,
356
+ 'executionProvenance.temporalBoundary',
357
+ ['engine', 'tier1AnchorHash', 'totalContextTokens', 'driftDetected'],
358
+ errors,
359
+ );
360
+ stringAt(temporalBoundary, 'engine', 'executionProvenance.temporalBoundary', errors);
361
+ lowercaseHexAt(temporalBoundary, 'tier1AnchorHash', 'executionProvenance.temporalBoundary', errors);
362
+ integerAt(temporalBoundary, 'totalContextTokens', 'executionProvenance.temporalBoundary', errors);
363
+ if (typeof temporalBoundary.totalContextTokens === 'number' && temporalBoundary.totalContextTokens < 0) {
364
+ errors.push('executionProvenance.temporalBoundary.totalContextTokens must be non-negative');
365
+ }
366
+ booleanAt(temporalBoundary, 'driftDetected', 'executionProvenance.temporalBoundary', errors);
367
+ }
368
+
369
+ const deterministicOutput = recordAt(value, 'deterministicOutput', 'executionProvenance', errors);
370
+ if (deterministicOutput) {
371
+ hasOnly(deterministicOutput, 'executionProvenance.deterministicOutput', ['status', 'payloadHash'], errors);
372
+ stringAt(deterministicOutput, 'status', 'executionProvenance.deterministicOutput', errors);
373
+ lowercaseHexAt(deterministicOutput, 'payloadHash', 'executionProvenance.deterministicOutput', errors);
374
+ }
375
+ };