cojson 0.16.4 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/coValue.d.ts.map +1 -1
- package/dist/coValue.js.map +1 -1
- package/dist/coValueCore/coValueCore.d.ts +6 -10
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +15 -122
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValues/group.d.ts +18 -10
- package/dist/coValues/group.d.ts.map +1 -1
- package/dist/coValues/group.js +221 -59
- package/dist/coValues/group.js.map +1 -1
- package/dist/ids.d.ts +3 -3
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js.map +1 -1
- package/dist/localNode.d.ts +5 -5
- package/dist/localNode.d.ts.map +1 -1
- package/dist/tests/group.inheritance.test.js +195 -0
- package/dist/tests/group.inheritance.test.js.map +1 -1
- package/dist/tests/group.removeMember.test.js +152 -1
- package/dist/tests/group.removeMember.test.js.map +1 -1
- package/dist/tests/group.roleOf.test.js +2 -2
- package/dist/tests/group.roleOf.test.js.map +1 -1
- package/dist/tests/group.test.js +81 -3
- package/dist/tests/group.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +6 -3
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/testUtils.d.ts +1 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +5 -0
- package/dist/tests/testUtils.js.map +1 -1
- package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts +2 -2
- package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts.map +1 -1
- package/dist/typeUtils/expectGroup.d.ts.map +1 -1
- package/dist/typeUtils/expectGroup.js +6 -5
- package/dist/typeUtils/expectGroup.js.map +1 -1
- package/package.json +1 -1
- package/src/coValue.ts +1 -4
- package/src/coValueCore/coValueCore.ts +23 -188
- package/src/coValues/group.ts +310 -91
- package/src/ids.ts +3 -3
- package/src/localNode.ts +7 -7
- package/src/tests/group.inheritance.test.ts +279 -0
- package/src/tests/group.removeMember.test.ts +244 -1
- package/src/tests/group.roleOf.test.ts +2 -2
- package/src/tests/group.test.ts +105 -5
- package/src/tests/sync.load.test.ts +6 -3
- package/src/tests/testUtils.ts +5 -0
- package/src/typeUtils/accountOrAgentIDfromSessionID.ts +2 -2
- package/src/typeUtils/expectGroup.ts +8 -5
package/src/coValues/group.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { base58 } from "@scure/base";
|
|
2
|
-
import { CoID } from "../coValue.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
529
|
-
|
|
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
|
|
547
|
-
|
|
548
|
-
const maybeCurrentReadKey = this.core.getCurrentReadKey();
|
|
733
|
+
const maybeCurrentReadKey = this.getCurrentReadKey();
|
|
549
734
|
|
|
550
735
|
if (!maybeCurrentReadKey.secret) {
|
|
551
|
-
throw new
|
|
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.
|
|
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,16 +842,26 @@ export class RawGroup<
|
|
|
655
842
|
);
|
|
656
843
|
}
|
|
657
844
|
|
|
658
|
-
|
|
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
|
-
|
|
850
|
+
return;
|
|
664
851
|
}
|
|
665
852
|
|
|
666
|
-
|
|
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 */
|
|
@@ -695,6 +892,19 @@ export class RawGroup<
|
|
|
695
892
|
}
|
|
696
893
|
}
|
|
697
894
|
|
|
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
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
698
908
|
extend(
|
|
699
909
|
parent: RawGroup,
|
|
700
910
|
role: "reader" | "writer" | "admin" | "inherit" = "inherit",
|
|
@@ -727,14 +937,15 @@ export class RawGroup<
|
|
|
727
937
|
);
|
|
728
938
|
}
|
|
729
939
|
|
|
730
|
-
|
|
731
|
-
parent.
|
|
940
|
+
let { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
941
|
+
parent.getCurrentReadKey();
|
|
942
|
+
|
|
732
943
|
if (!parentReadKeySecret) {
|
|
733
944
|
throw new Error("Can't extend group without parent read key secret");
|
|
734
945
|
}
|
|
735
946
|
|
|
736
947
|
const { id: childReadKeyID, secret: childReadKeySecret } =
|
|
737
|
-
this.
|
|
948
|
+
this.getCurrentReadKey();
|
|
738
949
|
if (!childReadKeySecret) {
|
|
739
950
|
throw new Error("Can't extend group without child read key secret");
|
|
740
951
|
}
|
|
@@ -755,7 +966,7 @@ export class RawGroup<
|
|
|
755
966
|
);
|
|
756
967
|
}
|
|
757
968
|
|
|
758
|
-
|
|
969
|
+
revokeExtend(parent: RawGroup) {
|
|
759
970
|
if (this.myRole() !== "admin") {
|
|
760
971
|
throw new Error(
|
|
761
972
|
"To unextend a group, the current account must be an admin in the child group",
|
|
@@ -786,8 +997,6 @@ export class RawGroup<
|
|
|
786
997
|
// Set the child key on the parent group to `revoked`
|
|
787
998
|
parent.set(`child_${this.id}`, "revoked", "trusting");
|
|
788
999
|
|
|
789
|
-
await this.loadAllChildGroups();
|
|
790
|
-
|
|
791
1000
|
// Rotate the keys on the child group
|
|
792
1001
|
this.rotateReadKey();
|
|
793
1002
|
}
|
|
@@ -799,19 +1008,7 @@ export class RawGroup<
|
|
|
799
1008
|
*
|
|
800
1009
|
* @category 2. Role changing
|
|
801
1010
|
*/
|
|
802
|
-
|
|
803
|
-
account: RawAccount | ControlledAccountOrAgent | Everyone,
|
|
804
|
-
) {
|
|
805
|
-
// Ensure all child groups are loaded before removing a member
|
|
806
|
-
await this.loadAllChildGroups();
|
|
807
|
-
|
|
808
|
-
this.removeMemberInternal(account);
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/** @internal */
|
|
812
|
-
removeMemberInternal(
|
|
813
|
-
account: RawAccount | ControlledAccountOrAgent | AgentID | Everyone,
|
|
814
|
-
) {
|
|
1011
|
+
removeMember(account: RawAccount | ControlledAccountOrAgent | Everyone) {
|
|
815
1012
|
const memberKey = typeof account === "string" ? account : account.id;
|
|
816
1013
|
|
|
817
1014
|
if (this.myRole() === "admin") {
|
|
@@ -1022,3 +1219,25 @@ export function secretSeedFromInviteSecret(inviteSecret: InviteSecret) {
|
|
|
1022
1219
|
|
|
1023
1220
|
return base58.decode(inviteSecret.slice("inviteSecret_z".length));
|
|
1024
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";
|