cojson 0.16.3 → 0.16.5

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.
Files changed (138) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/coValue.d.ts +1 -1
  4. package/dist/coValue.d.ts.map +1 -1
  5. package/dist/coValue.js.map +1 -1
  6. package/dist/coValueContentMessage.d.ts +10 -0
  7. package/dist/coValueContentMessage.d.ts.map +1 -0
  8. package/dist/coValueContentMessage.js +46 -0
  9. package/dist/coValueContentMessage.js.map +1 -0
  10. package/dist/coValueCore/coValueCore.d.ts +6 -10
  11. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  12. package/dist/coValueCore/coValueCore.js +20 -125
  13. package/dist/coValueCore/coValueCore.js.map +1 -1
  14. package/dist/coValueCore/verifiedState.d.ts +1 -0
  15. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  16. package/dist/coValueCore/verifiedState.js +14 -27
  17. package/dist/coValueCore/verifiedState.js.map +1 -1
  18. package/dist/coValues/group.d.ts +18 -10
  19. package/dist/coValues/group.d.ts.map +1 -1
  20. package/dist/coValues/group.js +237 -67
  21. package/dist/coValues/group.js.map +1 -1
  22. package/dist/ids.d.ts +3 -3
  23. package/dist/ids.d.ts.map +1 -1
  24. package/dist/ids.js.map +1 -1
  25. package/dist/localNode.d.ts +11 -6
  26. package/dist/localNode.d.ts.map +1 -1
  27. package/dist/localNode.js +7 -2
  28. package/dist/localNode.js.map +1 -1
  29. package/dist/queue/LocalTransactionsSyncQueue.d.ts +24 -0
  30. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -0
  31. package/dist/queue/LocalTransactionsSyncQueue.js +55 -0
  32. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -0
  33. package/dist/queue/StoreQueue.d.ts +9 -6
  34. package/dist/queue/StoreQueue.d.ts.map +1 -1
  35. package/dist/queue/StoreQueue.js +10 -2
  36. package/dist/queue/StoreQueue.js.map +1 -1
  37. package/dist/storage/storageAsync.d.ts +11 -3
  38. package/dist/storage/storageAsync.d.ts.map +1 -1
  39. package/dist/storage/storageAsync.js +59 -46
  40. package/dist/storage/storageAsync.js.map +1 -1
  41. package/dist/storage/storageSync.d.ts +9 -3
  42. package/dist/storage/storageSync.d.ts.map +1 -1
  43. package/dist/storage/storageSync.js +48 -35
  44. package/dist/storage/storageSync.js.map +1 -1
  45. package/dist/storage/syncUtils.d.ts +2 -1
  46. package/dist/storage/syncUtils.d.ts.map +1 -1
  47. package/dist/storage/syncUtils.js +4 -0
  48. package/dist/storage/syncUtils.js.map +1 -1
  49. package/dist/storage/types.d.ts +3 -2
  50. package/dist/storage/types.d.ts.map +1 -1
  51. package/dist/sync.d.ts +6 -6
  52. package/dist/sync.d.ts.map +1 -1
  53. package/dist/sync.js +33 -56
  54. package/dist/sync.js.map +1 -1
  55. package/dist/tests/StorageApiAsync.test.d.ts +2 -0
  56. package/dist/tests/StorageApiAsync.test.d.ts.map +1 -0
  57. package/dist/tests/StorageApiAsync.test.js +574 -0
  58. package/dist/tests/StorageApiAsync.test.js.map +1 -0
  59. package/dist/tests/StorageApiSync.test.d.ts +2 -0
  60. package/dist/tests/StorageApiSync.test.d.ts.map +1 -0
  61. package/dist/tests/StorageApiSync.test.js +426 -0
  62. package/dist/tests/StorageApiSync.test.js.map +1 -0
  63. package/dist/tests/StoreQueue.test.js +9 -21
  64. package/dist/tests/StoreQueue.test.js.map +1 -1
  65. package/dist/tests/SyncStateManager.test.js +18 -8
  66. package/dist/tests/SyncStateManager.test.js.map +1 -1
  67. package/dist/tests/group.inheritance.test.js +274 -2
  68. package/dist/tests/group.inheritance.test.js.map +1 -1
  69. package/dist/tests/group.removeMember.test.js +152 -1
  70. package/dist/tests/group.removeMember.test.js.map +1 -1
  71. package/dist/tests/group.roleOf.test.js +2 -2
  72. package/dist/tests/group.roleOf.test.js.map +1 -1
  73. package/dist/tests/group.test.js +81 -3
  74. package/dist/tests/group.test.js.map +1 -1
  75. package/dist/tests/sync.auth.test.js +22 -10
  76. package/dist/tests/sync.auth.test.js.map +1 -1
  77. package/dist/tests/sync.load.test.js +30 -25
  78. package/dist/tests/sync.load.test.js.map +1 -1
  79. package/dist/tests/sync.mesh.test.js +12 -6
  80. package/dist/tests/sync.mesh.test.js.map +1 -1
  81. package/dist/tests/sync.peerReconciliation.test.js +6 -4
  82. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  83. package/dist/tests/sync.storage.test.js +8 -14
  84. package/dist/tests/sync.storage.test.js.map +1 -1
  85. package/dist/tests/sync.storageAsync.test.js +31 -14
  86. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  87. package/dist/tests/sync.test.js +5 -9
  88. package/dist/tests/sync.test.js.map +1 -1
  89. package/dist/tests/sync.upload.test.js +31 -1
  90. package/dist/tests/sync.upload.test.js.map +1 -1
  91. package/dist/tests/testStorage.d.ts +2 -3
  92. package/dist/tests/testStorage.d.ts.map +1 -1
  93. package/dist/tests/testStorage.js +16 -8
  94. package/dist/tests/testStorage.js.map +1 -1
  95. package/dist/tests/testUtils.d.ts +4 -0
  96. package/dist/tests/testUtils.d.ts.map +1 -1
  97. package/dist/tests/testUtils.js +22 -4
  98. package/dist/tests/testUtils.js.map +1 -1
  99. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts +2 -2
  100. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts.map +1 -1
  101. package/dist/typeUtils/expectGroup.d.ts.map +1 -1
  102. package/dist/typeUtils/expectGroup.js +6 -5
  103. package/dist/typeUtils/expectGroup.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/coValue.ts +1 -4
  106. package/src/coValueContentMessage.ts +73 -0
  107. package/src/coValueCore/coValueCore.ts +36 -192
  108. package/src/coValueCore/verifiedState.ts +28 -35
  109. package/src/coValues/group.ts +329 -99
  110. package/src/ids.ts +3 -3
  111. package/src/localNode.ts +15 -10
  112. package/src/queue/LocalTransactionsSyncQueue.ts +96 -0
  113. package/src/queue/StoreQueue.ts +22 -12
  114. package/src/storage/storageAsync.ts +78 -56
  115. package/src/storage/storageSync.ts +66 -45
  116. package/src/storage/syncUtils.ts +9 -1
  117. package/src/storage/types.ts +6 -5
  118. package/src/sync.ts +47 -67
  119. package/src/tests/StorageApiAsync.test.ts +829 -0
  120. package/src/tests/StorageApiSync.test.ts +628 -0
  121. package/src/tests/StoreQueue.test.ts +10 -24
  122. package/src/tests/SyncStateManager.test.ts +22 -21
  123. package/src/tests/group.inheritance.test.ts +415 -1
  124. package/src/tests/group.removeMember.test.ts +244 -1
  125. package/src/tests/group.roleOf.test.ts +2 -2
  126. package/src/tests/group.test.ts +105 -5
  127. package/src/tests/sync.auth.test.ts +22 -10
  128. package/src/tests/sync.load.test.ts +32 -26
  129. package/src/tests/sync.mesh.test.ts +12 -6
  130. package/src/tests/sync.peerReconciliation.test.ts +6 -4
  131. package/src/tests/sync.storage.test.ts +8 -14
  132. package/src/tests/sync.storageAsync.test.ts +39 -14
  133. package/src/tests/sync.test.ts +6 -14
  134. package/src/tests/sync.upload.test.ts +38 -1
  135. package/src/tests/testStorage.ts +19 -13
  136. package/src/tests/testUtils.ts +29 -5
  137. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +2 -2
  138. package/src/typeUtils/expectGroup.ts +8 -5
@@ -1,8 +1,11 @@
1
1
  import { base58 } from "@scure/base";
2
- import { CoID } from "../coValue.js";
3
- import { AvailableCoValueCore } from "../coValueCore/coValueCore.js";
4
- import { CoValueUniqueness } from "../coValueCore/verifiedState.js";
5
- import {
2
+ import type { CoID } from "../coValue.js";
3
+ import type {
4
+ AvailableCoValueCore,
5
+ CoValueCore,
6
+ } from "../coValueCore/coValueCore.js";
7
+ import type { CoValueUniqueness } from "../coValueCore/verifiedState.js";
8
+ import type {
6
9
  CryptoProvider,
7
10
  Encrypted,
8
11
  KeyID,
@@ -21,8 +24,10 @@ import {
21
24
  } from "../ids.js";
22
25
  import { JsonObject } from "../jsonValue.js";
23
26
  import { logger } from "../logger.js";
24
- import { AccountRole, Role } from "../permissions.js";
27
+ import { AccountRole, Role, isKeyForKeyField } from "../permissions.js";
28
+ import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
25
29
  import { expectGroup } from "../typeUtils/expectGroup.js";
30
+ import { isAccountID } from "../typeUtils/isAccountID.js";
26
31
  import {
27
32
  ControlledAccountOrAgent,
28
33
  RawAccount,
@@ -60,6 +65,59 @@ export type GroupShape = {
60
65
  [child: ChildGroupReference]: "revoked" | "extend";
61
66
  };
62
67
 
68
+ // We had a bug on key rotation, where the new read key was not revealed to everyone
69
+ // TODO: remove this when we hit the 0.18.0 release (either the groups are healed or they are not used often, it's a minor issue anyway)
70
+ function healMissingKeyForEveryone(group: RawGroup) {
71
+ const readKeyId = group.get("readKey");
72
+
73
+ if (
74
+ !readKeyId ||
75
+ !canRead(group, EVERYONE) ||
76
+ group.get(`${readKeyId}_for_${EVERYONE}`)
77
+ ) {
78
+ return;
79
+ }
80
+
81
+ const hasAccessToReadKey = canRead(
82
+ group,
83
+ group.core.node.getCurrentAgent().id,
84
+ );
85
+
86
+ // If the current account has access to the read key, we can fix the group
87
+ if (hasAccessToReadKey) {
88
+ const secret = group.getReadKey(readKeyId);
89
+ if (secret) {
90
+ group.set(`${readKeyId}_for_${EVERYONE}`, secret, "trusting");
91
+ }
92
+ return;
93
+ }
94
+
95
+ // Fallback to the latest readable key for everyone
96
+ const keys = group
97
+ .keys()
98
+ .filter((key) => key.startsWith("key_") && key.endsWith("_for_everyone"));
99
+
100
+ let latestKey = keys[0];
101
+
102
+ for (const key of keys) {
103
+ if (!latestKey) {
104
+ latestKey = key;
105
+ continue;
106
+ }
107
+
108
+ const keyEntry = group.getRaw(key);
109
+ const latestKeyEntry = group.getRaw(latestKey);
110
+
111
+ if (keyEntry && latestKeyEntry && keyEntry.madeAt > latestKeyEntry.madeAt) {
112
+ latestKey = key;
113
+ }
114
+ }
115
+
116
+ if (latestKey) {
117
+ group._lastReadableKeyId = latestKey.replace("_for_everyone", "") as KeyID;
118
+ }
119
+ }
120
+
63
121
  /** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
64
122
  *
65
123
  * A `Group` object exposes methods for permission management and allows you to create new CoValues owned by that group.
@@ -86,6 +144,8 @@ export class RawGroup<
86
144
  > extends RawCoMap<GroupShape, Meta> {
87
145
  protected readonly crypto: CryptoProvider;
88
146
 
147
+ _lastReadableKeyId?: KeyID;
148
+
89
149
  constructor(
90
150
  core: AvailableCoValueCore,
91
151
  options?: {
@@ -94,6 +154,8 @@ export class RawGroup<
94
154
  ) {
95
155
  super(core, options);
96
156
  this.crypto = core.node.crypto;
157
+
158
+ healMissingKeyForEveryone(this);
97
159
  }
98
160
 
99
161
  /**
@@ -191,43 +253,7 @@ export class RawGroup<
191
253
  return groups;
192
254
  }
193
255
 
194
- loadAllChildGroups() {
195
- const requests: Promise<unknown>[] = [];
196
- const peers = this.core.node.syncManager.getServerPeers();
197
-
198
- for (const key of this.keys()) {
199
- if (!isChildGroupReference(key)) {
200
- continue;
201
- }
202
-
203
- const id = getChildGroupId(key);
204
- const child = this.core.node.getCoValue(id);
205
-
206
- if (
207
- child.loadingState === "unknown" ||
208
- child.loadingState === "unavailable"
209
- ) {
210
- child.load(peers);
211
- }
212
-
213
- requests.push(
214
- child.waitForAvailableOrUnavailable().then((coValue) => {
215
- if (!coValue.isAvailable()) {
216
- throw new Error(`Child group ${child.id} is unavailable`);
217
- }
218
-
219
- // Recursively load child groups
220
- return expectGroup(coValue.getCurrentContent()).loadAllChildGroups();
221
- }),
222
- );
223
- }
224
-
225
- return Promise.all(requests);
226
- }
227
-
228
- getChildGroups() {
229
- const groups: RawGroup[] = [];
230
-
256
+ forEachChildGroup(callback: (child: RawGroup) => void) {
231
257
  for (const key of this.keys()) {
232
258
  if (isChildGroupReference(key)) {
233
259
  // Check if the child group reference is revoked
@@ -235,15 +261,22 @@ export class RawGroup<
235
261
  continue;
236
262
  }
237
263
 
238
- const child = this.core.node.expectCoValueLoaded(
239
- getChildGroupId(key),
240
- "Expected child group to be loaded",
241
- );
242
- groups.push(expectGroup(child.getCurrentContent()));
264
+ const id = getChildGroupId(key);
265
+ const child = this.core.node.getCoValue(id);
266
+
267
+ if (child.isAvailable()) {
268
+ callback(expectGroup(child.getCurrentContent()));
269
+ } else {
270
+ this.core.node.load(id).then((child) => {
271
+ if (child !== "unavailable") {
272
+ callback(expectGroup(child));
273
+ } else {
274
+ logger.warn(`Unable to load child group ${id}, skipping`);
275
+ }
276
+ });
277
+ }
243
278
  }
244
279
  }
245
-
246
- return groups;
247
280
  }
248
281
 
249
282
  /**
@@ -279,7 +312,7 @@ export class RawGroup<
279
312
  "Can't make everyone something other than reader, writer or writeOnly",
280
313
  );
281
314
  }
282
- const currentReadKey = this.core.getCurrentReadKey();
315
+ const currentReadKey = this.getCurrentReadKey();
283
316
 
284
317
  if (!currentReadKey.secret) {
285
318
  throw new Error("Can't add member without read key secret");
@@ -306,7 +339,7 @@ export class RawGroup<
306
339
 
307
340
  if (role === "writeOnly") {
308
341
  if (previousRole === "reader" || previousRole === "writer") {
309
- this.rotateReadKey();
342
+ this.rotateReadKey("everyone");
310
343
  }
311
344
 
312
345
  this.delete(`${currentReadKey.id}_for_${EVERYONE}`);
@@ -349,7 +382,7 @@ export class RawGroup<
349
382
 
350
383
  this.internalCreateWriteOnlyKeyForMember(memberKey, agent);
351
384
  } else {
352
- const currentReadKey = this.core.getCurrentReadKey();
385
+ const currentReadKey = this.getCurrentReadKey();
353
386
 
354
387
  if (!currentReadKey.secret) {
355
388
  throw new Error("Can't add member without read key secret");
@@ -467,6 +500,10 @@ export class RawGroup<
467
500
  }
468
501
 
469
502
  getCurrentReadKeyId() {
503
+ if (this._lastReadableKeyId) {
504
+ return this._lastReadableKeyId;
505
+ }
506
+
470
507
  const myRole = this.myRole();
471
508
 
472
509
  if (myRole === "writeOnly") {
@@ -518,23 +555,173 @@ export class RawGroup<
518
555
  return memberKeys;
519
556
  }
520
557
 
558
+ getReadKey(keyID: KeyID): KeySecret | undefined {
559
+ const cache = this.core.readKeyCache;
560
+
561
+ let key = cache.get(keyID);
562
+ if (!key) {
563
+ key = this.getUncachedReadKey(keyID);
564
+ if (key) {
565
+ cache.set(keyID, key);
566
+ }
567
+ }
568
+ return key;
569
+ }
570
+
571
+ getUncachedReadKey(keyID: KeyID) {
572
+ const core = this.core;
573
+
574
+ const keyForEveryone = this.get(`${keyID}_for_everyone`);
575
+ if (keyForEveryone) {
576
+ return keyForEveryone;
577
+ }
578
+
579
+ // Try to find key revelation for us
580
+ const currentAgentOrAccountID = accountOrAgentIDfromSessionID(
581
+ core.node.currentSessionID,
582
+ );
583
+
584
+ // being careful here to avoid recursion
585
+ const lookupAccountOrAgentID = isAccountID(currentAgentOrAccountID)
586
+ ? core.id === currentAgentOrAccountID
587
+ ? core.node.crypto.getAgentID(core.node.agentSecret) // in accounts, the read key is revealed for the primitive agent
588
+ : currentAgentOrAccountID // current account ID
589
+ : currentAgentOrAccountID; // current agent ID
590
+
591
+ const lastReadyKeyEdit = this.lastEditAt(
592
+ `${keyID}_for_${lookupAccountOrAgentID}`,
593
+ );
594
+
595
+ if (lastReadyKeyEdit?.value) {
596
+ const revealer = lastReadyKeyEdit.by;
597
+ const revealerAgent = core.node
598
+ .resolveAccountAgent(revealer, "Expected to know revealer")
599
+ ._unsafeUnwrap({ withStackTrace: true });
600
+
601
+ const secret = this.crypto.unseal(
602
+ lastReadyKeyEdit.value,
603
+ this.crypto.getAgentSealerSecret(core.node.agentSecret), // being careful here to avoid recursion
604
+ this.crypto.getAgentSealerID(revealerAgent),
605
+ {
606
+ in: this.id,
607
+ tx: lastReadyKeyEdit.tx,
608
+ },
609
+ );
610
+
611
+ if (secret) {
612
+ return secret as KeySecret;
613
+ }
614
+ }
615
+
616
+ // Try to find indirect revelation through previousKeys
617
+ for (const co of this.keys()) {
618
+ if (isKeyForKeyField(co) && co.startsWith(keyID)) {
619
+ const encryptingKeyID = co.split("_for_")[1] as KeyID;
620
+ const encryptingKeySecret = this.getReadKey(encryptingKeyID);
621
+
622
+ if (!encryptingKeySecret) {
623
+ continue;
624
+ }
625
+
626
+ const encryptedPreviousKey = this.get(co)!;
627
+
628
+ const secret = this.crypto.decryptKeySecret(
629
+ {
630
+ encryptedID: keyID,
631
+ encryptingID: encryptingKeyID,
632
+ encrypted: encryptedPreviousKey,
633
+ },
634
+ encryptingKeySecret,
635
+ );
636
+
637
+ if (secret) {
638
+ return secret as KeySecret;
639
+ } else {
640
+ logger.warn(
641
+ `Encrypting ${encryptingKeyID} key didn't decrypt ${keyID}`,
642
+ );
643
+ }
644
+ }
645
+ }
646
+
647
+ // try to find revelation to parent group read keys
648
+ for (const co of this.keys()) {
649
+ if (isParentGroupReference(co)) {
650
+ const parentGroupID = getParentGroupId(co);
651
+ const parentGroup = core.node.expectCoValueLoaded(
652
+ parentGroupID,
653
+ "Expected parent group to be loaded",
654
+ );
655
+
656
+ const parentKeys = this.findValidParentKeys(keyID, parentGroup);
657
+
658
+ for (const parentKey of parentKeys) {
659
+ const revelationForParentKey = this.get(
660
+ `${keyID}_for_${parentKey.id}`,
661
+ );
662
+
663
+ if (revelationForParentKey) {
664
+ const secret = parentGroup.node.crypto.decryptKeySecret(
665
+ {
666
+ encryptedID: keyID,
667
+ encryptingID: parentKey.id,
668
+ encrypted: revelationForParentKey,
669
+ },
670
+ parentKey.secret,
671
+ );
672
+
673
+ if (secret) {
674
+ return secret as KeySecret;
675
+ } else {
676
+ logger.warn(
677
+ `Encrypting parent ${parentKey.id} key didn't decrypt ${keyID}`,
678
+ );
679
+ }
680
+ }
681
+ }
682
+ }
683
+ }
684
+
685
+ return undefined;
686
+ }
687
+
688
+ findValidParentKeys(keyID: KeyID, parentGroup: CoValueCore) {
689
+ const validParentKeys: { id: KeyID; secret: KeySecret }[] = [];
690
+
691
+ for (const co of this.keys()) {
692
+ if (isKeyForKeyField(co) && co.startsWith(keyID)) {
693
+ const encryptingKeyID = co.split("_for_")[1] as KeyID;
694
+ const encryptingKeySecret = parentGroup.getReadKey(encryptingKeyID);
695
+
696
+ if (!encryptingKeySecret) {
697
+ continue;
698
+ }
699
+
700
+ validParentKeys.push({
701
+ id: encryptingKeyID,
702
+ secret: encryptingKeySecret,
703
+ });
704
+ }
705
+ }
706
+
707
+ return validParentKeys;
708
+ }
709
+
521
710
  /** @internal */
522
711
  rotateReadKey(removedMemberKey?: RawAccountID | AgentID | "everyone") {
712
+ if (removedMemberKey !== EVERYONE && canRead(this, EVERYONE)) {
713
+ // When everyone has access to the group, rotating the key is useless
714
+ // because it would be stored unencrypted and available to everyone
715
+ return;
716
+ }
717
+
523
718
  const memberKeys = this.getMemberKeys().filter(
524
719
  (key) => key !== removedMemberKey,
525
720
  );
526
721
 
527
- const currentlyPermittedReaders = memberKeys.filter((key) => {
528
- const role = this.get(key);
529
- return (
530
- role === "admin" ||
531
- role === "writer" ||
532
- role === "reader" ||
533
- role === "adminInvite" ||
534
- role === "writerInvite" ||
535
- role === "readerInvite"
536
- );
537
- });
722
+ const currentlyPermittedReaders = memberKeys.filter((key) =>
723
+ canRead(this, key),
724
+ );
538
725
 
539
726
  const writeOnlyMembers = memberKeys.filter((key) => {
540
727
  const role = this.get(key);
@@ -543,12 +730,12 @@ export class RawGroup<
543
730
 
544
731
  // Get these early, so we fail fast if they are unavailable
545
732
  const parentGroups = this.getParentGroups();
546
- const childGroups = this.getChildGroups();
547
-
548
- const maybeCurrentReadKey = this.core.getCurrentReadKey();
733
+ const maybeCurrentReadKey = this.getCurrentReadKey();
549
734
 
550
735
  if (!maybeCurrentReadKey.secret) {
551
- throw new Error("Can't rotate read key secret we don't have access to");
736
+ throw new NoReadKeyAccessError(
737
+ "Can't rotate read key secret we don't have access to",
738
+ );
552
739
  }
553
740
 
554
741
  const currentReadKey = {
@@ -631,7 +818,7 @@ export class RawGroup<
631
818
  */
632
819
  for (const parent of parentGroups) {
633
820
  const { id: parentReadKeyID, secret: parentReadKeySecret } =
634
- parent.core.getCurrentReadKey();
821
+ parent.getCurrentReadKey();
635
822
 
636
823
  if (!parentReadKeySecret) {
637
824
  // We can't reveal the new child key to the parent group where we don't have access to the parent read key
@@ -655,33 +842,67 @@ export class RawGroup<
655
842
  );
656
843
  }
657
844
 
658
- for (const child of childGroups) {
845
+ this.forEachChildGroup((child) => {
659
846
  // Since child references are mantained only for the key rotation,
660
847
  // circular references are skipped here because it's more performant
661
848
  // than always checking for circular references in childs inside the permission checks
662
849
  if (child.isSelfExtension(this)) {
663
- continue;
850
+ return;
664
851
  }
665
852
 
666
- child.rotateReadKey(removedMemberKey);
667
- }
853
+ try {
854
+ child.rotateReadKey(removedMemberKey);
855
+ } catch (error) {
856
+ if (error instanceof NoReadKeyAccessError) {
857
+ logger.warn(
858
+ `Can't rotate read key on child ${child.id} because we don't have access to the read key`,
859
+ );
860
+ } else {
861
+ throw error;
862
+ }
863
+ }
864
+ });
668
865
  }
669
866
 
670
867
  /** Detect circular references in group inheritance */
671
868
  isSelfExtension(parent: RawGroup) {
672
- if (parent.id === this.id) {
673
- return true;
674
- }
869
+ const checkedGroups = new Set<string>();
870
+ const queue = [parent];
871
+
872
+ while (true) {
873
+ const current = queue.pop();
675
874
 
676
- const childGroups = this.getChildGroups();
875
+ if (!current) {
876
+ return false;
877
+ }
677
878
 
678
- for (const child of childGroups) {
679
- if (child.isSelfExtension(parent)) {
879
+ if (current.id === this.id) {
680
880
  return true;
681
881
  }
882
+
883
+ checkedGroups.add(current.id);
884
+
885
+ const parentGroups = current.getParentGroups();
886
+
887
+ for (const parent of parentGroups) {
888
+ if (!checkedGroups.has(parent.id)) {
889
+ queue.push(parent);
890
+ }
891
+ }
682
892
  }
893
+ }
683
894
 
684
- return false;
895
+ getCurrentReadKey() {
896
+ const keyId = this.getCurrentReadKeyId();
897
+
898
+ if (!keyId) {
899
+ throw new Error("No readKey set");
900
+ }
901
+
902
+ return {
903
+ secret: this.getReadKey(keyId),
904
+ id: keyId,
905
+ };
685
906
  }
686
907
 
687
908
  extend(
@@ -700,8 +921,8 @@ export class RawGroup<
700
921
 
701
922
  const value = role === "inherit" ? "extend" : role;
702
923
 
703
- this.set(`parent_${parent.id}`, value, "trusting");
704
924
  parent.set(`child_${this.id}`, "extend", "trusting");
925
+ this.set(`parent_${parent.id}`, value, "trusting");
705
926
 
706
927
  if (
707
928
  parent.myRole() !== "admin" &&
@@ -716,14 +937,15 @@ export class RawGroup<
716
937
  );
717
938
  }
718
939
 
719
- const { id: parentReadKeyID, secret: parentReadKeySecret } =
720
- parent.core.getCurrentReadKey();
940
+ let { id: parentReadKeyID, secret: parentReadKeySecret } =
941
+ parent.getCurrentReadKey();
942
+
721
943
  if (!parentReadKeySecret) {
722
944
  throw new Error("Can't extend group without parent read key secret");
723
945
  }
724
946
 
725
947
  const { id: childReadKeyID, secret: childReadKeySecret } =
726
- this.core.getCurrentReadKey();
948
+ this.getCurrentReadKey();
727
949
  if (!childReadKeySecret) {
728
950
  throw new Error("Can't extend group without child read key secret");
729
951
  }
@@ -744,7 +966,7 @@ export class RawGroup<
744
966
  );
745
967
  }
746
968
 
747
- async revokeExtend(parent: RawGroup) {
969
+ revokeExtend(parent: RawGroup) {
748
970
  if (this.myRole() !== "admin") {
749
971
  throw new Error(
750
972
  "To unextend a group, the current account must be an admin in the child group",
@@ -775,8 +997,6 @@ export class RawGroup<
775
997
  // Set the child key on the parent group to `revoked`
776
998
  parent.set(`child_${this.id}`, "revoked", "trusting");
777
999
 
778
- await this.loadAllChildGroups();
779
-
780
1000
  // Rotate the keys on the child group
781
1001
  this.rotateReadKey();
782
1002
  }
@@ -788,19 +1008,7 @@ export class RawGroup<
788
1008
  *
789
1009
  * @category 2. Role changing
790
1010
  */
791
- async removeMember(
792
- account: RawAccount | ControlledAccountOrAgent | Everyone,
793
- ) {
794
- // Ensure all child groups are loaded before removing a member
795
- await this.loadAllChildGroups();
796
-
797
- this.removeMemberInternal(account);
798
- }
799
-
800
- /** @internal */
801
- removeMemberInternal(
802
- account: RawAccount | ControlledAccountOrAgent | AgentID | Everyone,
803
- ) {
1011
+ removeMember(account: RawAccount | ControlledAccountOrAgent | Everyone) {
804
1012
  const memberKey = typeof account === "string" ? account : account.id;
805
1013
 
806
1014
  if (this.myRole() === "admin") {
@@ -1011,3 +1219,25 @@ export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
1011
1219
 
1012
1220
  return base58.decode(inviteSecret.slice("inviteSecret_z".length));
1013
1221
  }
1222
+
1223
+ const canRead = (
1224
+ group: RawGroup,
1225
+ key: RawAccountID | AgentID | "everyone",
1226
+ ): boolean => {
1227
+ const role = group.get(key);
1228
+ return (
1229
+ role === "admin" ||
1230
+ role === "writer" ||
1231
+ role === "reader" ||
1232
+ role === "adminInvite" ||
1233
+ role === "writerInvite" ||
1234
+ role === "readerInvite"
1235
+ );
1236
+ };
1237
+
1238
+ class NoReadKeyAccessError extends Error {
1239
+ constructor(message: string) {
1240
+ super(message);
1241
+ this.name = "NoReadKeyAccessError";
1242
+ }
1243
+ }
package/src/ids.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { base58 } from "@scure/base";
2
- import { CoID } from "./coValue.js";
3
- import { RawAccountID } from "./coValues/account.js";
2
+ import type { CoID } from "./coValue.js";
3
+ import type { RawAccountID } from "./coValues/account.js";
4
+ import type { RawGroup } from "./coValues/group.js";
4
5
  import { shortHashLength } from "./crypto/crypto.js";
5
- import { RawGroup } from "./exports.js";
6
6
 
7
7
  export type RawCoID = `co_z${string}`;
8
8
  export type ParentGroupReference = `parent_${CoID<RawGroup>}`;
package/src/localNode.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  import { Result, err, ok } from "neverthrow";
2
- import { CoID } from "./coValue.js";
3
- import { RawCoValue } from "./coValue.js";
2
+ import type { CoID } from "./coValue.js";
3
+ import type { RawCoValue } from "./coValue.js";
4
4
  import {
5
- AvailableCoValueCore,
5
+ type AvailableCoValueCore,
6
6
  CoValueCore,
7
7
  idforHeader,
8
8
  } from "./coValueCore/coValueCore.js";
9
9
  import {
10
- CoValueHeader,
11
- CoValueUniqueness,
10
+ type CoValueHeader,
11
+ type CoValueUniqueness,
12
12
  VerifiedState,
13
13
  } from "./coValueCore/verifiedState.js";
14
14
  import {
@@ -26,8 +26,8 @@ import {
26
26
  expectAccount,
27
27
  } from "./coValues/account.js";
28
28
  import {
29
- InviteSecret,
30
- RawGroup,
29
+ type InviteSecret,
30
+ type RawGroup,
31
31
  secretSeedFromInviteSecret,
32
32
  } from "./coValues/group.js";
33
33
  import { CO_VALUE_LOADING_CONFIG } from "./config.js";
@@ -351,7 +351,7 @@ export class LocalNode {
351
351
  new VerifiedState(id, this.crypto, header, new Map()),
352
352
  );
353
353
 
354
- void this.syncManager.requestCoValueSync(coValue);
354
+ this.syncManager.syncHeader(coValue.verified);
355
355
 
356
356
  return coValue;
357
357
  }
@@ -738,9 +738,14 @@ export class LocalNode {
738
738
  }
739
739
  }
740
740
 
741
- gracefulShutdown() {
742
- this.storage?.close();
741
+ /**
742
+ * Closes all the peer connections, drains all the queues and closes the storage.
743
+ *
744
+ * @returns Promise of the current pending store operation, if any.
745
+ */
746
+ gracefulShutdown(): Promise<unknown> | undefined {
743
747
  this.syncManager.gracefulShutdown();
748
+ return this.storage?.close();
744
749
  }
745
750
  }
746
751