@agentplat/trust 0.3.0-alpha.4
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/LICENSE +202 -0
- package/README.md +14 -0
- package/dist/canonical.d.ts +20 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +173 -0
- package/dist/canonical.js.map +1 -0
- package/dist/causal.d.ts +7 -0
- package/dist/causal.d.ts.map +1 -0
- package/dist/causal.js +155 -0
- package/dist/causal.js.map +1 -0
- package/dist/eligibility.d.ts +8 -0
- package/dist/eligibility.d.ts.map +1 -0
- package/dist/eligibility.js +325 -0
- package/dist/eligibility.js.map +1 -0
- package/dist/evidence.d.ts +20 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +424 -0
- package/dist/evidence.js.map +1 -0
- package/dist/fusion.d.ts +8 -0
- package/dist/fusion.d.ts.map +1 -0
- package/dist/fusion.js +1278 -0
- package/dist/fusion.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +41 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +1369 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/mesh-records.d.ts +36 -0
- package/dist/mesh-records.d.ts.map +1 -0
- package/dist/mesh-records.js +133 -0
- package/dist/mesh-records.js.map +1 -0
- package/dist/policy.d.ts +9 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +598 -0
- package/dist/policy.js.map +1 -0
- package/dist/profile.d.ts +21 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +253 -0
- package/dist/profile.js.map +1 -0
- package/dist/quarantine.d.ts +53 -0
- package/dist/quarantine.d.ts.map +1 -0
- package/dist/quarantine.js +742 -0
- package/dist/quarantine.js.map +1 -0
- package/dist/sha256.d.ts +2 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +66 -0
- package/dist/sha256.js.map +1 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +1015 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +862 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +60 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +18 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +223 -0
- package/dist/validation.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1369 @@
|
|
|
1
|
+
import { canonicalTrustJsonBytesV1, deepFreeze, digestTrustJsonV1, TrustValidationError, } from "./canonical.js";
|
|
2
|
+
import { digestScopeV1, validateEvidenceAttestationV1, validateEvidenceChallengeV1, validateEvidenceClaimV1, validateEvidenceRetractionV1, } from "./evidence.js";
|
|
3
|
+
import { validateEvidenceTrustStateV1 } from "./state.js";
|
|
4
|
+
import { evaluateEvidenceFusionV1 } from "./fusion.js";
|
|
5
|
+
import { dependencyBindingHeadV1, digestEvidenceFusionPolicyV1, validateEvidenceFusionPolicyV1, validateEvidenceTrustDependencyBindingV1, } from "./policy.js";
|
|
6
|
+
import { createEvidenceCausalAuthorizationV1 } from "./causal.js";
|
|
7
|
+
import { createTrustProfileV1, digestTrustProfileKeyV1, trustProfileHeadV1, } from "./profile.js";
|
|
8
|
+
import { createActiveQuarantineRecordV1, createRecoveredQuarantineRecordV1, createReviewRequiredQuarantineRecordV1, deriveQuarantineActivationsV1, digestTrustQuarantineKeyV1, evaluateQuarantineRecoveryV1, quarantineHeadV1, } from "./quarantine.js";
|
|
9
|
+
import { assertExactKeys, assertIdentifier, assertSafeInteger, assertToken, assertTrustDigest, validateReasonCodeV1, } from "./validation.js";
|
|
10
|
+
const STATE_LIMITS = {
|
|
11
|
+
maximumBytes: 67_108_864,
|
|
12
|
+
maximumDepth: 64,
|
|
13
|
+
maximumNodes: 100_000,
|
|
14
|
+
maximumKeysPerObject: 256,
|
|
15
|
+
maximumItemsPerArray: 100_000,
|
|
16
|
+
};
|
|
17
|
+
const compareUnicode = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
18
|
+
const reachedPendingAge = (laterLogicalMs, acceptedAtLogicalMs, maximumPendingAgeMs) => laterLogicalMs >= acceptedAtLogicalMs &&
|
|
19
|
+
laterLogicalMs - acceptedAtLogicalMs >= maximumPendingAgeMs;
|
|
20
|
+
function recordKind(record) {
|
|
21
|
+
if ("claimId" in record && "outcome" in record)
|
|
22
|
+
return "claim";
|
|
23
|
+
if ("attestationId" in record)
|
|
24
|
+
return "attestation";
|
|
25
|
+
if ("challengeId" in record)
|
|
26
|
+
return "challenge";
|
|
27
|
+
return "retraction";
|
|
28
|
+
}
|
|
29
|
+
function recordId(record) {
|
|
30
|
+
if ("claimId" in record && "outcome" in record)
|
|
31
|
+
return record.claimId;
|
|
32
|
+
if ("attestationId" in record)
|
|
33
|
+
return record.attestationId;
|
|
34
|
+
if ("challengeId" in record)
|
|
35
|
+
return record.challengeId;
|
|
36
|
+
return record.retractionId;
|
|
37
|
+
}
|
|
38
|
+
/** The record digest is the content-bound suffix of the validated derived ID. */
|
|
39
|
+
export function digestEvidenceRecordV1(recordValue) {
|
|
40
|
+
const record = validateEvidenceRecordV1(recordValue);
|
|
41
|
+
const id = recordId(record);
|
|
42
|
+
const separator = id.indexOf(":");
|
|
43
|
+
const digest = id.slice(separator + 1);
|
|
44
|
+
assertTrustDigest(digest, "recordDigest");
|
|
45
|
+
return digest;
|
|
46
|
+
}
|
|
47
|
+
export function validateEvidenceRecordV1(value) {
|
|
48
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
49
|
+
throw new TrustValidationError("evidence record is invalid");
|
|
50
|
+
const record = value;
|
|
51
|
+
if ("outcome" in record)
|
|
52
|
+
return validateEvidenceClaimV1(record);
|
|
53
|
+
if ("attestationId" in record)
|
|
54
|
+
return validateEvidenceAttestationV1(record);
|
|
55
|
+
if ("challengeId" in record)
|
|
56
|
+
return validateEvidenceChallengeV1(record);
|
|
57
|
+
if ("retractionId" in record)
|
|
58
|
+
return validateEvidenceRetractionV1(record);
|
|
59
|
+
throw new TrustValidationError("evidence record kind is invalid");
|
|
60
|
+
}
|
|
61
|
+
function relationDigest(record) {
|
|
62
|
+
if ("claimRelationDigest" in record)
|
|
63
|
+
return record.claimRelationDigest;
|
|
64
|
+
if ("attestationRelationDigest" in record)
|
|
65
|
+
return record.attestationRelationDigest;
|
|
66
|
+
if ("challengeRelationDigest" in record)
|
|
67
|
+
return record.challengeRelationDigest;
|
|
68
|
+
return record.retractionRelationDigest;
|
|
69
|
+
}
|
|
70
|
+
function recordScopeDigest(record) {
|
|
71
|
+
return digestScopeV1(record.scope);
|
|
72
|
+
}
|
|
73
|
+
function recordSource(record) {
|
|
74
|
+
return record.sourceId;
|
|
75
|
+
}
|
|
76
|
+
function targetOf(record) {
|
|
77
|
+
if ("claimId" in record && "outcome" in record)
|
|
78
|
+
return null;
|
|
79
|
+
if ("attestationId" in record)
|
|
80
|
+
return { kind: "claim", id: record.claimId, digest: record.claimDigest };
|
|
81
|
+
return {
|
|
82
|
+
kind: record.targetKind,
|
|
83
|
+
id: record.targetId,
|
|
84
|
+
digest: record.targetDigest,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function basisReferences(record) {
|
|
88
|
+
return "basisReferences" in record ? record.basisReferences : [];
|
|
89
|
+
}
|
|
90
|
+
function requireDigest(value, label) {
|
|
91
|
+
assertTrustDigest(value, label);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
export function validateEvidenceRecordStateV1(value) {
|
|
95
|
+
assertExactKeys(value, [
|
|
96
|
+
"schemaVersion",
|
|
97
|
+
"recordKind",
|
|
98
|
+
"recordId",
|
|
99
|
+
"recordDigest",
|
|
100
|
+
"record",
|
|
101
|
+
"origin",
|
|
102
|
+
"originBindingDigest",
|
|
103
|
+
"originVerifierBindingDigest",
|
|
104
|
+
"originProofDigest",
|
|
105
|
+
"acceptedAtLogicalMs",
|
|
106
|
+
"effectiveAtLogicalMs",
|
|
107
|
+
"status",
|
|
108
|
+
], "record state");
|
|
109
|
+
const input = value;
|
|
110
|
+
if (input.schemaVersion !== 1)
|
|
111
|
+
throw new TrustValidationError("record state schema is invalid");
|
|
112
|
+
const record = validateEvidenceRecordV1(input.record);
|
|
113
|
+
const kind = recordKind(record), id = recordId(record), digest = digestEvidenceRecordV1(record);
|
|
114
|
+
if (input.recordKind !== kind ||
|
|
115
|
+
input.recordId !== id ||
|
|
116
|
+
input.recordDigest !== digest)
|
|
117
|
+
throw new TrustValidationError("record state does not bind record content");
|
|
118
|
+
if (input.origin !== "local" && input.origin !== "verified_mesh")
|
|
119
|
+
throw new TrustValidationError("record origin is invalid");
|
|
120
|
+
requireDigest(input.originBindingDigest, "originBindingDigest");
|
|
121
|
+
if (input.origin === "local") {
|
|
122
|
+
if (input.originVerifierBindingDigest !== null ||
|
|
123
|
+
input.originProofDigest !== null)
|
|
124
|
+
throw new TrustValidationError("local origin metadata is invalid");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
requireDigest(input.originVerifierBindingDigest, "originVerifierBindingDigest");
|
|
128
|
+
requireDigest(input.originProofDigest, "originProofDigest");
|
|
129
|
+
}
|
|
130
|
+
assertSafeInteger(input.acceptedAtLogicalMs, "acceptedAtLogicalMs");
|
|
131
|
+
assertSafeInteger(input.effectiveAtLogicalMs, "effectiveAtLogicalMs");
|
|
132
|
+
if (input.effectiveAtLogicalMs >
|
|
133
|
+
input.acceptedAtLogicalMs)
|
|
134
|
+
throw new TrustValidationError("record effective time follows acceptance");
|
|
135
|
+
if (![
|
|
136
|
+
"active",
|
|
137
|
+
"pending",
|
|
138
|
+
"retracted",
|
|
139
|
+
"challenged",
|
|
140
|
+
"conflicted",
|
|
141
|
+
"unavailable",
|
|
142
|
+
].includes(input.status))
|
|
143
|
+
throw new TrustValidationError("record status is invalid");
|
|
144
|
+
return deepFreeze({ ...input, record });
|
|
145
|
+
}
|
|
146
|
+
function resolutionBody(resolution) {
|
|
147
|
+
const { resolutionId: _id, resolutionDigest: _digest, ...body } = resolution;
|
|
148
|
+
return body;
|
|
149
|
+
}
|
|
150
|
+
export function validateEvidenceContentResolutionV1(value) {
|
|
151
|
+
assertExactKeys(value, [
|
|
152
|
+
"schemaVersion",
|
|
153
|
+
"resolutionId",
|
|
154
|
+
"resolutionDigest",
|
|
155
|
+
"claimId",
|
|
156
|
+
"claimDigest",
|
|
157
|
+
"scopeDigest",
|
|
158
|
+
"referenceId",
|
|
159
|
+
"referenceDigest",
|
|
160
|
+
"contentDigest",
|
|
161
|
+
"mediaType",
|
|
162
|
+
"encodedBytes",
|
|
163
|
+
"result",
|
|
164
|
+
"resolverBindingDigest",
|
|
165
|
+
"resolvedAtLogicalMs",
|
|
166
|
+
], "content resolution");
|
|
167
|
+
const resolution = value;
|
|
168
|
+
if (resolution.schemaVersion !== 1)
|
|
169
|
+
throw new TrustValidationError("content resolution schema is invalid");
|
|
170
|
+
assertIdentifier(resolution.claimId, "claimId");
|
|
171
|
+
for (const key of [
|
|
172
|
+
"claimDigest",
|
|
173
|
+
"scopeDigest",
|
|
174
|
+
"contentDigest",
|
|
175
|
+
"resolverBindingDigest",
|
|
176
|
+
])
|
|
177
|
+
requireDigest(resolution[key], key);
|
|
178
|
+
assertIdentifier(resolution.referenceId, "referenceId");
|
|
179
|
+
assertToken(resolution.mediaType, "mediaType");
|
|
180
|
+
assertSafeInteger(resolution.encodedBytes, "encodedBytes");
|
|
181
|
+
assertSafeInteger(resolution.resolvedAtLogicalMs, "resolvedAtLogicalMs");
|
|
182
|
+
if (!["verified", "unavailable", "mismatched"].includes(resolution.result))
|
|
183
|
+
throw new TrustValidationError("content resolution result is invalid");
|
|
184
|
+
const candidate = resolution;
|
|
185
|
+
const digest = digestTrustJsonV1("content-resolution", resolutionBody(candidate));
|
|
186
|
+
if (candidate.resolutionDigest !== digest ||
|
|
187
|
+
candidate.resolutionId !== `content-resolution:${digest}`)
|
|
188
|
+
throw new TrustValidationError("content resolution digest is invalid");
|
|
189
|
+
return deepFreeze(structuredClone(candidate));
|
|
190
|
+
}
|
|
191
|
+
function invalidationBody(invalidation) {
|
|
192
|
+
const { invalidationId: _id, ...body } = invalidation;
|
|
193
|
+
return body;
|
|
194
|
+
}
|
|
195
|
+
export function validateEvidenceContentResolutionInvalidationV1(value) {
|
|
196
|
+
assertExactKeys(value, [
|
|
197
|
+
"schemaVersion",
|
|
198
|
+
"invalidationId",
|
|
199
|
+
"resolutionId",
|
|
200
|
+
"resolutionDigest",
|
|
201
|
+
"resolverBindingDigest",
|
|
202
|
+
"invalidatedAtLogicalMs",
|
|
203
|
+
"reasonCode",
|
|
204
|
+
], "content invalidation");
|
|
205
|
+
const invalidation = value;
|
|
206
|
+
if (invalidation.schemaVersion !== 1)
|
|
207
|
+
throw new TrustValidationError("content invalidation schema is invalid");
|
|
208
|
+
assertIdentifier(invalidation.resolutionId, "resolutionId");
|
|
209
|
+
requireDigest(invalidation.resolutionDigest, "resolutionDigest");
|
|
210
|
+
requireDigest(invalidation.resolverBindingDigest, "resolverBindingDigest");
|
|
211
|
+
assertSafeInteger(invalidation.invalidatedAtLogicalMs, "invalidatedAtLogicalMs");
|
|
212
|
+
validateReasonCodeV1(invalidation.reasonCode);
|
|
213
|
+
const candidate = invalidation;
|
|
214
|
+
const digest = digestTrustJsonV1("content-resolution-invalidation", invalidationBody(candidate));
|
|
215
|
+
if (candidate.invalidationId !== `content-resolution-invalidation:${digest}`)
|
|
216
|
+
throw new TrustValidationError("content invalidation digest is invalid");
|
|
217
|
+
return deepFreeze(structuredClone(candidate));
|
|
218
|
+
}
|
|
219
|
+
function diagnostic(record, reasonCode) {
|
|
220
|
+
return {
|
|
221
|
+
schemaVersion: 1,
|
|
222
|
+
recordId: record.recordId,
|
|
223
|
+
recordDigest: record.recordDigest,
|
|
224
|
+
reasonCode,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function findExact(records, id, digest) {
|
|
228
|
+
return records.find((item) => item.recordId === id && item.recordDigest === digest);
|
|
229
|
+
}
|
|
230
|
+
/** Policy-neutral content projection. Fusion decides whether an unavailable result matters. */
|
|
231
|
+
export function projectEvidenceContentStatusV1(record, resolutions, invalidations, currentBinding) {
|
|
232
|
+
if (record.recordKind !== "claim" || !("content" in record.record))
|
|
233
|
+
return "not_required";
|
|
234
|
+
const content = record.record.content;
|
|
235
|
+
if (!content || content.kind !== "reference")
|
|
236
|
+
return "not_required";
|
|
237
|
+
const exact = resolutions.filter((resolution) => resolution.claimId === record.recordId &&
|
|
238
|
+
resolution.claimDigest === record.recordDigest &&
|
|
239
|
+
resolution.scopeDigest === recordScopeDigest(record.record) &&
|
|
240
|
+
resolution.referenceId === content.reference.referenceId &&
|
|
241
|
+
resolution.referenceDigest === content.reference.referenceDigest &&
|
|
242
|
+
resolution.contentDigest === content.contentDigest &&
|
|
243
|
+
resolution.mediaType === content.mediaType &&
|
|
244
|
+
resolution.encodedBytes === content.encodedBytes);
|
|
245
|
+
const verified = exact.filter((resolution) => resolution.result === "verified");
|
|
246
|
+
if (verified.some((resolution) => currentBinding === resolution.resolverBindingDigest &&
|
|
247
|
+
!invalidations.some((invalidation) => invalidation.resolutionId === resolution.resolutionId &&
|
|
248
|
+
invalidation.resolutionDigest === resolution.resolutionDigest &&
|
|
249
|
+
invalidation.resolverBindingDigest ===
|
|
250
|
+
resolution.resolverBindingDigest)))
|
|
251
|
+
return "verified";
|
|
252
|
+
if (verified.length > 0)
|
|
253
|
+
return "stale";
|
|
254
|
+
if (exact.some((resolution) => resolution.result === "mismatched"))
|
|
255
|
+
return "mismatched";
|
|
256
|
+
return "unavailable";
|
|
257
|
+
}
|
|
258
|
+
function targetStatus(record, records) {
|
|
259
|
+
const target = targetOf(record.record);
|
|
260
|
+
if (!target)
|
|
261
|
+
return null;
|
|
262
|
+
const found = findExact(records, target.id, target.digest);
|
|
263
|
+
if (!found)
|
|
264
|
+
return "relationship_target_missing";
|
|
265
|
+
if (found.recordKind !== target.kind ||
|
|
266
|
+
recordScopeDigest(found.record) !== recordScopeDigest(record.record))
|
|
267
|
+
return "scope_mismatch";
|
|
268
|
+
if (record.recordKind === "retraction" &&
|
|
269
|
+
recordSource(found.record) !== recordSource(record.record))
|
|
270
|
+
return "claim_subject_authority_invalid";
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
function effect(kind, record, reasonCode) {
|
|
274
|
+
return {
|
|
275
|
+
schemaVersion: 1,
|
|
276
|
+
kind,
|
|
277
|
+
recordId: record?.recordId ?? null,
|
|
278
|
+
recordDigest: record?.recordDigest ?? null,
|
|
279
|
+
reasonCode,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function quarantineEffect(kind, record, reasonCode) {
|
|
283
|
+
return {
|
|
284
|
+
schemaVersion: 1,
|
|
285
|
+
kind,
|
|
286
|
+
recordId: record.quarantineId,
|
|
287
|
+
recordDigest: record.quarantineId.slice("quarantine-record:".length),
|
|
288
|
+
reasonCode,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function ordered(values) {
|
|
292
|
+
return [...values].sort((a, b) => compareUnicode(a.recordDigest ?? a.resolutionDigest ?? a.invalidationId ?? "", b.recordDigest ?? b.resolutionDigest ?? b.invalidationId ?? ""));
|
|
293
|
+
}
|
|
294
|
+
/** Re-derives all lifecycle status; no clocks, I/O, or mutable global state are read. */
|
|
295
|
+
export function projectEvidenceLifecycleV1(input) {
|
|
296
|
+
const records = ordered(input.records);
|
|
297
|
+
const recordKey = (recordId, recordDigest) => `${recordId}\u0000${recordDigest}`;
|
|
298
|
+
const byId = new Map(records.map((record) => [
|
|
299
|
+
recordKey(record.recordId, record.recordDigest),
|
|
300
|
+
record,
|
|
301
|
+
]));
|
|
302
|
+
const relations = new Map();
|
|
303
|
+
for (const record of records) {
|
|
304
|
+
const key = `${record.recordKind}:${relationDigest(record.record)}`;
|
|
305
|
+
relations.set(key, (relations.get(key) ?? 0) + 1);
|
|
306
|
+
}
|
|
307
|
+
const evaluateAll = (retracted, challenged) => {
|
|
308
|
+
const memo = new Map();
|
|
309
|
+
const evaluate = (record, remainingBasisDepth, path) => {
|
|
310
|
+
const memoKey = `${record.recordDigest}\u0000${remainingBasisDepth}`;
|
|
311
|
+
const cached = memo.get(memoKey);
|
|
312
|
+
if (cached)
|
|
313
|
+
return cached;
|
|
314
|
+
const result = derive(record, remainingBasisDepth, path);
|
|
315
|
+
memo.set(memoKey, result);
|
|
316
|
+
return result;
|
|
317
|
+
};
|
|
318
|
+
const derive = (record, remainingBasisDepth, path) => {
|
|
319
|
+
const unavailable = (reason, resolvedAt = record.acceptedAtLogicalMs, maximumBasisDepth = 0) => ({
|
|
320
|
+
status: "unavailable",
|
|
321
|
+
reason,
|
|
322
|
+
resolvedAt,
|
|
323
|
+
maximumBasisDepth,
|
|
324
|
+
});
|
|
325
|
+
if ((relations.get(`${record.recordKind}:${relationDigest(record.record)}`) ?? 0) > 1)
|
|
326
|
+
return {
|
|
327
|
+
status: "conflicted",
|
|
328
|
+
reason: "relationship_conflict",
|
|
329
|
+
resolvedAt: record.acceptedAtLogicalMs,
|
|
330
|
+
maximumBasisDepth: 0,
|
|
331
|
+
};
|
|
332
|
+
let resolvedAt = record.acceptedAtLogicalMs;
|
|
333
|
+
let maximumBasisDepth = 0;
|
|
334
|
+
const pending = () => reachedPendingAge(input.logicalTimeMs, record.acceptedAtLogicalMs, input.limits.maximumPendingAgeMs)
|
|
335
|
+
? unavailable("evidence_unavailable", resolvedAt, maximumBasisDepth)
|
|
336
|
+
: {
|
|
337
|
+
status: "pending",
|
|
338
|
+
reason: "pending_target",
|
|
339
|
+
resolvedAt,
|
|
340
|
+
maximumBasisDepth,
|
|
341
|
+
};
|
|
342
|
+
const direct = targetOf(record.record);
|
|
343
|
+
if (direct) {
|
|
344
|
+
const target = byId.get(recordKey(direct.id, direct.digest));
|
|
345
|
+
if (!target)
|
|
346
|
+
return pending();
|
|
347
|
+
if (target.recordKind !== direct.kind ||
|
|
348
|
+
recordScopeDigest(target.record) !== recordScopeDigest(record.record))
|
|
349
|
+
return unavailable("scope_mismatch", resolvedAt, maximumBasisDepth);
|
|
350
|
+
if (record.recordKind === "retraction" &&
|
|
351
|
+
recordSource(target.record) !== recordSource(record.record))
|
|
352
|
+
return unavailable("claim_subject_authority_invalid", resolvedAt, maximumBasisDepth);
|
|
353
|
+
// Direct relationship edges establish presence and causal timing. They
|
|
354
|
+
// do not consume evidence-basis depth and Challenge/Retraction records
|
|
355
|
+
// remain inspectable when their exact target is independently
|
|
356
|
+
// ineffective, as required by policy-bound resolution and audit.
|
|
357
|
+
if (path.has(target.recordDigest))
|
|
358
|
+
return unavailable("evidence_cycle", resolvedAt, maximumBasisDepth);
|
|
359
|
+
const targetResult = evaluate(target, remainingBasisDepth, new Set([...path, target.recordDigest]));
|
|
360
|
+
resolvedAt = Math.max(resolvedAt, targetResult.resolvedAt);
|
|
361
|
+
if (targetResult.status === "pending")
|
|
362
|
+
return pending();
|
|
363
|
+
if (record.recordKind === "attestation" &&
|
|
364
|
+
targetResult.status !== "active")
|
|
365
|
+
return unavailable(targetResult.reason === "evidence_cycle" ||
|
|
366
|
+
targetResult.reason === "evidence_depth_exceeded" ||
|
|
367
|
+
targetResult.reason === "scope_mismatch"
|
|
368
|
+
? targetResult.reason
|
|
369
|
+
: "evidence_unavailable", resolvedAt, maximumBasisDepth);
|
|
370
|
+
}
|
|
371
|
+
for (const reference of basisReferences(record.record)) {
|
|
372
|
+
if (reference.kind !== "evidence")
|
|
373
|
+
continue;
|
|
374
|
+
const target = byId.get(recordKey(reference.referenceId, reference.referenceDigest));
|
|
375
|
+
if (!target)
|
|
376
|
+
return pending();
|
|
377
|
+
if (recordScopeDigest(target.record) !== recordScopeDigest(record.record))
|
|
378
|
+
return unavailable("scope_mismatch", resolvedAt, maximumBasisDepth);
|
|
379
|
+
if (path.has(target.recordDigest))
|
|
380
|
+
return unavailable("evidence_cycle", resolvedAt, maximumBasisDepth);
|
|
381
|
+
if (remainingBasisDepth === 0)
|
|
382
|
+
return unavailable("evidence_depth_exceeded", resolvedAt, maximumBasisDepth + 1);
|
|
383
|
+
const targetResult = evaluate(target, remainingBasisDepth - 1, new Set([...path, target.recordDigest]));
|
|
384
|
+
resolvedAt = Math.max(resolvedAt, targetResult.resolvedAt);
|
|
385
|
+
maximumBasisDepth = Math.max(maximumBasisDepth, 1 + targetResult.maximumBasisDepth);
|
|
386
|
+
if (targetResult.status === "pending")
|
|
387
|
+
return pending();
|
|
388
|
+
if (targetResult.status !== "active" ||
|
|
389
|
+
retracted.has(target.recordDigest) ||
|
|
390
|
+
challenged.has(target.recordDigest))
|
|
391
|
+
return unavailable(record.recordKind === "challenge"
|
|
392
|
+
? "challenge_basis_unavailable"
|
|
393
|
+
: targetResult.reason === "evidence_cycle" ||
|
|
394
|
+
targetResult.reason === "evidence_depth_exceeded" ||
|
|
395
|
+
targetResult.reason === "scope_mismatch"
|
|
396
|
+
? targetResult.reason
|
|
397
|
+
: "evidence_unavailable", resolvedAt, maximumBasisDepth);
|
|
398
|
+
}
|
|
399
|
+
if (reachedPendingAge(resolvedAt, record.acceptedAtLogicalMs, input.limits.maximumPendingAgeMs))
|
|
400
|
+
return unavailable("evidence_unavailable", resolvedAt, maximumBasisDepth);
|
|
401
|
+
return {
|
|
402
|
+
status: "active",
|
|
403
|
+
reason: "accepted",
|
|
404
|
+
resolvedAt,
|
|
405
|
+
maximumBasisDepth,
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
return new Map(records.map((record) => [
|
|
409
|
+
record.recordDigest,
|
|
410
|
+
evaluate(record, input.limits.maximumRelationshipDepth, new Set([record.recordDigest])),
|
|
411
|
+
]));
|
|
412
|
+
};
|
|
413
|
+
const base = evaluateAll(new Set(), new Set());
|
|
414
|
+
const retracted = new Set(records
|
|
415
|
+
.filter((record) => record.recordKind === "retraction" &&
|
|
416
|
+
base.get(record.recordDigest)?.status === "active" &&
|
|
417
|
+
base.get(targetOf(record.record)?.digest ?? "")?.status === "active")
|
|
418
|
+
.map((record) => targetOf(record.record)?.digest)
|
|
419
|
+
.filter((digest) => digest !== undefined));
|
|
420
|
+
const afterRetractions = evaluateAll(retracted, new Set());
|
|
421
|
+
const challengeCandidates = new Set(records
|
|
422
|
+
.filter((record) => record.recordKind === "challenge" &&
|
|
423
|
+
afterRetractions.get(record.recordDigest)?.status === "active")
|
|
424
|
+
.map((record) => record.recordDigest));
|
|
425
|
+
const candidateRecords = records.filter((record) => challengeCandidates.has(record.recordDigest));
|
|
426
|
+
const targetDigests = [
|
|
427
|
+
...new Set(candidateRecords
|
|
428
|
+
.map((record) => targetOf(record.record)?.digest)
|
|
429
|
+
.filter((digest) => digest !== undefined)),
|
|
430
|
+
].sort(compareUnicode);
|
|
431
|
+
const targetIndex = new Map(targetDigests.map((recordDigest, index) => [recordDigest, index]));
|
|
432
|
+
const wordCount = Math.ceil(targetDigests.length / 32);
|
|
433
|
+
const sensitiveMemo = new Map();
|
|
434
|
+
const sensitiveVisiting = new Set();
|
|
435
|
+
const mergeBits = (target, source) => {
|
|
436
|
+
for (let index = 0; index < target.length; index += 1)
|
|
437
|
+
target[index] = (target[index] | source[index]) >>> 0;
|
|
438
|
+
};
|
|
439
|
+
const setBit = (bits, index) => {
|
|
440
|
+
bits[Math.floor(index / 32)] =
|
|
441
|
+
(bits[Math.floor(index / 32)] | (1 << (index % 32))) >>> 0;
|
|
442
|
+
};
|
|
443
|
+
const hasBit = (bits, index) => (bits[Math.floor(index / 32)] & (1 << (index % 32))) !== 0;
|
|
444
|
+
const challengeSensitiveTargets = (record) => {
|
|
445
|
+
const cached = sensitiveMemo.get(record.recordDigest);
|
|
446
|
+
if (cached)
|
|
447
|
+
return cached;
|
|
448
|
+
const bits = new Uint32Array(wordCount);
|
|
449
|
+
if (sensitiveVisiting.has(record.recordDigest))
|
|
450
|
+
return bits;
|
|
451
|
+
sensitiveVisiting.add(record.recordDigest);
|
|
452
|
+
if (record.recordKind === "attestation") {
|
|
453
|
+
const direct = targetOf(record.record);
|
|
454
|
+
const target = direct
|
|
455
|
+
? byId.get(recordKey(direct.id, direct.digest))
|
|
456
|
+
: undefined;
|
|
457
|
+
if (target)
|
|
458
|
+
mergeBits(bits, challengeSensitiveTargets(target));
|
|
459
|
+
}
|
|
460
|
+
for (const reference of basisReferences(record.record)) {
|
|
461
|
+
if (reference.kind !== "evidence")
|
|
462
|
+
continue;
|
|
463
|
+
const index = targetIndex.get(reference.referenceDigest);
|
|
464
|
+
if (index !== undefined)
|
|
465
|
+
setBit(bits, index);
|
|
466
|
+
const target = byId.get(recordKey(reference.referenceId, reference.referenceDigest));
|
|
467
|
+
if (target)
|
|
468
|
+
mergeBits(bits, challengeSensitiveTargets(target));
|
|
469
|
+
}
|
|
470
|
+
sensitiveVisiting.delete(record.recordDigest);
|
|
471
|
+
sensitiveMemo.set(record.recordDigest, bits);
|
|
472
|
+
return bits;
|
|
473
|
+
};
|
|
474
|
+
const sensitiveByChallenge = candidateRecords.map((record) => challengeSensitiveTargets(record));
|
|
475
|
+
const challengeTargetIndexes = candidateRecords.map((record) => targetIndex.get(targetOf(record.record).digest));
|
|
476
|
+
const groupUnknown = new Uint32Array(targetDigests.length);
|
|
477
|
+
for (const index of challengeTargetIndexes)
|
|
478
|
+
groupUnknown[index] += 1;
|
|
479
|
+
const groupHasActive = new Uint8Array(targetDigests.length);
|
|
480
|
+
const challengeState = new Uint8Array(candidateRecords.length);
|
|
481
|
+
const unresolvedGroups = new Uint32Array(candidateRecords.length);
|
|
482
|
+
for (let challenge = 0; challenge < candidateRecords.length; challenge += 1)
|
|
483
|
+
for (let group = 0; group < targetDigests.length; group += 1)
|
|
484
|
+
if (hasBit(sensitiveByChallenge[challenge], group))
|
|
485
|
+
unresolvedGroups[challenge] += 1;
|
|
486
|
+
const queue = [];
|
|
487
|
+
const assignChallenge = (index, state) => {
|
|
488
|
+
if (challengeState[index] !== 0)
|
|
489
|
+
return;
|
|
490
|
+
challengeState[index] = state;
|
|
491
|
+
queue.push(index);
|
|
492
|
+
};
|
|
493
|
+
for (let index = 0; index < candidateRecords.length; index += 1)
|
|
494
|
+
if (unresolvedGroups[index] === 0)
|
|
495
|
+
assignChallenge(index, 1);
|
|
496
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
497
|
+
const challenge = queue[cursor];
|
|
498
|
+
const group = challengeTargetIndexes[challenge];
|
|
499
|
+
groupUnknown[group] -= 1;
|
|
500
|
+
if (challengeState[challenge] === 1 && groupHasActive[group] === 0) {
|
|
501
|
+
groupHasActive[group] = 1;
|
|
502
|
+
for (let dependent = 0; dependent < candidateRecords.length; dependent += 1)
|
|
503
|
+
if (hasBit(sensitiveByChallenge[dependent], group))
|
|
504
|
+
assignChallenge(dependent, 2);
|
|
505
|
+
}
|
|
506
|
+
else if (challengeState[challenge] === 2 &&
|
|
507
|
+
groupHasActive[group] === 0 &&
|
|
508
|
+
groupUnknown[group] === 0) {
|
|
509
|
+
for (let dependent = 0; dependent < candidateRecords.length; dependent += 1) {
|
|
510
|
+
if (!hasBit(sensitiveByChallenge[dependent], group))
|
|
511
|
+
continue;
|
|
512
|
+
unresolvedGroups[dependent] -= 1;
|
|
513
|
+
if (unresolvedGroups[dependent] === 0)
|
|
514
|
+
assignChallenge(dependent, 1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// State 1 is the well-founded lower fixed point. State 0 is an unresolved
|
|
519
|
+
// recursive blocker cycle; state 2 is definitely blocked. Both are
|
|
520
|
+
// unavailable and therefore cannot mark a target.
|
|
521
|
+
const activeChallenges = new Set(candidateRecords
|
|
522
|
+
.filter((_record, index) => challengeState[index] === 1)
|
|
523
|
+
.map((record) => record.recordDigest));
|
|
524
|
+
const unresolvedChallengeDependencies = new Set([...challengeCandidates].filter((recordDigest) => !activeChallenges.has(recordDigest)));
|
|
525
|
+
const challenged = new Set(records
|
|
526
|
+
.filter((record) => activeChallenges.has(record.recordDigest))
|
|
527
|
+
.map((record) => targetOf(record.record)?.digest)
|
|
528
|
+
.filter((digest) => digest !== undefined));
|
|
529
|
+
const final = evaluateAll(retracted, challenged);
|
|
530
|
+
if ([...activeChallenges].some((recordDigest) => final.get(recordDigest)?.status !== "active"))
|
|
531
|
+
throw new TrustValidationError("challenge projection disagrees with lifecycle closure");
|
|
532
|
+
const projected = records.map((record) => {
|
|
533
|
+
const result = final.get(record.recordDigest);
|
|
534
|
+
const status = unresolvedChallengeDependencies.has(record.recordDigest)
|
|
535
|
+
? "unavailable"
|
|
536
|
+
: retracted.has(record.recordDigest)
|
|
537
|
+
? "retracted"
|
|
538
|
+
: result.status !== "active"
|
|
539
|
+
? result.status
|
|
540
|
+
: challenged.has(record.recordDigest)
|
|
541
|
+
? "challenged"
|
|
542
|
+
: "active";
|
|
543
|
+
const reason = unresolvedChallengeDependencies.has(record.recordDigest)
|
|
544
|
+
? "challenge_basis_unavailable"
|
|
545
|
+
: status === "challenged"
|
|
546
|
+
? "challenge_unresolved"
|
|
547
|
+
: status === "retracted"
|
|
548
|
+
? "evidence_unavailable"
|
|
549
|
+
: result.reason;
|
|
550
|
+
return { record: { ...record, status }, reason };
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
records: projected.map((entry) => entry.record),
|
|
554
|
+
diagnostics: projected.map((entry) => diagnostic(entry.record, entry.reason)),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function stateEncodedBytes(state) {
|
|
558
|
+
return canonicalTrustJsonBytesV1({ ...state, encodedBytes: 0 }, { ...STATE_LIMITS, maximumBytes: state.limits.maximumStateCanonicalBytes }).byteLength;
|
|
559
|
+
}
|
|
560
|
+
function validateInputShape(input) {
|
|
561
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
562
|
+
throw new TrustValidationError("reducer input is invalid");
|
|
563
|
+
const candidate = input;
|
|
564
|
+
if (candidate.kind === "record_admitted")
|
|
565
|
+
assertExactKeys(candidate, [
|
|
566
|
+
"schemaVersion",
|
|
567
|
+
"kind",
|
|
568
|
+
"record",
|
|
569
|
+
"origin",
|
|
570
|
+
"originBindingDigest",
|
|
571
|
+
"originVerifierBindingDigest",
|
|
572
|
+
"originProofDigest",
|
|
573
|
+
"effectiveAtLogicalMs",
|
|
574
|
+
"logicalTimeMs",
|
|
575
|
+
], "record admission input");
|
|
576
|
+
else if (candidate.kind === "policy_registered")
|
|
577
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "policy", "logicalTimeMs"], "policy registration input");
|
|
578
|
+
else if (candidate.kind === "dependency_binding_registered")
|
|
579
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "binding", "logicalTimeMs"], "dependency binding registration input");
|
|
580
|
+
else if (candidate.kind === "causal_authorization_recorded")
|
|
581
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "authorization", "logicalTimeMs"], "causal authorization input");
|
|
582
|
+
else if (candidate.kind === "fusion_evaluated")
|
|
583
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "request", "logicalTimeMs"], "fusion evaluation input");
|
|
584
|
+
else if (candidate.kind === "profile_evaluated")
|
|
585
|
+
assertExactKeys(candidate, [
|
|
586
|
+
"schemaVersion",
|
|
587
|
+
"kind",
|
|
588
|
+
"fusionDecisionId",
|
|
589
|
+
"fusionDecisionDigest",
|
|
590
|
+
"logicalTimeMs",
|
|
591
|
+
], "profile evaluation input");
|
|
592
|
+
else if (candidate.kind === "quarantine_reviewed")
|
|
593
|
+
assertExactKeys(candidate, [
|
|
594
|
+
"schemaVersion",
|
|
595
|
+
"kind",
|
|
596
|
+
"quarantineKey",
|
|
597
|
+
"quarantineId",
|
|
598
|
+
"fusionDecisionId",
|
|
599
|
+
"fusionDecisionDigest",
|
|
600
|
+
"profileId",
|
|
601
|
+
"profileDigest",
|
|
602
|
+
"logicalTimeMs",
|
|
603
|
+
], "quarantine review input");
|
|
604
|
+
else if (candidate.kind === "content_resolution_recorded")
|
|
605
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "resolution", "logicalTimeMs"], "content resolution input");
|
|
606
|
+
else if (candidate.kind === "content_resolution_invalidated")
|
|
607
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "invalidation", "logicalTimeMs"], "content invalidation input");
|
|
608
|
+
else if (candidate.kind === "advance_logical_time")
|
|
609
|
+
assertExactKeys(candidate, ["schemaVersion", "kind", "logicalTimeMs"], "logical time input");
|
|
610
|
+
else
|
|
611
|
+
throw new TrustValidationError("reducer input kind is invalid");
|
|
612
|
+
if (candidate.schemaVersion !== 1)
|
|
613
|
+
throw new TrustValidationError("reducer input schema is invalid");
|
|
614
|
+
assertSafeInteger(candidate.logicalTimeMs, "logicalTimeMs");
|
|
615
|
+
if (candidate.kind === "record_admitted") {
|
|
616
|
+
assertSafeInteger(candidate.effectiveAtLogicalMs, "effectiveAtLogicalMs");
|
|
617
|
+
if (candidate.effectiveAtLogicalMs >
|
|
618
|
+
candidate.logicalTimeMs)
|
|
619
|
+
throw new TrustValidationError("record effective time follows acceptance");
|
|
620
|
+
}
|
|
621
|
+
if (candidate.kind === "profile_evaluated") {
|
|
622
|
+
assertIdentifier(candidate.fusionDecisionId, "fusionDecisionId");
|
|
623
|
+
assertTrustDigest(candidate.fusionDecisionDigest, "fusionDecisionDigest");
|
|
624
|
+
}
|
|
625
|
+
if (candidate.kind === "quarantine_reviewed") {
|
|
626
|
+
assertTrustDigest(candidate.quarantineKey, "quarantineKey");
|
|
627
|
+
for (const key of [
|
|
628
|
+
"quarantineId",
|
|
629
|
+
"fusionDecisionId",
|
|
630
|
+
"profileId",
|
|
631
|
+
])
|
|
632
|
+
assertIdentifier(candidate[key], key);
|
|
633
|
+
for (const key of ["fusionDecisionDigest", "profileDigest"])
|
|
634
|
+
assertTrustDigest(candidate[key], key);
|
|
635
|
+
}
|
|
636
|
+
return candidate;
|
|
637
|
+
}
|
|
638
|
+
function normalizeReducerOptions(options) {
|
|
639
|
+
if (!options || typeof options !== "object" || Array.isArray(options))
|
|
640
|
+
throw new TrustValidationError("reducer options are invalid");
|
|
641
|
+
assertExactKeys(options, [
|
|
642
|
+
"verifiedMeshAdmissionVerifierRegistry",
|
|
643
|
+
"currentContentResolverBindingDigest",
|
|
644
|
+
"causalAuthorityVerifierRegistry",
|
|
645
|
+
].filter((key) => key in options), "reducer options");
|
|
646
|
+
const registry = options.verifiedMeshAdmissionVerifierRegistry;
|
|
647
|
+
if (registry !== undefined) {
|
|
648
|
+
assertExactKeys(registry, ["resolve"], "mesh admission verifier registry");
|
|
649
|
+
if (typeof registry.resolve !== "function")
|
|
650
|
+
throw new TrustValidationError("mesh admission verifier registry is invalid");
|
|
651
|
+
}
|
|
652
|
+
const causalRegistry = options.causalAuthorityVerifierRegistry;
|
|
653
|
+
if (causalRegistry !== undefined) {
|
|
654
|
+
assertExactKeys(causalRegistry, ["resolve"], "causal authority verifier registry");
|
|
655
|
+
if (typeof causalRegistry.resolve !== "function")
|
|
656
|
+
throw new TrustValidationError("causal authority verifier registry is invalid");
|
|
657
|
+
}
|
|
658
|
+
if (options.currentContentResolverBindingDigest !== undefined &&
|
|
659
|
+
options.currentContentResolverBindingDigest !== null)
|
|
660
|
+
assertTrustDigest(options.currentContentResolverBindingDigest, "currentContentResolverBindingDigest");
|
|
661
|
+
const checkedRegistry = registry;
|
|
662
|
+
const checkedCausalRegistry = causalRegistry;
|
|
663
|
+
return Object.freeze({
|
|
664
|
+
...(checkedRegistry
|
|
665
|
+
? {
|
|
666
|
+
verifiedMeshAdmissionVerifierRegistry: Object.freeze({
|
|
667
|
+
resolve: checkedRegistry.resolve.bind(checkedRegistry),
|
|
668
|
+
}),
|
|
669
|
+
}
|
|
670
|
+
: {}),
|
|
671
|
+
...(options.currentContentResolverBindingDigest !== undefined
|
|
672
|
+
? {
|
|
673
|
+
currentContentResolverBindingDigest: options.currentContentResolverBindingDigest,
|
|
674
|
+
}
|
|
675
|
+
: {}),
|
|
676
|
+
...(options.causalAuthorityVerifierRegistry !== undefined
|
|
677
|
+
? {
|
|
678
|
+
causalAuthorityVerifierRegistry: Object.freeze({
|
|
679
|
+
resolve: checkedCausalRegistry.resolve.bind(checkedCausalRegistry),
|
|
680
|
+
}),
|
|
681
|
+
}
|
|
682
|
+
: {}),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
export function resolveVerifiedMeshAdmissionVerifierV1(registry, bindingDigest) {
|
|
686
|
+
const verifier = registry.resolve(bindingDigest);
|
|
687
|
+
if (verifier === null)
|
|
688
|
+
return null;
|
|
689
|
+
assertExactKeys(verifier, ["verifierBindingDigest", "upstreamBindingDigest", "verify"], "mesh admission verifier");
|
|
690
|
+
assertTrustDigest(verifier.verifierBindingDigest, "verifierBindingDigest");
|
|
691
|
+
assertTrustDigest(verifier.upstreamBindingDigest, "upstreamBindingDigest");
|
|
692
|
+
if (typeof verifier.verify !== "function")
|
|
693
|
+
throw new TrustValidationError("mesh admission verifier is invalid");
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
verifierBindingDigest: verifier.verifierBindingDigest,
|
|
696
|
+
upstreamBindingDigest: verifier.upstreamBindingDigest,
|
|
697
|
+
verify: verifier.verify.bind(verifier),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
export function resolveEvidenceCausalAuthorityVerifierV1(registry, bindingDigest) {
|
|
701
|
+
const verifier = registry.resolve(bindingDigest);
|
|
702
|
+
if (verifier === null)
|
|
703
|
+
return null;
|
|
704
|
+
assertExactKeys(verifier, [
|
|
705
|
+
"authorityBindingDigest",
|
|
706
|
+
"policyDigest",
|
|
707
|
+
"upstreamBindingDigest",
|
|
708
|
+
"verify",
|
|
709
|
+
], "causal authority verifier");
|
|
710
|
+
assertTrustDigest(verifier.authorityBindingDigest, "authorityBindingDigest");
|
|
711
|
+
assertTrustDigest(verifier.policyDigest, "policyDigest");
|
|
712
|
+
assertTrustDigest(verifier.upstreamBindingDigest, "upstreamBindingDigest");
|
|
713
|
+
if (typeof verifier.verify !== "function")
|
|
714
|
+
throw new TrustValidationError("causal authority verifier is invalid");
|
|
715
|
+
return Object.freeze({
|
|
716
|
+
authorityBindingDigest: verifier.authorityBindingDigest,
|
|
717
|
+
policyDigest: verifier.policyDigest,
|
|
718
|
+
upstreamBindingDigest: verifier.upstreamBindingDigest,
|
|
719
|
+
verify: verifier.verify.bind(verifier),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
export function reduceEvidenceTrustStateV1(stateValue, inputValue, options = {}) {
|
|
723
|
+
const state = validateEvidenceTrustStateV1(stateValue), input = validateInputShape(inputValue), normalizedOptions = normalizeReducerOptions(options);
|
|
724
|
+
if (state.causalAuthorizations.length > 0) {
|
|
725
|
+
const registry = normalizedOptions.causalAuthorityVerifierRegistry;
|
|
726
|
+
if (!registry)
|
|
727
|
+
throw new TrustValidationError("retained causal authority requires verifier registry");
|
|
728
|
+
for (const authorization of state.causalAuthorizations) {
|
|
729
|
+
const binding = state.dependencyBindings.find((item) => item.bindingDigest === authorization.authorityBindingDigest);
|
|
730
|
+
const verifier = resolveEvidenceCausalAuthorityVerifierV1(registry, authorization.authorityBindingDigest);
|
|
731
|
+
if (!binding ||
|
|
732
|
+
!verifier ||
|
|
733
|
+
binding.bindingKind !== "causal_authority" ||
|
|
734
|
+
binding.policyDigest !== authorization.policyDigest ||
|
|
735
|
+
verifier.authorityBindingDigest !==
|
|
736
|
+
authorization.authorityBindingDigest ||
|
|
737
|
+
verifier.policyDigest !== authorization.policyDigest ||
|
|
738
|
+
verifier.upstreamBindingDigest !== binding.upstreamBindingDigest ||
|
|
739
|
+
!verifier.verify(authorization))
|
|
740
|
+
throw new TrustValidationError("retained causal authority verification failed");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (input.logicalTimeMs < state.logicalTimeHighWaterMs)
|
|
744
|
+
throw new TrustValidationError("logical time rollback");
|
|
745
|
+
let records = [...state.records], resolutions = [...state.contentResolutions], invalidations = [...state.contentInvalidations], policies = [...state.policies], dependencyBindings = [...state.dependencyBindings], fusionDecisions = [...state.fusionDecisions], causalAuthorizations = [...state.causalAuthorizations], profiles = [...state.profiles], quarantines = [...state.quarantines], recoveryDecisions = [...state.recoveryDecisions];
|
|
746
|
+
const effects = [];
|
|
747
|
+
const currentQuarantineHeads = () => [
|
|
748
|
+
...quarantines
|
|
749
|
+
.reduce((heads, quarantine) => {
|
|
750
|
+
const head = quarantineHeadV1(quarantine);
|
|
751
|
+
const previous = heads.get(head.quarantineKey);
|
|
752
|
+
if (!previous || previous.revision < head.revision)
|
|
753
|
+
heads.set(head.quarantineKey, head);
|
|
754
|
+
return heads;
|
|
755
|
+
}, new Map())
|
|
756
|
+
.values(),
|
|
757
|
+
].sort((left, right) => compareUnicode(left.quarantineKey, right.quarantineKey));
|
|
758
|
+
const dueReviews = currentQuarantineHeads().filter((head) => {
|
|
759
|
+
if (head.status !== "active")
|
|
760
|
+
return false;
|
|
761
|
+
const quarantine = quarantines.find((candidate) => candidate.quarantineId === head.quarantineId);
|
|
762
|
+
return (quarantine !== undefined &&
|
|
763
|
+
quarantine.reviewAfterLogicalMs <= input.logicalTimeMs);
|
|
764
|
+
});
|
|
765
|
+
for (const head of dueReviews) {
|
|
766
|
+
const current = quarantines.find((candidate) => candidate.quarantineId === head.quarantineId);
|
|
767
|
+
if (quarantines.filter((candidate) => candidate.quarantineKey === current.quarantineKey).length >= state.limits.maximumQuarantineRevisionsPerHead) {
|
|
768
|
+
if (state.logicalTimeHighWaterMs < current.reviewAfterLogicalMs)
|
|
769
|
+
effects.push(quarantineEffect("quarantine_review_required", current, "quarantine_review_required"));
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const review = createReviewRequiredQuarantineRecordV1(current, input.logicalTimeMs);
|
|
773
|
+
quarantines.push(review);
|
|
774
|
+
effects.push(quarantineEffect("quarantine_review_required", review, "quarantine_review_required"));
|
|
775
|
+
}
|
|
776
|
+
if (input.kind === "fusion_evaluated") {
|
|
777
|
+
const decision = evaluateEvidenceFusionV1(state, input.request, input.logicalTimeMs);
|
|
778
|
+
const existing = fusionDecisions.find((candidate) => candidate.fusionDecisionId === decision.fusionDecisionId);
|
|
779
|
+
if (existing) {
|
|
780
|
+
if (existing.fusionDecisionDigest !== decision.fusionDecisionDigest)
|
|
781
|
+
throw new TrustValidationError("fusion decision ID conflicts with existing content");
|
|
782
|
+
effects.push(effect("fusion_evaluated", null, "duplicate"));
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
if (fusionDecisions.length >= state.limits.maximumRetainedFusionDecisions)
|
|
786
|
+
throw new TrustValidationError("fusion decision capacity exceeded");
|
|
787
|
+
fusionDecisions.push(decision);
|
|
788
|
+
effects.push(effect("fusion_evaluated", null, "accepted"));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else if (input.kind === "profile_evaluated") {
|
|
792
|
+
const decision = fusionDecisions.find((candidate) => candidate.fusionDecisionId === input.fusionDecisionId &&
|
|
793
|
+
candidate.fusionDecisionDigest === input.fusionDecisionDigest);
|
|
794
|
+
if (!decision || decision.evaluatedAtLogicalMs !== input.logicalTimeMs)
|
|
795
|
+
throw new TrustValidationError("profile Fusion decision is unavailable");
|
|
796
|
+
const existing = profiles.find((profile) => profile.fusionDecisionId === decision.fusionDecisionId &&
|
|
797
|
+
profile.fusionDecisionDigest === decision.fusionDecisionDigest);
|
|
798
|
+
if (existing) {
|
|
799
|
+
effects.push(effect("profile_evaluated", null, "duplicate"));
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
const policy = policies.find((candidate) => candidate.policyId === decision.policyId &&
|
|
803
|
+
candidate.policyVersion === decision.policyVersion &&
|
|
804
|
+
digestEvidenceFusionPolicyV1(candidate) === decision.policyDigest);
|
|
805
|
+
const policyHead = state.policyHeads.find((head) => head.policyId === decision.policyId);
|
|
806
|
+
if (!policy ||
|
|
807
|
+
!policyHead ||
|
|
808
|
+
policyHead.policyVersion !== decision.policyVersion ||
|
|
809
|
+
policyHead.policyDigest !== decision.policyDigest)
|
|
810
|
+
throw new TrustValidationError("profile policy head is unavailable");
|
|
811
|
+
const profileKey = digestTrustProfileKeyV1({
|
|
812
|
+
tenantId: decision.tenantId,
|
|
813
|
+
scopeDigest: decision.scopeDigest,
|
|
814
|
+
subjectDigest: decision.subjectDigest,
|
|
815
|
+
policyDigest: decision.policyDigest,
|
|
816
|
+
});
|
|
817
|
+
const currentHead = state.profileHeads.find((head) => head.profileKey === profileKey);
|
|
818
|
+
if (decision.previousProfileDigest !== (currentHead?.profileDigest ?? null))
|
|
819
|
+
throw new TrustValidationError("profile Fusion decision does not bind the current head");
|
|
820
|
+
const historyLength = profiles.filter((profile) => digestTrustProfileKeyV1({
|
|
821
|
+
tenantId: profile.tenantId,
|
|
822
|
+
scopeDigest: profile.scopeDigest,
|
|
823
|
+
subjectDigest: profile.subjectDigest,
|
|
824
|
+
policyDigest: profile.policyDigest,
|
|
825
|
+
}) === profileKey).length;
|
|
826
|
+
if (!currentHead &&
|
|
827
|
+
state.profileHeads.length >= state.limits.maximumProfileHeads)
|
|
828
|
+
throw new TrustValidationError("profile head capacity exceeded");
|
|
829
|
+
if (historyLength >= state.limits.maximumProfileRevisionsPerHead)
|
|
830
|
+
throw new TrustValidationError("profile revision capacity exceeded");
|
|
831
|
+
const createdProfile = createTrustProfileV1({
|
|
832
|
+
fusionDecision: decision,
|
|
833
|
+
policy,
|
|
834
|
+
revision: (currentHead?.revision ?? 0) + 1,
|
|
835
|
+
previousProfileId: currentHead?.profileId ?? null,
|
|
836
|
+
previousProfileDigest: currentHead?.profileDigest ?? null,
|
|
837
|
+
updatedAtLogicalMs: input.logicalTimeMs,
|
|
838
|
+
});
|
|
839
|
+
const activationProjections = deriveQuarantineActivationsV1(state, createdProfile, decision, policy);
|
|
840
|
+
for (const initialProjection of activationProjections) {
|
|
841
|
+
const quarantineKey = digestTrustQuarantineKeyV1({
|
|
842
|
+
tenantId: decision.tenantId,
|
|
843
|
+
subjectDigest: decision.subjectDigest,
|
|
844
|
+
scopeDigest: decision.scopeDigest,
|
|
845
|
+
dimensionId: initialProjection.dimensionId,
|
|
846
|
+
policyDigest: decision.policyDigest,
|
|
847
|
+
});
|
|
848
|
+
const head = currentQuarantineHeads().find((candidate) => candidate.quarantineKey === quarantineKey);
|
|
849
|
+
if (head?.status === "active" || head?.status === "review_required")
|
|
850
|
+
continue;
|
|
851
|
+
const previous = head
|
|
852
|
+
? quarantines.find((candidate) => candidate.quarantineId === head.quarantineId)
|
|
853
|
+
: null;
|
|
854
|
+
const projection = previous?.status === "recovered"
|
|
855
|
+
? deriveQuarantineActivationsV1(state, createdProfile, decision, policy, previous.recoveredAtLogicalMs).find((candidate) => candidate.dimensionId === initialProjection.dimensionId)
|
|
856
|
+
: initialProjection;
|
|
857
|
+
if (!projection)
|
|
858
|
+
continue;
|
|
859
|
+
const heads = currentQuarantineHeads();
|
|
860
|
+
if (!head && heads.length >= state.limits.maximumQuarantineHeads)
|
|
861
|
+
throw new TrustValidationError("quarantine head capacity exceeded");
|
|
862
|
+
const historyLength = quarantines.filter((candidate) => candidate.quarantineKey === quarantineKey).length;
|
|
863
|
+
if (historyLength >= state.limits.maximumQuarantineRevisionsPerHead)
|
|
864
|
+
throw new TrustValidationError("quarantine revision capacity exceeded");
|
|
865
|
+
const activeForPolicy = heads.filter((candidate) => {
|
|
866
|
+
const record = quarantines.find((item) => item.quarantineId === candidate.quarantineId);
|
|
867
|
+
return (record?.policyDigest === decision.policyDigest &&
|
|
868
|
+
(candidate.status === "active" ||
|
|
869
|
+
candidate.status === "review_required"));
|
|
870
|
+
}).length;
|
|
871
|
+
if (activeForPolicy >= policy.quarantinePolicy.maximumActiveRecords)
|
|
872
|
+
throw new TrustValidationError("active quarantine capacity exceeded");
|
|
873
|
+
const active = createActiveQuarantineRecordV1({
|
|
874
|
+
revision: (head?.revision ?? 0) + 1,
|
|
875
|
+
previousRecordId: previous?.quarantineId ?? null,
|
|
876
|
+
tenantId: decision.tenantId,
|
|
877
|
+
subjectDigest: decision.subjectDigest,
|
|
878
|
+
scopeDigest: decision.scopeDigest,
|
|
879
|
+
dimensionId: projection.dimensionId,
|
|
880
|
+
policyDigest: decision.policyDigest,
|
|
881
|
+
fusionDecisionId: decision.fusionDecisionId,
|
|
882
|
+
activationEvidence: projection.activationEvidence,
|
|
883
|
+
activationDependencyGroupIds: projection.activationDependencyGroupIds,
|
|
884
|
+
activatedAtLogicalMs: input.logicalTimeMs,
|
|
885
|
+
reviewIntervalMs: projection.reviewIntervalMs,
|
|
886
|
+
});
|
|
887
|
+
quarantines.push(active);
|
|
888
|
+
effects.push(quarantineEffect("quarantine_activated", active, "quarantine_activated"));
|
|
889
|
+
}
|
|
890
|
+
profiles.push(createdProfile);
|
|
891
|
+
effects.push(effect("profile_evaluated", null, "accepted"));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else if (input.kind === "quarantine_reviewed") {
|
|
895
|
+
const target = quarantines.find((candidate) => candidate.quarantineId === input.quarantineId &&
|
|
896
|
+
candidate.quarantineKey === input.quarantineKey);
|
|
897
|
+
const decision = fusionDecisions.find((candidate) => candidate.fusionDecisionId === input.fusionDecisionId &&
|
|
898
|
+
candidate.fusionDecisionDigest === input.fusionDecisionDigest);
|
|
899
|
+
const profile = profiles.find((candidate) => candidate.profileId === input.profileId &&
|
|
900
|
+
candidate.profileDigest === input.profileDigest);
|
|
901
|
+
if (!target || !decision || !profile)
|
|
902
|
+
throw new TrustValidationError("quarantine review inputs are unavailable");
|
|
903
|
+
const existing = recoveryDecisions.find((candidate) => candidate.quarantineId === target.quarantineId &&
|
|
904
|
+
candidate.fusionDecisionId === decision.fusionDecisionId &&
|
|
905
|
+
candidate.evaluatedAtLogicalMs === input.logicalTimeMs);
|
|
906
|
+
if (existing) {
|
|
907
|
+
effects.push(quarantineEffect("quarantine_reviewed", target, "duplicate"));
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
const head = currentQuarantineHeads().find((candidate) => candidate.quarantineKey === target.quarantineKey);
|
|
911
|
+
const historyLength = quarantines.filter((candidate) => candidate.quarantineKey === target.quarantineKey).length;
|
|
912
|
+
const exhaustedActive = target.status === "active" &&
|
|
913
|
+
input.logicalTimeMs >= target.reviewAfterLogicalMs &&
|
|
914
|
+
historyLength >= state.limits.maximumQuarantineRevisionsPerHead;
|
|
915
|
+
if (head?.quarantineId !== target.quarantineId ||
|
|
916
|
+
(target.status !== "review_required" && !exhaustedActive) ||
|
|
917
|
+
input.logicalTimeMs < target.reviewAfterLogicalMs)
|
|
918
|
+
throw new TrustValidationError("quarantine review does not target the current review head");
|
|
919
|
+
if (decision.evaluatedAtLogicalMs !== input.logicalTimeMs ||
|
|
920
|
+
profile.fusionDecisionId !== decision.fusionDecisionId ||
|
|
921
|
+
profile.fusionDecisionDigest !== decision.fusionDecisionDigest ||
|
|
922
|
+
profile.updatedAtLogicalMs !== input.logicalTimeMs ||
|
|
923
|
+
profile.tenantId !== target.tenantId ||
|
|
924
|
+
profile.subjectDigest !== target.subjectDigest ||
|
|
925
|
+
profile.scopeDigest !== target.scopeDigest ||
|
|
926
|
+
profile.policyDigest !== target.policyDigest)
|
|
927
|
+
throw new TrustValidationError("quarantine review profile binding is invalid");
|
|
928
|
+
const profileKey = digestTrustProfileKeyV1({
|
|
929
|
+
tenantId: profile.tenantId,
|
|
930
|
+
subjectDigest: profile.subjectDigest,
|
|
931
|
+
scopeDigest: profile.scopeDigest,
|
|
932
|
+
policyDigest: profile.policyDigest,
|
|
933
|
+
});
|
|
934
|
+
const profileHead = state.profileHeads.find((candidate) => candidate.profileKey === profileKey);
|
|
935
|
+
if (profileHead?.profileId !== profile.profileId ||
|
|
936
|
+
profileHead.profileDigest !== profile.profileDigest)
|
|
937
|
+
throw new TrustValidationError("quarantine review profile is not the current head");
|
|
938
|
+
const policy = policies.find((candidate) => digestEvidenceFusionPolicyV1(candidate) === target.policyDigest);
|
|
939
|
+
const policyHead = state.policyHeads.find((candidate) => candidate.policyId === policy?.policyId);
|
|
940
|
+
if (!policy ||
|
|
941
|
+
policyHead?.policyVersion !== policy.policyVersion ||
|
|
942
|
+
policyHead.policyDigest !== target.policyDigest)
|
|
943
|
+
throw new TrustValidationError("quarantine review policy head is unavailable");
|
|
944
|
+
if (recoveryDecisions.length >= state.limits.maximumRecoveryDecisions)
|
|
945
|
+
throw new TrustValidationError("recovery decision capacity exceeded");
|
|
946
|
+
const recovery = evaluateQuarantineRecoveryV1(state, target, profile, decision, policy, input.logicalTimeMs);
|
|
947
|
+
recoveryDecisions.push(recovery);
|
|
948
|
+
effects.push(quarantineEffect("quarantine_reviewed", target, recovery.disposition === "unavailable"
|
|
949
|
+
? "quarantine_recovery_unavailable"
|
|
950
|
+
: recovery.disposition === "insufficient"
|
|
951
|
+
? "quarantine_recovery_insufficient"
|
|
952
|
+
: "quarantine_recovered"));
|
|
953
|
+
if (recovery.disposition === "recovered") {
|
|
954
|
+
const recovered = createRecoveredQuarantineRecordV1(target, recovery);
|
|
955
|
+
quarantines.push(recovered);
|
|
956
|
+
effects.push(quarantineEffect("quarantine_recovered", recovered, "quarantine_recovered"));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
else if (input.kind === "policy_registered") {
|
|
961
|
+
const policy = validateEvidenceFusionPolicyV1(input.policy, state.limits);
|
|
962
|
+
const policyDigest = digestEvidenceFusionPolicyV1(policy);
|
|
963
|
+
const existing = policies.find((candidate) => candidate.policyId === policy.policyId &&
|
|
964
|
+
candidate.policyVersion === policy.policyVersion);
|
|
965
|
+
if (existing) {
|
|
966
|
+
if (digestEvidenceFusionPolicyV1(existing) !== policyDigest)
|
|
967
|
+
throw new TrustValidationError("policy version conflicts with existing content");
|
|
968
|
+
effects.push(effect("record_duplicate", null, "duplicate"));
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
if (policies.length >= state.limits.maximumPolicies)
|
|
972
|
+
throw new TrustValidationError("policy capacity exceeded");
|
|
973
|
+
if ((policy.policyVersion === 1) !== (policy.parentPolicyDigest === null))
|
|
974
|
+
throw new TrustValidationError("policy lineage is invalid");
|
|
975
|
+
if (policy.parentPolicyDigest !== null) {
|
|
976
|
+
const parent = policies.find((candidate) => digestEvidenceFusionPolicyV1(candidate) ===
|
|
977
|
+
policy.parentPolicyDigest);
|
|
978
|
+
if (!parent ||
|
|
979
|
+
parent.policyId !== policy.policyId ||
|
|
980
|
+
parent.policyVersion + 1 !== policy.policyVersion)
|
|
981
|
+
throw new TrustValidationError("policy lineage is invalid");
|
|
982
|
+
}
|
|
983
|
+
policies.push(policy);
|
|
984
|
+
effects.push(effect("policy_registered", null, "accepted"));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
else if (input.kind === "dependency_binding_registered") {
|
|
988
|
+
const binding = validateEvidenceTrustDependencyBindingV1(input.binding);
|
|
989
|
+
const existing = dependencyBindings.find((candidate) => candidate.bindingKind === binding.bindingKind &&
|
|
990
|
+
candidate.bindingName === binding.bindingName &&
|
|
991
|
+
candidate.bindingVersion === binding.bindingVersion);
|
|
992
|
+
if (existing) {
|
|
993
|
+
if (existing.bindingDigest !== binding.bindingDigest)
|
|
994
|
+
throw new TrustValidationError("dependency binding version conflicts with existing content");
|
|
995
|
+
effects.push(effect("record_duplicate", null, "duplicate"));
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
if (dependencyBindings.length >=
|
|
999
|
+
state.limits.maximumDependencyBindingVersions)
|
|
1000
|
+
throw new TrustValidationError("dependency binding capacity exceeded");
|
|
1001
|
+
if (binding.registeredAtLogicalMs !== input.logicalTimeMs)
|
|
1002
|
+
throw new TrustValidationError("dependency binding registration time is invalid");
|
|
1003
|
+
if (binding.policyDigest !== null &&
|
|
1004
|
+
!policies.some((policy) => digestEvidenceFusionPolicyV1(policy) === binding.policyDigest))
|
|
1005
|
+
throw new TrustValidationError("dependency binding policy is unregistered");
|
|
1006
|
+
if ((binding.bindingVersion === 1) !==
|
|
1007
|
+
(binding.parentBindingDigest === null))
|
|
1008
|
+
throw new TrustValidationError("dependency binding lineage is invalid");
|
|
1009
|
+
if (binding.parentBindingDigest !== null) {
|
|
1010
|
+
const parent = dependencyBindings.find((candidate) => candidate.bindingDigest === binding.parentBindingDigest);
|
|
1011
|
+
if (!parent ||
|
|
1012
|
+
parent.bindingKind !== binding.bindingKind ||
|
|
1013
|
+
parent.bindingName !== binding.bindingName ||
|
|
1014
|
+
parent.bindingVersion + 1 !== binding.bindingVersion)
|
|
1015
|
+
throw new TrustValidationError("dependency binding lineage is invalid");
|
|
1016
|
+
}
|
|
1017
|
+
if (binding.upstreamBindingDigest !== null &&
|
|
1018
|
+
!dependencyBindings.some((candidate) => candidate.bindingDigest === binding.upstreamBindingDigest))
|
|
1019
|
+
throw new TrustValidationError("dependency binding upstream is unregistered");
|
|
1020
|
+
dependencyBindings.push(binding);
|
|
1021
|
+
effects.push(effect("dependency_binding_registered", null, "accepted"));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
else if (input.kind === "causal_authorization_recorded") {
|
|
1025
|
+
const authorization = createEvidenceCausalAuthorizationV1({
|
|
1026
|
+
...input.authorization,
|
|
1027
|
+
authorizedAtLogicalMs: input.logicalTimeMs,
|
|
1028
|
+
});
|
|
1029
|
+
const record = findExact(records, authorization.recordId, authorization.recordDigest);
|
|
1030
|
+
const binding = dependencyBindings.find((item) => item.bindingDigest === authorization.authorityBindingDigest);
|
|
1031
|
+
const visibleHead = binding
|
|
1032
|
+
? dependencyBindings
|
|
1033
|
+
.filter((item) => item.bindingKind === binding.bindingKind &&
|
|
1034
|
+
item.bindingName === binding.bindingName &&
|
|
1035
|
+
item.registeredAtLogicalMs <= input.logicalTimeMs)
|
|
1036
|
+
.sort((left, right) => right.bindingVersion - left.bindingVersion)[0]
|
|
1037
|
+
: null;
|
|
1038
|
+
if (!record ||
|
|
1039
|
+
record.recordKind !== authorization.recordKind ||
|
|
1040
|
+
!binding ||
|
|
1041
|
+
binding.bindingKind !== "causal_authority" ||
|
|
1042
|
+
binding.policyDigest !== authorization.policyDigest ||
|
|
1043
|
+
binding.registeredAtLogicalMs > input.logicalTimeMs ||
|
|
1044
|
+
binding.validFromLogicalMs > input.logicalTimeMs ||
|
|
1045
|
+
(binding.validUntilLogicalMs !== null &&
|
|
1046
|
+
input.logicalTimeMs >= binding.validUntilLogicalMs) ||
|
|
1047
|
+
visibleHead?.bindingDigest !== binding.bindingDigest)
|
|
1048
|
+
throw new TrustValidationError("causal authorization binding is invalid");
|
|
1049
|
+
const registry = normalizedOptions.causalAuthorityVerifierRegistry;
|
|
1050
|
+
const verifier = registry
|
|
1051
|
+
? resolveEvidenceCausalAuthorityVerifierV1(registry, authorization.authorityBindingDigest)
|
|
1052
|
+
: null;
|
|
1053
|
+
if (!verifier ||
|
|
1054
|
+
verifier.authorityBindingDigest !==
|
|
1055
|
+
authorization.authorityBindingDigest ||
|
|
1056
|
+
verifier.policyDigest !== authorization.policyDigest ||
|
|
1057
|
+
verifier.upstreamBindingDigest !== binding.upstreamBindingDigest ||
|
|
1058
|
+
!verifier.verify(authorization))
|
|
1059
|
+
throw new TrustValidationError("causal authorization proof is invalid");
|
|
1060
|
+
const existing = causalAuthorizations.find((item) => item.authorizationId === authorization.authorizationId);
|
|
1061
|
+
if (existing) {
|
|
1062
|
+
if (existing.authorizationDigest !== authorization.authorizationDigest)
|
|
1063
|
+
throw new TrustValidationError("causal authorization ID conflicts with existing content");
|
|
1064
|
+
effects.push(effect("causal_authorization_recorded", null, "duplicate"));
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
if (causalAuthorizations.length >= state.limits.maximumCausalAuthorizations)
|
|
1068
|
+
throw new TrustValidationError("causal authorization capacity exceeded");
|
|
1069
|
+
causalAuthorizations.push(authorization);
|
|
1070
|
+
effects.push(effect("causal_authorization_recorded", null, "accepted"));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
else if (input.kind === "record_admitted") {
|
|
1074
|
+
const record = validateEvidenceRecordV1(input.record), digest = digestEvidenceRecordV1(record), id = recordId(record);
|
|
1075
|
+
requireDigest(input.originBindingDigest, "originBindingDigest");
|
|
1076
|
+
if (input.origin !== "local" && input.origin !== "verified_mesh")
|
|
1077
|
+
throw new TrustValidationError("record origin is invalid");
|
|
1078
|
+
if (input.origin === "local") {
|
|
1079
|
+
if (input.originVerifierBindingDigest !== null ||
|
|
1080
|
+
input.originProofDigest !== null)
|
|
1081
|
+
throw new TrustValidationError("local origin metadata is invalid");
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
requireDigest(input.originVerifierBindingDigest, "originVerifierBindingDigest");
|
|
1085
|
+
requireDigest(input.originProofDigest, "originProofDigest");
|
|
1086
|
+
const registry = normalizedOptions.verifiedMeshAdmissionVerifierRegistry;
|
|
1087
|
+
const originVerifierBindingDigest = input.originVerifierBindingDigest;
|
|
1088
|
+
const originProofDigest = input.originProofDigest;
|
|
1089
|
+
const verifier = registry
|
|
1090
|
+
? resolveVerifiedMeshAdmissionVerifierV1(registry, originVerifierBindingDigest)
|
|
1091
|
+
: null;
|
|
1092
|
+
if (!verifier ||
|
|
1093
|
+
verifier.verifierBindingDigest !== originVerifierBindingDigest ||
|
|
1094
|
+
verifier.upstreamBindingDigest !== input.originBindingDigest ||
|
|
1095
|
+
!verifier.verify({
|
|
1096
|
+
recordId: id,
|
|
1097
|
+
recordDigest: digest,
|
|
1098
|
+
originBindingDigest: input.originBindingDigest,
|
|
1099
|
+
originVerifierBindingDigest,
|
|
1100
|
+
originProofDigest,
|
|
1101
|
+
effectiveAtLogicalMs: input.effectiveAtLogicalMs,
|
|
1102
|
+
}))
|
|
1103
|
+
throw new TrustValidationError("verified mesh origin proof is invalid");
|
|
1104
|
+
}
|
|
1105
|
+
if (basisReferences(record).length >
|
|
1106
|
+
state.limits.maximumBasisReferencesPerRecord)
|
|
1107
|
+
throw new TrustValidationError("basis reference limit exceeded");
|
|
1108
|
+
if (recordKind(record) === "claim" &&
|
|
1109
|
+
"content" in record &&
|
|
1110
|
+
record.content !== null) {
|
|
1111
|
+
const contentLimit = record.content.kind === "reference"
|
|
1112
|
+
? state.limits.maximumContentReferenceBytes
|
|
1113
|
+
: state.limits.maximumInlineSummaryBytes;
|
|
1114
|
+
if (record.content.encodedBytes > contentLimit)
|
|
1115
|
+
throw new TrustValidationError("evidence content size limit exceeded");
|
|
1116
|
+
}
|
|
1117
|
+
if (canonicalTrustJsonBytesV1(record).byteLength >
|
|
1118
|
+
state.limits.maximumRecordCanonicalBytes)
|
|
1119
|
+
throw new TrustValidationError("record size limit exceeded");
|
|
1120
|
+
const existing = records.find((item) => item.recordId === id);
|
|
1121
|
+
if (existing) {
|
|
1122
|
+
if (existing.recordDigest !== digest)
|
|
1123
|
+
throw new TrustValidationError("record ID conflicts with existing content");
|
|
1124
|
+
effects.push(effect("record_duplicate", existing, "duplicate"));
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
const familyLimit = {
|
|
1128
|
+
claim: state.limits.maximumClaims,
|
|
1129
|
+
attestation: state.limits.maximumAttestations,
|
|
1130
|
+
challenge: state.limits.maximumChallenges,
|
|
1131
|
+
retraction: state.limits.maximumRetractions,
|
|
1132
|
+
}[recordKind(record)];
|
|
1133
|
+
if (records.filter((item) => item.recordKind === recordKind(record))
|
|
1134
|
+
.length >= familyLimit)
|
|
1135
|
+
throw new TrustValidationError("record family capacity exceeded");
|
|
1136
|
+
if (recordKind(record) === "challenge") {
|
|
1137
|
+
const scopeDigest = recordScopeDigest(record);
|
|
1138
|
+
const sourceChallenges = records.filter((item) => item.recordKind === "challenge" &&
|
|
1139
|
+
recordSource(item.record) === record.sourceId &&
|
|
1140
|
+
item.record.sourceKind === record.sourceKind &&
|
|
1141
|
+
recordScopeDigest(item.record) === scopeDigest);
|
|
1142
|
+
if (sourceChallenges.length >=
|
|
1143
|
+
state.limits.maximumChallengesPerSourceScope)
|
|
1144
|
+
throw new TrustValidationError("challenge source scope capacity exceeded");
|
|
1145
|
+
if (targetStatus({ record }, records) ===
|
|
1146
|
+
"relationship_target_missing" &&
|
|
1147
|
+
sourceChallenges.filter((item) => item.status === "pending").length >=
|
|
1148
|
+
state.limits.maximumPendingChallengesPerSourceScope)
|
|
1149
|
+
throw new TrustValidationError("pending challenge source scope capacity exceeded");
|
|
1150
|
+
}
|
|
1151
|
+
const admitted = {
|
|
1152
|
+
schemaVersion: 1,
|
|
1153
|
+
recordKind: recordKind(record),
|
|
1154
|
+
recordId: id,
|
|
1155
|
+
recordDigest: digest,
|
|
1156
|
+
record,
|
|
1157
|
+
origin: input.origin,
|
|
1158
|
+
originBindingDigest: input.originBindingDigest,
|
|
1159
|
+
originVerifierBindingDigest: input.originVerifierBindingDigest,
|
|
1160
|
+
originProofDigest: input.originProofDigest,
|
|
1161
|
+
acceptedAtLogicalMs: input.logicalTimeMs,
|
|
1162
|
+
effectiveAtLogicalMs: input.effectiveAtLogicalMs,
|
|
1163
|
+
status: "active",
|
|
1164
|
+
};
|
|
1165
|
+
records.push(admitted);
|
|
1166
|
+
effects.push(effect("record_accepted", admitted, "accepted"));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
else if (input.kind === "content_resolution_recorded") {
|
|
1170
|
+
const resolution = input.resolution;
|
|
1171
|
+
assertExactKeys(resolution, [
|
|
1172
|
+
"schemaVersion",
|
|
1173
|
+
"claimId",
|
|
1174
|
+
"claimDigest",
|
|
1175
|
+
"scopeDigest",
|
|
1176
|
+
"referenceId",
|
|
1177
|
+
"referenceDigest",
|
|
1178
|
+
"contentDigest",
|
|
1179
|
+
"mediaType",
|
|
1180
|
+
"encodedBytes",
|
|
1181
|
+
"result",
|
|
1182
|
+
"resolverBindingDigest",
|
|
1183
|
+
], "content resolution input");
|
|
1184
|
+
const body = { ...resolution, resolvedAtLogicalMs: input.logicalTimeMs };
|
|
1185
|
+
const digest = digestTrustJsonV1("content-resolution", body);
|
|
1186
|
+
const accepted = validateEvidenceContentResolutionV1({
|
|
1187
|
+
...body,
|
|
1188
|
+
resolutionDigest: digest,
|
|
1189
|
+
resolutionId: `content-resolution:${digest}`,
|
|
1190
|
+
});
|
|
1191
|
+
const claim = findExact(records, accepted.claimId, accepted.claimDigest);
|
|
1192
|
+
if (!claim ||
|
|
1193
|
+
claim.recordKind !== "claim" ||
|
|
1194
|
+
recordScopeDigest(claim.record) !== accepted.scopeDigest)
|
|
1195
|
+
throw new TrustValidationError("content resolution target is invalid");
|
|
1196
|
+
const content = "content" in claim.record ? claim.record.content : null;
|
|
1197
|
+
if (!content ||
|
|
1198
|
+
content.kind !== "reference" ||
|
|
1199
|
+
content.reference.referenceId !== accepted.referenceId ||
|
|
1200
|
+
content.reference.referenceDigest !== accepted.referenceDigest ||
|
|
1201
|
+
content.contentDigest !== accepted.contentDigest ||
|
|
1202
|
+
content.mediaType !== accepted.mediaType ||
|
|
1203
|
+
content.encodedBytes !== accepted.encodedBytes)
|
|
1204
|
+
throw new TrustValidationError("content resolution does not match claim descriptor");
|
|
1205
|
+
if (accepted.result === "verified" &&
|
|
1206
|
+
normalizedOptions.currentContentResolverBindingDigest !==
|
|
1207
|
+
accepted.resolverBindingDigest)
|
|
1208
|
+
throw new TrustValidationError("content resolver binding is not current");
|
|
1209
|
+
if (!resolutions.some((item) => item.resolutionId === accepted.resolutionId)) {
|
|
1210
|
+
if (resolutions.length >= state.limits.maximumContentResolutions)
|
|
1211
|
+
throw new TrustValidationError("content resolution capacity exceeded");
|
|
1212
|
+
resolutions.push(accepted);
|
|
1213
|
+
effects.push(effect("content_resolution_recorded", null, "accepted"));
|
|
1214
|
+
}
|
|
1215
|
+
else
|
|
1216
|
+
effects.push(effect("record_duplicate", null, "duplicate"));
|
|
1217
|
+
}
|
|
1218
|
+
else if (input.kind === "content_resolution_invalidated") {
|
|
1219
|
+
const invalidation = input.invalidation;
|
|
1220
|
+
assertExactKeys(invalidation, [
|
|
1221
|
+
"schemaVersion",
|
|
1222
|
+
"resolutionId",
|
|
1223
|
+
"resolutionDigest",
|
|
1224
|
+
"resolverBindingDigest",
|
|
1225
|
+
"reasonCode",
|
|
1226
|
+
], "content invalidation input");
|
|
1227
|
+
const body = {
|
|
1228
|
+
...invalidation,
|
|
1229
|
+
invalidatedAtLogicalMs: input.logicalTimeMs,
|
|
1230
|
+
};
|
|
1231
|
+
const digest = digestTrustJsonV1("content-resolution-invalidation", body);
|
|
1232
|
+
const accepted = validateEvidenceContentResolutionInvalidationV1({
|
|
1233
|
+
...body,
|
|
1234
|
+
invalidationId: `content-resolution-invalidation:${digest}`,
|
|
1235
|
+
});
|
|
1236
|
+
const resolution = resolutions.find((item) => item.resolutionId === accepted.resolutionId &&
|
|
1237
|
+
item.resolutionDigest === accepted.resolutionDigest);
|
|
1238
|
+
if (!resolution ||
|
|
1239
|
+
resolution.resolverBindingDigest !== accepted.resolverBindingDigest)
|
|
1240
|
+
throw new TrustValidationError("content invalidation target is invalid");
|
|
1241
|
+
if (!invalidations.some((item) => item.invalidationId === accepted.invalidationId)) {
|
|
1242
|
+
if (invalidations.length >= state.limits.maximumContentInvalidations)
|
|
1243
|
+
throw new TrustValidationError("content invalidation capacity exceeded");
|
|
1244
|
+
invalidations.push(accepted);
|
|
1245
|
+
effects.push(effect("content_invalidation_recorded", null, "accepted"));
|
|
1246
|
+
}
|
|
1247
|
+
else
|
|
1248
|
+
effects.push(effect("record_duplicate", null, "duplicate"));
|
|
1249
|
+
}
|
|
1250
|
+
else
|
|
1251
|
+
effects.push(effect("logical_time_advanced", null, "accepted"));
|
|
1252
|
+
const derived = projectEvidenceLifecycleV1({
|
|
1253
|
+
records,
|
|
1254
|
+
logicalTimeMs: input.logicalTimeMs,
|
|
1255
|
+
limits: state.limits,
|
|
1256
|
+
contentResolutions: resolutions,
|
|
1257
|
+
contentInvalidations: invalidations,
|
|
1258
|
+
currentContentResolverBindingDigest: normalizedOptions.currentContentResolverBindingDigest,
|
|
1259
|
+
});
|
|
1260
|
+
if (derived.records.filter((item) => item.status === "pending").length >
|
|
1261
|
+
state.limits.maximumPendingRecords)
|
|
1262
|
+
throw new TrustValidationError("pending record capacity exceeded");
|
|
1263
|
+
const statusReasons = new Map(derived.diagnostics.map((item) => [item.recordDigest, item.reasonCode]));
|
|
1264
|
+
const finalizedEffects = effects.map((item) => item.kind === "record_accepted" && item.recordDigest !== null
|
|
1265
|
+
? {
|
|
1266
|
+
...item,
|
|
1267
|
+
reasonCode: statusReasons.get(item.recordDigest) ?? item.reasonCode,
|
|
1268
|
+
}
|
|
1269
|
+
: item);
|
|
1270
|
+
effects.splice(0, effects.length, ...finalizedEffects);
|
|
1271
|
+
for (const item of derived.records) {
|
|
1272
|
+
const before = state.records.find((candidate) => candidate.recordDigest === item.recordDigest);
|
|
1273
|
+
if (before && before.status !== item.status)
|
|
1274
|
+
effects.push(effect("record_status_changed", item, statusReasons.get(item.recordDigest) ?? "evidence_unavailable"));
|
|
1275
|
+
}
|
|
1276
|
+
const nextBase = {
|
|
1277
|
+
...state,
|
|
1278
|
+
logicalTimeHighWaterMs: input.logicalTimeMs,
|
|
1279
|
+
policies: policies.sort((a, b) => compareUnicode(`${a.policyId}\u0000${String(a.policyVersion).padStart(16, "0")}\u0000${digestEvidenceFusionPolicyV1(a)}`, `${b.policyId}\u0000${String(b.policyVersion).padStart(16, "0")}\u0000${digestEvidenceFusionPolicyV1(b)}`)),
|
|
1280
|
+
policyHeads: [
|
|
1281
|
+
...[...policies]
|
|
1282
|
+
.reduce((heads, policy) => {
|
|
1283
|
+
const prior = heads.get(policy.policyId);
|
|
1284
|
+
if (!prior || prior.policyVersion < policy.policyVersion)
|
|
1285
|
+
heads.set(policy.policyId, {
|
|
1286
|
+
policyId: policy.policyId,
|
|
1287
|
+
policyVersion: policy.policyVersion,
|
|
1288
|
+
policyDigest: digestEvidenceFusionPolicyV1(policy),
|
|
1289
|
+
});
|
|
1290
|
+
return heads;
|
|
1291
|
+
}, new Map())
|
|
1292
|
+
.values(),
|
|
1293
|
+
].sort((a, b) => compareUnicode(a.policyId, b.policyId)),
|
|
1294
|
+
dependencyBindings: dependencyBindings.sort((a, b) => compareUnicode(`${a.bindingKind}\u0000${a.bindingName}\u0000${String(a.bindingVersion).padStart(16, "0")}\u0000${a.bindingDigest}`, `${b.bindingKind}\u0000${b.bindingName}\u0000${String(b.bindingVersion).padStart(16, "0")}\u0000${b.bindingDigest}`)),
|
|
1295
|
+
dependencyBindingHeads: [
|
|
1296
|
+
...[...dependencyBindings]
|
|
1297
|
+
.reduce((heads, binding) => {
|
|
1298
|
+
const key = `${binding.bindingKind}\u0000${binding.bindingName}`;
|
|
1299
|
+
const prior = heads.get(key);
|
|
1300
|
+
if (!prior || prior.bindingVersion < binding.bindingVersion)
|
|
1301
|
+
heads.set(key, dependencyBindingHeadV1(binding));
|
|
1302
|
+
return heads;
|
|
1303
|
+
}, new Map())
|
|
1304
|
+
.values(),
|
|
1305
|
+
].sort((a, b) => compareUnicode(`${a.bindingKind}\u0000${a.bindingName}`, `${b.bindingKind}\u0000${b.bindingName}`)),
|
|
1306
|
+
causalAuthorizations: causalAuthorizations.sort((a, b) => compareUnicode(a.authorizationDigest, b.authorizationDigest)),
|
|
1307
|
+
fusionDecisions: fusionDecisions.sort((a, b) => compareUnicode(a.fusionDecisionDigest, b.fusionDecisionDigest)),
|
|
1308
|
+
profiles: profiles.sort((a, b) => {
|
|
1309
|
+
const leftKey = digestTrustProfileKeyV1({
|
|
1310
|
+
tenantId: a.tenantId,
|
|
1311
|
+
scopeDigest: a.scopeDigest,
|
|
1312
|
+
subjectDigest: a.subjectDigest,
|
|
1313
|
+
policyDigest: a.policyDigest,
|
|
1314
|
+
});
|
|
1315
|
+
const rightKey = digestTrustProfileKeyV1({
|
|
1316
|
+
tenantId: b.tenantId,
|
|
1317
|
+
scopeDigest: b.scopeDigest,
|
|
1318
|
+
subjectDigest: b.subjectDigest,
|
|
1319
|
+
policyDigest: b.policyDigest,
|
|
1320
|
+
});
|
|
1321
|
+
return (compareUnicode(leftKey, rightKey) ||
|
|
1322
|
+
a.revision - b.revision ||
|
|
1323
|
+
compareUnicode(a.profileDigest, b.profileDigest));
|
|
1324
|
+
}),
|
|
1325
|
+
profileHeads: [
|
|
1326
|
+
...profiles
|
|
1327
|
+
.reduce((heads, profile) => {
|
|
1328
|
+
const head = trustProfileHeadV1(profile);
|
|
1329
|
+
const prior = heads.get(head.profileKey);
|
|
1330
|
+
if (!prior || prior.revision < head.revision)
|
|
1331
|
+
heads.set(head.profileKey, head);
|
|
1332
|
+
return heads;
|
|
1333
|
+
}, new Map())
|
|
1334
|
+
.values(),
|
|
1335
|
+
].sort((a, b) => compareUnicode(a.profileKey, b.profileKey)),
|
|
1336
|
+
quarantines: quarantines.sort((a, b) => compareUnicode(`${a.quarantineKey}\u0000${String(a.revision).padStart(16, "0")}\u0000${a.quarantineId}`, `${b.quarantineKey}\u0000${String(b.revision).padStart(16, "0")}\u0000${b.quarantineId}`)),
|
|
1337
|
+
quarantineHeads: currentQuarantineHeads(),
|
|
1338
|
+
recoveryDecisions: recoveryDecisions.sort((a, b) => compareUnicode(a.recoveryDecisionDigest, b.recoveryDecisionDigest)),
|
|
1339
|
+
records: derived.records,
|
|
1340
|
+
contentResolutions: ordered(resolutions),
|
|
1341
|
+
contentInvalidations: ordered(invalidations),
|
|
1342
|
+
pendingRecords: derived.records
|
|
1343
|
+
.filter((item) => item.status === "pending")
|
|
1344
|
+
.map((item) => item.recordId)
|
|
1345
|
+
.sort(compareUnicode),
|
|
1346
|
+
diagnostics: derived.diagnostics.slice(0, state.limits.maximumDiagnostics),
|
|
1347
|
+
traceDigest: digestTrustJsonV1("trace", {
|
|
1348
|
+
previousTraceDigest: state.traceDigest,
|
|
1349
|
+
inputKind: input.kind,
|
|
1350
|
+
logicalTimeMs: input.logicalTimeMs,
|
|
1351
|
+
effects: effects
|
|
1352
|
+
.map((item) => ({
|
|
1353
|
+
kind: item.kind,
|
|
1354
|
+
recordDigest: item.recordDigest,
|
|
1355
|
+
reasonCode: item.reasonCode,
|
|
1356
|
+
}))
|
|
1357
|
+
.sort((a, b) => compareUnicode(`${a.kind}:${a.recordDigest ?? ""}`, `${b.kind}:${b.recordDigest ?? ""}`)),
|
|
1358
|
+
}),
|
|
1359
|
+
};
|
|
1360
|
+
const encodedBytes = stateEncodedBytes(nextBase);
|
|
1361
|
+
if (encodedBytes > state.limits.maximumStateCanonicalBytes)
|
|
1362
|
+
throw new TrustValidationError("state capacity exceeded");
|
|
1363
|
+
const next = validateEvidenceTrustStateV1({ ...nextBase, encodedBytes });
|
|
1364
|
+
return deepFreeze({
|
|
1365
|
+
state: next,
|
|
1366
|
+
effects: [...effects].sort((a, b) => compareUnicode(`${a.kind}:${a.recordDigest ?? ""}`, `${b.kind}:${b.recordDigest ?? ""}`)),
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
//# sourceMappingURL=lifecycle.js.map
|