@peerbit/trusted-network 6.0.128 → 6.0.130

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.
@@ -1,4 +1,11 @@
1
1
  import { deserialize, serialize } from "@dao-xyz/borsh";
2
+ import {
3
+ calculateRawCid,
4
+ cidifyString,
5
+ codecMap,
6
+ defaultHasher,
7
+ stringifyCid,
8
+ } from "@peerbit/blocks-interface";
2
9
  import { PublicSignKey } from "@peerbit/crypto";
3
10
  import { compare, equals } from "uint8arrays";
4
11
  import { authenticateCapturedAuthorityEntryV0V2 } from "./v2-authority-entry.js";
@@ -24,6 +31,8 @@ import {
24
31
 
25
32
  /** Internal protocol ceiling; this module is not part of the package root. */
26
33
  export const TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES = 64;
34
+ /** Internal ceiling for one canonical raw policy-entry CID. */
35
+ export const TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_CID_CHARACTERS = 128;
27
36
  const DEFAULT_MAX_PENDING_POLICIES_V2 = TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES;
28
37
  const PENDING_POLICY_ACCOUNTING_OVERHEAD_V2 = 64;
29
38
  const MAX_PENDING_POLICY_ACCOUNTED_BYTES_V2 =
@@ -61,6 +70,33 @@ const isAbortSignalV2 = (value: unknown): value is AbortSignal => {
61
70
  }
62
71
  };
63
72
 
73
+ /** Capture a possibly structural signal behind a reducer-owned native signal. */
74
+ export const observeAbortSignalV2 = (source?: AbortSignal) => {
75
+ const controller = new AbortController();
76
+ let installed = false;
77
+ const abort = (): void => controller.abort();
78
+ const dispose = (): void => {
79
+ controller.abort();
80
+ if (!installed) return;
81
+ installed = false;
82
+ try {
83
+ source!.removeEventListener("abort", abort);
84
+ } catch {}
85
+ };
86
+ try {
87
+ if (source !== undefined) {
88
+ if (!isAbortSignalV2(source)) throw new TypeError();
89
+ installed = true;
90
+ source.addEventListener("abort", abort, { once: true });
91
+ if (source.aborted) abort();
92
+ }
93
+ } catch {
94
+ dispose();
95
+ throw new TypeError("AbortSignal could not be observed");
96
+ }
97
+ return [controller, dispose] as const;
98
+ };
99
+
64
100
  const exactUint8ArrayByteLength = (input: unknown): number => {
65
101
  if (
66
102
  !ARRAY_BUFFER_IS_VIEW(input) ||
@@ -164,6 +200,21 @@ export type PolicyHeadProjectionV2 = {
164
200
  bindings: PolicySubjectBindingV2[];
165
201
  };
166
202
 
203
+ export type AuthenticatedExactPolicyEntryV2 = {
204
+ policyEntryCid: string;
205
+ policy: PolicyHeadProjectionV2;
206
+ };
207
+
208
+ /** Opaque, reducer-owned result of exact CID and authority authentication. */
209
+ export type PreparedExactPolicyCandidateV2 =
210
+ Readonly<AuthenticatedExactPolicyEntryV2>;
211
+
212
+ export type ExactPolicyAdmissionOptionsV2 = {
213
+ maxParentEdges: number;
214
+ deadline: number;
215
+ signal?: AbortSignal;
216
+ };
217
+
167
218
  /** Internal read-only resolution used by the durable policy-prefix lease. */
168
219
  export type AcceptedPolicyPrefixResolutionV2 =
169
220
  | {
@@ -272,6 +323,12 @@ type ParentResolutionV2 =
272
323
  | { status: "unavailable"; digest: Uint8Array; reason: string }
273
324
  | { status: "reject"; digest: Uint8Array; reason: string };
274
325
 
326
+ type ExactPolicyTraversalV2 = {
327
+ remainingParentEdges: number;
328
+ deadline: number;
329
+ signal: AbortSignal;
330
+ };
331
+
275
332
  type SnapshotResolutionCacheV2 = Map<
276
333
  string,
277
334
  Promise<ValidatedPolicySnapshotV2 | undefined>
@@ -282,6 +339,7 @@ type EvaluationV2 =
282
339
  | { status: "duplicate" }
283
340
  | { status: "missing"; digest: Uint8Array; reason?: string }
284
341
  | { status: "reject"; reason: string }
342
+ | { status: "interrupted"; reason: string }
285
343
  | { status: "unavailable"; digest: Uint8Array; reason: string }
286
344
  | {
287
345
  status: "fork";
@@ -306,6 +364,11 @@ const validationMessage = (error: unknown): string =>
306
364
  const boundedUnavailableReason = (reason: string): string =>
307
365
  reason.slice(0, MAX_UNAVAILABLE_REASON_LENGTH_V2);
308
366
 
367
+ const PREPARED_EXACT_POLICIES = new WeakMap<
368
+ object,
369
+ [TrustedNetworkV2PolicyReducer, ValidatedPolicySnapshotV2]
370
+ >();
371
+
309
372
  class PolicyDependencyUnavailableErrorV2 extends Error {
310
373
  constructor(message: string) {
311
374
  super(message);
@@ -370,6 +433,76 @@ export const authenticatePolicySnapshotEntryV2 = async (
370
433
  );
371
434
  };
372
435
 
436
+ export const captureCanonicalPolicyEntryCidV2 = (value: unknown): string => {
437
+ if (
438
+ typeof value !== "string" ||
439
+ value.length < 1 ||
440
+ value.length > TRUSTED_NETWORK_V2_MAX_POLICY_ENTRY_CID_CHARACTERS
441
+ ) {
442
+ throw new Error("Policy entry CID must be a bounded canonical CID");
443
+ }
444
+ let parsed: ReturnType<typeof cidifyString>;
445
+ try {
446
+ parsed = cidifyString(value);
447
+ } catch {
448
+ throw new Error("Policy entry CID must be a canonical CID");
449
+ }
450
+ if (
451
+ parsed.version !== 1 ||
452
+ parsed.code !== codecMap.raw.code ||
453
+ parsed.multihash.code !== defaultHasher.code ||
454
+ parsed.multihash.digest.byteLength !== 32 ||
455
+ stringifyCid(parsed) !== value
456
+ ) {
457
+ throw new Error("Policy entry CID must use canonical CIDv1/raw/sha2-256");
458
+ }
459
+ return value;
460
+ };
461
+
462
+ /**
463
+ * Authenticate one CID-addressed policy wrapper and project its policy-body
464
+ * identity. The wrapper CID is deliberately not the policy identity: distinct
465
+ * canonical EntryV0 storage wrappers may carry the same signed policy body.
466
+ */
467
+ const authenticateExactPolicySnapshotV2 = async (properties: {
468
+ policyEntryCid: string;
469
+ entryBytes: Uint8Array;
470
+ descriptor: NetworkDescriptorV2;
471
+ }): Promise<{
472
+ policyEntryCid: string;
473
+ snapshot: ValidatedPolicySnapshotV2;
474
+ }> => {
475
+ const policyEntryCid = captureCanonicalPolicyEntryCidV2(
476
+ properties.policyEntryCid,
477
+ );
478
+ assertNetworkDescriptorV2(properties.descriptor);
479
+ const entryBytes = capturePolicySnapshotEntryBytesV2(properties.entryBytes);
480
+ const prepared = await calculateRawCid(entryBytes);
481
+ if (prepared.cid !== policyEntryCid) {
482
+ throw new Error(
483
+ "Resolved policy entry bytes do not match the requested CID",
484
+ );
485
+ }
486
+ const snapshot = await authenticateCapturedPolicySnapshotEntryV2(
487
+ entryBytes,
488
+ properties.descriptor,
489
+ );
490
+ return { policyEntryCid, snapshot };
491
+ };
492
+
493
+ export const authenticateExactPolicyEntryV2 = async (properties: {
494
+ policyEntryCid: string;
495
+ entryBytes: Uint8Array;
496
+ descriptor: NetworkDescriptorV2;
497
+ }): Promise<AuthenticatedExactPolicyEntryV2> => {
498
+ const { policyEntryCid, snapshot } =
499
+ await authenticateExactPolicySnapshotV2(properties);
500
+ return {
501
+ policyEntryCid,
502
+ policy: projectionFromSnapshot(snapshot),
503
+ };
504
+ };
505
+
373
506
  const projectionFromSnapshot = (
374
507
  snapshot: ValidatedPolicySnapshotV2,
375
508
  ): PolicyHeadProjectionV2 => ({
@@ -767,6 +900,76 @@ export class TrustedNetworkV2PolicyReducer {
767
900
  return (this.rolesFor(subject) & roles) === roles;
768
901
  }
769
902
 
903
+ async prepareExactPolicyEntry(
904
+ policyEntryCid: string,
905
+ entryBytes: Uint8Array,
906
+ ): Promise<PreparedExactPolicyCandidateV2> {
907
+ if (this.lifecycleController.signal.aborted) {
908
+ throw new Error("Policy reducer lifecycle is aborted");
909
+ }
910
+ const prepared = await authenticateExactPolicySnapshotV2({
911
+ policyEntryCid,
912
+ entryBytes,
913
+ descriptor: this.descriptor,
914
+ });
915
+ if (this.lifecycleController.signal.aborted) {
916
+ throw new Error("Policy reducer lifecycle is aborted");
917
+ }
918
+ const candidate = Object.freeze({
919
+ policyEntryCid: prepared.policyEntryCid,
920
+ policy: projectionFromSnapshot(prepared.snapshot),
921
+ });
922
+ PREPARED_EXACT_POLICIES.set(candidate, [this, prepared.snapshot]);
923
+ return candidate;
924
+ }
925
+
926
+ async ingestPreparedExactPolicy(
927
+ candidate: PreparedExactPolicyCandidateV2,
928
+ options: ExactPolicyAdmissionOptionsV2,
929
+ ): Promise<PolicyAdmissionResultV2> {
930
+ const prepared =
931
+ candidate !== null && typeof candidate === "object"
932
+ ? PREPARED_EXACT_POLICIES.get(candidate)
933
+ : undefined;
934
+ if (prepared?.[0] !== this) {
935
+ return this.result(
936
+ "rejected",
937
+ "Prepared exact policy does not belong to this reducer",
938
+ );
939
+ }
940
+ let traversal: ExactPolicyTraversalV2;
941
+ let disposeSignal = (): void => {};
942
+ try {
943
+ const maxParentEdges = options.maxParentEdges;
944
+ const deadline = options.deadline;
945
+ const sourceSignal = options.signal;
946
+ if (
947
+ !Number.isSafeInteger(maxParentEdges) ||
948
+ maxParentEdges < 0 ||
949
+ maxParentEdges > TRUSTED_NETWORK_V2_MAX_PENDING_POLICIES ||
950
+ !Number.isSafeInteger(deadline) ||
951
+ deadline < 0
952
+ ) {
953
+ throw new Error();
954
+ }
955
+ const observed = observeAbortSignalV2(sourceSignal);
956
+ disposeSignal = observed[1];
957
+ traversal = {
958
+ remainingParentEdges: maxParentEdges,
959
+ deadline,
960
+ signal: observed[0].signal,
961
+ };
962
+ } catch {
963
+ return this.result(
964
+ "rejected",
965
+ "Exact policy admission bounds are invalid",
966
+ );
967
+ }
968
+ return this.enqueueAdmission(() =>
969
+ this.ingestSnapshot(prepared[1], traversal),
970
+ ).finally(disposeSignal);
971
+ }
972
+
770
973
  /**
771
974
  * Resolve one exact digest on the currently accepted policy prefix.
772
975
  *
@@ -1301,14 +1504,34 @@ export class TrustedNetworkV2PolicyReducer {
1301
1504
  };
1302
1505
  }
1303
1506
 
1507
+ private takeExactParentEdges(
1508
+ traversal: ExactPolicyTraversalV2 | undefined,
1509
+ count: number,
1510
+ ): Extract<EvaluationV2, { status: "interrupted" }> | undefined {
1511
+ if (traversal === undefined) return undefined;
1512
+ const reason = traversal.signal.aborted
1513
+ ? "Exact policy admission was aborted by the caller"
1514
+ : Date.now() >= traversal.deadline
1515
+ ? "Exact policy admission deadline elapsed"
1516
+ : traversal.remainingParentEdges < count
1517
+ ? "Exact policy admission reached its parent-edge budget"
1518
+ : undefined;
1519
+ if (reason !== undefined) return { status: "interrupted", reason };
1520
+ traversal.remainingParentEdges -= count;
1521
+ return undefined;
1522
+ }
1523
+
1304
1524
  private async evaluate(
1305
1525
  candidate: ValidatedPolicySnapshotV2,
1526
+ traversal?: ExactPolicyTraversalV2,
1306
1527
  ): Promise<EvaluationV2> {
1307
1528
  const resolutionCache: SnapshotResolutionCacheV2 = new Map();
1308
1529
  if (this.acceptedHead === undefined) {
1309
1530
  let cursor = candidate;
1310
1531
  while (cursor.body.sequence !== 0n) {
1311
- const parent = await this.parentOf(cursor, resolutionCache);
1532
+ const interrupted = this.takeExactParentEdges(traversal, 1);
1533
+ if (interrupted !== undefined) return interrupted;
1534
+ const parent = await this.parentOf(cursor, resolutionCache, traversal);
1312
1535
  if (parent.status !== "found") {
1313
1536
  return this.candidateAncestryResult(parent);
1314
1537
  }
@@ -1324,7 +1547,13 @@ export class TrustedNetworkV2PolicyReducer {
1324
1547
 
1325
1548
  while (candidateCursor.body.sequence > acceptedCursor.body.sequence) {
1326
1549
  candidateChild = candidateCursor;
1327
- const parent = await this.parentOf(candidateCursor, resolutionCache);
1550
+ const interrupted = this.takeExactParentEdges(traversal, 1);
1551
+ if (interrupted !== undefined) return interrupted;
1552
+ const parent = await this.parentOf(
1553
+ candidateCursor,
1554
+ resolutionCache,
1555
+ traversal,
1556
+ );
1328
1557
  if (parent.status !== "found") {
1329
1558
  return this.candidateAncestryResult(parent);
1330
1559
  }
@@ -1332,7 +1561,13 @@ export class TrustedNetworkV2PolicyReducer {
1332
1561
  }
1333
1562
  while (acceptedCursor.body.sequence > candidateCursor.body.sequence) {
1334
1563
  acceptedChild = acceptedCursor;
1335
- const parent = await this.parentOf(acceptedCursor, resolutionCache);
1564
+ const interrupted = this.takeExactParentEdges(traversal, 1);
1565
+ if (interrupted !== undefined) return interrupted;
1566
+ const parent = await this.parentOf(
1567
+ acceptedCursor,
1568
+ resolutionCache,
1569
+ traversal,
1570
+ );
1336
1571
  if (parent.status !== "found") {
1337
1572
  return this.acceptedAncestryUnavailable(parent);
1338
1573
  }
@@ -1351,9 +1586,11 @@ export class TrustedNetworkV2PolicyReducer {
1351
1586
  }
1352
1587
  candidateChild = candidateCursor;
1353
1588
  acceptedChild = acceptedCursor;
1589
+ const interrupted = this.takeExactParentEdges(traversal, 2);
1590
+ if (interrupted !== undefined) return interrupted;
1354
1591
  const [candidateParent, acceptedParent] = await Promise.all([
1355
- this.parentOf(candidateCursor, resolutionCache),
1356
- this.parentOf(acceptedCursor, resolutionCache),
1592
+ this.parentOf(candidateCursor, resolutionCache, traversal),
1593
+ this.parentOf(acceptedCursor, resolutionCache, traversal),
1357
1594
  ]);
1358
1595
  if (acceptedParent.status !== "found") {
1359
1596
  return this.acceptedAncestryUnavailable(acceptedParent);
@@ -1633,6 +1870,13 @@ export class TrustedNetworkV2PolicyReducer {
1633
1870
  } catch (error) {
1634
1871
  return this.result("rejected", validationMessage(error));
1635
1872
  }
1873
+ return this.ingestSnapshot(snapshot);
1874
+ }
1875
+
1876
+ private async ingestSnapshot(
1877
+ snapshot: ValidatedPolicySnapshotV2,
1878
+ traversal?: ExactPolicyTraversalV2,
1879
+ ): Promise<PolicyAdmissionResultV2> {
1636
1880
  if (this.lifecycleController.signal.aborted) return this.haltedResult();
1637
1881
 
1638
1882
  if (this.fork !== undefined) {
@@ -1644,9 +1888,10 @@ export class TrustedNetworkV2PolicyReducer {
1644
1888
  forkObservation === undefined ? undefined : [forkObservation],
1645
1889
  );
1646
1890
  }
1647
- this.retainCanonicalHeadEntry(snapshot);
1891
+ if (traversal === undefined) this.retainCanonicalHeadEntry(snapshot);
1648
1892
 
1649
1893
  if (this.unavailable !== undefined) {
1894
+ if (traversal !== undefined) return this.unavailableResult();
1650
1895
  if (this.acceptedHead?.digestKey === snapshot.digestKey) {
1651
1896
  return this.result("unavailable", this.unavailable.reason);
1652
1897
  }
@@ -1672,17 +1917,27 @@ export class TrustedNetworkV2PolicyReducer {
1672
1917
  }
1673
1918
 
1674
1919
  const existingPending = this.pending.get(snapshot.digestKey);
1675
- if (existingPending !== undefined) {
1920
+ if (existingPending !== undefined && traversal === undefined) {
1676
1921
  this.addPending(snapshot, existingPending.missingParentDigest);
1677
1922
  return this.result("pending", "Policy snapshot is already pending");
1678
1923
  }
1679
1924
 
1680
- const evaluation = await this.evaluate(snapshot);
1925
+ const evaluation = await this.evaluate(snapshot, traversal);
1681
1926
  if (this.lifecycleController.signal.aborted) return this.haltedResult();
1927
+ const interrupted = this.takeExactParentEdges(traversal, 0);
1928
+ if (evaluation.status === "interrupted" || interrupted !== undefined) {
1929
+ return this.result(
1930
+ "unavailable",
1931
+ evaluation.status === "interrupted"
1932
+ ? evaluation.reason
1933
+ : interrupted!.reason,
1934
+ );
1935
+ }
1682
1936
  if (evaluation.status === "reject") {
1683
1937
  return this.result("rejected", evaluation.reason);
1684
1938
  }
1685
1939
  if (evaluation.status === "duplicate") {
1940
+ if (traversal !== undefined) this.pending.delete(snapshot.digestKey);
1686
1941
  return this.result("duplicate");
1687
1942
  }
1688
1943
  if (evaluation.status === "fork") {
@@ -1703,6 +1958,11 @@ export class TrustedNetworkV2PolicyReducer {
1703
1958
  );
1704
1959
  }
1705
1960
 
1961
+ if (traversal !== undefined) {
1962
+ this.pending.delete(snapshot.digestKey);
1963
+ this.project(snapshot);
1964
+ return this.result("accepted");
1965
+ }
1706
1966
  this.project(snapshot);
1707
1967
  return (
1708
1968
  this.completedDrainResult(await this.drainPending()) ??