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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +20 -0
- package/dist/coValue.d.ts +1 -1
- package/dist/coValue.d.ts.map +1 -1
- package/dist/coValue.js.map +1 -1
- package/dist/coValueContentMessage.d.ts +10 -0
- package/dist/coValueContentMessage.d.ts.map +1 -0
- package/dist/coValueContentMessage.js +46 -0
- package/dist/coValueContentMessage.js.map +1 -0
- package/dist/coValueCore/coValueCore.d.ts +6 -10
- package/dist/coValueCore/coValueCore.d.ts.map +1 -1
- package/dist/coValueCore/coValueCore.js +20 -125
- package/dist/coValueCore/coValueCore.js.map +1 -1
- package/dist/coValueCore/verifiedState.d.ts +1 -0
- package/dist/coValueCore/verifiedState.d.ts.map +1 -1
- package/dist/coValueCore/verifiedState.js +14 -27
- package/dist/coValueCore/verifiedState.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 +237 -67
- 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 +11 -6
- package/dist/localNode.d.ts.map +1 -1
- package/dist/localNode.js +7 -2
- package/dist/localNode.js.map +1 -1
- package/dist/queue/LocalTransactionsSyncQueue.d.ts +24 -0
- package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -0
- package/dist/queue/LocalTransactionsSyncQueue.js +55 -0
- package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -0
- package/dist/queue/StoreQueue.d.ts +9 -6
- package/dist/queue/StoreQueue.d.ts.map +1 -1
- package/dist/queue/StoreQueue.js +10 -2
- package/dist/queue/StoreQueue.js.map +1 -1
- package/dist/storage/storageAsync.d.ts +11 -3
- package/dist/storage/storageAsync.d.ts.map +1 -1
- package/dist/storage/storageAsync.js +59 -46
- package/dist/storage/storageAsync.js.map +1 -1
- package/dist/storage/storageSync.d.ts +9 -3
- package/dist/storage/storageSync.d.ts.map +1 -1
- package/dist/storage/storageSync.js +48 -35
- package/dist/storage/storageSync.js.map +1 -1
- package/dist/storage/syncUtils.d.ts +2 -1
- package/dist/storage/syncUtils.d.ts.map +1 -1
- package/dist/storage/syncUtils.js +4 -0
- package/dist/storage/syncUtils.js.map +1 -1
- package/dist/storage/types.d.ts +3 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/sync.d.ts +6 -6
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +33 -56
- package/dist/sync.js.map +1 -1
- package/dist/tests/StorageApiAsync.test.d.ts +2 -0
- package/dist/tests/StorageApiAsync.test.d.ts.map +1 -0
- package/dist/tests/StorageApiAsync.test.js +574 -0
- package/dist/tests/StorageApiAsync.test.js.map +1 -0
- package/dist/tests/StorageApiSync.test.d.ts +2 -0
- package/dist/tests/StorageApiSync.test.d.ts.map +1 -0
- package/dist/tests/StorageApiSync.test.js +426 -0
- package/dist/tests/StorageApiSync.test.js.map +1 -0
- package/dist/tests/StoreQueue.test.js +9 -21
- package/dist/tests/StoreQueue.test.js.map +1 -1
- package/dist/tests/SyncStateManager.test.js +18 -8
- package/dist/tests/SyncStateManager.test.js.map +1 -1
- package/dist/tests/group.inheritance.test.js +274 -2
- 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.auth.test.js +22 -10
- package/dist/tests/sync.auth.test.js.map +1 -1
- package/dist/tests/sync.load.test.js +30 -25
- package/dist/tests/sync.load.test.js.map +1 -1
- package/dist/tests/sync.mesh.test.js +12 -6
- package/dist/tests/sync.mesh.test.js.map +1 -1
- package/dist/tests/sync.peerReconciliation.test.js +6 -4
- package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
- package/dist/tests/sync.storage.test.js +8 -14
- package/dist/tests/sync.storage.test.js.map +1 -1
- package/dist/tests/sync.storageAsync.test.js +31 -14
- package/dist/tests/sync.storageAsync.test.js.map +1 -1
- package/dist/tests/sync.test.js +5 -9
- package/dist/tests/sync.test.js.map +1 -1
- package/dist/tests/sync.upload.test.js +31 -1
- package/dist/tests/sync.upload.test.js.map +1 -1
- package/dist/tests/testStorage.d.ts +2 -3
- package/dist/tests/testStorage.d.ts.map +1 -1
- package/dist/tests/testStorage.js +16 -8
- package/dist/tests/testStorage.js.map +1 -1
- package/dist/tests/testUtils.d.ts +4 -0
- package/dist/tests/testUtils.d.ts.map +1 -1
- package/dist/tests/testUtils.js +22 -4
- 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/coValueContentMessage.ts +73 -0
- package/src/coValueCore/coValueCore.ts +36 -192
- package/src/coValueCore/verifiedState.ts +28 -35
- package/src/coValues/group.ts +329 -99
- package/src/ids.ts +3 -3
- package/src/localNode.ts +15 -10
- package/src/queue/LocalTransactionsSyncQueue.ts +96 -0
- package/src/queue/StoreQueue.ts +22 -12
- package/src/storage/storageAsync.ts +78 -56
- package/src/storage/storageSync.ts +66 -45
- package/src/storage/syncUtils.ts +9 -1
- package/src/storage/types.ts +6 -5
- package/src/sync.ts +47 -67
- package/src/tests/StorageApiAsync.test.ts +829 -0
- package/src/tests/StorageApiSync.test.ts +628 -0
- package/src/tests/StoreQueue.test.ts +10 -24
- package/src/tests/SyncStateManager.test.ts +22 -21
- package/src/tests/group.inheritance.test.ts +415 -1
- 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.auth.test.ts +22 -10
- package/src/tests/sync.load.test.ts +32 -26
- package/src/tests/sync.mesh.test.ts +12 -6
- package/src/tests/sync.peerReconciliation.test.ts +6 -4
- package/src/tests/sync.storage.test.ts +8 -14
- package/src/tests/sync.storageAsync.test.ts +39 -14
- package/src/tests/sync.test.ts +6 -14
- package/src/tests/sync.upload.test.ts +38 -1
- package/src/tests/testStorage.ts +19 -13
- package/src/tests/testUtils.ts +29 -5
- 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,33 +842,67 @@ 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 */
|
|
671
868
|
isSelfExtension(parent: RawGroup) {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
869
|
+
const checkedGroups = new Set<string>();
|
|
870
|
+
const queue = [parent];
|
|
871
|
+
|
|
872
|
+
while (true) {
|
|
873
|
+
const current = queue.pop();
|
|
675
874
|
|
|
676
|
-
|
|
875
|
+
if (!current) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
677
878
|
|
|
678
|
-
|
|
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
|
-
|
|
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
|
-
|
|
720
|
-
parent.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
|
|
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
|
|