@peerbit/trusted-network 6.0.104 → 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.
@@ -7,6 +7,7 @@ import {
7
7
  PolicySnapshotBodyV2,
8
8
  PolicySubjectBindingV2,
9
9
  TRUSTED_NETWORK_V2_KNOWN_ROLE_BITS,
10
+ TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES,
10
11
  assertNetworkDescriptorV2,
11
12
  decodePolicySnapshotBodyV2,
12
13
  digestPolicySnapshotBodyV2,
@@ -22,8 +23,14 @@ import {
22
23
  */
23
24
 
24
25
  const DEFAULT_MAX_PENDING_POLICIES_V2 = 64;
25
- const DEFAULT_MAX_PENDING_POLICY_BYTES_V2 = 256 * 1024;
26
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;
27
34
  const MAX_UNAVAILABLE_REASON_LENGTH_V2 = 512;
28
35
 
29
36
  const copyBytes = (bytes: Uint8Array): Uint8Array => Uint8Array.from(bytes);
@@ -76,7 +83,8 @@ const copySnapshot = (
76
83
 
77
84
  export type PolicySnapshotResolverV2 = (
78
85
  digest: Uint8Array,
79
- ) => EntryV0<Uint8Array> | undefined | Promise<EntryV0<Uint8Array> | undefined>;
86
+ options: { signal: AbortSignal },
87
+ ) => Uint8Array | undefined | Promise<Uint8Array | undefined>;
80
88
 
81
89
  export type PolicyParentFetchHintV2 = {
82
90
  kind: "policy-parent";
@@ -134,6 +142,7 @@ type UnavailablePolicyComparisonV2 = {
134
142
  type ParentResolutionV2 =
135
143
  | { status: "found"; parent: ValidatedPolicySnapshotV2 }
136
144
  | { status: "missing"; digest: Uint8Array }
145
+ | { status: "unavailable"; digest: Uint8Array; reason: string }
137
146
  | { status: "reject"; digest: Uint8Array; reason: string };
138
147
 
139
148
  type SnapshotResolutionCacheV2 = Map<
@@ -144,7 +153,7 @@ type SnapshotResolutionCacheV2 = Map<
144
153
  type EvaluationV2 =
145
154
  | { status: "accept" }
146
155
  | { status: "duplicate" }
147
- | { status: "missing"; digest: Uint8Array }
156
+ | { status: "missing"; digest: Uint8Array; reason?: string }
148
157
  | { status: "reject"; reason: string }
149
158
  | { status: "unavailable"; digest: Uint8Array; reason: string }
150
159
  | {
@@ -157,6 +166,7 @@ type EvaluationV2 =
157
166
  type PendingDrainOutcomeV2 =
158
167
  | { status: "accepted" }
159
168
  | { status: "forked" }
169
+ | { status: "halted" }
160
170
  | {
161
171
  status: "unavailable";
162
172
  retained: boolean;
@@ -169,34 +179,61 @@ const validationMessage = (error: unknown): string =>
169
179
  const boundedUnavailableReason = (reason: string): string =>
170
180
  reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
171
181
 
172
- export const authenticatePolicySnapshotEntryV2 = async (
173
- entry: unknown,
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,
174
211
  descriptor: NetworkDescriptorV2,
175
212
  ): Promise<ValidatedPolicySnapshotV2> => {
176
- assertNetworkDescriptorV2(descriptor);
177
- if (!(entry instanceof EntryV0)) {
213
+ const authenticatedEntry = deserialize(canonicalEntryBytes, Entry);
214
+ if (!(authenticatedEntry instanceof EntryV0)) {
178
215
  throw new Error("Policy snapshot must use EntryV0");
179
216
  }
180
- if (!(entry._meta instanceof DecryptedThing)) {
217
+ if (!equals(canonicalEntryBytes, serialize(authenticatedEntry))) {
218
+ throw new Error("Policy snapshot entry encoding is not canonical");
219
+ }
220
+ if (!(authenticatedEntry._meta instanceof DecryptedThing)) {
181
221
  throw new Error("Policy snapshot metadata must be public");
182
222
  }
183
- if (!(entry._payload instanceof DecryptedThing)) {
223
+ if (!(authenticatedEntry._payload instanceof DecryptedThing)) {
184
224
  throw new Error("Policy snapshot payload must be public");
185
225
  }
186
226
  if (
187
- entry._signatures === undefined ||
188
- entry._signatures.signatures.length !== 1
227
+ authenticatedEntry._signatures === undefined ||
228
+ authenticatedEntry._signatures.signatures.length !== 1
189
229
  ) {
190
230
  throw new Error("Policy snapshot must contain exactly one signature");
191
231
  }
192
- if (!(entry._signatures.signatures[0] instanceof DecryptedThing)) {
232
+ if (
233
+ !(authenticatedEntry._signatures.signatures[0] instanceof DecryptedThing)
234
+ ) {
193
235
  throw new Error("Policy snapshot signature must be public");
194
236
  }
195
- const entryBytes = serialize(entry);
196
- const authenticatedEntry = deserialize(entryBytes, Entry);
197
- if (!(authenticatedEntry instanceof EntryV0)) {
198
- throw new Error("Policy snapshot must decode as EntryV0");
199
- }
200
237
  authenticatedEntry.init({ encoding: NO_ENCODING });
201
238
 
202
239
  const signatures = await authenticatedEntry.getSignatures();
@@ -228,14 +265,25 @@ export const authenticatePolicySnapshotEntryV2 = async (
228
265
  body: copyBody(body),
229
266
  digest: copyBytes(digest),
230
267
  digestKey: bytesKey(digest),
231
- entryBytes,
268
+ entryBytes: canonicalEntryBytes,
232
269
  accountedBytes:
233
- entryBytes.byteLength +
270
+ canonicalEntryBytes.byteLength +
234
271
  serialize(body).byteLength +
235
272
  PENDING_POLICY_ACCOUNTING_OVERHEAD_V2,
236
273
  };
237
274
  };
238
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
+
239
287
  const projectionFromSnapshot = (
240
288
  snapshot: ValidatedPolicySnapshotV2,
241
289
  ): PolicyHeadProjectionV2 => ({
@@ -291,8 +339,12 @@ const copyForkEvidence = (
291
339
  export class TrustedNetworkV2PolicyReducer {
292
340
  private readonly descriptor: NetworkDescriptorV2;
293
341
  private readonly resolvePolicyEntry: PolicySnapshotResolverV2;
342
+ private readonly resolveTimeoutMs: number;
294
343
  private readonly maxPending: number;
295
344
  private readonly maxPendingPolicyBytes: number;
345
+ private readonly lifecycleController = new AbortController();
346
+ private externalSignal?: AbortSignal;
347
+ private externalAbortListener?: () => void;
296
348
  private acceptedHead?: ValidatedPolicySnapshotV2;
297
349
  private projectedRoles = new Map<string, number>();
298
350
  private readonly pending = new Map<string, PendingPolicySnapshotV2>();
@@ -305,6 +357,8 @@ export class TrustedNetworkV2PolicyReducer {
305
357
  constructor(properties: {
306
358
  descriptor: NetworkDescriptorV2;
307
359
  resolvePolicyEntry: PolicySnapshotResolverV2;
360
+ resolveTimeoutMs?: number;
361
+ signal?: AbortSignal;
308
362
  maxPending?: number;
309
363
  maxPendingPolicyBytes?: number;
310
364
  }) {
@@ -317,20 +371,52 @@ export class TrustedNetworkV2PolicyReducer {
317
371
  properties.maxPendingPolicyBytes ?? DEFAULT_MAX_PENDING_POLICY_BYTES_V2;
318
372
  if (
319
373
  !Number.isSafeInteger(maxPendingPolicyBytes) ||
320
- maxPendingPolicyBytes < 1
374
+ maxPendingPolicyBytes < 1 ||
375
+ maxPendingPolicyBytes > MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2
321
376
  ) {
322
- throw new Error("maxPendingPolicyBytes must be a positive safe integer");
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
+ );
323
391
  }
324
392
  this.descriptor = deserialize(
325
393
  serialize(properties.descriptor),
326
394
  NetworkDescriptorV2,
327
395
  );
328
396
  this.resolvePolicyEntry = properties.resolvePolicyEntry;
397
+ this.resolveTimeoutMs = resolveTimeoutMs;
329
398
  this.maxPending = maxPending;
330
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
+ }
331
416
  }
332
417
 
333
- get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" {
418
+ get state(): "EMPTY" | "ACTIVE" | "UNAVAILABLE" | "FORKED" | "HALTED" {
419
+ if (this.lifecycleController.signal.aborted) return "HALTED";
334
420
  if (this.fork !== undefined) return "FORKED";
335
421
  if (this.unavailable !== undefined) return "UNAVAILABLE";
336
422
  return this.acceptedHead === undefined ? "EMPTY" : "ACTIVE";
@@ -380,6 +466,21 @@ export class TrustedNetworkV2PolicyReducer {
380
466
  return (this.rolesFor(subject) & roles) === roles;
381
467
  }
382
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
+
383
484
  private fetchHints(): PolicyParentFetchHintV2[] {
384
485
  const unique = new Map<string, Uint8Array>();
385
486
  for (const { missingParentDigest } of this.pending.values()) {
@@ -422,6 +523,10 @@ export class TrustedNetworkV2PolicyReducer {
422
523
  return this.result("forked", "Policy authority signed competing children");
423
524
  }
424
525
 
526
+ private haltedResult(): PolicyAdmissionResultV2 {
527
+ return this.result("halted", "Policy reducer lifecycle is aborted");
528
+ }
529
+
425
530
  private unavailableResult(retention?: {
426
531
  retained: boolean;
427
532
  evictedPolicyDigests: Uint8Array[];
@@ -447,6 +552,7 @@ export class TrustedNetworkV2PolicyReducer {
447
552
  outcome: PendingDrainOutcomeV2 | undefined,
448
553
  ): PolicyAdmissionResultV2 | undefined {
449
554
  if (outcome?.status === "forked") return this.forkedResult();
555
+ if (outcome?.status === "halted") return this.haltedResult();
450
556
  return outcome?.status === "unavailable"
451
557
  ? this.unavailableResult(outcome)
452
558
  : undefined;
@@ -471,6 +577,88 @@ export class TrustedNetworkV2PolicyReducer {
471
577
  }
472
578
  }
473
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
+
474
662
  private async resolveSnapshot(
475
663
  digest: Uint8Array,
476
664
  cache?: SnapshotResolutionCacheV2,
@@ -483,18 +671,7 @@ export class TrustedNetworkV2PolicyReducer {
483
671
  if (pending !== undefined) return copySnapshot(pending.snapshot);
484
672
  let resolution = cache?.get(digestKey);
485
673
  if (resolution === undefined) {
486
- resolution = (async () => {
487
- const entry = await this.resolvePolicyEntry(copyBytes(digest));
488
- if (entry === undefined) return undefined;
489
- const snapshot = await authenticatePolicySnapshotEntryV2(
490
- entry,
491
- this.descriptor,
492
- );
493
- if (!equals(snapshot.digest, digest)) {
494
- throw new Error("Policy resolver returned the wrong body digest");
495
- }
496
- return snapshot;
497
- })();
674
+ resolution = this.resolveExternalSnapshot(digest);
498
675
  cache?.set(digestKey, resolution);
499
676
  }
500
677
  const snapshot = await resolution;
@@ -520,9 +697,9 @@ export class TrustedNetworkV2PolicyReducer {
520
697
  );
521
698
  } catch (error) {
522
699
  return {
523
- status: "reject",
700
+ status: "unavailable",
524
701
  digest: copyBytes(child.body.previousPolicyDigest),
525
- reason: `Policy parent validation failed: ${validationMessage(error)}`,
702
+ reason: `Policy parent dependency is unavailable: ${validationMessage(error)}`,
526
703
  };
527
704
  }
528
705
  if (parent === undefined) {
@@ -541,6 +718,18 @@ export class TrustedNetworkV2PolicyReducer {
541
718
  return { status: "found", parent };
542
719
  }
543
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
+
544
733
  private acceptedAncestryUnavailable(
545
734
  resolution: Exclude<ParentResolutionV2, { status: "found" }>,
546
735
  ): Extract<EvaluationV2, { status: "unavailable" }> {
@@ -550,7 +739,9 @@ export class TrustedNetworkV2PolicyReducer {
550
739
  reason:
551
740
  resolution.status === "missing"
552
741
  ? "Accepted policy ancestry is unavailable from the resolver"
553
- : `Accepted policy ancestry validation failed: ${resolution.reason}`,
742
+ : resolution.status === "unavailable"
743
+ ? `Accepted policy ancestry is unavailable: ${resolution.reason}`
744
+ : `Accepted policy ancestry validation failed: ${resolution.reason}`,
554
745
  };
555
746
  }
556
747
 
@@ -562,7 +753,9 @@ export class TrustedNetworkV2PolicyReducer {
562
753
  let cursor = candidate;
563
754
  while (cursor.body.sequence !== 0n) {
564
755
  const parent = await this.parentOf(cursor, resolutionCache);
565
- if (parent.status !== "found") return parent;
756
+ if (parent.status !== "found") {
757
+ return this.candidateAncestryResult(parent);
758
+ }
566
759
  cursor = parent.parent;
567
760
  }
568
761
  return { status: "accept" };
@@ -576,7 +769,9 @@ export class TrustedNetworkV2PolicyReducer {
576
769
  while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
577
770
  candidateChild = candidateCursor;
578
771
  const parent = await this.parentOf(candidateCursor, resolutionCache);
579
- if (parent.status !== "found") return parent;
772
+ if (parent.status !== "found") {
773
+ return this.candidateAncestryResult(parent);
774
+ }
580
775
  candidateCursor = parent.parent;
581
776
  }
582
777
  while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
@@ -607,7 +802,9 @@ export class TrustedNetworkV2PolicyReducer {
607
802
  if (acceptedParent.status !== "found") {
608
803
  return this.acceptedAncestryUnavailable(acceptedParent);
609
804
  }
610
- if (candidateParent.status !== "found") return candidateParent;
805
+ if (candidateParent.status !== "found") {
806
+ return this.candidateAncestryResult(candidateParent);
807
+ }
611
808
  candidateCursor = candidateParent.parent;
612
809
  acceptedCursor = acceptedParent.parent;
613
810
  }
@@ -742,6 +939,9 @@ export class TrustedNetworkV2PolicyReducer {
742
939
  }
743
940
 
744
941
  private async drainPending(): Promise<PendingDrainOutcomeV2 | undefined> {
942
+ if (this.lifecycleController.signal.aborted) {
943
+ return { status: "halted" };
944
+ }
745
945
  let accepted = false;
746
946
  let progress = true;
747
947
  while (
@@ -757,6 +957,9 @@ export class TrustedNetworkV2PolicyReducer {
757
957
  for (const pending of ordered) {
758
958
  if (!this.pending.has(pending.snapshot.digestKey)) continue;
759
959
  const evaluation = await this.evaluate(pending.snapshot);
960
+ if (this.lifecycleController.signal.aborted) {
961
+ return { status: "halted" };
962
+ }
760
963
  if (evaluation.status === "missing") {
761
964
  pending.missingParentDigest = copyBytes(evaluation.digest);
762
965
  continue;
@@ -782,7 +985,15 @@ export class TrustedNetworkV2PolicyReducer {
782
985
  private enqueueAdmission(
783
986
  operation: () => Promise<PolicyAdmissionResultV2>,
784
987
  ): Promise<PolicyAdmissionResultV2> {
785
- const result = this.admissionTail.then(operation);
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
+ });
786
997
  this.admissionTail = result.then(
787
998
  (): void => {},
788
999
  (_reason: unknown): void => {},
@@ -790,24 +1001,39 @@ export class TrustedNetworkV2PolicyReducer {
790
1001
  return result;
791
1002
  }
792
1003
 
793
- ingest(entry: unknown): Promise<PolicyAdmissionResultV2> {
794
- return this.enqueueAdmission(() => this.ingestOne(entry));
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));
795
1018
  }
796
1019
 
797
1020
  retryUnavailable(): Promise<PolicyAdmissionResultV2> {
798
1021
  return this.enqueueAdmission(() => this.retryUnavailableOne());
799
1022
  }
800
1023
 
801
- private async ingestOne(entry: unknown): Promise<PolicyAdmissionResultV2> {
1024
+ private async ingestOne(
1025
+ entryBytes: Uint8Array,
1026
+ ): Promise<PolicyAdmissionResultV2> {
802
1027
  let snapshot: ValidatedPolicySnapshotV2;
803
1028
  try {
804
- snapshot = await authenticatePolicySnapshotEntryV2(
805
- entry,
1029
+ snapshot = await authenticateCapturedPolicySnapshotEntryV2(
1030
+ entryBytes,
806
1031
  this.descriptor,
807
1032
  );
808
1033
  } catch (error) {
809
1034
  return this.result("rejected", validationMessage(error));
810
1035
  }
1036
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
811
1037
 
812
1038
  if (this.fork !== undefined) {
813
1039
  this.observeAfterFork(snapshot);
@@ -841,6 +1067,7 @@ export class TrustedNetworkV2PolicyReducer {
841
1067
  }
842
1068
 
843
1069
  const evaluation = await this.evaluate(snapshot);
1070
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
844
1071
  if (evaluation.status === "reject") {
845
1072
  return this.result("rejected", evaluation.reason);
846
1073
  }
@@ -860,7 +1087,7 @@ export class TrustedNetworkV2PolicyReducer {
860
1087
  return this.result(
861
1088
  pending.retained ? "pending" : "capacity",
862
1089
  pending.retained
863
- ? "Policy parent is missing"
1090
+ ? (evaluation.reason ?? "Policy parent is missing")
864
1091
  : "Policy pending capacity did not retain this candidate",
865
1092
  pending.evictedPolicyDigests,
866
1093
  );
@@ -874,6 +1101,7 @@ export class TrustedNetworkV2PolicyReducer {
874
1101
  }
875
1102
 
876
1103
  private async retryUnavailableOne(): Promise<PolicyAdmissionResultV2> {
1104
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
877
1105
  if (this.fork !== undefined) {
878
1106
  return this.result(
879
1107
  "halted",
@@ -893,6 +1121,7 @@ export class TrustedNetworkV2PolicyReducer {
893
1121
  }
894
1122
 
895
1123
  const evaluation = await this.evaluate(pending.snapshot);
1124
+ if (this.lifecycleController.signal.aborted) return this.haltedResult();
896
1125
  if (evaluation.status === "unavailable") {
897
1126
  pending.missingParentDigest = copyBytes(evaluation.digest);
898
1127
  this.unavailable = {
@@ -909,7 +1138,7 @@ export class TrustedNetworkV2PolicyReducer {
909
1138
  if (evaluation.status === "missing") {
910
1139
  pending.missingParentDigest = copyBytes(evaluation.digest);
911
1140
  status = "pending";
912
- reason = "Policy parent is missing";
1141
+ reason = evaluation.reason ?? "Policy parent is missing";
913
1142
  } else {
914
1143
  this.pending.delete(pending.snapshot.digestKey);
915
1144
  if (evaluation.status === "fork") {
package/src/v2.ts CHANGED
@@ -33,6 +33,13 @@ export const TRUSTED_NETWORK_V2_POLICY_HASH_SHA256 = 1;
33
33
  */
34
34
  export const TRUSTED_NETWORK_V2_ENTRY_V0_AUTHORITY_ONLY_SIGNATURE_PROFILE = 1;
35
35
 
36
+ /**
37
+ * Consensus ceiling for the canonical serialized EntryV0 carrying a v2 policy
38
+ * snapshot. Every replica applies this exact bound before decoding the entry;
39
+ * it is part of signature profile 1 rather than a replica-local resource knob.
40
+ */
41
+ export const TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_BYTES = 128 * 1024;
42
+
36
43
  export const TRUSTED_NETWORK_V2_NETWORK_ID_DOMAIN =
37
44
  "peerbit/trusted-network/v2/network-id/v1";
38
45
  export const TRUSTED_NETWORK_V2_POLICY_DIGEST_DOMAIN =