@peerbit/trusted-network 6.0.103 → 6.0.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,812 @@
1
+ import { deserialize, serialize } from "@dao-xyz/borsh";
2
+ import { DecryptedThing, PublicSignKey, verify } from "@peerbit/crypto";
3
+ import { Entry, EntryV0, NO_ENCODING } from "@peerbit/log";
4
+ import { compare, equals } from "uint8arrays";
5
+ import { NetworkDescriptorV2, PolicySnapshotBodyV2, PolicySubjectBindingV2, TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS, TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES, assertNetworkDescriptorV2, decodePolicySnapshotBodyV2, digestPolicySnapshotBodyV2, } from "./v2.js";
6
+ /**
7
+ * Internal policy reducer for the non-activatable TrustedNetwork v2 scaffold.
8
+ *
9
+ * The reducer intentionally is not exported from the package entry point. It
10
+ * retains one accepted snapshot, a bounded pending working set, and (after
11
+ * equivocation) two child proofs. Historical snapshots are supplied by the
12
+ * resolver instead of being accumulated in memory.
13
+ */
14
+ const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
15
+ const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
16
+ const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 = TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2 +
17
+ PENDING_POLICY_ACCOUNTING_OVERHEAD_V2;
18
+ const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2;
19
+ const DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2 = 10 * 1000;
20
+ const MAX_TIMER_DELAY_MS_V2 = 0x7fffffff;
21
+ const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
22
+ const copyBytes = (bytes) => Uint8Array.from(bytes);
23
+ const bytesKey = (bytes) => {
24
+ let key = "";
25
+ for (const byte of bytes)
26
+ key += byte.toString(16).padStart(2, "0");
27
+ return key;
28
+ };
29
+ const compareKeys = (left, right) => left < right ? -1 : left > right ? 1 : 0;
30
+ const copyPublicKey = (key) => deserialize(serialize(key), PublicSignKey);
31
+ const publicKeyId = (key) => bytesKey(serialize(key));
32
+ const copyBinding = (binding) => new PolicySubjectBindingV2({
33
+ signingKey: copyPublicKey(binding.signingKey),
34
+ roles: binding.roles,
35
+ });
36
+ const copyBody = (body) => new PolicySnapshotBodyV2({
37
+ networkId: copyBytes(body.networkId),
38
+ sequence: body.sequence,
39
+ previousPolicyDigest: copyBytes(body.previousPolicyDigest),
40
+ bindings: body.bindings.map(copyBinding),
41
+ });
42
+ const copySnapshot = (snapshot) => ({
43
+ body: copyBody(snapshot.body),
44
+ digest: copyBytes(snapshot.digest),
45
+ digestKey: snapshot.digestKey,
46
+ entryBytes: copyBytes(snapshot.entryBytes),
47
+ accountedBytes: snapshot.accountedBytes,
48
+ });
49
+ const validationMessage = (error) => error instanceof Error ? error.message : String(error);
50
+ const boundedUnavailableReason = (reason) => reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
51
+ class PolicyDependencyUnavailableErrorV2 extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "PolicyDependencyUnavailableErrorV2";
55
+ }
56
+ }
57
+ const capturePolicySnapshotEntryBytesV2 = (entryBytes) => {
58
+ if (!(entryBytes instanceof Uint8Array)) {
59
+ throw new Error("Policy snapshot must use canonical EntryV0 bytes");
60
+ }
61
+ if (entryBytes.byteLength === 0) {
62
+ throw new Error("Policy snapshot must use EntryV0");
63
+ }
64
+ if (entryBytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
65
+ throw new Error(`Policy snapshot entry must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`);
66
+ }
67
+ // Apply the protocol byte ceiling before this first copy, decode, or crypto
68
+ // operation. The retained copy also prevents caller mutation during awaits.
69
+ return copyBytes(entryBytes);
70
+ };
71
+ const authenticateCapturedPolicySnapshotEntryV2 = async (canonicalEntryBytes, descriptor) => {
72
+ const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
73
+ if (!(authenticatedEntry instanceof EntryV0)) {
74
+ throw new Error("Policy snapshot must use EntryV0");
75
+ }
76
+ if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
77
+ throw new Error("Policy snapshot entry encoding is not canonical");
78
+ }
79
+ if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
80
+ throw new Error("Policy snapshot metadata must be public");
81
+ }
82
+ if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
83
+ throw new Error("Policy snapshot payload must be public");
84
+ }
85
+ if (authenticatedEntry._signatures === undefined ||
86
+ authenticatedEntry._signatures.signatures.length !== 1) {
87
+ throw new Error("Policy snapshot must contain exactly one signature");
88
+ }
89
+ if (!(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)) {
90
+ throw new Error("Policy snapshot signature must be public");
91
+ }
92
+ authenticatedEntry.init({ encoding: NO_ENCODING });
93
+ const signatures = await authenticatedEntry.getSignatures();
94
+ if (signatures.length !== 1) {
95
+ throw new Error("Policy snapshot must resolve exactly one signature");
96
+ }
97
+ const signature = signatures[0];
98
+ if (!equals(serialize(signature.publicKey), serialize(descriptor.policyAuthority))) {
99
+ throw new Error("Policy snapshot signer is not the policy authority");
100
+ }
101
+ if (!(await verify(signature, authenticatedEntry.getSignableBytes()))) {
102
+ throw new Error("Policy snapshot authority signature is invalid");
103
+ }
104
+ const payload = await authenticatedEntry.getPayloadValue();
105
+ if (!(payload instanceof Uint8Array)) {
106
+ throw new Error("Policy snapshot payload must contain canonical body bytes");
107
+ }
108
+ const body = decodePolicySnapshotBodyV2(copyBytes(payload), descriptor);
109
+ const digest = digestPolicySnapshotBodyV2(body);
110
+ return {
111
+ body: copyBody(body),
112
+ digest: copyBytes(digest),
113
+ digestKey: bytesKey(digest),
114
+ entryBytes: canonicalEntryBytes,
115
+ accountedBytes: canonicalEntryBytes.byteLength +
116
+ serialize(body).byteLength +
117
+ PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
118
+ };
119
+ };
120
+ export const authenticatePolicySnapshotEntryV2 = async (entryBytes, descriptor) => {
121
+ assertNetworkDescriptorV2(descriptor);
122
+ return authenticateCapturedPolicySnapshotEntryV2(capturePolicySnapshotEntryBytesV2(entryBytes), descriptor);
123
+ };
124
+ const projectionFromSnapshot = (snapshot) => ({
125
+ sequence: snapshot.body.sequence,
126
+ digest: copyBytes(snapshot.digest),
127
+ bindings: snapshot.body.bindings.map(copyBinding),
128
+ });
129
+ const forkProofFromSnapshot = (snapshot) => ({
130
+ sequence: snapshot.body.sequence,
131
+ digest: copyBytes(snapshot.digest),
132
+ entryBytes: copyBytes(snapshot.entryBytes),
133
+ });
134
+ const copyForkChildProof = (proof) => ({
135
+ sequence: proof.sequence,
136
+ digest: copyBytes(proof.digest),
137
+ entryBytes: copyBytes(proof.entryBytes),
138
+ });
139
+ const compareForkChildProofs = (left, right) => {
140
+ const digestOrder = compare(left.digest, right.digest);
141
+ return digestOrder === 0
142
+ ? compare(left.entryBytes, right.entryBytes)
143
+ : digestOrder;
144
+ };
145
+ const copyProjection = (projection) => ({
146
+ sequence: projection.sequence,
147
+ digest: copyBytes(projection.digest),
148
+ bindings: projection.bindings.map(copyBinding),
149
+ });
150
+ const copyForkEvidence = (evidence) => ({
151
+ commonParent: copyProjection(evidence.commonParent),
152
+ children: [
153
+ copyForkChildProof(evidence.children[0]),
154
+ copyForkChildProof(evidence.children[1]),
155
+ ],
156
+ });
157
+ export class TrustedNetworkV2PolicyReducer {
158
+ descriptor;
159
+ resolvePolicyEntry;
160
+ resolveTimeoutMs;
161
+ maxPending;
162
+ maxPendingPolicyBytes;
163
+ lifecycleController = new AbortController();
164
+ externalSignal;
165
+ externalAbortListener;
166
+ acceptedHead;
167
+ projectedRoles = new Map();
168
+ pending = new Map();
169
+ // Recovery is bound to this exact candidate. Unrelated admissions may enter
170
+ // the bounded pending set but can never restore ACTIVE authorization.
171
+ unavailable;
172
+ fork;
173
+ admissionTail = Promise.resolve();
174
+ constructor(properties) {
175
+ assertNetworkDescriptorV2(properties.descriptor);
176
+ const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
177
+ if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
178
+ throw new Error("maxPending must be a positive safe integer");
179
+ }
180
+ const maxPendingPolicyBytes = properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
181
+ if (!Number.isSafeInteger(maxPendingPolicyBytes) ||
182
+ maxPendingPolicyBytes < 1 ||
183
+ maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2) {
184
+ throw new Error(`maxPendingPolicyBytes must be a positive safe integer no greater than ${MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2}`);
185
+ }
186
+ const resolveTimeoutMs = properties.resolveTimeoutMs ?? DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2;
187
+ if (!Number.isSafeInteger(resolveTimeoutMs) ||
188
+ resolveTimeoutMs < 1 ||
189
+ resolveTimeoutMs > MAX_TIMER_DELAY_MS_V2) {
190
+ throw new Error(`resolveTimeoutMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS_V2}`);
191
+ }
192
+ this.descriptor = deserialize(serialize(properties.descriptor), NetworkDescriptorV2);
193
+ this.resolvePolicyEntry = properties.resolvePolicyEntry;
194
+ this.resolveTimeoutMs = resolveTimeoutMs;
195
+ this.maxPending = maxPending;
196
+ this.maxPendingPolicyBytes = maxPendingPolicyBytes;
197
+ if (properties.signal?.aborted) {
198
+ this.lifecycleController.abort();
199
+ }
200
+ else if (properties.signal !== undefined) {
201
+ this.externalSignal = properties.signal;
202
+ this.externalAbortListener = () => {
203
+ this.externalSignal = undefined;
204
+ this.externalAbortListener = undefined;
205
+ this.lifecycleController.abort();
206
+ };
207
+ this.externalSignal.addEventListener("abort", this.externalAbortListener, { once: true });
208
+ }
209
+ }
210
+ get state() {
211
+ if (this.lifecycleController.signal.aborted)
212
+ return "HALTED";
213
+ if (this.fork !== undefined)
214
+ return "FORKED";
215
+ if (this.unavailable !== undefined)
216
+ return "UNAVAILABLE";
217
+ return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
218
+ }
219
+ get head() {
220
+ return this.acceptedHead === undefined
221
+ ? undefined
222
+ : projectionFromSnapshot(this.acceptedHead);
223
+ }
224
+ get forkEvidence() {
225
+ return this.fork === undefined ? undefined : copyForkEvidence(this.fork);
226
+ }
227
+ get pendingCount() {
228
+ return this.pending.size;
229
+ }
230
+ get pendingBytes() {
231
+ let total = 0;
232
+ for (const { snapshot } of this.pending.values()) {
233
+ total += snapshot.accountedBytes;
234
+ }
235
+ return total;
236
+ }
237
+ get pendingDigests() {
238
+ return [...this.pending.values()]
239
+ .sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey))
240
+ .map(({ snapshot }) => copyBytes(snapshot.digest));
241
+ }
242
+ rolesFor(subject) {
243
+ return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
244
+ }
245
+ isAuthorized(subject, roles) {
246
+ if (this.state !== "ACTIVE" ||
247
+ !Number.isInteger(roles) ||
248
+ roles === 0 ||
249
+ (roles & ~TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS) !== 0) {
250
+ return false;
251
+ }
252
+ return (this.rolesFor(subject) & roles) === roles;
253
+ }
254
+ abort() {
255
+ if (this.externalSignal !== undefined &&
256
+ this.externalAbortListener !== undefined) {
257
+ this.externalSignal.removeEventListener("abort", this.externalAbortListener);
258
+ }
259
+ this.externalSignal = undefined;
260
+ this.externalAbortListener = undefined;
261
+ this.lifecycleController.abort();
262
+ }
263
+ fetchHints() {
264
+ const unique = new Map();
265
+ for (const { missingParentDigest } of this.pending.values()) {
266
+ unique.set(bytesKey(missingParentDigest), missingParentDigest);
267
+ }
268
+ if (this.unavailable !== undefined) {
269
+ unique.set(bytesKey(this.unavailable.acceptedAncestorDigest), this.unavailable.acceptedAncestorDigest);
270
+ }
271
+ return [...unique.entries()]
272
+ .sort(([a], [b]) => compareKeys(a, b))
273
+ .map(([, digest]) => ({
274
+ kind: "policy-parent",
275
+ digest: copyBytes(digest),
276
+ }));
277
+ }
278
+ result(status, reason, evictedPolicyDigests) {
279
+ return {
280
+ status,
281
+ reason,
282
+ head: this.head,
283
+ fetchHints: this.fetchHints(),
284
+ pendingCount: this.pending.size,
285
+ pendingBytes: this.pendingBytes,
286
+ evictedPolicyDigests: evictedPolicyDigests === undefined
287
+ ? undefined
288
+ : evictedPolicyDigests.map(copyBytes),
289
+ };
290
+ }
291
+ forkedResult() {
292
+ return this.result("forked", "Policy authority signed competing children");
293
+ }
294
+ haltedResult() {
295
+ return this.result("halted", "Policy reducer lifecycle is aborted");
296
+ }
297
+ unavailableResult(retention) {
298
+ const blockedCandidateRetained = this.unavailable !== undefined &&
299
+ this.pending.has(this.unavailable.candidateDigestKey);
300
+ const recoverable = (retention?.retained ?? true) && blockedCandidateRetained;
301
+ const reason = recoverable
302
+ ? this.unavailable.reason
303
+ : blockedCandidateRetained
304
+ ? "Policy pending capacity did not retain this candidate"
305
+ : "Policy reducer remains unavailable because pending capacity did not retain the blocked candidate";
306
+ return this.result(recoverable ? "unavailable" : "capacity", reason, retention?.evictedPolicyDigests);
307
+ }
308
+ completedDrainResult(outcome) {
309
+ if (outcome?.status === "forked")
310
+ return this.forkedResult();
311
+ if (outcome?.status === "halted")
312
+ return this.haltedResult();
313
+ return outcome?.status === "unavailable"
314
+ ? this.unavailableResult(outcome)
315
+ : undefined;
316
+ }
317
+ project(snapshot) {
318
+ this.acceptedHead = copySnapshot(snapshot);
319
+ this.projectedRoles = new Map(snapshot.body.bindings.map((binding) => [
320
+ publicKeyId(binding.signingKey),
321
+ binding.roles,
322
+ ]));
323
+ }
324
+ retainCanonicalHeadEntry(snapshot) {
325
+ if (this.acceptedHead?.digestKey === snapshot.digestKey &&
326
+ compare(snapshot.entryBytes, this.acceptedHead.entryBytes) < 0) {
327
+ this.acceptedHead = copySnapshot(snapshot);
328
+ }
329
+ }
330
+ async resolveExternalSnapshot(digest) {
331
+ if (this.lifecycleController.signal.aborted) {
332
+ throw new PolicyDependencyUnavailableErrorV2("Policy resolver lifecycle is aborted");
333
+ }
334
+ const attemptController = new AbortController();
335
+ let timedOut = false;
336
+ const abortFromLifecycle = () => attemptController.abort();
337
+ this.lifecycleController.signal.addEventListener("abort", abortFromLifecycle, { once: true });
338
+ const timeout = setTimeout(() => {
339
+ timedOut = true;
340
+ attemptController.abort();
341
+ }, this.resolveTimeoutMs);
342
+ const abortError = () => new PolicyDependencyUnavailableErrorV2(timedOut
343
+ ? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
344
+ : "Policy resolver attempt was aborted");
345
+ let rejectOnAbort;
346
+ const abortPromise = new Promise((_resolve, reject) => {
347
+ rejectOnAbort = () => {
348
+ reject(abortError());
349
+ };
350
+ attemptController.signal.addEventListener("abort", rejectOnAbort, {
351
+ once: true,
352
+ });
353
+ });
354
+ const resolution = Promise.resolve().then(async () => {
355
+ if (attemptController.signal.aborted)
356
+ throw abortError();
357
+ const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
358
+ signal: attemptController.signal,
359
+ });
360
+ // A resolver may ignore cancellation. Do not spend decode or signature
361
+ // verification work on bytes that arrive after this attempt expired.
362
+ if (attemptController.signal.aborted)
363
+ throw abortError();
364
+ if (entryBytes === undefined)
365
+ return undefined;
366
+ const snapshot = await authenticatePolicySnapshotEntryV2(entryBytes, this.descriptor);
367
+ if (!equals(snapshot.digest, digest)) {
368
+ throw new Error("Policy resolver returned the wrong body digest");
369
+ }
370
+ return snapshot;
371
+ });
372
+ // Promise.race installs handlers, but this explicit observer documents and
373
+ // preserves consumption if the resolver settles after its deadline.
374
+ void resolution.then(() => undefined, () => undefined);
375
+ try {
376
+ return await Promise.race([resolution, abortPromise]);
377
+ }
378
+ catch (error) {
379
+ if (error instanceof PolicyDependencyUnavailableErrorV2)
380
+ throw error;
381
+ throw new PolicyDependencyUnavailableErrorV2(`Policy resolver dependency is unavailable: ${validationMessage(error)}`);
382
+ }
383
+ finally {
384
+ clearTimeout(timeout);
385
+ this.lifecycleController.signal.removeEventListener("abort", abortFromLifecycle);
386
+ if (rejectOnAbort !== undefined) {
387
+ attemptController.signal.removeEventListener("abort", rejectOnAbort);
388
+ }
389
+ }
390
+ }
391
+ async resolveSnapshot(digest, cache) {
392
+ const digestKey = bytesKey(digest);
393
+ if (this.acceptedHead?.digestKey === digestKey) {
394
+ return copySnapshot(this.acceptedHead);
395
+ }
396
+ const pending = this.pending.get(digestKey);
397
+ if (pending !== undefined)
398
+ return copySnapshot(pending.snapshot);
399
+ let resolution = cache?.get(digestKey);
400
+ if (resolution === undefined) {
401
+ resolution = this.resolveExternalSnapshot(digest);
402
+ cache?.set(digestKey, resolution);
403
+ }
404
+ const snapshot = await resolution;
405
+ return snapshot === undefined ? undefined : copySnapshot(snapshot);
406
+ }
407
+ async parentOf(child, cache) {
408
+ if (child.body.sequence === 0n) {
409
+ return {
410
+ status: "reject",
411
+ digest: copyBytes(child.body.previousPolicyDigest),
412
+ reason: "Genesis policy has no parent",
413
+ };
414
+ }
415
+ let parent;
416
+ try {
417
+ parent = await this.resolveSnapshot(child.body.previousPolicyDigest, cache);
418
+ }
419
+ catch (error) {
420
+ return {
421
+ status: "unavailable",
422
+ digest: copyBytes(child.body.previousPolicyDigest),
423
+ reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
424
+ };
425
+ }
426
+ if (parent === undefined) {
427
+ return {
428
+ status: "missing",
429
+ digest: copyBytes(child.body.previousPolicyDigest),
430
+ };
431
+ }
432
+ if (child.body.sequence !== parent.body.sequence + 1n) {
433
+ return {
434
+ status: "reject",
435
+ digest: copyBytes(child.body.previousPolicyDigest),
436
+ reason: "Policy sequence is not contiguous with its parent",
437
+ };
438
+ }
439
+ return { status: "found", parent };
440
+ }
441
+ candidateAncestryResult(resolution) {
442
+ return resolution.status === "unavailable"
443
+ ? {
444
+ status: "missing",
445
+ digest: copyBytes(resolution.digest),
446
+ reason: resolution.reason,
447
+ }
448
+ : resolution;
449
+ }
450
+ acceptedAncestryUnavailable(resolution) {
451
+ return {
452
+ status: "unavailable",
453
+ digest: copyBytes(resolution.digest),
454
+ reason: resolution.status === "missing"
455
+ ? "Accepted policy ancestry is unavailable from the resolver"
456
+ : resolution.status === "unavailable"
457
+ ? `Accepted policy ancestry is unavailable: ${resolution.reason}`
458
+ : `Accepted policy ancestry validation failed: ${resolution.reason}`,
459
+ };
460
+ }
461
+ async evaluate(candidate) {
462
+ const resolutionCache = new Map();
463
+ if (this.acceptedHead === undefined) {
464
+ let cursor = candidate;
465
+ while (cursor.body.sequence !== 0n) {
466
+ const parent = await this.parentOf(cursor, resolutionCache);
467
+ if (parent.status !== "found") {
468
+ return this.candidateAncestryResult(parent);
469
+ }
470
+ cursor = parent.parent;
471
+ }
472
+ return { status: "accept" };
473
+ }
474
+ let candidateCursor = candidate;
475
+ let acceptedCursor = this.acceptedHead;
476
+ let candidateChild;
477
+ let acceptedChild;
478
+ while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
479
+ candidateChild = candidateCursor;
480
+ const parent = await this.parentOf(candidateCursor, resolutionCache);
481
+ if (parent.status !== "found") {
482
+ return this.candidateAncestryResult(parent);
483
+ }
484
+ candidateCursor = parent.parent;
485
+ }
486
+ while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
487
+ acceptedChild = acceptedCursor;
488
+ const parent = await this.parentOf(acceptedCursor, resolutionCache);
489
+ if (parent.status !== "found") {
490
+ return this.acceptedAncestryUnavailable(parent);
491
+ }
492
+ acceptedCursor = parent.parent;
493
+ }
494
+ while (!equals(candidateCursor.digest, acceptedCursor.digest)) {
495
+ if (candidateCursor.body.sequence === 0n ||
496
+ acceptedCursor.body.sequence === 0n) {
497
+ return {
498
+ status: "reject",
499
+ reason: "Policy branches do not share the descriptor genesis",
500
+ };
501
+ }
502
+ candidateChild = candidateCursor;
503
+ acceptedChild = acceptedCursor;
504
+ const [candidateParent, acceptedParent] = await Promise.all([
505
+ this.parentOf(candidateCursor, resolutionCache),
506
+ this.parentOf(acceptedCursor, resolutionCache),
507
+ ]);
508
+ if (acceptedParent.status !== "found") {
509
+ return this.acceptedAncestryUnavailable(acceptedParent);
510
+ }
511
+ if (candidateParent.status !== "found") {
512
+ return this.candidateAncestryResult(candidateParent);
513
+ }
514
+ candidateCursor = candidateParent.parent;
515
+ acceptedCursor = acceptedParent.parent;
516
+ }
517
+ if (candidateChild === undefined) {
518
+ return { status: "duplicate" };
519
+ }
520
+ if (acceptedChild === undefined) {
521
+ return { status: "accept" };
522
+ }
523
+ if (equals(candidateChild.digest, acceptedChild.digest)) {
524
+ return { status: "duplicate" };
525
+ }
526
+ return {
527
+ status: "fork",
528
+ commonParent: candidateCursor,
529
+ candidateChild,
530
+ acceptedChild,
531
+ };
532
+ }
533
+ setFork(evaluation) {
534
+ this.project(evaluation.commonParent);
535
+ const children = [
536
+ forkProofFromSnapshot(evaluation.candidateChild),
537
+ forkProofFromSnapshot(evaluation.acceptedChild),
538
+ ].sort(compareForkChildProofs);
539
+ this.fork = {
540
+ commonParent: projectionFromSnapshot(evaluation.commonParent),
541
+ children,
542
+ };
543
+ this.unavailable = undefined;
544
+ this.pending.clear();
545
+ }
546
+ retainCanonicalForkChild(snapshot) {
547
+ if (this.fork === undefined)
548
+ return;
549
+ const byDigest = new Map();
550
+ for (const proof of [
551
+ ...this.fork.children,
552
+ forkProofFromSnapshot(snapshot),
553
+ ]) {
554
+ const digestKey = bytesKey(proof.digest);
555
+ const retained = byDigest.get(digestKey);
556
+ if (retained === undefined ||
557
+ compare(proof.entryBytes, retained.entryBytes) < 0) {
558
+ byDigest.set(digestKey, copyForkChildProof(proof));
559
+ }
560
+ }
561
+ const canonical = [...byDigest.values()]
562
+ .sort(compareForkChildProofs)
563
+ .slice(0, 2);
564
+ if (canonical.length !== 2)
565
+ return;
566
+ this.fork.children = [canonical[0], canonical[1]];
567
+ }
568
+ observeAfterFork(snapshot) {
569
+ if (this.fork === undefined || this.acceptedHead === undefined)
570
+ return;
571
+ const commonParent = this.acceptedHead;
572
+ if (snapshot.body.sequence !== commonParent.body.sequence + 1n ||
573
+ !equals(snapshot.body.previousPolicyDigest, commonParent.digest)) {
574
+ return;
575
+ }
576
+ // This bounded kernel deliberately retains only the canonical two direct
577
+ // child proofs. Durable storage of every authenticated fork observation is
578
+ // an outer-layer responsibility for a later integration slice.
579
+ this.retainCanonicalForkChild(snapshot);
580
+ }
581
+ addPending(snapshot, missingParentDigest) {
582
+ const existing = this.pending.get(snapshot.digestKey);
583
+ if (existing !== undefined) {
584
+ existing.missingParentDigest = copyBytes(missingParentDigest);
585
+ if (snapshot.accountedBytes <= this.maxPendingPolicyBytes &&
586
+ compare(snapshot.entryBytes, existing.snapshot.entryBytes) < 0) {
587
+ existing.snapshot = copySnapshot(snapshot);
588
+ }
589
+ return { retained: true, evictedPolicyDigests: [] };
590
+ }
591
+ if (snapshot.accountedBytes > this.maxPendingPolicyBytes) {
592
+ return {
593
+ retained: false,
594
+ evictedPolicyDigests: [copyBytes(snapshot.digest)],
595
+ };
596
+ }
597
+ this.pending.set(snapshot.digestKey, {
598
+ snapshot: copySnapshot(snapshot),
599
+ missingParentDigest: copyBytes(missingParentDigest),
600
+ });
601
+ const ordered = [...this.pending.entries()].sort(([a], [b]) => compareKeys(a, b));
602
+ const retainedKeys = new Set(ordered.slice(0, this.maxPending).map(([key]) => key));
603
+ const evictedPolicyDigests = [];
604
+ for (const [key, pending] of ordered) {
605
+ if (retainedKeys.has(key))
606
+ continue;
607
+ evictedPolicyDigests.push(copyBytes(pending.snapshot.digest));
608
+ this.pending.delete(key);
609
+ }
610
+ return {
611
+ retained: retainedKeys.has(snapshot.digestKey),
612
+ evictedPolicyDigests,
613
+ };
614
+ }
615
+ enterUnavailable(snapshot, evaluation) {
616
+ const retention = this.addPending(snapshot, evaluation.digest);
617
+ this.unavailable = {
618
+ acceptedAncestorDigest: copyBytes(evaluation.digest),
619
+ candidateDigestKey: snapshot.digestKey,
620
+ reason: boundedUnavailableReason(evaluation.reason),
621
+ };
622
+ return retention;
623
+ }
624
+ async drainPending() {
625
+ if (this.lifecycleController.signal.aborted) {
626
+ return { status: "halted" };
627
+ }
628
+ let accepted = false;
629
+ let progress = true;
630
+ while (progress &&
631
+ this.pending.size > 0 &&
632
+ this.fork === undefined &&
633
+ this.unavailable === undefined) {
634
+ progress = false;
635
+ const ordered = [...this.pending.values()].sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey));
636
+ for (const pending of ordered) {
637
+ if (!this.pending.has(pending.snapshot.digestKey))
638
+ continue;
639
+ const evaluation = await this.evaluate(pending.snapshot);
640
+ if (this.lifecycleController.signal.aborted) {
641
+ return { status: "halted" };
642
+ }
643
+ if (evaluation.status === "missing") {
644
+ pending.missingParentDigest = copyBytes(evaluation.digest);
645
+ continue;
646
+ }
647
+ if (evaluation.status === "unavailable") {
648
+ const retention = this.enterUnavailable(pending.snapshot, evaluation);
649
+ return { status: "unavailable", ...retention };
650
+ }
651
+ this.pending.delete(pending.snapshot.digestKey);
652
+ progress = true;
653
+ if (evaluation.status === "accept") {
654
+ this.project(pending.snapshot);
655
+ accepted = true;
656
+ }
657
+ else if (evaluation.status === "fork") {
658
+ this.setFork(evaluation);
659
+ return { status: "forked" };
660
+ }
661
+ }
662
+ }
663
+ return accepted ? { status: "accepted" } : undefined;
664
+ }
665
+ enqueueAdmission(operation) {
666
+ const result = this.admissionTail.then(async () => {
667
+ if (this.lifecycleController.signal.aborted) {
668
+ return this.haltedResult();
669
+ }
670
+ const admission = await operation();
671
+ return this.lifecycleController.signal.aborted
672
+ ? this.haltedResult()
673
+ : admission;
674
+ });
675
+ this.admissionTail = result.then(() => { }, (_reason) => { });
676
+ return result;
677
+ }
678
+ ingest(entryBytes) {
679
+ if (this.lifecycleController.signal.aborted) {
680
+ return Promise.resolve(this.haltedResult());
681
+ }
682
+ let capturedEntryBytes;
683
+ try {
684
+ // Capture at the API boundary, before this admission waits behind earlier
685
+ // work. Otherwise a caller could mutate a queued entry before validation.
686
+ capturedEntryBytes = capturePolicySnapshotEntryBytesV2(entryBytes);
687
+ }
688
+ catch (error) {
689
+ const reason = validationMessage(error);
690
+ return this.enqueueAdmission(async () => this.result("rejected", reason));
691
+ }
692
+ return this.enqueueAdmission(() => this.ingestOne(capturedEntryBytes));
693
+ }
694
+ retryUnavailable() {
695
+ return this.enqueueAdmission(() => this.retryUnavailableOne());
696
+ }
697
+ async ingestOne(entryBytes) {
698
+ let snapshot;
699
+ try {
700
+ snapshot = await authenticateCapturedPolicySnapshotEntryV2(entryBytes, this.descriptor);
701
+ }
702
+ catch (error) {
703
+ return this.result("rejected", validationMessage(error));
704
+ }
705
+ if (this.lifecycleController.signal.aborted)
706
+ return this.haltedResult();
707
+ if (this.fork !== undefined) {
708
+ this.observeAfterFork(snapshot);
709
+ return this.result("halted", "Policy reducer is halted by authority equivocation");
710
+ }
711
+ this.retainCanonicalHeadEntry(snapshot);
712
+ if (this.unavailable !== undefined) {
713
+ if (this.acceptedHead?.digestKey === snapshot.digestKey) {
714
+ return this.result("unavailable", this.unavailable.reason);
715
+ }
716
+ const existingPending = this.pending.get(snapshot.digestKey);
717
+ if (existingPending !== undefined) {
718
+ this.addPending(snapshot, existingPending.missingParentDigest);
719
+ return this.result("unavailable", this.unavailable.reason);
720
+ }
721
+ const retention = this.addPending(snapshot, this.unavailable.acceptedAncestorDigest);
722
+ return this.unavailableResult(retention);
723
+ }
724
+ const existingPending = this.pending.get(snapshot.digestKey);
725
+ if (existingPending !== undefined) {
726
+ this.addPending(snapshot, existingPending.missingParentDigest);
727
+ return this.result("pending", "Policy snapshot is already pending");
728
+ }
729
+ const evaluation = await this.evaluate(snapshot);
730
+ if (this.lifecycleController.signal.aborted)
731
+ return this.haltedResult();
732
+ if (evaluation.status === "reject") {
733
+ return this.result("rejected", evaluation.reason);
734
+ }
735
+ if (evaluation.status === "duplicate") {
736
+ return this.result("duplicate");
737
+ }
738
+ if (evaluation.status === "fork") {
739
+ this.setFork(evaluation);
740
+ return this.forkedResult();
741
+ }
742
+ if (evaluation.status === "unavailable") {
743
+ const retention = this.enterUnavailable(snapshot, evaluation);
744
+ return this.unavailableResult(retention);
745
+ }
746
+ if (evaluation.status === "missing") {
747
+ const pending = this.addPending(snapshot, evaluation.digest);
748
+ return this.result(pending.retained ? "pending" : "capacity", pending.retained
749
+ ? (evaluation.reason ?? "Policy parent is missing")
750
+ : "Policy pending capacity did not retain this candidate", pending.evictedPolicyDigests);
751
+ }
752
+ this.project(snapshot);
753
+ return (this.completedDrainResult(await this.drainPending()) ??
754
+ this.result("accepted"));
755
+ }
756
+ async retryUnavailableOne() {
757
+ if (this.lifecycleController.signal.aborted)
758
+ return this.haltedResult();
759
+ if (this.fork !== undefined) {
760
+ return this.result("halted", "Policy reducer is halted by authority equivocation");
761
+ }
762
+ const unavailable = this.unavailable;
763
+ if (unavailable === undefined) {
764
+ return this.result("duplicate", "Policy reducer is not unavailable");
765
+ }
766
+ const pending = this.pending.get(unavailable.candidateDigestKey);
767
+ if (pending === undefined) {
768
+ return this.result("capacity", "Unavailable comparison candidate is not retained; re-ingest it before retrying");
769
+ }
770
+ const evaluation = await this.evaluate(pending.snapshot);
771
+ if (this.lifecycleController.signal.aborted)
772
+ return this.haltedResult();
773
+ if (evaluation.status === "unavailable") {
774
+ pending.missingParentDigest = copyBytes(evaluation.digest);
775
+ this.unavailable = {
776
+ acceptedAncestorDigest: copyBytes(evaluation.digest),
777
+ candidateDigestKey: pending.snapshot.digestKey,
778
+ reason: boundedUnavailableReason(evaluation.reason),
779
+ };
780
+ return this.result("unavailable", this.unavailable.reason);
781
+ }
782
+ this.unavailable = undefined;
783
+ let status;
784
+ let reason;
785
+ if (evaluation.status === "missing") {
786
+ pending.missingParentDigest = copyBytes(evaluation.digest);
787
+ status = "pending";
788
+ reason = evaluation.reason ?? "Policy parent is missing";
789
+ }
790
+ else {
791
+ this.pending.delete(pending.snapshot.digestKey);
792
+ if (evaluation.status === "fork") {
793
+ this.setFork(evaluation);
794
+ return this.forkedResult();
795
+ }
796
+ if (evaluation.status === "accept") {
797
+ this.project(pending.snapshot);
798
+ status = "accepted";
799
+ }
800
+ else if (evaluation.status === "duplicate") {
801
+ status = "duplicate";
802
+ }
803
+ else {
804
+ status = "rejected";
805
+ reason = evaluation.reason;
806
+ }
807
+ }
808
+ return (this.completedDrainResult(await this.drainPending()) ??
809
+ this.result(status, reason));
810
+ }
811
+ }
812
+ //# sourceMappingURL=v2-policy-engine.js.map