cojson 0.8.18 → 0.8.19-group-inheritance.0

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.
@@ -1,5 +1,5 @@
1
1
  import { Result, err, ok } from "neverthrow";
2
- import { AnyRawCoValue, RawCoValue } from "./coValue.js";
2
+ import { AnyRawCoValue, CoID, RawCoValue } from "./coValue.js";
3
3
  import { ControlledAccountOrAgent, RawAccountID } from "./coValues/account.js";
4
4
  import { RawGroup } from "./coValues/group.js";
5
5
  import { coreToCoValue } from "./coreToCoValue.js";
@@ -785,6 +785,46 @@ export class CoValueCore {
785
785
  }
786
786
  }
787
787
 
788
+ // try to find revelation to parent group read keys
789
+
790
+ for (const co of content.keys()) {
791
+ if (co.startsWith("parent_")) {
792
+ const parentGroupID = co.slice("parent_".length) as CoID<RawGroup>;
793
+ const parentGroup = this.node.expectCoValueLoaded(
794
+ parentGroupID,
795
+ "Expected parent group to be loaded",
796
+ );
797
+
798
+ const parentKey = parentGroup.getCurrentReadKey();
799
+ if (!parentKey.secret) {
800
+ continue;
801
+ }
802
+
803
+ const revelationForParentKey = content.get(
804
+ `${keyID}_for_${parentKey.id}`,
805
+ );
806
+
807
+ if (revelationForParentKey) {
808
+ const secret = parentGroup.crypto.decryptKeySecret(
809
+ {
810
+ encryptedID: keyID,
811
+ encryptingID: parentKey.id,
812
+ encrypted: revelationForParentKey,
813
+ },
814
+ parentKey.secret,
815
+ );
816
+
817
+ if (secret) {
818
+ return secret as KeySecret;
819
+ } else {
820
+ console.error(
821
+ `Encrypting parent ${parentKey.id} key didn't decrypt ${keyID}`,
822
+ );
823
+ }
824
+ }
825
+ }
826
+ }
827
+
788
828
  return undefined;
789
829
  } else if (this.header.ruleset.type === "ownedByGroup") {
790
830
  return this.node
@@ -950,9 +990,15 @@ export class CoValueCore {
950
990
  /** @internal */
951
991
  getDependedOnCoValuesUncached(): RawCoID[] {
952
992
  return this.header.ruleset.type === "group"
953
- ? expectGroup(this.getCurrentContent())
954
- .keys()
955
- .filter((k): k is RawAccountID => k.startsWith("co_"))
993
+ ? [
994
+ ...expectGroup(this.getCurrentContent())
995
+ .keys()
996
+ .filter((k): k is RawAccountID => k.startsWith("co_")),
997
+ ...expectGroup(this.getCurrentContent())
998
+ .keys()
999
+ .filter((k) => k.startsWith("parent_"))
1000
+ .map((k) => k.replace("parent_", "") as RawCoID),
1001
+ ]
956
1002
  : this.header.ruleset.type === "ownedByGroup"
957
1003
  ? [
958
1004
  this.header.ruleset.group,
@@ -5,6 +5,7 @@ import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
5
5
  import { AgentID, isAgentID } from "../ids.js";
6
6
  import { JsonObject } from "../jsonValue.js";
7
7
  import { Role } from "../permissions.js";
8
+ import { expectGroup } from "../typeUtils/expectGroup.js";
8
9
  import {
9
10
  ControlledAccountOrAgent,
10
11
  RawAccount,
@@ -29,6 +30,8 @@ export type GroupShape = {
29
30
  KeySecret,
30
31
  { encryptedID: KeyID; encryptingID: KeyID }
31
32
  >;
33
+ [parent: `parent_${CoID<RawGroup>}`]: "extend";
34
+ [child: `child_${CoID<RawGroup>}`]: "extend";
32
35
  };
33
36
 
34
37
  /** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
@@ -61,12 +64,68 @@ export class RawGroup<
61
64
  * @category 1. Role reading
62
65
  */
63
66
  roleOf(accountID: RawAccountID): Role | undefined {
64
- return this.roleOfInternal(accountID);
67
+ return this.roleOfInternal(accountID)?.role;
65
68
  }
66
69
 
67
70
  /** @internal */
68
- roleOfInternal(accountID: RawAccountID | AgentID): Role | undefined {
69
- return this.get(accountID);
71
+ roleOfInternal(
72
+ accountID: RawAccountID | AgentID | typeof EVERYONE,
73
+ ): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
74
+ const roleHere = this.get(accountID);
75
+ if (roleHere === "revoked") {
76
+ return undefined;
77
+ }
78
+
79
+ let roleInfo:
80
+ | {
81
+ role: Exclude<Role, "revoked">;
82
+ via: CoID<RawGroup> | undefined;
83
+ }
84
+ | undefined = roleHere && { role: roleHere, via: undefined };
85
+
86
+ const parentGroups = this.getParentGroups();
87
+
88
+ for (const parentGroup of parentGroups) {
89
+ const roleInParent = parentGroup.roleOfInternal(accountID);
90
+
91
+ if (
92
+ roleInParent &&
93
+ roleInParent.role !== "revoked" &&
94
+ isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
95
+ ) {
96
+ roleInfo = { role: roleInParent.role, via: parentGroup.id };
97
+ }
98
+ }
99
+
100
+ return roleInfo;
101
+ }
102
+
103
+ getParentGroups(): RawGroup[] {
104
+ return (
105
+ this.keys().filter((key) =>
106
+ key.startsWith("parent_"),
107
+ ) as `parent_${CoID<RawGroup>}`[]
108
+ ).map((parentKey) => {
109
+ const parent = this.core.node.expectCoValueLoaded(
110
+ parentKey.slice("parent_".length) as CoID<RawGroup>,
111
+ "Expected parent group to be loaded",
112
+ );
113
+ return expectGroup(parent.getCurrentContent());
114
+ });
115
+ }
116
+
117
+ getChildGroups(): RawGroup[] {
118
+ return (
119
+ this.keys().filter((key) =>
120
+ key.startsWith("child_"),
121
+ ) as `child_${CoID<RawGroup>}`[]
122
+ ).map((childKey) => {
123
+ const child = this.core.node.expectCoValueLoaded(
124
+ childKey.slice("child_".length) as CoID<RawGroup>,
125
+ "Expected child group to be loaded",
126
+ );
127
+ return expectGroup(child.getCurrentContent());
128
+ });
70
129
  }
71
130
 
72
131
  /**
@@ -75,7 +134,7 @@ export class RawGroup<
75
134
  * @category 1. Role reading
76
135
  */
77
136
  myRole(): Role | undefined {
78
- return this.roleOfInternal(this.core.node.account.id);
137
+ return this.roleOfInternal(this.core.node.account.id)?.role;
79
138
  }
80
139
 
81
140
  /**
@@ -203,7 +262,75 @@ export class RawGroup<
203
262
  "trusting",
204
263
  );
205
264
 
265
+ console.log("Setting", `readKey`, "to", newReadKey.id, "in", this.id);
266
+
206
267
  this.set("readKey", newReadKey.id, "trusting");
268
+
269
+ for (const parent of this.getParentGroups()) {
270
+ const { id: parentReadKeyID, secret: parentReadKeySecret } =
271
+ parent.core.getCurrentReadKey();
272
+ if (!parentReadKeySecret) {
273
+ throw new Error(
274
+ "Can't reveal new child key to parent where we don't have access to the parent read key",
275
+ );
276
+ }
277
+
278
+ console.log(
279
+ "Setting",
280
+ `${newReadKey.id}_for_${parentReadKeyID}`,
281
+ "in",
282
+ this.id,
283
+ );
284
+
285
+ this.set(
286
+ `${newReadKey.id}_for_${parentReadKeyID}`,
287
+ this.core.crypto.encryptKeySecret({
288
+ encrypting: {
289
+ id: parentReadKeyID,
290
+ secret: parentReadKeySecret,
291
+ },
292
+ toEncrypt: newReadKey,
293
+ }).encrypted,
294
+ "trusting",
295
+ );
296
+ }
297
+
298
+ for (const child of this.getChildGroups()) {
299
+ console.log("Rotating child", child.id);
300
+ child.rotateReadKey();
301
+ }
302
+ }
303
+
304
+ extend(parent: RawGroup) {
305
+ this.set(`parent_${parent.id}`, "extend", "trusting");
306
+ parent.set(`child_${this.id}`, "extend", "trusting");
307
+
308
+ const { id: parentReadKeyID, secret: parentReadKeySecret } =
309
+ parent.core.getCurrentReadKey();
310
+ if (!parentReadKeySecret) {
311
+ throw new Error("Can't extend group without parent read key secret");
312
+ }
313
+
314
+ const { id: childReadKeyID, secret: childReadKeySecret } =
315
+ this.core.getCurrentReadKey();
316
+ if (!childReadKeySecret) {
317
+ throw new Error("Can't extend group without child read key secret");
318
+ }
319
+
320
+ this.set(
321
+ `${childReadKeyID}_for_${parentReadKeyID}`,
322
+ this.core.crypto.encryptKeySecret({
323
+ encrypting: {
324
+ id: parentReadKeyID,
325
+ secret: parentReadKeySecret,
326
+ },
327
+ toEncrypt: {
328
+ id: childReadKeyID,
329
+ secret: childReadKeySecret,
330
+ },
331
+ }).encrypted,
332
+ "trusting",
333
+ );
207
334
  }
208
335
 
209
336
  /**
@@ -347,6 +474,34 @@ export class RawGroup<
347
474
  }
348
475
  }
349
476
 
477
+ function isMorePermissiveAndShouldInherit(
478
+ roleInParent: Role,
479
+ roleInChild: Exclude<Role, "revoked"> | undefined,
480
+ ) {
481
+ // invites should never be inherited
482
+ if (
483
+ roleInParent === "adminInvite" ||
484
+ roleInParent === "writerInvite" ||
485
+ roleInParent === "readerInvite"
486
+ ) {
487
+ return false;
488
+ }
489
+
490
+ if (roleInParent === "admin") {
491
+ return !roleInChild || roleInChild !== "admin";
492
+ }
493
+
494
+ if (roleInParent === "writer") {
495
+ return !roleInChild || roleInChild === "reader";
496
+ }
497
+
498
+ if (roleInParent === "reader") {
499
+ return !roleInChild;
500
+ }
501
+
502
+ return false;
503
+ }
504
+
350
505
  export type InviteSecret = `inviteSecret_z${string}`;
351
506
 
352
507
  function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {