cojson 0.8.49 → 0.9.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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/CHANGELOG.md +13 -0
  3. package/dist/native/coValueCore.js +3 -1
  4. package/dist/native/coValueCore.js.map +1 -1
  5. package/dist/native/coValues/coList.js +14 -7
  6. package/dist/native/coValues/coList.js.map +1 -1
  7. package/dist/native/coValues/coMap.js +11 -0
  8. package/dist/native/coValues/coMap.js.map +1 -1
  9. package/dist/native/coValues/group.js +25 -7
  10. package/dist/native/coValues/group.js.map +1 -1
  11. package/dist/native/crypto/WasmCrypto.js +130 -0
  12. package/dist/native/crypto/WasmCrypto.js.map +1 -0
  13. package/dist/native/crypto/export.js +3 -0
  14. package/dist/native/crypto/export.js.map +1 -0
  15. package/dist/native/permissions.js +15 -4
  16. package/dist/native/permissions.js.map +1 -1
  17. package/dist/web/coValueCore.js +3 -1
  18. package/dist/web/coValueCore.js.map +1 -1
  19. package/dist/web/coValues/coList.js +14 -7
  20. package/dist/web/coValues/coList.js.map +1 -1
  21. package/dist/web/coValues/coMap.js +11 -0
  22. package/dist/web/coValues/coMap.js.map +1 -1
  23. package/dist/web/coValues/group.js +25 -7
  24. package/dist/web/coValues/group.js.map +1 -1
  25. package/dist/web/crypto/export.js +3 -0
  26. package/dist/web/crypto/export.js.map +1 -0
  27. package/dist/web/permissions.js +15 -4
  28. package/dist/web/permissions.js.map +1 -1
  29. package/package.json +6 -1
  30. package/src/coValueCore.ts +4 -1
  31. package/src/coValues/coList.ts +24 -11
  32. package/src/coValues/coMap.ts +20 -0
  33. package/src/coValues/group.ts +31 -7
  34. package/src/crypto/export.ts +2 -0
  35. package/src/permissions.ts +29 -2
  36. package/src/tests/coList.test.ts +40 -0
  37. package/src/tests/coMap.test.ts +32 -0
  38. package/src/tests/group.test.ts +29 -0
  39. package/src/tests/permissions.test.ts +54 -0
@@ -288,6 +288,7 @@ export class RawCoListView<
288
288
  ) {
289
289
  const entry =
290
290
  this.insertions[opID.sessionID]?.[opID.txIndex]?.[opID.changeIdx];
291
+
291
292
  if (!entry) {
292
293
  throw new Error("Missing op " + opID);
293
294
  }
@@ -412,6 +413,14 @@ export class RawCoList<
412
413
  item: Item,
413
414
  after?: number,
414
415
  privacy: "private" | "trusting" = "private",
416
+ ) {
417
+ this.appendItems([item], after, privacy);
418
+ }
419
+
420
+ appendItems(
421
+ items: Item[],
422
+ after?: number,
423
+ privacy: "private" | "trusting" = "private",
415
424
  ) {
416
425
  const entries = this.entries();
417
426
  after =
@@ -420,7 +429,7 @@ export class RawCoList<
420
429
  ? entries.length - 1
421
430
  : 0
422
431
  : after;
423
- let opIDBefore;
432
+ let opIDBefore: OpID | "start";
424
433
  if (entries.length > 0) {
425
434
  const entryBefore = entries[after];
426
435
  if (!entryBefore) {
@@ -433,16 +442,20 @@ export class RawCoList<
433
442
  }
434
443
  opIDBefore = "start";
435
444
  }
436
- this.core.makeTransaction(
437
- [
438
- {
439
- op: "app",
440
- value: isCoValue(item) ? item.id : item,
441
- after: opIDBefore,
442
- },
443
- ],
444
- privacy,
445
- );
445
+
446
+ const changes = items.map((item) => ({
447
+ op: "app",
448
+ value: isCoValue(item) ? item.id : item,
449
+ after: opIDBefore,
450
+ }));
451
+
452
+ if (opIDBefore !== "start") {
453
+ // When added as successors we need to reverse the items
454
+ // to keep the same insertion order
455
+ changes.reverse();
456
+ }
457
+
458
+ this.core.makeTransaction(changes, privacy);
446
459
 
447
460
  const listAfter = new RawCoList(this.core) as this;
448
461
 
@@ -383,6 +383,26 @@ export class RawCoMap<
383
383
  this.processNewTransactions();
384
384
  }
385
385
 
386
+ assign(
387
+ entries: Partial<Shape>,
388
+ privacy: "private" | "trusting" = "private",
389
+ ): void {
390
+ if (this.isTimeTravelEntity()) {
391
+ throw new Error("Cannot set value on a time travel entity");
392
+ }
393
+
394
+ this.core.makeTransaction(
395
+ Object.entries(entries).map(([key, value]) => ({
396
+ op: "set",
397
+ key,
398
+ value: isCoValue(value) ? value.id : value,
399
+ })),
400
+ privacy,
401
+ );
402
+
403
+ this.processNewTransactions();
404
+ }
405
+
386
406
  /** Delete the given key (setting it to undefined).
387
407
  *
388
408
  * If `privacy` is `"private"` **(default)**, `key` is encrypted in the transaction, only readable by other members of the group this `CoMap` belongs to. Not even sync servers can see the content in plaintext.
@@ -513,11 +513,39 @@ export class RawGroup<
513
513
  }
514
514
 
515
515
  for (const child of childGroups) {
516
+ // Since child references are mantained only for the key rotation,
517
+ // circular references are skipped here because it's more performant
518
+ // than always checking for circular references in childs inside the permission checks
519
+ if (child.isSelfExtension(this)) {
520
+ continue;
521
+ }
522
+
516
523
  child.rotateReadKey();
517
524
  }
518
525
  }
519
526
 
527
+ /** Detect circular references in group inheritance */
528
+ isSelfExtension(parent: RawGroup) {
529
+ if (parent.id === this.id) {
530
+ return true;
531
+ }
532
+
533
+ const childGroups = this.getChildGroups();
534
+
535
+ for (const child of childGroups) {
536
+ if (child.isSelfExtension(parent)) {
537
+ return true;
538
+ }
539
+ }
540
+
541
+ return false;
542
+ }
543
+
520
544
  extend(parent: RawGroup) {
545
+ if (this.isSelfExtension(parent)) {
546
+ return;
547
+ }
548
+
521
549
  if (this.myRole() !== "admin") {
522
550
  throw new Error(
523
551
  "To extend a group, the current account must be an admin in the child group",
@@ -634,9 +662,7 @@ export class RawGroup<
634
662
  .getCurrentContent() as M;
635
663
 
636
664
  if (init) {
637
- for (const [key, value] of Object.entries(init)) {
638
- map.set(key, value, initPrivacy);
639
- }
665
+ map.assign(init, initPrivacy);
640
666
  }
641
667
 
642
668
  return map;
@@ -666,10 +692,8 @@ export class RawGroup<
666
692
  })
667
693
  .getCurrentContent() as L;
668
694
 
669
- if (init) {
670
- for (const item of init) {
671
- list.append(item, undefined, initPrivacy);
672
- }
695
+ if (init?.length) {
696
+ list.appendItems(init, undefined, initPrivacy);
673
697
  }
674
698
 
675
699
  return list;
@@ -0,0 +1,2 @@
1
+ export * from "./PureJSCrypto.js";
2
+ export * from "./WasmCrypto.js";
@@ -137,6 +137,7 @@ function resolveMemberStateFromParentReference(
137
137
  coValue: CoValueCore,
138
138
  memberState: MemberState,
139
139
  parentReference: ParentGroupReference,
140
+ extendChain: Set<CoValueCore["id"]>,
140
141
  ) {
141
142
  const parentGroup = coValue.node.expectCoValueLoaded(
142
143
  getParentGroupId(parentReference),
@@ -147,14 +148,21 @@ function resolveMemberStateFromParentReference(
147
148
  return;
148
149
  }
149
150
 
151
+ // Skip circular references
152
+ if (extendChain.has(parentGroup.id)) {
153
+ return;
154
+ }
155
+
150
156
  const initialAdmin = parentGroup.header.ruleset.initialAdmin;
151
157
 
152
158
  if (!initialAdmin) {
153
159
  throw new Error("Group must have initialAdmin");
154
160
  }
155
161
 
162
+ extendChain.add(parentGroup.id);
163
+
156
164
  const { memberState: parentGroupMemberState } =
157
- determineValidTransactionsForGroup(parentGroup, initialAdmin);
165
+ determineValidTransactionsForGroup(parentGroup, initialAdmin, extendChain);
158
166
 
159
167
  for (const agent of Object.keys(parentGroupMemberState) as Array<
160
168
  keyof MemberState
@@ -171,6 +179,7 @@ function resolveMemberStateFromParentReference(
171
179
  function determineValidTransactionsForGroup(
172
180
  coValue: CoValueCore,
173
181
  initialAdmin: RawAccountID | AgentID,
182
+ extendChain?: Set<CoValueCore["id"]>,
174
183
  ): { validTransactions: ValidTransactionsResult[]; memberState: MemberState } {
175
184
  const allTransactionsSorted: {
176
185
  sessionID: SessionID;
@@ -305,7 +314,24 @@ function determineValidTransactionsForGroup(
305
314
  logPermissionError("Only admins can set parent extensions");
306
315
  continue;
307
316
  }
308
- resolveMemberStateFromParentReference(coValue, memberState, change.key);
317
+
318
+ extendChain = extendChain ?? new Set([]);
319
+
320
+ resolveMemberStateFromParentReference(
321
+ coValue,
322
+ memberState,
323
+ change.key,
324
+ extendChain,
325
+ );
326
+
327
+ // Circular reference detected, drop all the transactions involved
328
+ if (extendChain.has(coValue.id)) {
329
+ logPermissionError(
330
+ "Circular extend detected, dropping the transaction",
331
+ );
332
+ continue;
333
+ }
334
+
309
335
  validTransactions.push({ txID: { sessionID, txIndex }, tx });
310
336
  continue;
311
337
  } else if (isChildExtension(change.key)) {
@@ -320,6 +346,7 @@ function determineValidTransactionsForGroup(
320
346
  );
321
347
  continue;
322
348
  }
349
+
323
350
  validTransactions.push({ txID: { sessionID, txIndex }, tx });
324
351
  continue;
325
352
  } else if (isWriteKeyForMember(change.key)) {
@@ -75,6 +75,26 @@ test("Push is equivalent to append after last item", () => {
75
75
  expect(content.toJSON()).toEqual(["hello", "world", "hooray"]);
76
76
  });
77
77
 
78
+ test("appendItems add an array of items at the end of the list", () => {
79
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
80
+
81
+ const coValue = node.createCoValue({
82
+ type: "colist",
83
+ ruleset: { type: "unsafeAllowAll" },
84
+ meta: null,
85
+ ...Crypto.createdNowUnique(),
86
+ });
87
+
88
+ const content = expectList(coValue.getCurrentContent());
89
+
90
+ expect(content.type).toEqual("colist");
91
+
92
+ content.append("hello", 0, "trusting");
93
+ expect(content.toJSON()).toEqual(["hello"]);
94
+ content.appendItems(["world", "hooray", "universe"], undefined, "trusting");
95
+ expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
96
+ });
97
+
78
98
  test("Can push into empty list", () => {
79
99
  const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
80
100
 
@@ -92,3 +112,23 @@ test("Can push into empty list", () => {
92
112
  content.append("hello", undefined, "trusting");
93
113
  expect(content.toJSON()).toEqual(["hello"]);
94
114
  });
115
+
116
+ test("init the list correctly", () => {
117
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
118
+
119
+ const group = node.createGroup();
120
+
121
+ const content = group.createList(["hello", "world", "hooray", "universe"]);
122
+
123
+ expect(content.type).toEqual("colist");
124
+ expect(content.toJSON()).toEqual(["hello", "world", "hooray", "universe"]);
125
+
126
+ content.append("hello", content.toJSON().length - 1, "trusting");
127
+ expect(content.toJSON()).toEqual([
128
+ "hello",
129
+ "world",
130
+ "hooray",
131
+ "universe",
132
+ "hello",
133
+ ]);
134
+ });
@@ -175,3 +175,35 @@ test("Can get last tx ID for a key in CoMap", () => {
175
175
  content.set("hello", "C", "trusting");
176
176
  expect(content.lastEditAt("hello")?.tx.txIndex).toEqual(2);
177
177
  });
178
+
179
+ test("Can set items in bulk with assign", () => {
180
+ const node = new LocalNode(...randomAnonymousAccountAndSessionID(), Crypto);
181
+
182
+ const coValue = node.createCoValue({
183
+ type: "comap",
184
+ ruleset: { type: "unsafeAllowAll" },
185
+ meta: null,
186
+ ...Crypto.createdNowUnique(),
187
+ });
188
+
189
+ const content = expectMap(coValue.getCurrentContent());
190
+
191
+ expect(content.type).toEqual("comap");
192
+
193
+ content.set("key1", "set1", "trusting");
194
+
195
+ content.assign(
196
+ {
197
+ key1: "assign1",
198
+ key2: "assign2",
199
+ key3: "assign3",
200
+ },
201
+ "trusting",
202
+ );
203
+
204
+ expect(content.toJSON()).toEqual({
205
+ key1: "assign1",
206
+ key2: "assign2",
207
+ key3: "assign3",
208
+ });
209
+ });
@@ -609,4 +609,33 @@ describe("writeOnly", () => {
609
609
 
610
610
  expect(mapOnNode2.get("test")).toEqual("Written from node2");
611
611
  });
612
+
613
+ test("self-extend a group should not break anything", async () => {
614
+ const { node1 } = await createTwoConnectedNodes("server", "server");
615
+
616
+ const group = node1.node.createGroup();
617
+ group.extend(group);
618
+
619
+ const map = group.createMap();
620
+ map.set("test", "Hello!");
621
+
622
+ expect(map.get("test")).toEqual("Hello!");
623
+ });
624
+
625
+ test("should not break when introducing extend cycles", async () => {
626
+ const { node1 } = await createTwoConnectedNodes("server", "server");
627
+
628
+ const group = node1.node.createGroup();
629
+ const group2 = node1.node.createGroup();
630
+ const group3 = node1.node.createGroup();
631
+
632
+ group.extend(group2);
633
+ group2.extend(group3);
634
+ group3.extend(group);
635
+
636
+ const map = group.createMap();
637
+ map.set("test", "Hello!");
638
+
639
+ expect(map.get("test")).toEqual("Hello!");
640
+ });
612
641
  });
@@ -2854,3 +2854,57 @@ test("High-level permissions work correctly when a group is extended", async ()
2854
2854
 
2855
2855
  expect(mapAsReaderAfterRemove.get("foo")).not.toEqual("baz");
2856
2856
  });
2857
+
2858
+ test("self-extensions should not break the permissions checks", () => {
2859
+ const { group } = newGroupHighLevel();
2860
+
2861
+ group.set(`child_${group.id}`, "extend", "trusting");
2862
+ group.set(`parent_${group.id}`, "extend", "trusting");
2863
+
2864
+ const map = group.createMap();
2865
+ map.set("test", "Hello!");
2866
+
2867
+ expect(map.get("test")).toEqual("Hello!");
2868
+ });
2869
+
2870
+ test("extend cycles should not break the permissions checks", () => {
2871
+ const { group, node } = newGroupHighLevel();
2872
+
2873
+ const group2 = node.createGroup();
2874
+ const group3 = node.createGroup();
2875
+
2876
+ group.set(`child_${group2.id}`, "extend", "trusting");
2877
+ group2.set(`child_${group3.id}`, "extend", "trusting");
2878
+ group3.set(`child_${group.id}`, "extend", "trusting");
2879
+
2880
+ group.set(`parent_${group2.id}`, "extend", "trusting");
2881
+ group2.set(`parent_${group3.id}`, "extend", "trusting");
2882
+ group3.set(`parent_${group.id}`, "extend", "trusting");
2883
+
2884
+ const map = group.createMap();
2885
+ map.set("test", "Hello!");
2886
+
2887
+ expect(map.get("test")).toEqual("Hello!");
2888
+ });
2889
+
2890
+ test("extend cycles should not break the keys rotation", () => {
2891
+ const { group, node } = newGroupHighLevel();
2892
+
2893
+ const group2 = node.createGroup();
2894
+ const group3 = node.createGroup();
2895
+
2896
+ group.set(`child_${group2.id}`, "extend", "trusting");
2897
+ group2.set(`child_${group3.id}`, "extend", "trusting");
2898
+ group3.set(`child_${group.id}`, "extend", "trusting");
2899
+
2900
+ group.set(`parent_${group2.id}`, "extend", "trusting");
2901
+ group2.set(`parent_${group3.id}`, "extend", "trusting");
2902
+ group3.set(`parent_${group.id}`, "extend", "trusting");
2903
+
2904
+ group.rotateReadKey();
2905
+
2906
+ const map = group.createMap();
2907
+ map.set("test", "Hello!");
2908
+
2909
+ expect(map.get("test")).toEqual("Hello!");
2910
+ });