cojson 0.8.32 → 0.8.35

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/native/coValueCore.js +73 -37
  3. package/dist/native/coValueCore.js.map +1 -1
  4. package/dist/native/coValues/coMap.js +2 -2
  5. package/dist/native/coValues/coMap.js.map +1 -1
  6. package/dist/native/coValues/group.js +132 -5
  7. package/dist/native/coValues/group.js.map +1 -1
  8. package/dist/native/exports.js +5 -2
  9. package/dist/native/exports.js.map +1 -1
  10. package/dist/native/ids.js +33 -0
  11. package/dist/native/ids.js.map +1 -1
  12. package/dist/native/permissions.js +206 -145
  13. package/dist/native/permissions.js.map +1 -1
  14. package/dist/native/storage/index.js +8 -4
  15. package/dist/native/storage/index.js.map +1 -1
  16. package/dist/native/sync.js +41 -31
  17. package/dist/native/sync.js.map +1 -1
  18. package/dist/web/coValueCore.js +73 -37
  19. package/dist/web/coValueCore.js.map +1 -1
  20. package/dist/web/coValues/coMap.js +2 -2
  21. package/dist/web/coValues/coMap.js.map +1 -1
  22. package/dist/web/coValues/group.js +132 -5
  23. package/dist/web/coValues/group.js.map +1 -1
  24. package/dist/web/exports.js +5 -2
  25. package/dist/web/exports.js.map +1 -1
  26. package/dist/web/ids.js +33 -0
  27. package/dist/web/ids.js.map +1 -1
  28. package/dist/web/permissions.js +206 -145
  29. package/dist/web/permissions.js.map +1 -1
  30. package/dist/web/storage/index.js +8 -4
  31. package/dist/web/storage/index.js.map +1 -1
  32. package/dist/web/sync.js +41 -31
  33. package/dist/web/sync.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/coValueCore.ts +119 -46
  36. package/src/coValues/coMap.ts +3 -6
  37. package/src/coValues/group.ts +219 -6
  38. package/src/exports.ts +18 -3
  39. package/src/ids.ts +48 -0
  40. package/src/permissions.ts +297 -204
  41. package/src/storage/index.ts +12 -4
  42. package/src/sync.ts +43 -34
  43. package/src/tests/group.test.ts +152 -1
  44. package/src/tests/permissions.test.ts +785 -2
  45. package/src/tests/sync.test.ts +29 -0
  46. package/src/tests/testUtils.ts +102 -1
@@ -2,9 +2,19 @@ import { base58 } from "@scure/base";
2
2
  import { CoID } from "../coValue.js";
3
3
  import { CoValueUniqueness } from "../coValueCore.js";
4
4
  import { Encrypted, KeyID, KeySecret, Sealed } from "../crypto/crypto.js";
5
- import { AgentID, isAgentID } from "../ids.js";
5
+ import {
6
+ AgentID,
7
+ ChildGroupReference,
8
+ ParentGroupReference,
9
+ getChildGroupId,
10
+ getParentGroupId,
11
+ isAgentID,
12
+ isChildGroupReference,
13
+ isParentGroupReference,
14
+ } from "../ids.js";
6
15
  import { JsonObject } from "../jsonValue.js";
7
16
  import { Role } from "../permissions.js";
17
+ import { expectGroup } from "../typeUtils/expectGroup.js";
8
18
  import {
9
19
  ControlledAccountOrAgent,
10
20
  RawAccount,
@@ -29,6 +39,8 @@ export type GroupShape = {
29
39
  KeySecret,
30
40
  { encryptedID: KeyID; encryptingID: KeyID }
31
41
  >;
42
+ [parent: ParentGroupReference]: "extend";
43
+ [child: ChildGroupReference]: "extend";
32
44
  };
33
45
 
34
46
  /** A `Group` is a scope for permissions of its members (`"reader" | "writer" | "admin"`), applying to objects owned by that group.
@@ -61,12 +73,109 @@ export class RawGroup<
61
73
  * @category 1. Role reading
62
74
  */
63
75
  roleOf(accountID: RawAccountID): Role | undefined {
64
- return this.roleOfInternal(accountID);
76
+ return this.roleOfInternal(accountID)?.role;
65
77
  }
66
78
 
67
79
  /** @internal */
68
- roleOfInternal(accountID: RawAccountID | AgentID): Role | undefined {
69
- return this.get(accountID);
80
+ roleOfInternal(
81
+ accountID: RawAccountID | AgentID | typeof EVERYONE,
82
+ ): { role: Role; via: CoID<RawGroup> | undefined } | undefined {
83
+ const roleHere = this.get(accountID);
84
+ if (roleHere === "revoked") {
85
+ return undefined;
86
+ }
87
+
88
+ let roleInfo:
89
+ | {
90
+ role: Exclude<Role, "revoked">;
91
+ via: CoID<RawGroup> | undefined;
92
+ }
93
+ | undefined = roleHere && { role: roleHere, via: undefined };
94
+
95
+ const parentGroups = this.getParentGroups();
96
+
97
+ for (const parentGroup of parentGroups) {
98
+ const roleInParent = parentGroup.roleOfInternal(accountID);
99
+
100
+ if (
101
+ roleInParent &&
102
+ roleInParent.role !== "revoked" &&
103
+ isMorePermissiveAndShouldInherit(roleInParent.role, roleInfo?.role)
104
+ ) {
105
+ roleInfo = { role: roleInParent.role, via: parentGroup.id };
106
+ }
107
+ }
108
+
109
+ return roleInfo;
110
+ }
111
+
112
+ getParentGroups() {
113
+ const groups: RawGroup[] = [];
114
+
115
+ for (const key of this.keys()) {
116
+ if (isParentGroupReference(key)) {
117
+ const parent = this.core.node.expectCoValueLoaded(
118
+ getParentGroupId(key),
119
+ "Expected parent group to be loaded",
120
+ );
121
+ groups.push(expectGroup(parent.getCurrentContent()));
122
+ }
123
+ }
124
+
125
+ return groups;
126
+ }
127
+
128
+ loadAllChildGroups() {
129
+ const requests: Promise<unknown>[] = [];
130
+ const store = this.core.node.coValuesStore;
131
+ const peers = this.core.node.syncManager.getServerAndStoragePeers();
132
+
133
+ for (const key of this.keys()) {
134
+ if (!isChildGroupReference(key)) {
135
+ continue;
136
+ }
137
+
138
+ const id = getChildGroupId(key);
139
+ const child = store.get(id);
140
+
141
+ if (
142
+ child.state.type === "unknown" ||
143
+ child.state.type === "unavailable"
144
+ ) {
145
+ child.loadFromPeers(peers).catch(() => {
146
+ console.error(`Failed to load child group ${id}`);
147
+ });
148
+ }
149
+
150
+ requests.push(
151
+ child.getCoValue().then((coValue) => {
152
+ if (coValue === "unavailable") {
153
+ throw new Error(`Child group ${child.id} is unavailable`);
154
+ }
155
+
156
+ // Recursively load child groups
157
+ return expectGroup(coValue.getCurrentContent()).loadAllChildGroups();
158
+ }),
159
+ );
160
+ }
161
+
162
+ return Promise.all(requests);
163
+ }
164
+
165
+ getChildGroups() {
166
+ const groups: RawGroup[] = [];
167
+
168
+ for (const key of this.keys()) {
169
+ if (isChildGroupReference(key)) {
170
+ const child = this.core.node.expectCoValueLoaded(
171
+ getChildGroupId(key),
172
+ "Expected child group to be loaded",
173
+ );
174
+ groups.push(expectGroup(child.getCurrentContent()));
175
+ }
176
+ }
177
+
178
+ return groups;
70
179
  }
71
180
 
72
181
  /**
@@ -75,7 +184,7 @@ export class RawGroup<
75
184
  * @category 1. Role reading
76
185
  */
77
186
  myRole(): Role | undefined {
78
- return this.roleOfInternal(this.core.node.account.id);
187
+ return this.roleOfInternal(this.core.node.account.id)?.role;
79
188
  }
80
189
 
81
190
  /**
@@ -158,6 +267,10 @@ export class RawGroup<
158
267
  }
159
268
  }) as (RawAccountID | AgentID)[];
160
269
 
270
+ // Get these early, so we fail fast if they are unavailable
271
+ const parentGroups = this.getParentGroups();
272
+ const childGroups = this.getChildGroups();
273
+
161
274
  const maybeCurrentReadKey = this.core.getCurrentReadKey();
162
275
 
163
276
  if (!maybeCurrentReadKey.secret) {
@@ -204,6 +317,73 @@ export class RawGroup<
204
317
  );
205
318
 
206
319
  this.set("readKey", newReadKey.id, "trusting");
320
+
321
+ // when we rotate our readKey (because someone got kicked out), we also need to (recursively)
322
+ // rotate the readKeys of all child groups (so they are kicked out there as well)
323
+ for (const parent of parentGroups) {
324
+ const { id: parentReadKeyID, secret: parentReadKeySecret } =
325
+ parent.core.getCurrentReadKey();
326
+
327
+ if (!parentReadKeySecret) {
328
+ throw new Error(
329
+ "Can't reveal new child key to parent where we don't have access to the parent read key",
330
+ );
331
+ }
332
+
333
+ this.set(
334
+ `${newReadKey.id}_for_${parentReadKeyID}`,
335
+ this.core.crypto.encryptKeySecret({
336
+ encrypting: {
337
+ id: parentReadKeyID,
338
+ secret: parentReadKeySecret,
339
+ },
340
+ toEncrypt: newReadKey,
341
+ }).encrypted,
342
+ "trusting",
343
+ );
344
+ }
345
+
346
+ for (const child of childGroups) {
347
+ child.rotateReadKey();
348
+ }
349
+ }
350
+
351
+ extend(parent: RawGroup) {
352
+ if (parent.myRole() !== "admin" || this.myRole() !== "admin") {
353
+ throw new Error(
354
+ "To extend a group, the current account must have admin role in both groups",
355
+ );
356
+ }
357
+
358
+ this.set(`parent_${parent.id}`, "extend", "trusting");
359
+ parent.set(`child_${this.id}`, "extend", "trusting");
360
+
361
+ const { id: parentReadKeyID, secret: parentReadKeySecret } =
362
+ parent.core.getCurrentReadKey();
363
+ if (!parentReadKeySecret) {
364
+ throw new Error("Can't extend group without parent read key secret");
365
+ }
366
+
367
+ const { id: childReadKeyID, secret: childReadKeySecret } =
368
+ this.core.getCurrentReadKey();
369
+ if (!childReadKeySecret) {
370
+ throw new Error("Can't extend group without child read key secret");
371
+ }
372
+
373
+ this.set(
374
+ `${childReadKeyID}_for_${parentReadKeyID}`,
375
+ this.core.crypto.encryptKeySecret({
376
+ encrypting: {
377
+ id: parentReadKeyID,
378
+ secret: parentReadKeySecret,
379
+ },
380
+ toEncrypt: {
381
+ id: childReadKeyID,
382
+ secret: childReadKeySecret,
383
+ },
384
+ }).encrypted,
385
+ "trusting",
386
+ );
207
387
  }
208
388
 
209
389
  /**
@@ -213,7 +393,12 @@ export class RawGroup<
213
393
  *
214
394
  * @category 2. Role changing
215
395
  */
216
- removeMember(account: RawAccount | ControlledAccountOrAgent | Everyone) {
396
+ async removeMember(
397
+ account: RawAccount | ControlledAccountOrAgent | Everyone,
398
+ ) {
399
+ // Ensure all child groups are loaded before removing a member
400
+ await this.loadAllChildGroups();
401
+
217
402
  this.removeMemberInternal(account);
218
403
  }
219
404
 
@@ -347,6 +532,34 @@ export class RawGroup<
347
532
  }
348
533
  }
349
534
 
535
+ function isMorePermissiveAndShouldInherit(
536
+ roleInParent: Role,
537
+ roleInChild: Exclude<Role, "revoked"> | undefined,
538
+ ) {
539
+ // invites should never be inherited
540
+ if (
541
+ roleInParent === "adminInvite" ||
542
+ roleInParent === "writerInvite" ||
543
+ roleInParent === "readerInvite"
544
+ ) {
545
+ return false;
546
+ }
547
+
548
+ if (roleInParent === "admin") {
549
+ return !roleInChild || roleInChild !== "admin";
550
+ }
551
+
552
+ if (roleInParent === "writer") {
553
+ return !roleInChild || roleInChild === "reader";
554
+ }
555
+
556
+ if (roleInParent === "reader") {
557
+ return !roleInChild;
558
+ }
559
+
560
+ return false;
561
+ }
562
+
350
563
  export type InviteSecret = `inviteSecret_z${string}`;
351
564
 
352
565
  function inviteSecretFromSecretSeed(secretSeed: Uint8Array): InviteSecret {
package/src/exports.ts CHANGED
@@ -23,8 +23,14 @@ import {
23
23
  secretSeedLength,
24
24
  shortHashLength,
25
25
  } from "./crypto/crypto.js";
26
- import { isRawCoID, rawCoIDfromBytes, rawCoIDtoBytes } from "./ids.js";
27
- import { parseJSON } from "./jsonStringify.js";
26
+ import {
27
+ getGroupDependentKey,
28
+ getGroupDependentKeyList,
29
+ isRawCoID,
30
+ rawCoIDfromBytes,
31
+ rawCoIDtoBytes,
32
+ } from "./ids.js";
33
+ import { Stringified, parseJSON } from "./jsonStringify.js";
28
34
  import { LocalNode } from "./localNode.js";
29
35
  import type { Role } from "./permissions.js";
30
36
  import { Channel, connectedPeers } from "./streamUtils.js";
@@ -53,7 +59,11 @@ import type {
53
59
  Peer,
54
60
  SyncMessage,
55
61
  } from "./sync.js";
56
- import { DisconnectedError, PingTimeoutError } from "./sync.js";
62
+ import {
63
+ DisconnectedError,
64
+ PingTimeoutError,
65
+ emptyKnownState,
66
+ } from "./sync.js";
57
67
 
58
68
  type Value = JsonValue | AnyRawCoValue;
59
69
 
@@ -79,6 +89,8 @@ export const cojsonInternals = {
79
89
  StreamingHash,
80
90
  Channel,
81
91
  getPriorityFromHeader,
92
+ getGroupDependentKeyList,
93
+ getGroupDependentKey,
82
94
  };
83
95
 
84
96
  export {
@@ -116,6 +128,7 @@ export {
116
128
  SyncMessage,
117
129
  isRawCoID,
118
130
  LSMStorage,
131
+ emptyKnownState,
119
132
  };
120
133
 
121
134
  export type {
@@ -128,6 +141,7 @@ export type {
128
141
  DisconnectedError,
129
142
  PingTimeoutError,
130
143
  CoValueUniqueness,
144
+ Stringified,
131
145
  };
132
146
 
133
147
  // eslint-disable-next-line @typescript-eslint/no-namespace
@@ -137,6 +151,7 @@ export namespace CojsonInternalTypes {
137
151
  export type KnownStateMessage = import("./sync.js").KnownStateMessage;
138
152
  export type LoadMessage = import("./sync.js").LoadMessage;
139
153
  export type NewContentMessage = import("./sync.js").NewContentMessage;
154
+ export type SessionNewContent = import("./sync.js").SessionNewContent;
140
155
  export type CoValueHeader = import("./coValueCore.js").CoValueHeader;
141
156
  export type Transaction = import("./coValueCore.js").Transaction;
142
157
  export type TransactionID = import("./ids.js").TransactionID;
package/src/ids.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import { base58 } from "@scure/base";
2
+ import { CoID } from "./coValue.js";
2
3
  import { RawAccountID } from "./coValues/account.js";
3
4
  import { shortHashLength } from "./crypto/crypto.js";
5
+ import { RawGroup } from "./exports.js";
4
6
 
5
7
  export type RawCoID = `co_z${string}`;
8
+ export type ParentGroupReference = `parent_${CoID<RawGroup>}`;
9
+ export type ChildGroupReference = `child_${CoID<RawGroup>}`;
6
10
 
7
11
  export function isRawCoID(id: unknown): id is RawCoID {
8
12
  return typeof id === "string" && id.startsWith("co_z");
@@ -29,3 +33,47 @@ export function isAgentID(id: string): id is AgentID {
29
33
  }
30
34
 
31
35
  export type SessionID = `${RawAccountID | AgentID}_session_z${string}`;
36
+
37
+ export function isParentGroupReference(
38
+ key: string,
39
+ ): key is ParentGroupReference {
40
+ return key.startsWith("parent_");
41
+ }
42
+
43
+ export function getParentGroupId(key: ParentGroupReference): CoID<RawGroup> {
44
+ return key.slice("parent_".length) as CoID<RawGroup>;
45
+ }
46
+
47
+ export function isChildGroupReference(key: string): key is ChildGroupReference {
48
+ return key.startsWith("child_");
49
+ }
50
+
51
+ export function getChildGroupId(key: ChildGroupReference): CoID<RawGroup> {
52
+ return key.slice("child_".length) as CoID<RawGroup>;
53
+ }
54
+
55
+ export function getGroupDependentKey(key: unknown) {
56
+ if (typeof key !== "string") return undefined;
57
+
58
+ if (isParentGroupReference(key)) {
59
+ return getParentGroupId(key);
60
+ } else if (key.startsWith("co_")) {
61
+ return key as RawCoID;
62
+ }
63
+
64
+ return undefined;
65
+ }
66
+
67
+ export function getGroupDependentKeyList(keys: unknown[]) {
68
+ const groupDependentKeys: RawCoID[] = [];
69
+
70
+ for (const key of keys) {
71
+ const value = getGroupDependentKey(key);
72
+
73
+ if (value) {
74
+ groupDependentKeys.push(value);
75
+ }
76
+ }
77
+
78
+ return groupDependentKeys;
79
+ }