@riddledc/riddle-proof 0.8.83 → 0.8.84
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 +55 -30
- package/dist/advanced/index.d.cts +1 -1
- package/dist/advanced/index.d.ts +1 -1
- package/dist/advanced/proof-run-engine.d.cts +1 -1
- package/dist/advanced/proof-run-engine.d.ts +1 -1
- package/dist/chunk-AQTCU6W5.js +1086 -0
- package/dist/index.cjs +569 -62
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -2
- package/dist/{proof-run-engine-MiKZt9oY.d.ts → proof-run-engine-BqRoA3Do.d.ts} +3 -3
- package/dist/{proof-run-engine-Baiv6l3A.d.cts → proof-run-engine-DpChFR5H.d.cts} +3 -3
- package/dist/proof-run-engine.d.cts +1 -1
- package/dist/proof-run-engine.d.ts +1 -1
- package/dist/semantic-certificate.cjs +571 -64
- package/dist/semantic-certificate.d.cts +120 -1
- package/dist/semantic-certificate.d.ts +120 -1
- package/dist/semantic-certificate.js +15 -3
- package/package.json +1 -1
- package/dist/chunk-DB5ZHRUP.js +0 -585
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
// src/semantic-certificate.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION = "riddle-proof.semantic-certificate.v0";
|
|
4
|
+
var RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION = "riddle-proof.semantic-certificate-closure.v0";
|
|
5
|
+
var RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_MAX_CERTIFICATES = 4096;
|
|
6
|
+
var SCOPE_FIELDS = [
|
|
7
|
+
"repository",
|
|
8
|
+
"revision",
|
|
9
|
+
"environment",
|
|
10
|
+
"target",
|
|
11
|
+
"proof_attempt"
|
|
12
|
+
];
|
|
13
|
+
var AUTHORITY_FIELDS = [
|
|
14
|
+
"authority",
|
|
15
|
+
"status",
|
|
16
|
+
"verdict",
|
|
17
|
+
"ready_to_ship",
|
|
18
|
+
"merge_ready",
|
|
19
|
+
"sync_allowed",
|
|
20
|
+
"ship_authorized",
|
|
21
|
+
"shipping_authorized",
|
|
22
|
+
"shipping_disabled",
|
|
23
|
+
"merge_recommended",
|
|
24
|
+
"merge_recommendation",
|
|
25
|
+
"shipping_authorization"
|
|
26
|
+
];
|
|
27
|
+
var CERTIFICATE_FIELDS = /* @__PURE__ */ new Set([
|
|
28
|
+
"version",
|
|
29
|
+
"certificate_id",
|
|
30
|
+
"scope",
|
|
31
|
+
"claim",
|
|
32
|
+
"evidence",
|
|
33
|
+
"derivation",
|
|
34
|
+
"issued_at"
|
|
35
|
+
]);
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
38
|
+
const prototype = Object.getPrototypeOf(value);
|
|
39
|
+
return prototype === Object.prototype || prototype === null;
|
|
40
|
+
}
|
|
41
|
+
function safeErrorMessage(error) {
|
|
42
|
+
try {
|
|
43
|
+
if (error instanceof Error) return String(error.message);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return String(error);
|
|
48
|
+
} catch {
|
|
49
|
+
return "unprintable thrown value";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function assertOnlyKeys(record, allowed, context) {
|
|
53
|
+
const allowedSet = new Set(allowed);
|
|
54
|
+
for (const key of Reflect.ownKeys(record)) {
|
|
55
|
+
if (typeof key !== "string") {
|
|
56
|
+
throw new Error(`${context} contains an unsupported symbol field.`);
|
|
57
|
+
}
|
|
58
|
+
if (!allowedSet.has(key)) {
|
|
59
|
+
throw new Error(`${context} contains unsupported field ${key}.`);
|
|
60
|
+
}
|
|
61
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
62
|
+
if (!descriptor || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0) {
|
|
63
|
+
throw new Error(`${context}.${key} must be an enumerable data field.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readDenseDataArray(value, context, maximumLength) {
|
|
68
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
69
|
+
throw new Error(`${context} must be a plain array.`);
|
|
70
|
+
}
|
|
71
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
72
|
+
if (!lengthDescriptor || typeof lengthDescriptor.value !== "number" || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) {
|
|
73
|
+
throw new Error(`${context}.length must be an own data field.`);
|
|
74
|
+
}
|
|
75
|
+
const length = lengthDescriptor.value;
|
|
76
|
+
if (maximumLength !== void 0 && length > maximumLength) {
|
|
77
|
+
throw new Error(`${context} exceeds ${maximumLength} entries.`);
|
|
78
|
+
}
|
|
79
|
+
const indexedElements = [];
|
|
80
|
+
let elementCount = 0;
|
|
81
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
82
|
+
if (key === "length") continue;
|
|
83
|
+
if (typeof key !== "string") {
|
|
84
|
+
throw new Error(`${context} contains an unsupported symbol field.`);
|
|
85
|
+
}
|
|
86
|
+
const index = Number(key);
|
|
87
|
+
if (!Number.isInteger(index) || index < 0 || index >= length || index >= 2 ** 32 - 1 || String(index) !== key) {
|
|
88
|
+
throw new Error(`${context} contains unsupported array field ${key}.`);
|
|
89
|
+
}
|
|
90
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
91
|
+
if (!descriptor || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0) {
|
|
92
|
+
throw new Error(`${context}[${key}] must be an enumerable data field.`);
|
|
93
|
+
}
|
|
94
|
+
indexedElements.push([index, descriptor.value]);
|
|
95
|
+
elementCount += 1;
|
|
96
|
+
}
|
|
97
|
+
if (elementCount !== length) {
|
|
98
|
+
throw new Error(`${context} must not contain sparse or inherited entries.`);
|
|
99
|
+
}
|
|
100
|
+
indexedElements.sort(([left], [right]) => left - right);
|
|
101
|
+
return indexedElements.map(([, entry]) => entry);
|
|
102
|
+
}
|
|
103
|
+
function requiredField(record, key, context) {
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
105
|
+
if (!descriptor) {
|
|
106
|
+
throw new Error(`${context}.${key} is required.`);
|
|
107
|
+
}
|
|
108
|
+
if (descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0) {
|
|
109
|
+
throw new Error(`${context}.${key} must be an enumerable data field.`);
|
|
110
|
+
}
|
|
111
|
+
return descriptor.value;
|
|
112
|
+
}
|
|
113
|
+
function optionalField(record, key) {
|
|
114
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
115
|
+
if (!descriptor) return void 0;
|
|
116
|
+
if (descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0) {
|
|
117
|
+
throw new Error(`${key} must be an enumerable data field.`);
|
|
118
|
+
}
|
|
119
|
+
return descriptor.value;
|
|
120
|
+
}
|
|
121
|
+
function requiredString(record, key, context) {
|
|
122
|
+
const value = requiredField(record, key, context);
|
|
123
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
124
|
+
throw new Error(`${context}.${key} must be a non-empty string.`);
|
|
125
|
+
}
|
|
126
|
+
return value.trim();
|
|
127
|
+
}
|
|
128
|
+
function optionalString(record, key, context) {
|
|
129
|
+
if (optionalField(record, key) === void 0) return void 0;
|
|
130
|
+
return requiredString(record, key, context);
|
|
131
|
+
}
|
|
132
|
+
function cloneJsonValue(value, context, ancestors = /* @__PURE__ */ new Set()) {
|
|
133
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === "number") {
|
|
137
|
+
if (!Number.isFinite(value)) throw new Error(`${context} must contain only finite numbers.`);
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
const elements = readDenseDataArray(value, context);
|
|
142
|
+
if (ancestors.has(value)) throw new Error(`${context} must not be cyclic.`);
|
|
143
|
+
ancestors.add(value);
|
|
144
|
+
const cloned2 = elements.map((entry, index) => cloneJsonValue(entry, `${context}[${index}]`, ancestors));
|
|
145
|
+
ancestors.delete(value);
|
|
146
|
+
return cloned2;
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(value)) throw new Error(`${context} must contain only JSON values.`);
|
|
149
|
+
if (ancestors.has(value)) throw new Error(`${context} must not be cyclic.`);
|
|
150
|
+
ancestors.add(value);
|
|
151
|
+
const cloned = {};
|
|
152
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
153
|
+
if (typeof key !== "string") {
|
|
154
|
+
throw new Error(`${context} contains an unsupported symbol field.`);
|
|
155
|
+
}
|
|
156
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
157
|
+
if (!descriptor || descriptor.enumerable !== true || descriptor.get !== void 0 || descriptor.set !== void 0) {
|
|
158
|
+
throw new Error(`${context}.${key} must be an enumerable data field.`);
|
|
159
|
+
}
|
|
160
|
+
Object.defineProperty(cloned, key, {
|
|
161
|
+
value: cloneJsonValue(descriptor.value, `${context}.${key}`, ancestors),
|
|
162
|
+
enumerable: true,
|
|
163
|
+
configurable: true,
|
|
164
|
+
writable: true
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
ancestors.delete(value);
|
|
168
|
+
return cloned;
|
|
169
|
+
}
|
|
170
|
+
function parseJsonObject(value, context) {
|
|
171
|
+
if (value === void 0) return void 0;
|
|
172
|
+
if (!isRecord(value)) {
|
|
173
|
+
throw new Error(`${context} must be a JSON object.`);
|
|
174
|
+
}
|
|
175
|
+
const cloned = cloneJsonValue(value, context);
|
|
176
|
+
if (!isRecord(cloned)) {
|
|
177
|
+
throw new Error(`${context} must remain a JSON object when serialized.`);
|
|
178
|
+
}
|
|
179
|
+
return cloned;
|
|
180
|
+
}
|
|
181
|
+
function parseIssuedAt(value, context) {
|
|
182
|
+
if (typeof value !== "string" || !value.trim() || !Number.isFinite(Date.parse(value))) {
|
|
183
|
+
throw new Error(`${context} must be a valid timestamp.`);
|
|
184
|
+
}
|
|
185
|
+
return value.trim();
|
|
186
|
+
}
|
|
187
|
+
function parseScope(value, context) {
|
|
188
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
189
|
+
assertOnlyKeys(value, SCOPE_FIELDS, context);
|
|
190
|
+
return {
|
|
191
|
+
repository: requiredString(value, "repository", context),
|
|
192
|
+
revision: requiredString(value, "revision", context),
|
|
193
|
+
environment: requiredString(value, "environment", context),
|
|
194
|
+
target: requiredString(value, "target", context),
|
|
195
|
+
proof_attempt: requiredString(value, "proof_attempt", context)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function parseClaimRef(value, context, allowedExtras = []) {
|
|
199
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
200
|
+
assertOnlyKeys(value, ["claim_id", "claim_version", "parameters", ...allowedExtras], context);
|
|
201
|
+
const parameters = parseJsonObject(optionalField(value, "parameters"), `${context}.parameters`);
|
|
202
|
+
return {
|
|
203
|
+
claim_id: requiredString(value, "claim_id", context),
|
|
204
|
+
claim_version: requiredString(value, "claim_version", context),
|
|
205
|
+
...parameters ? { parameters } : {}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function parseClaim(value, context) {
|
|
209
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
210
|
+
return {
|
|
211
|
+
...parseClaimRef(value, context, ["label"]),
|
|
212
|
+
label: requiredString(value, "label", context)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function parseEvidenceRef(value, context) {
|
|
216
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
217
|
+
assertOnlyKeys(
|
|
218
|
+
value,
|
|
219
|
+
["receipt_id", "artifact_digest", "role", "artifact_url", "artifact_path"],
|
|
220
|
+
context
|
|
221
|
+
);
|
|
222
|
+
const artifactDigest = requiredString(value, "artifact_digest", context).toLowerCase();
|
|
223
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(artifactDigest)) {
|
|
224
|
+
throw new Error(`${context}.artifact_digest must be a full sha256 digest.`);
|
|
225
|
+
}
|
|
226
|
+
const artifactUrl = optionalString(value, "artifact_url", context);
|
|
227
|
+
const artifactPath = optionalString(value, "artifact_path", context);
|
|
228
|
+
return {
|
|
229
|
+
receipt_id: requiredString(value, "receipt_id", context),
|
|
230
|
+
artifact_digest: artifactDigest,
|
|
231
|
+
role: requiredString(value, "role", context),
|
|
232
|
+
...artifactUrl ? { artifact_url: artifactUrl } : {},
|
|
233
|
+
...artifactPath ? { artifact_path: artifactPath } : {}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function parseEvidenceBundle(value, context) {
|
|
237
|
+
const entries = readDenseDataArray(value, context);
|
|
238
|
+
if (entries.length === 0) {
|
|
239
|
+
throw new Error(`${context} must contain at least one evidence reference.`);
|
|
240
|
+
}
|
|
241
|
+
return entries.map((entry, index) => parseEvidenceRef(entry, `${context}[${index}]`));
|
|
242
|
+
}
|
|
243
|
+
function parseContractRef(value, context) {
|
|
244
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
245
|
+
return {
|
|
246
|
+
contract_id: requiredString(value, "contract_id", context),
|
|
247
|
+
contract_version: requiredString(value, "contract_version", context),
|
|
248
|
+
label: requiredString(value, "label", context)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function parseContract(value, context, allowRuntimePredicate = false) {
|
|
252
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
253
|
+
assertOnlyKeys(
|
|
254
|
+
value,
|
|
255
|
+
["contract_id", "contract_version", "label", "claim", ...allowRuntimePredicate ? ["accepts"] : []],
|
|
256
|
+
context
|
|
257
|
+
);
|
|
258
|
+
if (allowRuntimePredicate && typeof requiredField(value, "accepts", context) !== "function") {
|
|
259
|
+
throw new Error(`${context}.accepts must be a function.`);
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
...parseContractRef(value, context),
|
|
263
|
+
claim: parseClaim(requiredField(value, "claim", context), `${context}.claim`)
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function parseRule(value, context, allowFullPremises = false) {
|
|
267
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
268
|
+
assertOnlyKeys(value, ["rule_id", "rule_version", "label", "premises", "conclusion"], context);
|
|
269
|
+
const premises = requiredField(value, "premises", context);
|
|
270
|
+
const premiseValues = readDenseDataArray(premises, `${context}.premises`);
|
|
271
|
+
if (premiseValues.length === 0) {
|
|
272
|
+
throw new Error(`${context}.premises must contain at least one claim reference.`);
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
rule_id: requiredString(value, "rule_id", context),
|
|
276
|
+
rule_version: requiredString(value, "rule_version", context),
|
|
277
|
+
label: requiredString(value, "label", context),
|
|
278
|
+
premises: premiseValues.map((premise, index) => parseClaimRef(
|
|
279
|
+
premise,
|
|
280
|
+
`${context}.premises[${index}]`,
|
|
281
|
+
allowFullPremises ? ["label"] : []
|
|
282
|
+
)),
|
|
283
|
+
conclusion: parseClaim(requiredField(value, "conclusion", context), `${context}.conclusion`)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function parsePremise(value, context) {
|
|
287
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
288
|
+
assertOnlyKeys(
|
|
289
|
+
value,
|
|
290
|
+
["certificate_id", "derivation_kind", "assurance", "scope", "claim", "evidence"],
|
|
291
|
+
context
|
|
292
|
+
);
|
|
293
|
+
const derivationKind = requiredString(value, "derivation_kind", context);
|
|
294
|
+
const assurance = requiredString(value, "assurance", context);
|
|
295
|
+
const validAssurance = derivationKind === "contract" && assurance === "runtime_contract_accepted" || derivationKind === "composition" && assurance === "declared_runtime_rule";
|
|
296
|
+
if (!validAssurance) {
|
|
297
|
+
throw new Error(`${context} must preserve a valid derivation_kind and assurance pair.`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
certificate_id: requiredString(value, "certificate_id", context),
|
|
301
|
+
derivation_kind: derivationKind,
|
|
302
|
+
assurance,
|
|
303
|
+
scope: parseScope(requiredField(value, "scope", context), `${context}.scope`),
|
|
304
|
+
claim: parseClaim(requiredField(value, "claim", context), `${context}.claim`),
|
|
305
|
+
evidence: parseEvidenceBundle(requiredField(value, "evidence", context), `${context}.evidence`)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function stableJson(value) {
|
|
309
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
310
|
+
if (isRecord(value)) {
|
|
311
|
+
return `{${Object.keys(value).filter((key) => value[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
|
|
312
|
+
}
|
|
313
|
+
const encoded = JSON.stringify(value);
|
|
314
|
+
if (encoded === void 0) throw new Error("Semantic certificate contains a non-JSON value.");
|
|
315
|
+
return encoded;
|
|
316
|
+
}
|
|
317
|
+
function sameClaimRef(left, right) {
|
|
318
|
+
return left.claim_id === right.claim_id && left.claim_version === right.claim_version && stableJson(left.parameters || {}) === stableJson(right.parameters || {});
|
|
319
|
+
}
|
|
320
|
+
function sameEvidence(left, right) {
|
|
321
|
+
return stableJson(left) === stableJson(right);
|
|
322
|
+
}
|
|
323
|
+
function certificateId(body) {
|
|
324
|
+
const digest = createHash("sha256").update(stableJson(body)).digest("hex");
|
|
325
|
+
return `rpsc_${digest}`;
|
|
326
|
+
}
|
|
327
|
+
function withCertificateId(body) {
|
|
328
|
+
return { ...body, certificate_id: certificateId(body) };
|
|
329
|
+
}
|
|
330
|
+
function parseDerivation(value, context) {
|
|
331
|
+
if (!isRecord(value)) throw new Error(`${context} must be an object.`);
|
|
332
|
+
const kind = requiredString(value, "kind", context);
|
|
333
|
+
if (kind === "contract") {
|
|
334
|
+
assertOnlyKeys(value, ["kind", "assurance", "contract"], context);
|
|
335
|
+
if (requiredField(value, "assurance", context) !== "runtime_contract_accepted") {
|
|
336
|
+
throw new Error(`${context}.assurance must be runtime_contract_accepted.`);
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
kind,
|
|
340
|
+
assurance: "runtime_contract_accepted",
|
|
341
|
+
contract: parseContract(requiredField(value, "contract", context), `${context}.contract`)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (kind === "composition") {
|
|
345
|
+
assertOnlyKeys(value, ["kind", "assurance", "rule", "premises"], context);
|
|
346
|
+
if (requiredField(value, "assurance", context) !== "declared_runtime_rule") {
|
|
347
|
+
throw new Error(`${context}.assurance must be declared_runtime_rule.`);
|
|
348
|
+
}
|
|
349
|
+
const premises = requiredField(value, "premises", context);
|
|
350
|
+
const premiseValues = readDenseDataArray(premises, `${context}.premises`);
|
|
351
|
+
if (premiseValues.length === 0) {
|
|
352
|
+
throw new Error(`${context}.premises must contain at least one certificate premise.`);
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
kind,
|
|
356
|
+
assurance: "declared_runtime_rule",
|
|
357
|
+
rule: parseRule(requiredField(value, "rule", context), `${context}.rule`),
|
|
358
|
+
premises: premiseValues.map((premise, index) => parsePremise(premise, `${context}.premises[${index}]`))
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
throw new Error(`${context}.kind must be contract or composition.`);
|
|
362
|
+
}
|
|
363
|
+
function riddleProofSemanticScopesEqual(left, right) {
|
|
364
|
+
return SCOPE_FIELDS.every((field) => left[field] === right[field]);
|
|
365
|
+
}
|
|
366
|
+
function createRiddleProofSemanticCertificate(input) {
|
|
367
|
+
if (!isRecord(input)) throw new Error("Semantic certificate input must be an object.");
|
|
368
|
+
assertOnlyKeys(
|
|
369
|
+
input,
|
|
370
|
+
["scope", "evidence", "observation", "contract", "issued_at"],
|
|
371
|
+
"semantic certificate input"
|
|
372
|
+
);
|
|
373
|
+
const scope = parseScope(requiredField(input, "scope", "semantic certificate input"), "semantic certificate scope");
|
|
374
|
+
const evidence = parseEvidenceBundle(
|
|
375
|
+
requiredField(input, "evidence", "semantic certificate input"),
|
|
376
|
+
"semantic certificate evidence"
|
|
377
|
+
);
|
|
378
|
+
const runtimeContract = requiredField(
|
|
379
|
+
input,
|
|
380
|
+
"contract",
|
|
381
|
+
"semantic certificate input"
|
|
382
|
+
);
|
|
383
|
+
const contract = parseContract(runtimeContract, "semantic certificate contract", true);
|
|
384
|
+
const accepts = requiredField(
|
|
385
|
+
runtimeContract,
|
|
386
|
+
"accepts",
|
|
387
|
+
"semantic certificate contract"
|
|
388
|
+
);
|
|
389
|
+
if (typeof accepts !== "function") {
|
|
390
|
+
throw new Error("semantic certificate contract.accepts must be a function.");
|
|
391
|
+
}
|
|
392
|
+
const contractRef = {
|
|
393
|
+
contract_id: contract.contract_id,
|
|
394
|
+
contract_version: contract.contract_version,
|
|
395
|
+
label: contract.label
|
|
396
|
+
};
|
|
397
|
+
const claim = contract.claim;
|
|
398
|
+
let accepted;
|
|
399
|
+
try {
|
|
400
|
+
accepted = accepts(
|
|
401
|
+
{ ...scope },
|
|
402
|
+
requiredField(input, "observation", "semantic certificate input")
|
|
403
|
+
) === true;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
error: {
|
|
408
|
+
code: "contract_error",
|
|
409
|
+
contract: contractRef,
|
|
410
|
+
message: `Semantic contract evaluation failed: ${safeErrorMessage(error)}.`
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (!accepted) {
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
error: {
|
|
418
|
+
code: "contract_rejected",
|
|
419
|
+
contract: contractRef,
|
|
420
|
+
message: "Semantic contract rejected the supplied observation at this scope."
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const body = {
|
|
425
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION,
|
|
426
|
+
scope,
|
|
427
|
+
claim,
|
|
428
|
+
evidence,
|
|
429
|
+
derivation: { kind: "contract", assurance: "runtime_contract_accepted", contract },
|
|
430
|
+
issued_at: parseIssuedAt(
|
|
431
|
+
optionalField(input, "issued_at") || (/* @__PURE__ */ new Date()).toISOString(),
|
|
432
|
+
"semantic certificate issued_at"
|
|
433
|
+
)
|
|
434
|
+
};
|
|
435
|
+
return { ok: true, certificate: withCertificateId(body) };
|
|
436
|
+
}
|
|
437
|
+
function parseRiddleProofSemanticCertificate(value) {
|
|
438
|
+
if (!isRecord(value)) throw new Error("Semantic certificate must be an object.");
|
|
439
|
+
const version = optionalField(value, "version");
|
|
440
|
+
if (version !== RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION) {
|
|
441
|
+
throw new Error(`Unsupported Semantic certificate version ${String(version || "missing")}.`);
|
|
442
|
+
}
|
|
443
|
+
for (const field of AUTHORITY_FIELDS) {
|
|
444
|
+
if (Object.prototype.hasOwnProperty.call(value, field)) {
|
|
445
|
+
throw new Error(`Semantic certificate must not contain authority field ${field}.`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
assertOnlyKeys(value, [...CERTIFICATE_FIELDS], "Semantic certificate");
|
|
449
|
+
const scope = parseScope(requiredField(value, "scope", "semantic certificate"), "semantic certificate scope");
|
|
450
|
+
const claim = parseClaim(requiredField(value, "claim", "semantic certificate"), "semantic certificate claim");
|
|
451
|
+
const evidence = parseEvidenceBundle(
|
|
452
|
+
requiredField(value, "evidence", "semantic certificate"),
|
|
453
|
+
"semantic certificate evidence"
|
|
454
|
+
);
|
|
455
|
+
const derivation = parseDerivation(
|
|
456
|
+
requiredField(value, "derivation", "semantic certificate"),
|
|
457
|
+
"semantic certificate derivation"
|
|
458
|
+
);
|
|
459
|
+
if (derivation.kind === "contract" && !sameClaimRef(claim, derivation.contract.claim)) {
|
|
460
|
+
throw new Error("Semantic contract-derived claim must match its contract claim.");
|
|
461
|
+
}
|
|
462
|
+
if (derivation.kind === "composition") {
|
|
463
|
+
if (derivation.premises.length !== derivation.rule.premises.length) {
|
|
464
|
+
throw new Error("Semantic composition premises must match the rule premise count.");
|
|
465
|
+
}
|
|
466
|
+
derivation.premises.forEach((premise, index) => {
|
|
467
|
+
if (!riddleProofSemanticScopesEqual(scope, premise.scope)) {
|
|
468
|
+
throw new Error(`Semantic composition premise ${index} must have the certificate scope.`);
|
|
469
|
+
}
|
|
470
|
+
if (!sameClaimRef(premise.claim, derivation.rule.premises[index])) {
|
|
471
|
+
throw new Error(`Semantic composition premise ${index} must match its rule claim.`);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
if (!sameClaimRef(claim, derivation.rule.conclusion)) {
|
|
475
|
+
throw new Error("Semantic composition claim must match its rule conclusion.");
|
|
476
|
+
}
|
|
477
|
+
const expectedEvidence = derivation.premises.flatMap((premise) => premise.evidence);
|
|
478
|
+
if (!sameEvidence(evidence, expectedEvidence)) {
|
|
479
|
+
throw new Error("Semantic composition evidence must be the ordered concatenation of premise evidence.");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const body = {
|
|
483
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION,
|
|
484
|
+
scope,
|
|
485
|
+
claim,
|
|
486
|
+
evidence,
|
|
487
|
+
derivation,
|
|
488
|
+
issued_at: parseIssuedAt(
|
|
489
|
+
requiredField(value, "issued_at", "semantic certificate"),
|
|
490
|
+
"semantic certificate issued_at"
|
|
491
|
+
)
|
|
492
|
+
};
|
|
493
|
+
const observedId = requiredString(value, "certificate_id", "semantic certificate");
|
|
494
|
+
const expectedId = certificateId(body);
|
|
495
|
+
if (observedId !== expectedId) {
|
|
496
|
+
throw new Error("Semantic certificate_id must match its content.");
|
|
497
|
+
}
|
|
498
|
+
return { ...body, certificate_id: observedId };
|
|
499
|
+
}
|
|
500
|
+
function matchRiddleProofSemanticCertificate(input) {
|
|
501
|
+
if (!isRecord(input)) throw new Error("Semantic certificate match input must be a plain object.");
|
|
502
|
+
assertOnlyKeys(
|
|
503
|
+
input,
|
|
504
|
+
[
|
|
505
|
+
"certificate",
|
|
506
|
+
"expected_certificate_id",
|
|
507
|
+
"expected_scope",
|
|
508
|
+
"expected_claim",
|
|
509
|
+
"expected_assurance"
|
|
510
|
+
],
|
|
511
|
+
"semantic certificate match input"
|
|
512
|
+
);
|
|
513
|
+
const expectedCertificateId = requiredString(
|
|
514
|
+
input,
|
|
515
|
+
"expected_certificate_id",
|
|
516
|
+
"semantic certificate match input"
|
|
517
|
+
);
|
|
518
|
+
if (!/^rpsc_[0-9a-f]{64}$/u.test(expectedCertificateId)) {
|
|
519
|
+
throw new Error(
|
|
520
|
+
"Semantic certificate match input.expected_certificate_id must be a full rpsc content ID."
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
const expectedScope = parseScope(
|
|
524
|
+
requiredField(input, "expected_scope", "semantic certificate match input"),
|
|
525
|
+
"semantic certificate match expected_scope"
|
|
526
|
+
);
|
|
527
|
+
const expectedClaim = parseClaimRef(
|
|
528
|
+
requiredField(input, "expected_claim", "semantic certificate match input"),
|
|
529
|
+
"semantic certificate match expected_claim",
|
|
530
|
+
["label"]
|
|
531
|
+
);
|
|
532
|
+
const expectedAssurance = requiredString(
|
|
533
|
+
input,
|
|
534
|
+
"expected_assurance",
|
|
535
|
+
"semantic certificate match input"
|
|
536
|
+
);
|
|
537
|
+
if (expectedAssurance !== "runtime_contract_accepted" && expectedAssurance !== "declared_runtime_rule") {
|
|
538
|
+
throw new Error(
|
|
539
|
+
"Semantic certificate match input.expected_assurance must be runtime_contract_accepted or declared_runtime_rule."
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
let certificate;
|
|
543
|
+
try {
|
|
544
|
+
certificate = parseRiddleProofSemanticCertificate(
|
|
545
|
+
requiredField(input, "certificate", "semantic certificate match input")
|
|
546
|
+
);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
error: {
|
|
551
|
+
code: "invalid_certificate",
|
|
552
|
+
message: `Semantic certificate did not parse: ${safeErrorMessage(error)}`
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (certificate.certificate_id !== expectedCertificateId) {
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
error: {
|
|
560
|
+
code: "certificate_id_mismatch",
|
|
561
|
+
expected: expectedCertificateId,
|
|
562
|
+
observed: certificate.certificate_id,
|
|
563
|
+
message: "Semantic certificate does not match the trusted expected content ID."
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
const scopeMismatch = firstScopeMismatch(expectedScope, certificate.scope);
|
|
568
|
+
if (scopeMismatch) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
error: {
|
|
572
|
+
code: "scope_mismatch",
|
|
573
|
+
...scopeMismatch,
|
|
574
|
+
message: `Semantic certificate has a different ${scopeMismatch.field} than the consumer expected.`
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (!sameClaimRef(expectedClaim, certificate.claim)) {
|
|
579
|
+
return {
|
|
580
|
+
ok: false,
|
|
581
|
+
error: {
|
|
582
|
+
code: "claim_mismatch",
|
|
583
|
+
expected: expectedClaim,
|
|
584
|
+
observed: parseClaimRef(
|
|
585
|
+
certificate.claim,
|
|
586
|
+
"semantic certificate match observed claim",
|
|
587
|
+
["label"]
|
|
588
|
+
),
|
|
589
|
+
message: "Semantic certificate does not state the claim the consumer expected."
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
if (certificate.derivation.assurance !== expectedAssurance) {
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: {
|
|
597
|
+
code: "assurance_mismatch",
|
|
598
|
+
expected: expectedAssurance,
|
|
599
|
+
observed: certificate.derivation.assurance,
|
|
600
|
+
message: "Semantic certificate does not have the assurance the consumer expected."
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
return { ok: true, certificate };
|
|
605
|
+
}
|
|
606
|
+
function firstScopeMismatch(expected, observed) {
|
|
607
|
+
for (const field of SCOPE_FIELDS) {
|
|
608
|
+
if (expected[field] !== observed[field]) {
|
|
609
|
+
return { field, expected: expected[field], observed: observed[field] };
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return void 0;
|
|
613
|
+
}
|
|
614
|
+
function premiseFromCertificate(certificate) {
|
|
615
|
+
return {
|
|
616
|
+
certificate_id: certificate.certificate_id,
|
|
617
|
+
derivation_kind: certificate.derivation.kind,
|
|
618
|
+
assurance: certificate.derivation.assurance,
|
|
619
|
+
scope: { ...certificate.scope },
|
|
620
|
+
claim: parseClaim(certificate.claim, "semantic composition premise claim"),
|
|
621
|
+
evidence: certificate.evidence.map((entry) => ({ ...entry }))
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function composeRiddleProofSemanticCertificates(input) {
|
|
625
|
+
if (!isRecord(input)) throw new Error("Semantic composition input must be an object.");
|
|
626
|
+
assertOnlyKeys(
|
|
627
|
+
input,
|
|
628
|
+
["rule", "certificates", "issued_at"],
|
|
629
|
+
"semantic composition input"
|
|
630
|
+
);
|
|
631
|
+
const rule = parseRule(
|
|
632
|
+
requiredField(input, "rule", "semantic composition input"),
|
|
633
|
+
"semantic composition rule",
|
|
634
|
+
true
|
|
635
|
+
);
|
|
636
|
+
const inputCertificates = requiredField(input, "certificates", "semantic composition input");
|
|
637
|
+
const certificateValues = readDenseDataArray(
|
|
638
|
+
inputCertificates,
|
|
639
|
+
"semantic composition input.certificates"
|
|
640
|
+
);
|
|
641
|
+
if (certificateValues.length === 0) {
|
|
642
|
+
throw new Error("Semantic composition requires at least one certificate.");
|
|
643
|
+
}
|
|
644
|
+
const certificates = certificateValues.map((certificate) => parseRiddleProofSemanticCertificate(certificate));
|
|
645
|
+
if (certificates.length !== rule.premises.length) {
|
|
646
|
+
return {
|
|
647
|
+
ok: false,
|
|
648
|
+
error: {
|
|
649
|
+
code: "premise_count_mismatch",
|
|
650
|
+
expected: rule.premises.length,
|
|
651
|
+
observed: certificates.length,
|
|
652
|
+
message: `Semantic rule expected ${rule.premises.length} certificate(s), received ${certificates.length}.`
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const expectedScope = certificates[0].scope;
|
|
657
|
+
for (let index = 1; index < certificates.length; index += 1) {
|
|
658
|
+
const mismatch = firstScopeMismatch(expectedScope, certificates[index].scope);
|
|
659
|
+
if (mismatch) {
|
|
660
|
+
return {
|
|
661
|
+
ok: false,
|
|
662
|
+
error: {
|
|
663
|
+
code: "scope_mismatch",
|
|
664
|
+
input_index: index,
|
|
665
|
+
...mismatch,
|
|
666
|
+
message: `Semantic certificate ${index} has a different ${mismatch.field}.`
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (let index = 0; index < certificates.length; index += 1) {
|
|
672
|
+
const expected = rule.premises[index];
|
|
673
|
+
const observed = certificates[index].claim;
|
|
674
|
+
if (!sameClaimRef(expected, observed)) {
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
error: {
|
|
678
|
+
code: "premise_mismatch",
|
|
679
|
+
input_index: index,
|
|
680
|
+
expected,
|
|
681
|
+
observed: parseClaimRef(observed, "semantic composition observed claim", ["label"]),
|
|
682
|
+
message: `Semantic certificate ${index} does not satisfy its declared rule premise.`
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const evidence = certificates.flatMap((certificate) => certificate.evidence);
|
|
688
|
+
const body = {
|
|
689
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION,
|
|
690
|
+
scope: { ...expectedScope },
|
|
691
|
+
claim: { ...rule.conclusion },
|
|
692
|
+
evidence,
|
|
693
|
+
derivation: {
|
|
694
|
+
kind: "composition",
|
|
695
|
+
assurance: "declared_runtime_rule",
|
|
696
|
+
rule,
|
|
697
|
+
premises: certificates.map(premiseFromCertificate)
|
|
698
|
+
},
|
|
699
|
+
issued_at: parseIssuedAt(
|
|
700
|
+
optionalField(input, "issued_at") || (/* @__PURE__ */ new Date()).toISOString(),
|
|
701
|
+
"semantic certificate issued_at"
|
|
702
|
+
)
|
|
703
|
+
};
|
|
704
|
+
return { ok: true, certificate: withCertificateId(body) };
|
|
705
|
+
}
|
|
706
|
+
function invalidClosure(message) {
|
|
707
|
+
return { ok: false, error: { code: "invalid_closure", message } };
|
|
708
|
+
}
|
|
709
|
+
function firstPremiseSnapshotMismatch(snapshot, certificate) {
|
|
710
|
+
const observed = premiseFromCertificate(certificate);
|
|
711
|
+
if (snapshot.derivation_kind !== observed.derivation_kind) return "derivation_kind";
|
|
712
|
+
if (snapshot.assurance !== observed.assurance) return "assurance";
|
|
713
|
+
if (stableJson(snapshot.scope) !== stableJson(observed.scope)) return "scope";
|
|
714
|
+
if (stableJson(snapshot.claim) !== stableJson(observed.claim)) return "claim";
|
|
715
|
+
if (stableJson(snapshot.evidence) !== stableJson(observed.evidence)) return "evidence";
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
function validateSemanticCertificateClosureInternal(value) {
|
|
719
|
+
if (!isRecord(value)) {
|
|
720
|
+
return invalidClosure("Semantic certificate closure must be a plain object.");
|
|
721
|
+
}
|
|
722
|
+
assertOnlyKeys(
|
|
723
|
+
value,
|
|
724
|
+
["version", "root_certificate_id", "certificates"],
|
|
725
|
+
"Semantic certificate closure"
|
|
726
|
+
);
|
|
727
|
+
if (requiredField(value, "version", "Semantic certificate closure") !== RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION) {
|
|
728
|
+
return invalidClosure("Unsupported Semantic certificate closure version.");
|
|
729
|
+
}
|
|
730
|
+
const rootCertificateId = requiredString(
|
|
731
|
+
value,
|
|
732
|
+
"root_certificate_id",
|
|
733
|
+
"Semantic certificate closure"
|
|
734
|
+
);
|
|
735
|
+
if (!/^rpsc_[0-9a-f]{64}$/u.test(rootCertificateId)) {
|
|
736
|
+
return invalidClosure(
|
|
737
|
+
"Semantic certificate closure.root_certificate_id must be a full rpsc content ID."
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
const inputCertificates = requiredField(
|
|
741
|
+
value,
|
|
742
|
+
"certificates",
|
|
743
|
+
"Semantic certificate closure"
|
|
744
|
+
);
|
|
745
|
+
const certificateValues = readDenseDataArray(
|
|
746
|
+
inputCertificates,
|
|
747
|
+
"Semantic certificate closure.certificates",
|
|
748
|
+
RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_MAX_CERTIFICATES
|
|
749
|
+
);
|
|
750
|
+
if (certificateValues.length === 0) {
|
|
751
|
+
return invalidClosure("Semantic certificate closure must contain at least one certificate.");
|
|
752
|
+
}
|
|
753
|
+
const certificates = [];
|
|
754
|
+
for (let index = 0; index < certificateValues.length; index += 1) {
|
|
755
|
+
try {
|
|
756
|
+
certificates.push(parseRiddleProofSemanticCertificate(certificateValues[index]));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
error: {
|
|
761
|
+
code: "invalid_closure_certificate",
|
|
762
|
+
input_index: index,
|
|
763
|
+
message: `Semantic certificate closure certificate ${index} did not parse: ${safeErrorMessage(error)}`
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const certificatesById = /* @__PURE__ */ new Map();
|
|
769
|
+
const firstIndexById = /* @__PURE__ */ new Map();
|
|
770
|
+
for (let index = 0; index < certificates.length; index += 1) {
|
|
771
|
+
const certificate = certificates[index];
|
|
772
|
+
const firstIndex = firstIndexById.get(certificate.certificate_id);
|
|
773
|
+
if (firstIndex !== void 0) {
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
error: {
|
|
777
|
+
code: "duplicate_certificate_id",
|
|
778
|
+
certificate_id: certificate.certificate_id,
|
|
779
|
+
first_index: firstIndex,
|
|
780
|
+
duplicate_index: index,
|
|
781
|
+
message: `Semantic certificate closure repeats certificate ${certificate.certificate_id}.`
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
firstIndexById.set(certificate.certificate_id, index);
|
|
786
|
+
certificatesById.set(certificate.certificate_id, certificate);
|
|
787
|
+
}
|
|
788
|
+
const rootCertificate = certificatesById.get(rootCertificateId);
|
|
789
|
+
if (!rootCertificate) {
|
|
790
|
+
return {
|
|
791
|
+
ok: false,
|
|
792
|
+
error: {
|
|
793
|
+
code: "root_certificate_missing",
|
|
794
|
+
root_certificate_id: rootCertificateId,
|
|
795
|
+
message: "Semantic certificate closure does not contain its declared root certificate."
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const states = /* @__PURE__ */ new Map();
|
|
800
|
+
const stack = [{ certificate: rootCertificate, next_premise_index: 0 }];
|
|
801
|
+
const dependencyFirst = [];
|
|
802
|
+
states.set(rootCertificateId, "visiting");
|
|
803
|
+
while (stack.length > 0) {
|
|
804
|
+
const frame = stack[stack.length - 1];
|
|
805
|
+
const premises = frame.certificate.derivation.kind === "composition" ? frame.certificate.derivation.premises : [];
|
|
806
|
+
if (frame.next_premise_index < premises.length) {
|
|
807
|
+
const premiseIndex = frame.next_premise_index;
|
|
808
|
+
frame.next_premise_index += 1;
|
|
809
|
+
const snapshot = premises[premiseIndex];
|
|
810
|
+
const child = certificatesById.get(snapshot.certificate_id);
|
|
811
|
+
if (!child) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
error: {
|
|
815
|
+
code: "dangling_premise",
|
|
816
|
+
parent_certificate_id: frame.certificate.certificate_id,
|
|
817
|
+
premise_index: premiseIndex,
|
|
818
|
+
premise_certificate_id: snapshot.certificate_id,
|
|
819
|
+
message: `Semantic certificate ${frame.certificate.certificate_id} premise ${premiseIndex} has no full certificate body.`
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const mismatch = firstPremiseSnapshotMismatch(snapshot, child);
|
|
824
|
+
if (mismatch) {
|
|
825
|
+
return {
|
|
826
|
+
ok: false,
|
|
827
|
+
error: {
|
|
828
|
+
code: "premise_snapshot_mismatch",
|
|
829
|
+
parent_certificate_id: frame.certificate.certificate_id,
|
|
830
|
+
premise_index: premiseIndex,
|
|
831
|
+
premise_certificate_id: snapshot.certificate_id,
|
|
832
|
+
field: mismatch,
|
|
833
|
+
message: `Semantic certificate ${frame.certificate.certificate_id} premise ${premiseIndex} has a different ${mismatch} than its full certificate body.`
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
const childState = states.get(child.certificate_id);
|
|
838
|
+
if (childState === "visiting") {
|
|
839
|
+
const cycleStart = stack.findIndex(
|
|
840
|
+
(candidate) => candidate.certificate.certificate_id === child.certificate_id
|
|
841
|
+
);
|
|
842
|
+
const certificateIds = stack.slice(Math.max(0, cycleStart)).map((candidate) => candidate.certificate.certificate_id);
|
|
843
|
+
certificateIds.push(child.certificate_id);
|
|
844
|
+
return {
|
|
845
|
+
ok: false,
|
|
846
|
+
error: {
|
|
847
|
+
code: "certificate_cycle",
|
|
848
|
+
certificate_ids: certificateIds,
|
|
849
|
+
message: "Semantic certificate closure contains a certificate cycle."
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
if (childState !== "visited") {
|
|
854
|
+
states.set(child.certificate_id, "visiting");
|
|
855
|
+
stack.push({ certificate: child, next_premise_index: 0 });
|
|
856
|
+
}
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
stack.pop();
|
|
860
|
+
states.set(frame.certificate.certificate_id, "visited");
|
|
861
|
+
dependencyFirst.push(frame.certificate);
|
|
862
|
+
}
|
|
863
|
+
if (dependencyFirst.length !== certificates.length) {
|
|
864
|
+
const reachable = new Set(dependencyFirst.map((certificate) => certificate.certificate_id));
|
|
865
|
+
const unreachable = certificates.filter((certificate) => !reachable.has(certificate.certificate_id)).map((certificate) => certificate.certificate_id).sort();
|
|
866
|
+
return {
|
|
867
|
+
ok: false,
|
|
868
|
+
error: {
|
|
869
|
+
code: "unreachable_certificates",
|
|
870
|
+
certificate_ids: unreachable,
|
|
871
|
+
message: "Semantic certificate closure contains certificates unreachable from its root."
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const closure = {
|
|
876
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION,
|
|
877
|
+
root_certificate_id: rootCertificateId,
|
|
878
|
+
certificates: dependencyFirst
|
|
879
|
+
};
|
|
880
|
+
return { ok: true, closure, root_certificate: rootCertificate };
|
|
881
|
+
}
|
|
882
|
+
function validateRiddleProofSemanticCertificateClosure(value) {
|
|
883
|
+
try {
|
|
884
|
+
return validateSemanticCertificateClosureInternal(value);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
return invalidClosure(
|
|
887
|
+
`Semantic certificate closure did not parse: ${safeErrorMessage(error)}`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function createRiddleProofSemanticAtomicCertificateClosure(input) {
|
|
892
|
+
if (!isRecord(input)) throw new Error("Semantic certificate closure input must be a plain object.");
|
|
893
|
+
assertOnlyKeys(input, ["certificate"], "Semantic certificate closure input");
|
|
894
|
+
const certificate = requiredField(input, "certificate", "Semantic certificate closure input");
|
|
895
|
+
let parsedCertificate;
|
|
896
|
+
try {
|
|
897
|
+
parsedCertificate = parseRiddleProofSemanticCertificate(certificate);
|
|
898
|
+
} catch (error) {
|
|
899
|
+
return {
|
|
900
|
+
ok: false,
|
|
901
|
+
error: {
|
|
902
|
+
code: "invalid_closure_certificate",
|
|
903
|
+
input_index: 0,
|
|
904
|
+
message: `Semantic certificate closure certificate 0 did not parse: ${safeErrorMessage(error)}`
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (parsedCertificate.derivation.kind !== "contract") {
|
|
909
|
+
return invalidClosure(
|
|
910
|
+
"An atomic Semantic certificate closure requires a contract-derived certificate."
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
return validateRiddleProofSemanticCertificateClosure({
|
|
914
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION,
|
|
915
|
+
root_certificate_id: parsedCertificate.certificate_id,
|
|
916
|
+
certificates: [certificate]
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function composeRiddleProofSemanticCertificateClosures(input) {
|
|
920
|
+
if (!isRecord(input)) throw new Error("Semantic closure composition input must be a plain object.");
|
|
921
|
+
assertOnlyKeys(
|
|
922
|
+
input,
|
|
923
|
+
["rule", "closures", "issued_at"],
|
|
924
|
+
"Semantic closure composition input"
|
|
925
|
+
);
|
|
926
|
+
const inputClosures = requiredField(
|
|
927
|
+
input,
|
|
928
|
+
"closures",
|
|
929
|
+
"Semantic closure composition input"
|
|
930
|
+
);
|
|
931
|
+
const closureValues = readDenseDataArray(
|
|
932
|
+
inputClosures,
|
|
933
|
+
"Semantic closure composition input.closures"
|
|
934
|
+
);
|
|
935
|
+
if (closureValues.length === 0) {
|
|
936
|
+
throw new Error("Semantic closure composition requires at least one closure.");
|
|
937
|
+
}
|
|
938
|
+
const validatedClosures = [];
|
|
939
|
+
for (let index = 0; index < closureValues.length; index += 1) {
|
|
940
|
+
const result = validateRiddleProofSemanticCertificateClosure(closureValues[index]);
|
|
941
|
+
if (!result.ok) {
|
|
942
|
+
return {
|
|
943
|
+
ok: false,
|
|
944
|
+
error: {
|
|
945
|
+
code: "input_closure_invalid",
|
|
946
|
+
input_index: index,
|
|
947
|
+
cause: result.error,
|
|
948
|
+
message: `Semantic input closure ${index} is invalid: ${result.error.message}`
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
validatedClosures.push(result);
|
|
953
|
+
}
|
|
954
|
+
const roots = validatedClosures.map((result) => {
|
|
955
|
+
if (!result.ok) throw new Error("Validated Semantic closure unexpectedly became invalid.");
|
|
956
|
+
return result.root_certificate;
|
|
957
|
+
});
|
|
958
|
+
const composition = composeRiddleProofSemanticCertificates({
|
|
959
|
+
rule: requiredField(input, "rule", "Semantic closure composition input"),
|
|
960
|
+
certificates: roots,
|
|
961
|
+
issued_at: optionalField(input, "issued_at")
|
|
962
|
+
});
|
|
963
|
+
if (!composition.ok) return composition;
|
|
964
|
+
const merged = [];
|
|
965
|
+
const byId = /* @__PURE__ */ new Map();
|
|
966
|
+
for (const result of validatedClosures) {
|
|
967
|
+
if (!result.ok) continue;
|
|
968
|
+
for (const certificate of result.closure.certificates) {
|
|
969
|
+
const existing = byId.get(certificate.certificate_id);
|
|
970
|
+
if (existing) {
|
|
971
|
+
if (stableJson(existing) !== stableJson(certificate)) {
|
|
972
|
+
return {
|
|
973
|
+
ok: false,
|
|
974
|
+
error: {
|
|
975
|
+
code: "certificate_id_collision",
|
|
976
|
+
certificate_id: certificate.certificate_id,
|
|
977
|
+
message: `Semantic closures contain unequal bodies for ${certificate.certificate_id}.`
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
byId.set(certificate.certificate_id, certificate);
|
|
984
|
+
merged.push(certificate);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (byId.has(composition.certificate.certificate_id)) {
|
|
988
|
+
return {
|
|
989
|
+
ok: false,
|
|
990
|
+
error: {
|
|
991
|
+
code: "certificate_id_collision",
|
|
992
|
+
certificate_id: composition.certificate.certificate_id,
|
|
993
|
+
message: "Semantic composition produced a root ID already present in its premise closures."
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
merged.push(composition.certificate);
|
|
998
|
+
const validation = validateRiddleProofSemanticCertificateClosure({
|
|
999
|
+
version: RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION,
|
|
1000
|
+
root_certificate_id: composition.certificate.certificate_id,
|
|
1001
|
+
certificates: merged
|
|
1002
|
+
});
|
|
1003
|
+
if (!validation.ok) {
|
|
1004
|
+
return {
|
|
1005
|
+
ok: false,
|
|
1006
|
+
error: {
|
|
1007
|
+
code: "closure_construction_failed",
|
|
1008
|
+
cause: validation.error,
|
|
1009
|
+
message: `Semantic closure construction failed: ${validation.error.message}`
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
ok: true,
|
|
1015
|
+
certificate: composition.certificate,
|
|
1016
|
+
closure: validation.closure
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function matchRiddleProofSemanticCertificateClosure(input) {
|
|
1020
|
+
if (!isRecord(input)) throw new Error("Semantic certificate closure match input must be a plain object.");
|
|
1021
|
+
assertOnlyKeys(
|
|
1022
|
+
input,
|
|
1023
|
+
[
|
|
1024
|
+
"closure",
|
|
1025
|
+
"expected_root_certificate_id",
|
|
1026
|
+
"expected_scope",
|
|
1027
|
+
"expected_claim",
|
|
1028
|
+
"expected_assurance"
|
|
1029
|
+
],
|
|
1030
|
+
"Semantic certificate closure match input"
|
|
1031
|
+
);
|
|
1032
|
+
const validation = validateRiddleProofSemanticCertificateClosure(
|
|
1033
|
+
requiredField(input, "closure", "Semantic certificate closure match input")
|
|
1034
|
+
);
|
|
1035
|
+
if (!validation.ok) return validation;
|
|
1036
|
+
const expectedRootCertificateId = requiredString(
|
|
1037
|
+
input,
|
|
1038
|
+
"expected_root_certificate_id",
|
|
1039
|
+
"Semantic certificate closure match input"
|
|
1040
|
+
);
|
|
1041
|
+
if (!/^rpsc_[0-9a-f]{64}$/u.test(expectedRootCertificateId)) {
|
|
1042
|
+
throw new Error(
|
|
1043
|
+
"Semantic certificate closure match input.expected_root_certificate_id must be a full rpsc content ID."
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const rootMatch = matchRiddleProofSemanticCertificate({
|
|
1047
|
+
certificate: validation.root_certificate,
|
|
1048
|
+
expected_certificate_id: expectedRootCertificateId,
|
|
1049
|
+
expected_scope: requiredField(
|
|
1050
|
+
input,
|
|
1051
|
+
"expected_scope",
|
|
1052
|
+
"Semantic certificate closure match input"
|
|
1053
|
+
),
|
|
1054
|
+
expected_claim: requiredField(
|
|
1055
|
+
input,
|
|
1056
|
+
"expected_claim",
|
|
1057
|
+
"Semantic certificate closure match input"
|
|
1058
|
+
),
|
|
1059
|
+
expected_assurance: requiredField(
|
|
1060
|
+
input,
|
|
1061
|
+
"expected_assurance",
|
|
1062
|
+
"Semantic certificate closure match input"
|
|
1063
|
+
)
|
|
1064
|
+
});
|
|
1065
|
+
if (!rootMatch.ok) return rootMatch;
|
|
1066
|
+
return {
|
|
1067
|
+
ok: true,
|
|
1068
|
+
closure: validation.closure,
|
|
1069
|
+
root_certificate: rootMatch.certificate
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
export {
|
|
1074
|
+
RIDDLE_PROOF_SEMANTIC_CERTIFICATE_VERSION,
|
|
1075
|
+
RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_VERSION,
|
|
1076
|
+
RIDDLE_PROOF_SEMANTIC_CERTIFICATE_CLOSURE_MAX_CERTIFICATES,
|
|
1077
|
+
riddleProofSemanticScopesEqual,
|
|
1078
|
+
createRiddleProofSemanticCertificate,
|
|
1079
|
+
parseRiddleProofSemanticCertificate,
|
|
1080
|
+
matchRiddleProofSemanticCertificate,
|
|
1081
|
+
composeRiddleProofSemanticCertificates,
|
|
1082
|
+
validateRiddleProofSemanticCertificateClosure,
|
|
1083
|
+
createRiddleProofSemanticAtomicCertificateClosure,
|
|
1084
|
+
composeRiddleProofSemanticCertificateClosures,
|
|
1085
|
+
matchRiddleProofSemanticCertificateClosure
|
|
1086
|
+
};
|