cojson 0.8.38 → 0.8.41
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/CHANGELOG.md +15 -0
- package/dist/native/PeerState.js +11 -2
- package/dist/native/PeerState.js.map +1 -1
- package/dist/native/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
- package/dist/native/SyncStateManager.js.map +1 -0
- package/dist/native/coValueCore.js +25 -5
- package/dist/native/coValueCore.js.map +1 -1
- package/dist/native/coValues/coMap.js +98 -103
- package/dist/native/coValues/coMap.js.map +1 -1
- package/dist/native/coValues/coStream.js +17 -6
- package/dist/native/coValues/coStream.js.map +1 -1
- package/dist/native/coValues/group.js +127 -39
- package/dist/native/coValues/group.js.map +1 -1
- package/dist/native/exports.js.map +1 -1
- package/dist/native/localNode.js +5 -2
- package/dist/native/localNode.js.map +1 -1
- package/dist/native/permissions.js +51 -3
- package/dist/native/permissions.js.map +1 -1
- package/dist/native/sync.js +34 -10
- package/dist/native/sync.js.map +1 -1
- package/dist/web/PeerState.js +11 -2
- package/dist/web/PeerState.js.map +1 -1
- package/dist/web/{SyncStateSubscriptionManager.js → SyncStateManager.js} +35 -24
- package/dist/web/SyncStateManager.js.map +1 -0
- package/dist/web/coValueCore.js +25 -5
- package/dist/web/coValueCore.js.map +1 -1
- package/dist/web/coValues/coMap.js +98 -103
- package/dist/web/coValues/coMap.js.map +1 -1
- package/dist/web/coValues/coStream.js +17 -6
- package/dist/web/coValues/coStream.js.map +1 -1
- package/dist/web/coValues/group.js +127 -39
- package/dist/web/coValues/group.js.map +1 -1
- package/dist/web/exports.js.map +1 -1
- package/dist/web/localNode.js +5 -2
- package/dist/web/localNode.js.map +1 -1
- package/dist/web/permissions.js +51 -3
- package/dist/web/permissions.js.map +1 -1
- package/dist/web/sync.js +34 -10
- package/dist/web/sync.js.map +1 -1
- package/package.json +3 -5
- package/src/PeerState.ts +12 -2
- package/src/{SyncStateSubscriptionManager.ts → SyncStateManager.ts} +48 -35
- package/src/coValueCore.ts +43 -9
- package/src/coValues/coMap.ts +126 -127
- package/src/coValues/coStream.ts +27 -10
- package/src/coValues/group.ts +218 -50
- package/src/exports.ts +2 -1
- package/src/localNode.ts +5 -2
- package/src/permissions.ts +71 -8
- package/src/sync.ts +57 -23
- package/src/tests/PeerState.test.ts +49 -0
- package/src/tests/PriorityBasedMessageQueue.test.ts +6 -73
- package/src/tests/{SyncStateSubscriptionManager.test.ts → SyncStateManager.test.ts} +109 -25
- package/src/tests/coMap.test.ts +2 -2
- package/src/tests/group.test.ts +338 -47
- package/src/tests/permissions.test.ts +324 -0
- package/src/tests/sync.test.ts +112 -71
- package/src/tests/testUtils.ts +126 -17
- package/dist/native/SyncStateSubscriptionManager.js.map +0 -1
- package/dist/web/SyncStateSubscriptionManager.js.map +0 -1
package/src/coValues/group.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
isParentGroupReference,
|
|
14
14
|
} from "../ids.js";
|
|
15
15
|
import { JsonObject } from "../jsonValue.js";
|
|
16
|
-
import { Role } from "../permissions.js";
|
|
16
|
+
import { AccountRole, Role } from "../permissions.js";
|
|
17
17
|
import { expectGroup } from "../typeUtils/expectGroup.js";
|
|
18
18
|
import {
|
|
19
19
|
ControlledAccountOrAgent,
|
|
@@ -33,6 +33,7 @@ export type GroupShape = {
|
|
|
33
33
|
[key: RawAccountID | AgentID]: Role;
|
|
34
34
|
[EVERYONE]?: Role;
|
|
35
35
|
readKey?: KeyID;
|
|
36
|
+
[writeKeyFor: `writeKeyFor_${RawAccountID | AgentID}`]: KeyID;
|
|
36
37
|
[revelationFor: `${KeyID}_for_${RawAccountID | AgentID}`]: Sealed<KeySecret>;
|
|
37
38
|
[revelationFor: `${KeyID}_for_${Everyone}`]: KeySecret;
|
|
38
39
|
[oldKeyForNewKey: `${KeyID}_for_${KeyID}`]: Encrypted<
|
|
@@ -93,7 +94,7 @@ export class RawGroup<
|
|
|
93
94
|
}
|
|
94
95
|
| undefined = roleHere && { role: roleHere, via: undefined };
|
|
95
96
|
|
|
96
|
-
const parentGroups = this.getParentGroups(this.
|
|
97
|
+
const parentGroups = this.getParentGroups(this.atTimeFilter);
|
|
97
98
|
|
|
98
99
|
for (const parentGroup of parentGroups) {
|
|
99
100
|
const roleInParent = parentGroup.roleOfInternal(accountID);
|
|
@@ -213,18 +214,19 @@ export class RawGroup<
|
|
|
213
214
|
account: RawAccount | ControlledAccountOrAgent | AgentID | Everyone,
|
|
214
215
|
role: Role,
|
|
215
216
|
) {
|
|
216
|
-
const currentReadKey = this.core.getCurrentReadKey();
|
|
217
|
-
|
|
218
|
-
if (!currentReadKey.secret) {
|
|
219
|
-
throw new Error("Can't add member without read key secret");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
217
|
if (account === EVERYONE) {
|
|
223
218
|
if (!(role === "reader" || role === "writer")) {
|
|
224
219
|
throw new Error(
|
|
225
220
|
"Can't make everyone something other than reader or writer",
|
|
226
221
|
);
|
|
227
222
|
}
|
|
223
|
+
|
|
224
|
+
const currentReadKey = this.core.getCurrentReadKey();
|
|
225
|
+
|
|
226
|
+
if (!currentReadKey.secret) {
|
|
227
|
+
throw new Error("Can't add member without read key secret");
|
|
228
|
+
}
|
|
229
|
+
|
|
228
230
|
this.set(account, role, "trusting");
|
|
229
231
|
|
|
230
232
|
if (this.get(account) !== role) {
|
|
@@ -236,44 +238,168 @@ export class RawGroup<
|
|
|
236
238
|
currentReadKey.secret,
|
|
237
239
|
"trusting",
|
|
238
240
|
);
|
|
241
|
+
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const memberKey = typeof account === "string" ? account : account.id;
|
|
246
|
+
const agent =
|
|
247
|
+
typeof account === "string"
|
|
248
|
+
? account
|
|
249
|
+
: account.currentAgentID()._unsafeUnwrap({ withStackTrace: true });
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* WriteOnly members can only see their own changes.
|
|
253
|
+
*
|
|
254
|
+
* We don't want to reveal the readKey to them so we create a new one specifically for them and also reveal it to everyone else with a reader or higher-capability role (but crucially not to other writer-only members)
|
|
255
|
+
* to everyone else.
|
|
256
|
+
*
|
|
257
|
+
* To never reveal the readKey to writeOnly members we also create a dedicated writeKey for the
|
|
258
|
+
* invite.
|
|
259
|
+
*/
|
|
260
|
+
if (role === "writeOnly" || role === "writeOnlyInvite") {
|
|
261
|
+
const writeKeyForNewMember = this.core.crypto.newRandomKeySecret();
|
|
262
|
+
|
|
263
|
+
this.set(memberKey, role, "trusting");
|
|
264
|
+
this.set(`writeKeyFor_${memberKey}`, writeKeyForNewMember.id, "trusting");
|
|
265
|
+
|
|
266
|
+
this.storeKeyRevelationForMember(
|
|
267
|
+
memberKey,
|
|
268
|
+
agent,
|
|
269
|
+
writeKeyForNewMember.id,
|
|
270
|
+
writeKeyForNewMember.secret,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
for (const otherMemberKey of this.getMemberKeys()) {
|
|
274
|
+
const memberRole = this.get(otherMemberKey);
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
memberRole === "reader" ||
|
|
278
|
+
memberRole === "writer" ||
|
|
279
|
+
memberRole === "admin" ||
|
|
280
|
+
memberRole === "readerInvite" ||
|
|
281
|
+
memberRole === "writerInvite" ||
|
|
282
|
+
memberRole === "adminInvite"
|
|
283
|
+
) {
|
|
284
|
+
const otherMemberAgent = this.core.node
|
|
285
|
+
.resolveAccountAgent(
|
|
286
|
+
otherMemberKey,
|
|
287
|
+
"Expected member agent to be loaded",
|
|
288
|
+
)
|
|
289
|
+
._unsafeUnwrap({ withStackTrace: true });
|
|
290
|
+
|
|
291
|
+
this.storeKeyRevelationForMember(
|
|
292
|
+
otherMemberKey,
|
|
293
|
+
otherMemberAgent,
|
|
294
|
+
writeKeyForNewMember.id,
|
|
295
|
+
writeKeyForNewMember.secret,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
239
299
|
} else {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
300
|
+
const currentReadKey = this.core.getCurrentReadKey();
|
|
301
|
+
|
|
302
|
+
if (!currentReadKey.secret) {
|
|
303
|
+
throw new Error("Can't add member without read key secret");
|
|
304
|
+
}
|
|
305
|
+
|
|
245
306
|
this.set(memberKey, role, "trusting");
|
|
246
307
|
|
|
247
308
|
if (this.get(memberKey) !== role) {
|
|
248
309
|
throw new Error("Failed to set role");
|
|
249
310
|
}
|
|
250
311
|
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
to: this.core.crypto.getAgentSealerID(agent),
|
|
257
|
-
nOnceMaterial: {
|
|
258
|
-
in: this.id,
|
|
259
|
-
tx: this.core.nextTransactionID(),
|
|
260
|
-
},
|
|
261
|
-
}),
|
|
262
|
-
"trusting",
|
|
312
|
+
this.storeKeyRevelationForMember(
|
|
313
|
+
memberKey,
|
|
314
|
+
agent,
|
|
315
|
+
currentReadKey.id,
|
|
316
|
+
currentReadKey.secret,
|
|
263
317
|
);
|
|
318
|
+
|
|
319
|
+
for (const keyID of this.getWriteOnlyKeys()) {
|
|
320
|
+
const secret = this.core.getReadKey(keyID);
|
|
321
|
+
|
|
322
|
+
if (!secret) {
|
|
323
|
+
console.error("Can't find key", keyID);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.storeKeyRevelationForMember(memberKey, agent, keyID, secret);
|
|
328
|
+
}
|
|
264
329
|
}
|
|
265
330
|
}
|
|
266
331
|
|
|
332
|
+
private storeKeyRevelationForMember(
|
|
333
|
+
memberKey: RawAccountID | AgentID,
|
|
334
|
+
agent: AgentID,
|
|
335
|
+
keyID: KeyID,
|
|
336
|
+
secret: KeySecret,
|
|
337
|
+
) {
|
|
338
|
+
this.set(
|
|
339
|
+
`${keyID}_for_${memberKey}`,
|
|
340
|
+
this.core.crypto.seal({
|
|
341
|
+
message: secret,
|
|
342
|
+
from: this.core.node.account.currentSealerSecret(),
|
|
343
|
+
to: this.core.crypto.getAgentSealerID(agent),
|
|
344
|
+
nOnceMaterial: {
|
|
345
|
+
in: this.id,
|
|
346
|
+
tx: this.core.nextTransactionID(),
|
|
347
|
+
},
|
|
348
|
+
}),
|
|
349
|
+
"trusting",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private getWriteOnlyKeys() {
|
|
354
|
+
const keys: KeyID[] = [];
|
|
355
|
+
|
|
356
|
+
for (const key of this.keys()) {
|
|
357
|
+
if (key.startsWith("writeKeyFor_")) {
|
|
358
|
+
keys.push(
|
|
359
|
+
this.get(key as `writeKeyFor_${RawAccountID | AgentID}`) as KeyID,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return keys;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
getCurrentReadKeyId() {
|
|
368
|
+
if (this.myRole() === "writeOnly") {
|
|
369
|
+
const accountId = this.core.node.account.id;
|
|
370
|
+
|
|
371
|
+
return this.get(`writeKeyFor_${accountId}`) as KeyID;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return this.get("readKey");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
getMemberKeys(): (RawAccountID | AgentID)[] {
|
|
378
|
+
return this.keys().filter((key): key is RawAccountID | AgentID => {
|
|
379
|
+
return key.startsWith("co_") || isAgentID(key);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
267
383
|
/** @internal */
|
|
268
384
|
rotateReadKey() {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
385
|
+
const memberKeys = this.getMemberKeys();
|
|
386
|
+
|
|
387
|
+
const currentlyPermittedReaders = memberKeys.filter((key) => {
|
|
388
|
+
const role = this.get(key);
|
|
389
|
+
return (
|
|
390
|
+
role === "admin" ||
|
|
391
|
+
role === "writer" ||
|
|
392
|
+
role === "reader" ||
|
|
393
|
+
role === "adminInvite" ||
|
|
394
|
+
role === "writerInvite" ||
|
|
395
|
+
role === "readerInvite"
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const writeOnlyMembers = memberKeys.filter((key) => {
|
|
400
|
+
const role = this.get(key);
|
|
401
|
+
return role === "writeOnly" || role === "writeOnlyInvite";
|
|
402
|
+
});
|
|
277
403
|
|
|
278
404
|
// Get these early, so we fail fast if they are unavailable
|
|
279
405
|
const parentGroups = this.getParentGroups();
|
|
@@ -293,28 +419,60 @@ export class RawGroup<
|
|
|
293
419
|
const newReadKey = this.core.crypto.newRandomKeySecret();
|
|
294
420
|
|
|
295
421
|
for (const readerID of currentlyPermittedReaders) {
|
|
296
|
-
const
|
|
422
|
+
const agent = this.core.node
|
|
297
423
|
.resolveAccountAgent(
|
|
298
424
|
readerID,
|
|
299
425
|
"Expected to know currently permitted reader",
|
|
300
426
|
)
|
|
301
427
|
._unsafeUnwrap({ withStackTrace: true });
|
|
302
428
|
|
|
303
|
-
this.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
to: this.core.crypto.getAgentSealerID(reader),
|
|
309
|
-
nOnceMaterial: {
|
|
310
|
-
in: this.id,
|
|
311
|
-
tx: this.core.nextTransactionID(),
|
|
312
|
-
},
|
|
313
|
-
}),
|
|
314
|
-
"trusting",
|
|
429
|
+
this.storeKeyRevelationForMember(
|
|
430
|
+
readerID,
|
|
431
|
+
agent,
|
|
432
|
+
newReadKey.id,
|
|
433
|
+
newReadKey.secret,
|
|
315
434
|
);
|
|
316
435
|
}
|
|
317
436
|
|
|
437
|
+
/**
|
|
438
|
+
* If there are some writeOnly members we need to rotate their keys
|
|
439
|
+
* and reveal them to the other non-writeOnly members
|
|
440
|
+
*/
|
|
441
|
+
for (const writeOnlyMemberID of writeOnlyMembers) {
|
|
442
|
+
const agent = this.core.node
|
|
443
|
+
.resolveAccountAgent(
|
|
444
|
+
writeOnlyMemberID,
|
|
445
|
+
"Expected to know writeOnly member",
|
|
446
|
+
)
|
|
447
|
+
._unsafeUnwrap({ withStackTrace: true });
|
|
448
|
+
|
|
449
|
+
const writeOnlyKey = this.core.crypto.newRandomKeySecret();
|
|
450
|
+
|
|
451
|
+
this.storeKeyRevelationForMember(
|
|
452
|
+
writeOnlyMemberID,
|
|
453
|
+
agent,
|
|
454
|
+
writeOnlyKey.id,
|
|
455
|
+
writeOnlyKey.secret,
|
|
456
|
+
);
|
|
457
|
+
this.set(`writeKeyFor_${writeOnlyMemberID}`, writeOnlyKey.id, "trusting");
|
|
458
|
+
|
|
459
|
+
for (const readerID of currentlyPermittedReaders) {
|
|
460
|
+
const agent = this.core.node
|
|
461
|
+
.resolveAccountAgent(
|
|
462
|
+
readerID,
|
|
463
|
+
"Expected to know currently permitted reader",
|
|
464
|
+
)
|
|
465
|
+
._unsafeUnwrap({ withStackTrace: true });
|
|
466
|
+
|
|
467
|
+
this.storeKeyRevelationForMember(
|
|
468
|
+
readerID,
|
|
469
|
+
agent,
|
|
470
|
+
writeOnlyKey.id,
|
|
471
|
+
writeOnlyKey.secret,
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
318
476
|
this.set(
|
|
319
477
|
`${currentReadKey.id}_for_${newReadKey.id}`,
|
|
320
478
|
this.core.crypto.encryptKeySecret({
|
|
@@ -326,8 +484,11 @@ export class RawGroup<
|
|
|
326
484
|
|
|
327
485
|
this.set("readKey", newReadKey.id, "trusting");
|
|
328
486
|
|
|
329
|
-
|
|
330
|
-
|
|
487
|
+
/**
|
|
488
|
+
* The new read key needs to be revealed to the parent groups
|
|
489
|
+
*
|
|
490
|
+
* This way the members from the parent groups can still have access to this group
|
|
491
|
+
*/
|
|
331
492
|
for (const parent of parentGroups) {
|
|
332
493
|
const { id: parentReadKeyID, secret: parentReadKeySecret } =
|
|
333
494
|
parent.core.getCurrentReadKey();
|
|
@@ -426,7 +587,7 @@ export class RawGroup<
|
|
|
426
587
|
*
|
|
427
588
|
* @category 2. Role changing
|
|
428
589
|
*/
|
|
429
|
-
createInvite(role:
|
|
590
|
+
createInvite(role: AccountRole): InviteSecret {
|
|
430
591
|
const secretSeed = this.core.crypto.newRandomSecretSeed();
|
|
431
592
|
|
|
432
593
|
const inviteSecret = this.core.crypto.agentSecretFromSecretSeed(secretSeed);
|
|
@@ -558,13 +719,20 @@ function isMorePermissiveAndShouldInherit(
|
|
|
558
719
|
}
|
|
559
720
|
|
|
560
721
|
if (roleInParent === "writer") {
|
|
561
|
-
return
|
|
722
|
+
return (
|
|
723
|
+
!roleInChild || roleInChild === "reader" || roleInChild === "writeOnly"
|
|
724
|
+
);
|
|
562
725
|
}
|
|
563
726
|
|
|
564
727
|
if (roleInParent === "reader") {
|
|
565
728
|
return !roleInChild;
|
|
566
729
|
}
|
|
567
730
|
|
|
731
|
+
// writeOnly can't be inherited
|
|
732
|
+
if (roleInParent === "writeOnly") {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
|
|
568
736
|
return false;
|
|
569
737
|
}
|
|
570
738
|
|
package/src/exports.ts
CHANGED
|
@@ -50,7 +50,7 @@ import type {
|
|
|
50
50
|
} from "./coValues/coStream.js";
|
|
51
51
|
import type { InviteSecret } from "./coValues/group.js";
|
|
52
52
|
import type { AgentSecret } from "./crypto/crypto.js";
|
|
53
|
-
import type { AgentID, SessionID } from "./ids.js";
|
|
53
|
+
import type { AgentID, RawCoID, SessionID } from "./ids.js";
|
|
54
54
|
import type { JsonValue } from "./jsonValue.js";
|
|
55
55
|
import type * as Media from "./media.js";
|
|
56
56
|
import type {
|
|
@@ -104,6 +104,7 @@ export {
|
|
|
104
104
|
RawCoStream,
|
|
105
105
|
RawBinaryCoStream,
|
|
106
106
|
RawCoValue,
|
|
107
|
+
RawCoID,
|
|
107
108
|
CoID,
|
|
108
109
|
AnyRawCoValue,
|
|
109
110
|
RawAccount,
|
package/src/localNode.ts
CHANGED
|
@@ -387,7 +387,8 @@ export class LocalNode {
|
|
|
387
387
|
existingRole === "admin" ||
|
|
388
388
|
(existingRole === "writer" && inviteRole === "writerInvite") ||
|
|
389
389
|
(existingRole === "writer" && inviteRole === "reader") ||
|
|
390
|
-
(existingRole === "reader" && inviteRole === "readerInvite")
|
|
390
|
+
(existingRole === "reader" && inviteRole === "readerInvite") ||
|
|
391
|
+
(existingRole && inviteRole === "writeOnlyInvite")
|
|
391
392
|
) {
|
|
392
393
|
console.debug(
|
|
393
394
|
"Not accepting invite that would replace or downgrade role",
|
|
@@ -410,7 +411,9 @@ export class LocalNode {
|
|
|
410
411
|
? "admin"
|
|
411
412
|
: inviteRole === "writerInvite"
|
|
412
413
|
? "writer"
|
|
413
|
-
: "
|
|
414
|
+
: inviteRole === "writeOnlyInvite"
|
|
415
|
+
? "writeOnly"
|
|
416
|
+
: "reader",
|
|
414
417
|
);
|
|
415
418
|
|
|
416
419
|
group.core._sessionLogs = groupAsInvite.core.sessionLogs;
|
package/src/permissions.ts
CHANGED
|
@@ -22,14 +22,15 @@ export type PermissionsDef =
|
|
|
22
22
|
| { type: "ownedByGroup"; group: RawCoID }
|
|
23
23
|
| { type: "unsafeAllowAll" };
|
|
24
24
|
|
|
25
|
+
export type AccountRole = "reader" | "writer" | "admin" | "writeOnly";
|
|
26
|
+
|
|
25
27
|
export type Role =
|
|
26
|
-
|
|
|
27
|
-
| "writer"
|
|
28
|
-
| "admin"
|
|
28
|
+
| AccountRole
|
|
29
29
|
| "revoked"
|
|
30
30
|
| "adminInvite"
|
|
31
31
|
| "writerInvite"
|
|
32
|
-
| "readerInvite"
|
|
32
|
+
| "readerInvite"
|
|
33
|
+
| "writeOnlyInvite";
|
|
33
34
|
|
|
34
35
|
type ValidTransactionsResult = { txID: TransactionID; tx: Transaction };
|
|
35
36
|
type MemberState = { [agent: RawAccountID | AgentID]: Role; [EVERYONE]?: Role };
|
|
@@ -81,7 +82,8 @@ export function determineValidTransactions(
|
|
|
81
82
|
|
|
82
83
|
if (
|
|
83
84
|
transactorRoleAtTxTime !== "admin" &&
|
|
84
|
-
transactorRoleAtTxTime !== "writer"
|
|
85
|
+
transactorRoleAtTxTime !== "writer" &&
|
|
86
|
+
transactorRoleAtTxTime !== "writeOnly"
|
|
85
87
|
) {
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
@@ -177,6 +179,9 @@ function determineValidTransactionsForGroup(
|
|
|
177
179
|
const memberState: MemberState = {};
|
|
178
180
|
const validTransactions: ValidTransactionsResult[] = [];
|
|
179
181
|
|
|
182
|
+
const keyRevelations = new Set<string>();
|
|
183
|
+
const writeKeys = new Set<string>();
|
|
184
|
+
|
|
180
185
|
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
|
181
186
|
// console.log("before", { memberState, validTransactions });
|
|
182
187
|
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
@@ -254,14 +259,31 @@ function determineValidTransactionsForGroup(
|
|
|
254
259
|
memberState[transactor] !== "admin" &&
|
|
255
260
|
memberState[transactor] !== "adminInvite" &&
|
|
256
261
|
memberState[transactor] !== "writerInvite" &&
|
|
257
|
-
memberState[transactor] !== "readerInvite"
|
|
262
|
+
memberState[transactor] !== "readerInvite" &&
|
|
263
|
+
memberState[transactor] !== "writeOnlyInvite"
|
|
258
264
|
) {
|
|
259
265
|
console.warn("Only admins can reveal keys");
|
|
260
266
|
continue;
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
|
|
269
|
+
/**
|
|
270
|
+
* We don't want to give the ability to invite members to override
|
|
271
|
+
* key revelations, otherwise they could hide a key revelation to any user
|
|
272
|
+
* blocking them from accessing the group.
|
|
273
|
+
*/
|
|
274
|
+
if (
|
|
275
|
+
keyRevelations.has(change.key) &&
|
|
276
|
+
memberState[transactor] !== "admin"
|
|
277
|
+
) {
|
|
278
|
+
console.warn(
|
|
279
|
+
"Key revelation already exists and can't be overridden by invite",
|
|
280
|
+
);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
keyRevelations.add(change.key);
|
|
264
285
|
|
|
286
|
+
// TODO: check validity of agents who the key is revealed to?
|
|
265
287
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
266
288
|
continue;
|
|
267
289
|
} else if (isParentExtension(change.key)) {
|
|
@@ -277,6 +299,34 @@ function determineValidTransactionsForGroup(
|
|
|
277
299
|
console.warn("Only admins can set child extensions");
|
|
278
300
|
continue;
|
|
279
301
|
}
|
|
302
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
303
|
+
continue;
|
|
304
|
+
} else if (isWriteKeyForMember(change.key)) {
|
|
305
|
+
if (
|
|
306
|
+
memberState[transactor] !== "admin" &&
|
|
307
|
+
memberState[transactor] !== "writeOnlyInvite"
|
|
308
|
+
) {
|
|
309
|
+
console.warn("Only admins can set writeKeys");
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* writeOnlyInvite need to be able to set writeKeys because every new writeOnly
|
|
315
|
+
* member comes with their own write key.
|
|
316
|
+
*
|
|
317
|
+
* We don't want to give the ability to invite members to override
|
|
318
|
+
* write keys, otherwise they could hide a write key to other writeOnly users
|
|
319
|
+
* blocking them from accessing the group.ß
|
|
320
|
+
*/
|
|
321
|
+
if (writeKeys.has(change.key) && memberState[transactor] !== "admin") {
|
|
322
|
+
console.warn(
|
|
323
|
+
"Write key already exists and can't be overridden by invite",
|
|
324
|
+
);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
writeKeys.add(change.key);
|
|
329
|
+
|
|
280
330
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
281
331
|
continue;
|
|
282
332
|
}
|
|
@@ -288,10 +338,12 @@ function determineValidTransactionsForGroup(
|
|
|
288
338
|
change.value !== "admin" &&
|
|
289
339
|
change.value !== "writer" &&
|
|
290
340
|
change.value !== "reader" &&
|
|
341
|
+
change.value !== "writeOnly" &&
|
|
291
342
|
change.value !== "revoked" &&
|
|
292
343
|
change.value !== "adminInvite" &&
|
|
293
344
|
change.value !== "writerInvite" &&
|
|
294
|
-
change.value !== "readerInvite"
|
|
345
|
+
change.value !== "readerInvite" &&
|
|
346
|
+
change.value !== "writeOnlyInvite"
|
|
295
347
|
) {
|
|
296
348
|
console.warn("Group transaction must set a valid role");
|
|
297
349
|
continue;
|
|
@@ -341,6 +393,11 @@ function determineValidTransactionsForGroup(
|
|
|
341
393
|
console.warn("ReaderInvites can only create reader.");
|
|
342
394
|
continue;
|
|
343
395
|
}
|
|
396
|
+
} else if (memberState[transactor] === "writeOnlyInvite") {
|
|
397
|
+
if (change.value !== "writeOnly") {
|
|
398
|
+
console.warn("WriteOnlyInvites can only create writeOnly.");
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
344
401
|
} else {
|
|
345
402
|
console.warn(
|
|
346
403
|
"Group transaction must be made by current admin or invite",
|
|
@@ -377,6 +434,12 @@ function agentInAccountOrMemberInGroup(
|
|
|
377
434
|
return transactor;
|
|
378
435
|
}
|
|
379
436
|
|
|
437
|
+
export function isWriteKeyForMember(
|
|
438
|
+
co: string,
|
|
439
|
+
): co is `writeKeyFor_${RawAccountID | AgentID}` {
|
|
440
|
+
return co.startsWith("writeKeyFor_");
|
|
441
|
+
}
|
|
442
|
+
|
|
380
443
|
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
|
|
381
444
|
return co.startsWith("key_") && co.includes("_for_key");
|
|
382
445
|
}
|
package/src/sync.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { ValueType, metrics } from "@opentelemetry/api";
|
|
1
2
|
import { PeerState } from "./PeerState.js";
|
|
2
|
-
import {
|
|
3
|
+
import { SyncStateManager } from "./SyncStateManager.js";
|
|
3
4
|
import { CoValueHeader, Transaction } from "./coValueCore.js";
|
|
4
5
|
import { CoValueCore } from "./coValueCore.js";
|
|
5
6
|
import { Signature } from "./crypto/crypto.js";
|
|
@@ -115,12 +116,18 @@ export class SyncManager {
|
|
|
115
116
|
| undefined;
|
|
116
117
|
} = {};
|
|
117
118
|
|
|
119
|
+
peersCounter = metrics.getMeter("cojson").createUpDownCounter("jazz.peers", {
|
|
120
|
+
description: "Amount of connected peers",
|
|
121
|
+
valueType: ValueType.INT,
|
|
122
|
+
unit: "peer",
|
|
123
|
+
});
|
|
124
|
+
|
|
118
125
|
constructor(local: LocalNode) {
|
|
119
126
|
this.local = local;
|
|
120
|
-
this.
|
|
127
|
+
this.syncState = new SyncStateManager(this);
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
|
|
130
|
+
syncState: SyncStateManager;
|
|
124
131
|
|
|
125
132
|
peersInPriorityOrder(): PeerState[] {
|
|
126
133
|
return Object.values(this.peers).sort((a, b) => {
|
|
@@ -296,9 +303,11 @@ export class SyncManager {
|
|
|
296
303
|
prevPeer.gracefulShutdown();
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
this.peersCounter.add(1, { role: peer.role });
|
|
307
|
+
|
|
299
308
|
const unsubscribeFromKnownStatesUpdates = peerState.knownStates.subscribe(
|
|
300
309
|
(id) => {
|
|
301
|
-
this.
|
|
310
|
+
this.syncState.triggerUpdate(peer.id, id);
|
|
302
311
|
},
|
|
303
312
|
);
|
|
304
313
|
|
|
@@ -367,6 +376,7 @@ export class SyncManager {
|
|
|
367
376
|
const state = this.peers[peer.id];
|
|
368
377
|
state?.gracefulShutdown();
|
|
369
378
|
unsubscribeFromKnownStatesUpdates();
|
|
379
|
+
this.peersCounter.add(-1, { role: peer.role });
|
|
370
380
|
|
|
371
381
|
if (peer.deletePeerStateOnClose) {
|
|
372
382
|
delete this.peers[peer.id];
|
|
@@ -708,35 +718,59 @@ export class SyncManager {
|
|
|
708
718
|
}
|
|
709
719
|
|
|
710
720
|
for (const peer of this.getPeers()) {
|
|
711
|
-
this.
|
|
721
|
+
this.syncState.triggerUpdate(peer.id, coValue.id);
|
|
712
722
|
}
|
|
713
723
|
}
|
|
714
724
|
|
|
715
|
-
async
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
peerId,
|
|
719
|
-
id,
|
|
720
|
-
);
|
|
725
|
+
async waitForSyncWithPeer(peerId: PeerID, id: RawCoID, timeout: number) {
|
|
726
|
+
const { syncState } = this;
|
|
727
|
+
const currentSyncState = syncState.getCurrentSyncState(peerId, id);
|
|
721
728
|
|
|
722
|
-
|
|
729
|
+
const isTheConditionAlreadyMet = currentSyncState.uploaded;
|
|
730
|
+
|
|
731
|
+
if (isTheConditionAlreadyMet) {
|
|
723
732
|
return true;
|
|
724
733
|
}
|
|
725
734
|
|
|
726
|
-
return new Promise((resolve) => {
|
|
727
|
-
const unsubscribe =
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
(knownState
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
735
|
+
return new Promise((resolve, reject) => {
|
|
736
|
+
const unsubscribe = this.syncState.subscribeToPeerUpdates(
|
|
737
|
+
peerId,
|
|
738
|
+
(knownState, syncState) => {
|
|
739
|
+
if (syncState.uploaded && knownState.id === id) {
|
|
740
|
+
resolve(true);
|
|
741
|
+
unsubscribe?.();
|
|
742
|
+
clearTimeout(timeoutId);
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
const timeoutId = setTimeout(() => {
|
|
748
|
+
reject(new Error(`Timeout waiting for sync on ${peerId}/${id}`));
|
|
749
|
+
unsubscribe?.();
|
|
750
|
+
}, timeout);
|
|
737
751
|
});
|
|
738
752
|
}
|
|
739
753
|
|
|
754
|
+
async waitForSync(id: RawCoID, timeout = 30_000) {
|
|
755
|
+
const peers = this.getPeers();
|
|
756
|
+
|
|
757
|
+
return Promise.all(
|
|
758
|
+
peers.map((peer) => this.waitForSyncWithPeer(peer.id, id, timeout)),
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async waitForAllCoValuesSync(timeout = 60_000) {
|
|
763
|
+
const coValues = this.local.coValuesStore.getValues();
|
|
764
|
+
const validCoValues = Array.from(coValues).filter(
|
|
765
|
+
(coValue) =>
|
|
766
|
+
coValue.state.type === "available" || coValue.state.type === "loading",
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
return Promise.all(
|
|
770
|
+
validCoValues.map((coValue) => this.waitForSync(coValue.id, timeout)),
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
740
774
|
gracefulShutdown() {
|
|
741
775
|
for (const peer of Object.values(this.peers)) {
|
|
742
776
|
peer.gracefulShutdown();
|