cojson 0.8.39 → 0.8.44
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 +12 -0
- package/CHANGELOG.md +14 -0
- package/dist/native/coValueCore.js +22 -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 +2 -0
- 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 +77 -19
- package/dist/native/permissions.js.map +1 -1
- package/dist/web/coValueCore.js +22 -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 +2 -0
- 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 +77 -19
- package/dist/web/permissions.js.map +1 -1
- package/package.json +3 -5
- package/src/coValueCore.ts +37 -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 -0
- package/src/localNode.ts +5 -2
- package/src/permissions.ts +105 -24
- package/src/tests/coMap.test.ts +2 -2
- package/src/tests/group.test.ts +332 -38
- package/src/tests/permissions.test.ts +324 -0
- package/src/tests/testUtils.ts +18 -13
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
|
@@ -53,6 +53,7 @@ import type { AgentSecret } from "./crypto/crypto.js";
|
|
|
53
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
|
+
import { disablePermissionErrors } from "./permissions.js";
|
|
56
57
|
import type {
|
|
57
58
|
IncomingSyncStream,
|
|
58
59
|
OutgoingSyncQueue,
|
|
@@ -91,6 +92,7 @@ export const cojsonInternals = {
|
|
|
91
92
|
getPriorityFromHeader,
|
|
92
93
|
getGroupDependentKeyList,
|
|
93
94
|
getGroupDependentKey,
|
|
95
|
+
disablePermissionErrors,
|
|
94
96
|
};
|
|
95
97
|
|
|
96
98
|
export {
|
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,18 +22,33 @@ 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 };
|
|
36
37
|
|
|
38
|
+
let logPermissionErrors = true;
|
|
39
|
+
|
|
40
|
+
export function disablePermissionErrors() {
|
|
41
|
+
logPermissionErrors = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function logPermissionError(...args: unknown[]) {
|
|
45
|
+
if (logPermissionErrors === false) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.warn(...args);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
export function determineValidTransactions(
|
|
38
53
|
coValue: CoValueCore,
|
|
39
54
|
): { txID: TransactionID; tx: Transaction }[] {
|
|
@@ -81,7 +96,8 @@ export function determineValidTransactions(
|
|
|
81
96
|
|
|
82
97
|
if (
|
|
83
98
|
transactorRoleAtTxTime !== "admin" &&
|
|
84
|
-
transactorRoleAtTxTime !== "writer"
|
|
99
|
+
transactorRoleAtTxTime !== "writer" &&
|
|
100
|
+
transactorRoleAtTxTime !== "writeOnly"
|
|
85
101
|
) {
|
|
86
102
|
return;
|
|
87
103
|
}
|
|
@@ -177,6 +193,9 @@ function determineValidTransactionsForGroup(
|
|
|
177
193
|
const memberState: MemberState = {};
|
|
178
194
|
const validTransactions: ValidTransactionsResult[] = [];
|
|
179
195
|
|
|
196
|
+
const keyRevelations = new Set<string>();
|
|
197
|
+
const writeKeys = new Set<string>();
|
|
198
|
+
|
|
180
199
|
for (const { sessionID, txIndex, tx } of allTransactionsSorted) {
|
|
181
200
|
// console.log("before", { memberState, validTransactions });
|
|
182
201
|
const transactor = accountOrAgentIDfromSessionID(sessionID);
|
|
@@ -189,7 +208,9 @@ function determineValidTransactionsForGroup(
|
|
|
189
208
|
});
|
|
190
209
|
continue;
|
|
191
210
|
} else {
|
|
192
|
-
|
|
211
|
+
logPermissionError(
|
|
212
|
+
"Only admins can make private transactions in groups",
|
|
213
|
+
);
|
|
193
214
|
continue;
|
|
194
215
|
}
|
|
195
216
|
}
|
|
@@ -199,7 +220,7 @@ function determineValidTransactionsForGroup(
|
|
|
199
220
|
try {
|
|
200
221
|
changes = parseJSON(tx.changes);
|
|
201
222
|
} catch (e) {
|
|
202
|
-
|
|
223
|
+
logPermissionError(
|
|
203
224
|
coValue.id,
|
|
204
225
|
"Invalid JSON in transaction",
|
|
205
226
|
e,
|
|
@@ -221,18 +242,18 @@ function determineValidTransactionsForGroup(
|
|
|
221
242
|
| MapOpPayload<`child_${CoID<RawGroup>}`, CoID<RawGroup>>;
|
|
222
243
|
|
|
223
244
|
if (changes.length !== 1) {
|
|
224
|
-
|
|
245
|
+
logPermissionError("Group transaction must have exactly one change");
|
|
225
246
|
continue;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
249
|
if (change.op !== "set") {
|
|
229
|
-
|
|
250
|
+
logPermissionError("Group transaction must set a role or readKey");
|
|
230
251
|
continue;
|
|
231
252
|
}
|
|
232
253
|
|
|
233
254
|
if (change.key === "readKey") {
|
|
234
255
|
if (memberState[transactor] !== "admin") {
|
|
235
|
-
|
|
256
|
+
logPermissionError("Only admins can set readKeys");
|
|
236
257
|
continue;
|
|
237
258
|
}
|
|
238
259
|
|
|
@@ -240,7 +261,7 @@ function determineValidTransactionsForGroup(
|
|
|
240
261
|
continue;
|
|
241
262
|
} else if (change.key === "profile") {
|
|
242
263
|
if (memberState[transactor] !== "admin") {
|
|
243
|
-
|
|
264
|
+
logPermissionError("Only admins can set profile");
|
|
244
265
|
continue;
|
|
245
266
|
}
|
|
246
267
|
|
|
@@ -254,19 +275,36 @@ function determineValidTransactionsForGroup(
|
|
|
254
275
|
memberState[transactor] !== "admin" &&
|
|
255
276
|
memberState[transactor] !== "adminInvite" &&
|
|
256
277
|
memberState[transactor] !== "writerInvite" &&
|
|
257
|
-
memberState[transactor] !== "readerInvite"
|
|
278
|
+
memberState[transactor] !== "readerInvite" &&
|
|
279
|
+
memberState[transactor] !== "writeOnlyInvite"
|
|
258
280
|
) {
|
|
259
|
-
|
|
281
|
+
logPermissionError("Only admins can reveal keys");
|
|
260
282
|
continue;
|
|
261
283
|
}
|
|
262
284
|
|
|
263
|
-
|
|
285
|
+
/**
|
|
286
|
+
* We don't want to give the ability to invite members to override
|
|
287
|
+
* key revelations, otherwise they could hide a key revelation to any user
|
|
288
|
+
* blocking them from accessing the group.
|
|
289
|
+
*/
|
|
290
|
+
if (
|
|
291
|
+
keyRevelations.has(change.key) &&
|
|
292
|
+
memberState[transactor] !== "admin"
|
|
293
|
+
) {
|
|
294
|
+
logPermissionError(
|
|
295
|
+
"Key revelation already exists and can't be overridden by invite",
|
|
296
|
+
);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
264
299
|
|
|
300
|
+
keyRevelations.add(change.key);
|
|
301
|
+
|
|
302
|
+
// TODO: check validity of agents who the key is revealed to?
|
|
265
303
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
266
304
|
continue;
|
|
267
305
|
} else if (isParentExtension(change.key)) {
|
|
268
306
|
if (memberState[transactor] !== "admin") {
|
|
269
|
-
|
|
307
|
+
logPermissionError("Only admins can set parent extensions");
|
|
270
308
|
continue;
|
|
271
309
|
}
|
|
272
310
|
resolveMemberStateFromParentReference(coValue, memberState, change.key);
|
|
@@ -274,9 +312,37 @@ function determineValidTransactionsForGroup(
|
|
|
274
312
|
continue;
|
|
275
313
|
} else if (isChildExtension(change.key)) {
|
|
276
314
|
if (memberState[transactor] !== "admin") {
|
|
277
|
-
|
|
315
|
+
logPermissionError("Only admins can set child extensions");
|
|
278
316
|
continue;
|
|
279
317
|
}
|
|
318
|
+
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
319
|
+
continue;
|
|
320
|
+
} else if (isWriteKeyForMember(change.key)) {
|
|
321
|
+
if (
|
|
322
|
+
memberState[transactor] !== "admin" &&
|
|
323
|
+
memberState[transactor] !== "writeOnlyInvite"
|
|
324
|
+
) {
|
|
325
|
+
logPermissionError("Only admins can set writeKeys");
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* writeOnlyInvite need to be able to set writeKeys because every new writeOnly
|
|
331
|
+
* member comes with their own write key.
|
|
332
|
+
*
|
|
333
|
+
* We don't want to give the ability to invite members to override
|
|
334
|
+
* write keys, otherwise they could hide a write key to other writeOnly users
|
|
335
|
+
* blocking them from accessing the group.ß
|
|
336
|
+
*/
|
|
337
|
+
if (writeKeys.has(change.key) && memberState[transactor] !== "admin") {
|
|
338
|
+
logPermissionError(
|
|
339
|
+
"Write key already exists and can't be overridden by invite",
|
|
340
|
+
);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
writeKeys.add(change.key);
|
|
345
|
+
|
|
280
346
|
validTransactions.push({ txID: { sessionID, txIndex }, tx });
|
|
281
347
|
continue;
|
|
282
348
|
}
|
|
@@ -288,12 +354,14 @@ function determineValidTransactionsForGroup(
|
|
|
288
354
|
change.value !== "admin" &&
|
|
289
355
|
change.value !== "writer" &&
|
|
290
356
|
change.value !== "reader" &&
|
|
357
|
+
change.value !== "writeOnly" &&
|
|
291
358
|
change.value !== "revoked" &&
|
|
292
359
|
change.value !== "adminInvite" &&
|
|
293
360
|
change.value !== "writerInvite" &&
|
|
294
|
-
change.value !== "readerInvite"
|
|
361
|
+
change.value !== "readerInvite" &&
|
|
362
|
+
change.value !== "writeOnlyInvite"
|
|
295
363
|
) {
|
|
296
|
-
|
|
364
|
+
logPermissionError("Group transaction must set a valid role");
|
|
297
365
|
continue;
|
|
298
366
|
}
|
|
299
367
|
|
|
@@ -305,7 +373,9 @@ function determineValidTransactionsForGroup(
|
|
|
305
373
|
change.value === "revoked"
|
|
306
374
|
)
|
|
307
375
|
) {
|
|
308
|
-
|
|
376
|
+
logPermissionError(
|
|
377
|
+
"Everyone can only be set to reader, writer or revoked",
|
|
378
|
+
);
|
|
309
379
|
continue;
|
|
310
380
|
}
|
|
311
381
|
|
|
@@ -323,26 +393,31 @@ function determineValidTransactionsForGroup(
|
|
|
323
393
|
affectedMember !== transactor &&
|
|
324
394
|
assignedRole !== "admin"
|
|
325
395
|
) {
|
|
326
|
-
|
|
396
|
+
logPermissionError("Admins can only demote themselves.");
|
|
327
397
|
continue;
|
|
328
398
|
}
|
|
329
399
|
} else if (memberState[transactor] === "adminInvite") {
|
|
330
400
|
if (change.value !== "admin") {
|
|
331
|
-
|
|
401
|
+
logPermissionError("AdminInvites can only create admins.");
|
|
332
402
|
continue;
|
|
333
403
|
}
|
|
334
404
|
} else if (memberState[transactor] === "writerInvite") {
|
|
335
405
|
if (change.value !== "writer") {
|
|
336
|
-
|
|
406
|
+
logPermissionError("WriterInvites can only create writers.");
|
|
337
407
|
continue;
|
|
338
408
|
}
|
|
339
409
|
} else if (memberState[transactor] === "readerInvite") {
|
|
340
410
|
if (change.value !== "reader") {
|
|
341
|
-
|
|
411
|
+
logPermissionError("ReaderInvites can only create reader.");
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
} else if (memberState[transactor] === "writeOnlyInvite") {
|
|
415
|
+
if (change.value !== "writeOnly") {
|
|
416
|
+
logPermissionError("WriteOnlyInvites can only create writeOnly.");
|
|
342
417
|
continue;
|
|
343
418
|
}
|
|
344
419
|
} else {
|
|
345
|
-
|
|
420
|
+
logPermissionError(
|
|
346
421
|
"Group transaction must be made by current admin or invite",
|
|
347
422
|
);
|
|
348
423
|
continue;
|
|
@@ -377,6 +452,12 @@ function agentInAccountOrMemberInGroup(
|
|
|
377
452
|
return transactor;
|
|
378
453
|
}
|
|
379
454
|
|
|
455
|
+
export function isWriteKeyForMember(
|
|
456
|
+
co: string,
|
|
457
|
+
): co is `writeKeyFor_${RawAccountID | AgentID}` {
|
|
458
|
+
return co.startsWith("writeKeyFor_");
|
|
459
|
+
}
|
|
460
|
+
|
|
380
461
|
export function isKeyForKeyField(co: string): co is `${KeyID}_for_${KeyID}` {
|
|
381
462
|
return co.startsWith("key_") && co.includes("_for_key");
|
|
382
463
|
}
|
package/src/tests/coMap.test.ts
CHANGED
|
@@ -75,10 +75,10 @@ test("Can get CoMap entry values at different points in time", () => {
|
|
|
75
75
|
expect(content.atTime(beforeB).get("hello")).toEqual("A");
|
|
76
76
|
expect(content.atTime(beforeC).get("hello")).toEqual("B");
|
|
77
77
|
|
|
78
|
-
const ops = content.
|
|
78
|
+
const ops = content.ops["hello"]!;
|
|
79
79
|
|
|
80
80
|
expect(content.atTime(beforeC).lastEditAt("hello")).toEqual(
|
|
81
|
-
operationToEditEntry(ops
|
|
81
|
+
operationToEditEntry(ops[1]!),
|
|
82
82
|
);
|
|
83
83
|
expect(content.atTime(beforeC).nthEditAt("hello", 0)).toEqual(
|
|
84
84
|
operationToEditEntry(ops![0]!),
|