@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,1164 @@
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 {
6
+ NetworkDescriptorV2,
7
+ PolicySnapshotBodyV2,
8
+ PolicySubjectBindingV2,
9
+ TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS,
10
+ TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES,
11
+ assertNetworkDescriptorV2,
12
+ decodePolicySnapshotBodyV2,
13
+ digestPolicySnapshotBodyV2,
14
+ } from "./v2.js";
15
+
16
+ /**
17
+ * Internal policy reducer for the non-activatable TrustedNetwork v2 scaffold.
18
+ *
19
+ * The reducer intentionally is not exported from the package entry point. It
20
+ * retains one accepted snapshot, a bounded pending working set, and (after
21
+ * equivocation) two child proofs. Historical snapshots are supplied by the
22
+ * resolver instead of being accumulated in memory.
23
+ */
24
+
25
+ const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
26
+ const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
27
+ const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 =
28
+ TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES * 2 +
29
+ PENDING_POLICY_ACCOUNTING_OVERHEAD_V2;
30
+ const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 =
31
+ MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2;
32
+ const DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2 = 10 * 1000;
33
+ const MAX_TIMER_DELAY_MS_V2 = 0x7fffffff;
34
+ const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
35
+
36
+ const copyBytes = (bytes: Uint8Array): Uint8Array => Uint8Array.from(bytes);
37
+
38
+ const bytesKey = (bytes: Uint8Array): string => {
39
+ let key = "";
40
+ for (const byte of bytes) key += byte.toString(16).padStart(2, "0");
41
+ return key;
42
+ };
43
+
44
+ const compareKeys = (left: string, right: string): number =>
45
+ left < right ? -1 : left > right ? 1 : 0;
46
+
47
+ const copyPublicKey = (key: PublicSignKey): PublicSignKey =>
48
+ deserialize(serialize(key), PublicSignKey);
49
+
50
+ const publicKeyId = (key: PublicSignKey): string => bytesKey(serialize(key));
51
+
52
+ type ValidatedPolicySnapshotV2 = {
53
+ body: PolicySnapshotBodyV2;
54
+ digest: Uint8Array;
55
+ digestKey: string;
56
+ entryBytes: Uint8Array;
57
+ accountedBytes: number;
58
+ };
59
+
60
+ const copyBinding = (binding: PolicySubjectBindingV2): PolicySubjectBindingV2 =>
61
+ new PolicySubjectBindingV2({
62
+ signingKey: copyPublicKey(binding.signingKey),
63
+ roles: binding.roles,
64
+ });
65
+
66
+ const copyBody = (body: PolicySnapshotBodyV2): PolicySnapshotBodyV2 =>
67
+ new PolicySnapshotBodyV2({
68
+ networkId: copyBytes(body.networkId),
69
+ sequence: body.sequence,
70
+ previousPolicyDigest: copyBytes(body.previousPolicyDigest),
71
+ bindings: body.bindings.map(copyBinding),
72
+ });
73
+
74
+ const copySnapshot = (
75
+ snapshot: ValidatedPolicySnapshotV2,
76
+ ): ValidatedPolicySnapshotV2 => ({
77
+ body: copyBody(snapshot.body),
78
+ digest: copyBytes(snapshot.digest),
79
+ digestKey: snapshot.digestKey,
80
+ entryBytes: copyBytes(snapshot.entryBytes),
81
+ accountedBytes: snapshot.accountedBytes,
82
+ });
83
+
84
+ export type PolicySnapshotResolverV2 = (
85
+ digest: Uint8Array,
86
+ options: { signal: AbortSignal },
87
+ ) => Uint8Array | undefined | Promise<Uint8Array | undefined>;
88
+
89
+ export type PolicyParentFetchHintV2 = {
90
+ kind: "policy-parent";
91
+ digest: Uint8Array;
92
+ };
93
+
94
+ export type PolicyHeadProjectionV2 = {
95
+ sequence: bigint;
96
+ digest: Uint8Array;
97
+ bindings: PolicySubjectBindingV2[];
98
+ };
99
+
100
+ export type PolicyForkChildProofV2 = {
101
+ sequence: bigint;
102
+ digest: Uint8Array;
103
+ entryBytes: Uint8Array;
104
+ };
105
+
106
+ export type PolicyForkEvidenceV2 = {
107
+ commonParent: PolicyHeadProjectionV2;
108
+ children: [PolicyForkChildProofV2, PolicyForkChildProofV2];
109
+ };
110
+
111
+ export type PolicyAdmissionStatusV2 =
112
+ | "accepted"
113
+ | "duplicate"
114
+ | "pending"
115
+ | "unavailable"
116
+ | "capacity"
117
+ | "rejected"
118
+ | "forked"
119
+ | "halted";
120
+
121
+ export type PolicyAdmissionResultV2 = {
122
+ status: PolicyAdmissionStatusV2;
123
+ reason?: string;
124
+ head?: PolicyHeadProjectionV2;
125
+ fetchHints: PolicyParentFetchHintV2[];
126
+ pendingCount: number;
127
+ pendingBytes: number;
128
+ evictedPolicyDigests?: Uint8Array[];
129
+ };
130
+
131
+ type PendingPolicySnapshotV2 = {
132
+ snapshot: ValidatedPolicySnapshotV2;
133
+ missingParentDigest: Uint8Array;
134
+ };
135
+
136
+ type UnavailablePolicyComparisonV2 = {
137
+ acceptedAncestorDigest: Uint8Array;
138
+ candidateDigestKey: string;
139
+ reason: string;
140
+ };
141
+
142
+ type ParentResolutionV2 =
143
+ | { status: "found"; parent: ValidatedPolicySnapshotV2 }
144
+ | { status: "missing"; digest: Uint8Array }
145
+ | { status: "unavailable"; digest: Uint8Array; reason: string }
146
+ | { status: "reject"; digest: Uint8Array; reason: string };
147
+
148
+ type SnapshotResolutionCacheV2 = Map<
149
+ string,
150
+ Promise<ValidatedPolicySnapshotV2 | undefined>
151
+ >;
152
+
153
+ type EvaluationV2 =
154
+ | { status: "accept" }
155
+ | { status: "duplicate" }
156
+ | { status: "missing"; digest: Uint8Array; reason?: string }
157
+ | { status: "reject"; reason: string }
158
+ | { status: "unavailable"; digest: Uint8Array; reason: string }
159
+ | {
160
+ status: "fork";
161
+ commonParent: ValidatedPolicySnapshotV2;
162
+ candidateChild: ValidatedPolicySnapshotV2;
163
+ acceptedChild: ValidatedPolicySnapshotV2;
164
+ };
165
+
166
+ type PendingDrainOutcomeV2 =
167
+ | { status: "accepted" }
168
+ | { status: "forked" }
169
+ | { status: "halted" }
170
+ | {
171
+ status: "unavailable";
172
+ retained: boolean;
173
+ evictedPolicyDigests: Uint8Array[];
174
+ };
175
+
176
+ const validationMessage = (error: unknown): string =>
177
+ error instanceof Error ? error.message : String(error);
178
+
179
+ const boundedUnavailableReason = (reason: string): string =>
180
+ reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
181
+
182
+ class PolicyDependencyUnavailableErrorV2 extends Error {
183
+ constructor(message: string) {
184
+ super(message);
185
+ this.name = "PolicyDependencyUnavailableErrorV2";
186
+ }
187
+ }
188
+
189
+ const capturePolicySnapshotEntryBytesV2 = (
190
+ entryBytes: Uint8Array,
191
+ ): Uint8Array => {
192
+ if (!(entryBytes instanceof Uint8Array)) {
193
+ throw new Error("Policy snapshot must use canonical EntryV0 bytes");
194
+ }
195
+ if (entryBytes.byteLength === 0) {
196
+ throw new Error("Policy snapshot must use EntryV0");
197
+ }
198
+ if (entryBytes.byteLength > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES) {
199
+ throw new Error(
200
+ `Policy snapshot entry must contain 1-${TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES} bytes`,
201
+ );
202
+ }
203
+
204
+ // Apply the protocol byte ceiling before this first copy, decode, or crypto
205
+ // operation. The retained copy also prevents caller mutation during awaits.
206
+ return copyBytes(entryBytes);
207
+ };
208
+
209
+ const authenticateCapturedPolicySnapshotEntryV2 = async (
210
+ canonicalEntryBytes: Uint8Array,
211
+ descriptor: NetworkDescriptorV2,
212
+ ): Promise<ValidatedPolicySnapshotV2> => {
213
+ const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
214
+ if (!(authenticatedEntry instanceof EntryV0)) {
215
+ throw new Error("Policy snapshot must use EntryV0");
216
+ }
217
+ if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
218
+ throw new Error("Policy snapshot entry encoding is not canonical");
219
+ }
220
+ if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
221
+ throw new Error("Policy snapshot metadata must be public");
222
+ }
223
+ if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
224
+ throw new Error("Policy snapshot payload must be public");
225
+ }
226
+ if (
227
+ authenticatedEntry._signatures === undefined ||
228
+ authenticatedEntry._signatures.signatures.length !== 1
229
+ ) {
230
+ throw new Error("Policy snapshot must contain exactly one signature");
231
+ }
232
+ if (
233
+ !(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)
234
+ ) {
235
+ throw new Error("Policy snapshot signature must be public");
236
+ }
237
+ authenticatedEntry.init({ encoding: NO_ENCODING });
238
+
239
+ const signatures = await authenticatedEntry.getSignatures();
240
+ if (signatures.length !== 1) {
241
+ throw new Error("Policy snapshot must resolve exactly one signature");
242
+ }
243
+ const signature = signatures[0]!;
244
+ if (
245
+ !equals(
246
+ serialize(signature.publicKey),
247
+ serialize(descriptor.policyAuthority),
248
+ )
249
+ ) {
250
+ throw new Error("Policy snapshot signer is not the policy authority");
251
+ }
252
+ if (!(await verify(signature, authenticatedEntry.getSignableBytes()))) {
253
+ throw new Error("Policy snapshot authority signature is invalid");
254
+ }
255
+
256
+ const payload = await authenticatedEntry.getPayloadValue();
257
+ if (!(payload instanceof Uint8Array)) {
258
+ throw new Error(
259
+ "Policy snapshot payload must contain canonical body bytes",
260
+ );
261
+ }
262
+ const body = decodePolicySnapshotBodyV2(copyBytes(payload), descriptor);
263
+ const digest = digestPolicySnapshotBodyV2(body);
264
+ return {
265
+ body: copyBody(body),
266
+ digest: copyBytes(digest),
267
+ digestKey: bytesKey(digest),
268
+ entryBytes: canonicalEntryBytes,
269
+ accountedBytes:
270
+ canonicalEntryBytes.byteLength +
271
+ serialize(body).byteLength +
272
+ PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
273
+ };
274
+ };
275
+
276
+ export const authenticatePolicySnapshotEntryV2 = async (
277
+ entryBytes: Uint8Array,
278
+ descriptor: NetworkDescriptorV2,
279
+ ): Promise<ValidatedPolicySnapshotV2> => {
280
+ assertNetworkDescriptorV2(descriptor);
281
+ return authenticateCapturedPolicySnapshotEntryV2(
282
+ capturePolicySnapshotEntryBytesV2(entryBytes),
283
+ descriptor,
284
+ );
285
+ };
286
+
287
+ const projectionFromSnapshot = (
288
+ snapshot: ValidatedPolicySnapshotV2,
289
+ ): PolicyHeadProjectionV2 => ({
290
+ sequence: snapshot.body.sequence,
291
+ digest: copyBytes(snapshot.digest),
292
+ bindings: snapshot.body.bindings.map(copyBinding),
293
+ });
294
+
295
+ const forkProofFromSnapshot = (
296
+ snapshot: ValidatedPolicySnapshotV2,
297
+ ): PolicyForkChildProofV2 => ({
298
+ sequence: snapshot.body.sequence,
299
+ digest: copyBytes(snapshot.digest),
300
+ entryBytes: copyBytes(snapshot.entryBytes),
301
+ });
302
+
303
+ const copyForkChildProof = (
304
+ proof: PolicyForkChildProofV2,
305
+ ): PolicyForkChildProofV2 => ({
306
+ sequence: proof.sequence,
307
+ digest: copyBytes(proof.digest),
308
+ entryBytes: copyBytes(proof.entryBytes),
309
+ });
310
+
311
+ const compareForkChildProofs = (
312
+ left: PolicyForkChildProofV2,
313
+ right: PolicyForkChildProofV2,
314
+ ): number => {
315
+ const digestOrder = compare(left.digest, right.digest);
316
+ return digestOrder === 0
317
+ ? compare(left.entryBytes, right.entryBytes)
318
+ : digestOrder;
319
+ };
320
+
321
+ const copyProjection = (
322
+ projection: PolicyHeadProjectionV2,
323
+ ): PolicyHeadProjectionV2 => ({
324
+ sequence: projection.sequence,
325
+ digest: copyBytes(projection.digest),
326
+ bindings: projection.bindings.map(copyBinding),
327
+ });
328
+
329
+ const copyForkEvidence = (
330
+ evidence: PolicyForkEvidenceV2,
331
+ ): PolicyForkEvidenceV2 => ({
332
+ commonParent: copyProjection(evidence.commonParent),
333
+ children: [
334
+ copyForkChildProof(evidence.children[0]),
335
+ copyForkChildProof(evidence.children[1]),
336
+ ],
337
+ });
338
+
339
+ export class TrustedNetworkV2PolicyReducer {
340
+ private readonly descriptor: NetworkDescriptorV2;
341
+ private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
342
+ private readonly resolveTimeoutMs: number;
343
+ private readonly maxPending: number;
344
+ private readonly maxPendingPolicyBytes: number;
345
+ private readonly lifecycleController = new AbortController();
346
+ private externalSignal?: AbortSignal;
347
+ private externalAbortListener?: () => void;
348
+ private acceptedHead?: ValidatedPolicySnapshotV2;
349
+ private projectedRoles = new Map<string, number>();
350
+ private readonly pending = new Map<string, PendingPolicySnapshotV2>();
351
+ // Recovery is bound to this exact candidate. Unrelated admissions may enter
352
+ // the bounded pending set but can never restore ACTIVE authorization.
353
+ private unavailable?: UnavailablePolicyComparisonV2;
354
+ private fork?: PolicyForkEvidenceV2;
355
+ private admissionTail: Promise<void> = Promise.resolve();
356
+
357
+ constructor(properties: {
358
+ descriptor: NetworkDescriptorV2;
359
+ resolvePolicyEntry: PolicySnapshotResolverV2;
360
+ resolveTimeoutMs?: number;
361
+ signal?: AbortSignal;
362
+ maxPending?: number;
363
+ maxPendingPolicyBytes?: number;
364
+ }) {
365
+ assertNetworkDescriptorV2(properties.descriptor);
366
+ const maxPending = properties.maxPending ?? DEFAULT_MAX_PENDING_POLICIES_V2;
367
+ if (!Number.isSafeInteger(maxPending) || maxPending < 1) {
368
+ throw new Error("maxPending must be a positive safe integer");
369
+ }
370
+ const maxPendingPolicyBytes =
371
+ properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
372
+ if (
373
+ !Number.isSafeInteger(maxPendingPolicyBytes) ||
374
+ maxPendingPolicyBytes < 1 ||
375
+ maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2
376
+ ) {
377
+ throw new Error(
378
+ `maxPendingPolicyBytes must be a positive safe integer no greater than ${MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2}`,
379
+ );
380
+ }
381
+ const resolveTimeoutMs =
382
+ properties.resolveTimeoutMs ?? DEFAULT_POLICY_RESOLUTION_TIMEOUT_MS_V2;
383
+ if (
384
+ !Number.isSafeInteger(resolveTimeoutMs) ||
385
+ resolveTimeoutMs < 1 ||
386
+ resolveTimeoutMs > MAX_TIMER_DELAY_MS_V2
387
+ ) {
388
+ throw new Error(
389
+ `resolveTimeoutMs must be a positive safe integer no greater than ${MAX_TIMER_DELAY_MS_V2}`,
390
+ );
391
+ }
392
+ this.descriptor = deserialize(
393
+ serialize(properties.descriptor),
394
+ NetworkDescriptorV2,
395
+ );
396
+ this.resolvePolicyEntry = properties.resolvePolicyEntry;
397
+ this.resolveTimeoutMs = resolveTimeoutMs;
398
+ this.maxPending = maxPending;
399
+ this.maxPendingPolicyBytes = maxPendingPolicyBytes;
400
+
401
+ if (properties.signal?.aborted) {
402
+ this.lifecycleController.abort();
403
+ } else if (properties.signal !== undefined) {
404
+ this.externalSignal = properties.signal;
405
+ this.externalAbortListener = (): void => {
406
+ this.externalSignal = undefined;
407
+ this.externalAbortListener = undefined;
408
+ this.lifecycleController.abort();
409
+ };
410
+ this.externalSignal.addEventListener(
411
+ "abort",
412
+ this.externalAbortListener,
413
+ { once: true },
414
+ );
415
+ }
416
+ }
417
+
418
+ get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
419
+ if (this.lifecycleController.signal.aborted) return "HALTED";
420
+ if (this.fork !== undefined) return "FORKED";
421
+ if (this.unavailable !== undefined) return "UNAVAILABLE";
422
+ return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
423
+ }
424
+
425
+ get head(): PolicyHeadProjectionV2 | undefined {
426
+ return this.acceptedHead === undefined
427
+ ? undefined
428
+ : projectionFromSnapshot(this.acceptedHead);
429
+ }
430
+
431
+ get forkEvidence(): PolicyForkEvidenceV2 | undefined {
432
+ return this.fork === undefined ? undefined : copyForkEvidence(this.fork);
433
+ }
434
+
435
+ get pendingCount(): number {
436
+ return this.pending.size;
437
+ }
438
+
439
+ get pendingBytes(): number {
440
+ let total = 0;
441
+ for (const { snapshot } of this.pending.values()) {
442
+ total += snapshot.accountedBytes;
443
+ }
444
+ return total;
445
+ }
446
+
447
+ get pendingDigests(): Uint8Array[] {
448
+ return [...this.pending.values()]
449
+ .sort((a, b) => compareKeys(a.snapshot.digestKey, b.snapshot.digestKey))
450
+ .map(({ snapshot }) => copyBytes(snapshot.digest));
451
+ }
452
+
453
+ rolesFor(subject: PublicSignKey): number {
454
+ return this.projectedRoles.get(publicKeyId(subject)) ?? 0;
455
+ }
456
+
457
+ isAuthorized(subject: PublicSignKey, roles: number): boolean {
458
+ if (
459
+ this.state !== "ACTIVE" ||
460
+ !Number.isInteger(roles) ||
461
+ roles === 0 ||
462
+ (roles & ~TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS) !== 0
463
+ ) {
464
+ return false;
465
+ }
466
+ return (this.rolesFor(subject) & roles) === roles;
467
+ }
468
+
469
+ abort(): void {
470
+ if (
471
+ this.externalSignal !== undefined &&
472
+ this.externalAbortListener !== undefined
473
+ ) {
474
+ this.externalSignal.removeEventListener(
475
+ "abort",
476
+ this.externalAbortListener,
477
+ );
478
+ }
479
+ this.externalSignal = undefined;
480
+ this.externalAbortListener = undefined;
481
+ this.lifecycleController.abort();
482
+ }
483
+
484
+ private fetchHints(): PolicyParentFetchHintV2[] {
485
+ const unique = new Map<string, Uint8Array>();
486
+ for (const { missingParentDigest } of this.pending.values()) {
487
+ unique.set(bytesKey(missingParentDigest), missingParentDigest);
488
+ }
489
+ if (this.unavailable !== undefined) {
490
+ unique.set(
491
+ bytesKey(this.unavailable.acceptedAncestorDigest),
492
+ this.unavailable.acceptedAncestorDigest,
493
+ );
494
+ }
495
+ return [...unique.entries()]
496
+ .sort(([a], [b]) => compareKeys(a, b))
497
+ .map(([, digest]) => ({
498
+ kind: "policy-parent" as const,
499
+ digest: copyBytes(digest),
500
+ }));
501
+ }
502
+
503
+ private result(
504
+ status: PolicyAdmissionStatusV2,
505
+ reason?: string,
506
+ evictedPolicyDigests?: Uint8Array[],
507
+ ): PolicyAdmissionResultV2 {
508
+ return {
509
+ status,
510
+ reason,
511
+ head: this.head,
512
+ fetchHints: this.fetchHints(),
513
+ pendingCount: this.pending.size,
514
+ pendingBytes: this.pendingBytes,
515
+ evictedPolicyDigests:
516
+ evictedPolicyDigests === undefined
517
+ ? undefined
518
+ : evictedPolicyDigests.map(copyBytes),
519
+ };
520
+ }
521
+
522
+ private forkedResult(): PolicyAdmissionResultV2 {
523
+ return this.result("forked", "Policy authority signed competing children");
524
+ }
525
+
526
+ private haltedResult(): PolicyAdmissionResultV2 {
527
+ return this.result("halted", "Policy reducer lifecycle is aborted");
528
+ }
529
+
530
+ private unavailableResult(retention?: {
531
+ retained: boolean;
532
+ evictedPolicyDigests: Uint8Array[];
533
+ }): PolicyAdmissionResultV2 {
534
+ const blockedCandidateRetained =
535
+ this.unavailable !== undefined &&
536
+ this.pending.has(this.unavailable.candidateDigestKey);
537
+ const recoverable =
538
+ (retention?.retained ?? true) && blockedCandidateRetained;
539
+ const reason = recoverable
540
+ ? this.unavailable!.reason
541
+ : blockedCandidateRetained
542
+ ? "Policy pending capacity did not retain this candidate"
543
+ : "Policy reducer remains unavailable because pending capacity did not retain the blocked candidate";
544
+ return this.result(
545
+ recoverable ? "unavailable" : "capacity",
546
+ reason,
547
+ retention?.evictedPolicyDigests,
548
+ );
549
+ }
550
+
551
+ private completedDrainResult(
552
+ outcome: PendingDrainOutcomeV2 | undefined,
553
+ ): PolicyAdmissionResultV2 | undefined {
554
+ if (outcome?.status === "forked") return this.forkedResult();
555
+ if (outcome?.status === "halted") return this.haltedResult();
556
+ return outcome?.status === "unavailable"
557
+ ? this.unavailableResult(outcome)
558
+ : undefined;
559
+ }
560
+
561
+ private project(snapshot: ValidatedPolicySnapshotV2): void {
562
+ this.acceptedHead = copySnapshot(snapshot);
563
+ this.projectedRoles = new Map(
564
+ snapshot.body.bindings.map((binding) => [
565
+ publicKeyId(binding.signingKey),
566
+ binding.roles,
567
+ ]),
568
+ );
569
+ }
570
+
571
+ private retainCanonicalHeadEntry(snapshot: ValidatedPolicySnapshotV2): void {
572
+ if (
573
+ this.acceptedHead?.digestKey === snapshot.digestKey &&
574
+ compare(snapshot.entryBytes, this.acceptedHead.entryBytes) < 0
575
+ ) {
576
+ this.acceptedHead = copySnapshot(snapshot);
577
+ }
578
+ }
579
+
580
+ private async resolveExternalSnapshot(
581
+ digest: Uint8Array,
582
+ ): Promise<ValidatedPolicySnapshotV2 | undefined> {
583
+ if (this.lifecycleController.signal.aborted) {
584
+ throw new PolicyDependencyUnavailableErrorV2(
585
+ "Policy resolver lifecycle is aborted",
586
+ );
587
+ }
588
+
589
+ const attemptController = new AbortController();
590
+ let timedOut = false;
591
+ const abortFromLifecycle = (): void => attemptController.abort();
592
+ this.lifecycleController.signal.addEventListener(
593
+ "abort",
594
+ abortFromLifecycle,
595
+ { once: true },
596
+ );
597
+ const timeout = setTimeout(() => {
598
+ timedOut = true;
599
+ attemptController.abort();
600
+ }, this.resolveTimeoutMs);
601
+ const abortError = (): PolicyDependencyUnavailableErrorV2 =>
602
+ new PolicyDependencyUnavailableErrorV2(
603
+ timedOut
604
+ ? `Policy resolver timed out after ${this.resolveTimeoutMs} ms`
605
+ : "Policy resolver attempt was aborted",
606
+ );
607
+
608
+ let rejectOnAbort: (() => void) | undefined;
609
+ const abortPromise = new Promise<never>((_resolve, reject) => {
610
+ rejectOnAbort = (): void => {
611
+ reject(abortError());
612
+ };
613
+ attemptController.signal.addEventListener("abort", rejectOnAbort, {
614
+ once: true,
615
+ });
616
+ });
617
+
618
+ const resolution = Promise.resolve().then(async () => {
619
+ if (attemptController.signal.aborted) throw abortError();
620
+ const entryBytes = await this.resolvePolicyEntry(copyBytes(digest), {
621
+ signal: attemptController.signal,
622
+ });
623
+ // A resolver may ignore cancellation. Do not spend decode or signature
624
+ // verification work on bytes that arrive after this attempt expired.
625
+ if (attemptController.signal.aborted) throw abortError();
626
+ if (entryBytes === undefined) return undefined;
627
+ const snapshot = await authenticatePolicySnapshotEntryV2(
628
+ entryBytes,
629
+ this.descriptor,
630
+ );
631
+ if (!equals(snapshot.digest, digest)) {
632
+ throw new Error("Policy resolver returned the wrong body digest");
633
+ }
634
+ return snapshot;
635
+ });
636
+ // Promise.race installs handlers, but this explicit observer documents and
637
+ // preserves consumption if the resolver settles after its deadline.
638
+ void resolution.then(
639
+ (): void => undefined,
640
+ (): void => undefined,
641
+ );
642
+
643
+ try {
644
+ return await Promise.race([resolution, abortPromise]);
645
+ } catch (error) {
646
+ if (error instanceof PolicyDependencyUnavailableErrorV2) throw error;
647
+ throw new PolicyDependencyUnavailableErrorV2(
648
+ `Policy resolver dependency is unavailable: ${validationMessage(error)}`,
649
+ );
650
+ } finally {
651
+ clearTimeout(timeout);
652
+ this.lifecycleController.signal.removeEventListener(
653
+ "abort",
654
+ abortFromLifecycle,
655
+ );
656
+ if (rejectOnAbort !== undefined) {
657
+ attemptController.signal.removeEventListener("abort", rejectOnAbort);
658
+ }
659
+ }
660
+ }
661
+
662
+ private async resolveSnapshot(
663
+ digest: Uint8Array,
664
+ cache?: SnapshotResolutionCacheV2,
665
+ ): Promise<ValidatedPolicySnapshotV2 | undefined> {
666
+ const digestKey = bytesKey(digest);
667
+ if (this.acceptedHead?.digestKey === digestKey) {
668
+ return copySnapshot(this.acceptedHead);
669
+ }
670
+ const pending = this.pending.get(digestKey);
671
+ if (pending !== undefined) return copySnapshot(pending.snapshot);
672
+ let resolution = cache?.get(digestKey);
673
+ if (resolution === undefined) {
674
+ resolution = this.resolveExternalSnapshot(digest);
675
+ cache?.set(digestKey, resolution);
676
+ }
677
+ const snapshot = await resolution;
678
+ return snapshot === undefined ? undefined : copySnapshot(snapshot);
679
+ }
680
+
681
+ private async parentOf(
682
+ child: ValidatedPolicySnapshotV2,
683
+ cache?: SnapshotResolutionCacheV2,
684
+ ): Promise<ParentResolutionV2> {
685
+ if (child.body.sequence === 0n) {
686
+ return {
687
+ status: "reject",
688
+ digest: copyBytes(child.body.previousPolicyDigest),
689
+ reason: "Genesis policy has no parent",
690
+ };
691
+ }
692
+ let parent: ValidatedPolicySnapshotV2 | undefined;
693
+ try {
694
+ parent = await this.resolveSnapshot(
695
+ child.body.previousPolicyDigest,
696
+ cache,
697
+ );
698
+ } catch (error) {
699
+ return {
700
+ status: "unavailable",
701
+ digest: copyBytes(child.body.previousPolicyDigest),
702
+ reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
703
+ };
704
+ }
705
+ if (parent === undefined) {
706
+ return {
707
+ status: "missing",
708
+ digest: copyBytes(child.body.previousPolicyDigest),
709
+ };
710
+ }
711
+ if (child.body.sequence !== parent.body.sequence + 1n) {
712
+ return {
713
+ status: "reject",
714
+ digest: copyBytes(child.body.previousPolicyDigest),
715
+ reason: "Policy sequence is not contiguous with its parent",
716
+ };
717
+ }
718
+ return { status: "found", parent };
719
+ }
720
+
721
+ private candidateAncestryResult(
722
+ resolution: Exclude<ParentResolutionV2, { status: "found" }>,
723
+ ): EvaluationV2 {
724
+ return resolution.status === "unavailable"
725
+ ? {
726
+ status: "missing",
727
+ digest: copyBytes(resolution.digest),
728
+ reason: resolution.reason,
729
+ }
730
+ : resolution;
731
+ }
732
+
733
+ private acceptedAncestryUnavailable(
734
+ resolution: Exclude<ParentResolutionV2, { status: "found" }>,
735
+ ): Extract<EvaluationV2, { status: "unavailable" }> {
736
+ return {
737
+ status: "unavailable",
738
+ digest: copyBytes(resolution.digest),
739
+ reason:
740
+ resolution.status === "missing"
741
+ ? "Accepted policy ancestry is unavailable from the resolver"
742
+ : resolution.status === "unavailable"
743
+ ? `Accepted policy ancestry is unavailable: ${resolution.reason}`
744
+ : `Accepted policy ancestry validation failed: ${resolution.reason}`,
745
+ };
746
+ }
747
+
748
+ private async evaluate(
749
+ candidate: ValidatedPolicySnapshotV2,
750
+ ): Promise<EvaluationV2> {
751
+ const resolutionCache: SnapshotResolutionCacheV2 = new Map();
752
+ if (this.acceptedHead === undefined) {
753
+ let cursor = candidate;
754
+ while (cursor.body.sequence !== 0n) {
755
+ const parent = await this.parentOf(cursor, resolutionCache);
756
+ if (parent.status !== "found") {
757
+ return this.candidateAncestryResult(parent);
758
+ }
759
+ cursor = parent.parent;
760
+ }
761
+ return { status: "accept" };
762
+ }
763
+
764
+ let candidateCursor = candidate;
765
+ let acceptedCursor = this.acceptedHead;
766
+ let candidateChild: ValidatedPolicySnapshotV2 | undefined;
767
+ let acceptedChild: ValidatedPolicySnapshotV2 | undefined;
768
+
769
+ while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
770
+ candidateChild = candidateCursor;
771
+ const parent = await this.parentOf(candidateCursor, resolutionCache);
772
+ if (parent.status !== "found") {
773
+ return this.candidateAncestryResult(parent);
774
+ }
775
+ candidateCursor = parent.parent;
776
+ }
777
+ while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
778
+ acceptedChild = acceptedCursor;
779
+ const parent = await this.parentOf(acceptedCursor, resolutionCache);
780
+ if (parent.status !== "found") {
781
+ return this.acceptedAncestryUnavailable(parent);
782
+ }
783
+ acceptedCursor = parent.parent;
784
+ }
785
+
786
+ while (!equals(candidateCursor.digest, acceptedCursor.digest)) {
787
+ if (
788
+ candidateCursor.body.sequence === 0n ||
789
+ acceptedCursor.body.sequence === 0n
790
+ ) {
791
+ return {
792
+ status: "reject",
793
+ reason: "Policy branches do not share the descriptor genesis",
794
+ };
795
+ }
796
+ candidateChild = candidateCursor;
797
+ acceptedChild = acceptedCursor;
798
+ const [candidateParent, acceptedParent] = await Promise.all([
799
+ this.parentOf(candidateCursor, resolutionCache),
800
+ this.parentOf(acceptedCursor, resolutionCache),
801
+ ]);
802
+ if (acceptedParent.status !== "found") {
803
+ return this.acceptedAncestryUnavailable(acceptedParent);
804
+ }
805
+ if (candidateParent.status !== "found") {
806
+ return this.candidateAncestryResult(candidateParent);
807
+ }
808
+ candidateCursor = candidateParent.parent;
809
+ acceptedCursor = acceptedParent.parent;
810
+ }
811
+
812
+ if (candidateChild === undefined) {
813
+ return { status: "duplicate" };
814
+ }
815
+ if (acceptedChild === undefined) {
816
+ return { status: "accept" };
817
+ }
818
+ if (equals(candidateChild.digest, acceptedChild.digest)) {
819
+ return { status: "duplicate" };
820
+ }
821
+ return {
822
+ status: "fork",
823
+ commonParent: candidateCursor,
824
+ candidateChild,
825
+ acceptedChild,
826
+ };
827
+ }
828
+
829
+ private setFork(evaluation: Extract<EvaluationV2, { status: "fork" }>): void {
830
+ this.project(evaluation.commonParent);
831
+ const children = [
832
+ forkProofFromSnapshot(evaluation.candidateChild),
833
+ forkProofFromSnapshot(evaluation.acceptedChild),
834
+ ].sort(compareForkChildProofs) as [
835
+ PolicyForkChildProofV2,
836
+ PolicyForkChildProofV2,
837
+ ];
838
+ this.fork = {
839
+ commonParent: projectionFromSnapshot(evaluation.commonParent),
840
+ children,
841
+ };
842
+ this.unavailable = undefined;
843
+ this.pending.clear();
844
+ }
845
+
846
+ private retainCanonicalForkChild(snapshot: ValidatedPolicySnapshotV2): void {
847
+ if (this.fork === undefined) return;
848
+ const byDigest = new Map<string, PolicyForkChildProofV2>();
849
+ for (const proof of [
850
+ ...this.fork.children,
851
+ forkProofFromSnapshot(snapshot),
852
+ ]) {
853
+ const digestKey = bytesKey(proof.digest);
854
+ const retained = byDigest.get(digestKey);
855
+ if (
856
+ retained === undefined ||
857
+ compare(proof.entryBytes, retained.entryBytes) < 0
858
+ ) {
859
+ byDigest.set(digestKey, copyForkChildProof(proof));
860
+ }
861
+ }
862
+ const canonical = [...byDigest.values()]
863
+ .sort(compareForkChildProofs)
864
+ .slice(0, 2);
865
+ if (canonical.length !== 2) return;
866
+ this.fork.children = [canonical[0]!, canonical[1]!];
867
+ }
868
+
869
+ private observeAfterFork(snapshot: ValidatedPolicySnapshotV2): void {
870
+ if (this.fork === undefined || this.acceptedHead === undefined) return;
871
+ const commonParent = this.acceptedHead;
872
+ if (
873
+ snapshot.body.sequence !== commonParent.body.sequence + 1n ||
874
+ !equals(snapshot.body.previousPolicyDigest, commonParent.digest)
875
+ ) {
876
+ return;
877
+ }
878
+
879
+ // This bounded kernel deliberately retains only the canonical two direct
880
+ // child proofs. Durable storage of every authenticated fork observation is
881
+ // an outer-layer responsibility for a later integration slice.
882
+ this.retainCanonicalForkChild(snapshot);
883
+ }
884
+
885
+ private addPending(
886
+ snapshot: ValidatedPolicySnapshotV2,
887
+ missingParentDigest: Uint8Array,
888
+ ): { retained: boolean; evictedPolicyDigests: Uint8Array[] } {
889
+ const existing = this.pending.get(snapshot.digestKey);
890
+ if (existing !== undefined) {
891
+ existing.missingParentDigest = copyBytes(missingParentDigest);
892
+ if (
893
+ snapshot.accountedBytes <= this.maxPendingPolicyBytes &&
894
+ compare(snapshot.entryBytes, existing.snapshot.entryBytes) < 0
895
+ ) {
896
+ existing.snapshot = copySnapshot(snapshot);
897
+ }
898
+ return { retained: true, evictedPolicyDigests: [] };
899
+ }
900
+ if (snapshot.accountedBytes > this.maxPendingPolicyBytes) {
901
+ return {
902
+ retained: false,
903
+ evictedPolicyDigests: [copyBytes(snapshot.digest)],
904
+ };
905
+ }
906
+ this.pending.set(snapshot.digestKey, {
907
+ snapshot: copySnapshot(snapshot),
908
+ missingParentDigest: copyBytes(missingParentDigest),
909
+ });
910
+ const ordered = [...this.pending.entries()].sort(([a], [b]) =>
911
+ compareKeys(a, b),
912
+ );
913
+ const retainedKeys = new Set(
914
+ ordered.slice(0, this.maxPending).map(([key]) => key),
915
+ );
916
+ const evictedPolicyDigests: Uint8Array[] = [];
917
+ for (const [key, pending] of ordered) {
918
+ if (retainedKeys.has(key)) continue;
919
+ evictedPolicyDigests.push(copyBytes(pending.snapshot.digest));
920
+ this.pending.delete(key);
921
+ }
922
+ return {
923
+ retained: retainedKeys.has(snapshot.digestKey),
924
+ evictedPolicyDigests,
925
+ };
926
+ }
927
+
928
+ private enterUnavailable(
929
+ snapshot: ValidatedPolicySnapshotV2,
930
+ evaluation: Extract<EvaluationV2, { status: "unavailable" }>,
931
+ ): { retained: boolean; evictedPolicyDigests: Uint8Array[] } {
932
+ const retention = this.addPending(snapshot, evaluation.digest);
933
+ this.unavailable = {
934
+ acceptedAncestorDigest: copyBytes(evaluation.digest),
935
+ candidateDigestKey: snapshot.digestKey,
936
+ reason: boundedUnavailableReason(evaluation.reason),
937
+ };
938
+ return retention;
939
+ }
940
+
941
+ private async drainPending(): Promise<PendingDrainOutcomeV2 | undefined> {
942
+ if (this.lifecycleController.signal.aborted) {
943
+ return { status: "halted" };
944
+ }
945
+ let accepted = false;
946
+ let progress = true;
947
+ while (
948
+ progress &&
949
+ this.pending.size > 0 &&
950
+ this.fork === undefined &&
951
+ this.unavailable === undefined
952
+ ) {
953
+ progress = false;
954
+ const ordered = [...this.pending.values()].sort((a, b) =>
955
+ compareKeys(a.snapshot.digestKey, b.snapshot.digestKey),
956
+ );
957
+ for (const pending of ordered) {
958
+ if (!this.pending.has(pending.snapshot.digestKey)) continue;
959
+ const evaluation = await this.evaluate(pending.snapshot);
960
+ if (this.lifecycleController.signal.aborted) {
961
+ return { status: "halted" };
962
+ }
963
+ if (evaluation.status === "missing") {
964
+ pending.missingParentDigest = copyBytes(evaluation.digest);
965
+ continue;
966
+ }
967
+ if (evaluation.status === "unavailable") {
968
+ const retention = this.enterUnavailable(pending.snapshot, evaluation);
969
+ return { status: "unavailable", ...retention };
970
+ }
971
+ this.pending.delete(pending.snapshot.digestKey);
972
+ progress = true;
973
+ if (evaluation.status === "accept") {
974
+ this.project(pending.snapshot);
975
+ accepted = true;
976
+ } else if (evaluation.status === "fork") {
977
+ this.setFork(evaluation);
978
+ return { status: "forked" };
979
+ }
980
+ }
981
+ }
982
+ return accepted ? { status: "accepted" } : undefined;
983
+ }
984
+
985
+ private enqueueAdmission(
986
+ operation: () => Promise<PolicyAdmissionResultV2>,
987
+ ): Promise<PolicyAdmissionResultV2> {
988
+ const result = this.admissionTail.then(async () => {
989
+ if (this.lifecycleController.signal.aborted) {
990
+ return this.haltedResult();
991
+ }
992
+ const admission = await operation();
993
+ return this.lifecycleController.signal.aborted
994
+ ? this.haltedResult()
995
+ : admission;
996
+ });
997
+ this.admissionTail = result.then(
998
+ (): void => {},
999
+ (_reason: unknown): void => {},
1000
+ );
1001
+ return result;
1002
+ }
1003
+
1004
+ ingest(entryBytes: Uint8Array): Promise<PolicyAdmissionResultV2> {
1005
+ if (this.lifecycleController.signal.aborted) {
1006
+ return Promise.resolve(this.haltedResult());
1007
+ }
1008
+ let capturedEntryBytes: Uint8Array;
1009
+ try {
1010
+ // Capture at the API boundary, before this admission waits behind earlier
1011
+ // work. Otherwise a caller could mutate a queued entry before validation.
1012
+ capturedEntryBytes = capturePolicySnapshotEntryBytesV2(entryBytes);
1013
+ } catch (error) {
1014
+ const reason = validationMessage(error);
1015
+ return this.enqueueAdmission(async () => this.result("rejected", reason));
1016
+ }
1017
+ return this.enqueueAdmission(() => this.ingestOne(capturedEntryBytes));
1018
+ }
1019
+
1020
+ retryUnavailable(): Promise<PolicyAdmissionResultV2> {
1021
+ return this.enqueueAdmission(() => this.retryUnavailableOne());
1022
+ }
1023
+
1024
+ private async ingestOne(
1025
+ entryBytes: Uint8Array,
1026
+ ): Promise<PolicyAdmissionResultV2> {
1027
+ let snapshot: ValidatedPolicySnapshotV2;
1028
+ try {
1029
+ snapshot = await authenticateCapturedPolicySnapshotEntryV2(
1030
+ entryBytes,
1031
+ this.descriptor,
1032
+ );
1033
+ } catch (error) {
1034
+ return this.result("rejected", validationMessage(error));
1035
+ }
1036
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
1037
+
1038
+ if (this.fork !== undefined) {
1039
+ this.observeAfterFork(snapshot);
1040
+ return this.result(
1041
+ "halted",
1042
+ "Policy reducer is halted by authority equivocation",
1043
+ );
1044
+ }
1045
+ this.retainCanonicalHeadEntry(snapshot);
1046
+
1047
+ if (this.unavailable !== undefined) {
1048
+ if (this.acceptedHead?.digestKey === snapshot.digestKey) {
1049
+ return this.result("unavailable", this.unavailable.reason);
1050
+ }
1051
+ const existingPending = this.pending.get(snapshot.digestKey);
1052
+ if (existingPending !== undefined) {
1053
+ this.addPending(snapshot, existingPending.missingParentDigest);
1054
+ return this.result("unavailable", this.unavailable.reason);
1055
+ }
1056
+ const retention = this.addPending(
1057
+ snapshot,
1058
+ this.unavailable.acceptedAncestorDigest,
1059
+ );
1060
+ return this.unavailableResult(retention);
1061
+ }
1062
+
1063
+ const existingPending = this.pending.get(snapshot.digestKey);
1064
+ if (existingPending !== undefined) {
1065
+ this.addPending(snapshot, existingPending.missingParentDigest);
1066
+ return this.result("pending", "Policy snapshot is already pending");
1067
+ }
1068
+
1069
+ const evaluation = await this.evaluate(snapshot);
1070
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
1071
+ if (evaluation.status === "reject") {
1072
+ return this.result("rejected", evaluation.reason);
1073
+ }
1074
+ if (evaluation.status === "duplicate") {
1075
+ return this.result("duplicate");
1076
+ }
1077
+ if (evaluation.status === "fork") {
1078
+ this.setFork(evaluation);
1079
+ return this.forkedResult();
1080
+ }
1081
+ if (evaluation.status === "unavailable") {
1082
+ const retention = this.enterUnavailable(snapshot, evaluation);
1083
+ return this.unavailableResult(retention);
1084
+ }
1085
+ if (evaluation.status === "missing") {
1086
+ const pending = this.addPending(snapshot, evaluation.digest);
1087
+ return this.result(
1088
+ pending.retained ? "pending" : "capacity",
1089
+ pending.retained
1090
+ ? (evaluation.reason ?? "Policy parent is missing")
1091
+ : "Policy pending capacity did not retain this candidate",
1092
+ pending.evictedPolicyDigests,
1093
+ );
1094
+ }
1095
+
1096
+ this.project(snapshot);
1097
+ return (
1098
+ this.completedDrainResult(await this.drainPending()) ??
1099
+ this.result("accepted")
1100
+ );
1101
+ }
1102
+
1103
+ private async retryUnavailableOne(): Promise<PolicyAdmissionResultV2> {
1104
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
1105
+ if (this.fork !== undefined) {
1106
+ return this.result(
1107
+ "halted",
1108
+ "Policy reducer is halted by authority equivocation",
1109
+ );
1110
+ }
1111
+ const unavailable = this.unavailable;
1112
+ if (unavailable === undefined) {
1113
+ return this.result("duplicate", "Policy reducer is not unavailable");
1114
+ }
1115
+ const pending = this.pending.get(unavailable.candidateDigestKey);
1116
+ if (pending === undefined) {
1117
+ return this.result(
1118
+ "capacity",
1119
+ "Unavailable comparison candidate is not retained; re-ingest it before retrying",
1120
+ );
1121
+ }
1122
+
1123
+ const evaluation = await this.evaluate(pending.snapshot);
1124
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
1125
+ if (evaluation.status === "unavailable") {
1126
+ pending.missingParentDigest = copyBytes(evaluation.digest);
1127
+ this.unavailable = {
1128
+ acceptedAncestorDigest: copyBytes(evaluation.digest),
1129
+ candidateDigestKey: pending.snapshot.digestKey,
1130
+ reason: boundedUnavailableReason(evaluation.reason),
1131
+ };
1132
+ return this.result("unavailable", this.unavailable.reason);
1133
+ }
1134
+
1135
+ this.unavailable = undefined;
1136
+ let status: PolicyAdmissionStatusV2;
1137
+ let reason: string | undefined;
1138
+ if (evaluation.status === "missing") {
1139
+ pending.missingParentDigest = copyBytes(evaluation.digest);
1140
+ status = "pending";
1141
+ reason = evaluation.reason ?? "Policy parent is missing";
1142
+ } else {
1143
+ this.pending.delete(pending.snapshot.digestKey);
1144
+ if (evaluation.status === "fork") {
1145
+ this.setFork(evaluation);
1146
+ return this.forkedResult();
1147
+ }
1148
+ if (evaluation.status === "accept") {
1149
+ this.project(pending.snapshot);
1150
+ status = "accepted";
1151
+ } else if (evaluation.status === "duplicate") {
1152
+ status = "duplicate";
1153
+ } else {
1154
+ status = "rejected";
1155
+ reason = evaluation.reason;
1156
+ }
1157
+ }
1158
+
1159
+ return (
1160
+ this.completedDrainResult(await this.drainPending()) ??
1161
+ this.result(status, reason)
1162
+ );
1163
+ }
1164
+ }