cojson 0.18.27 → 0.18.28

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 (60) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/dist/coValueCore/branching.d.ts +2 -1
  4. package/dist/coValueCore/branching.d.ts.map +1 -1
  5. package/dist/coValueCore/branching.js +20 -2
  6. package/dist/coValueCore/branching.js.map +1 -1
  7. package/dist/coValueCore/coValueCore.d.ts +26 -23
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +161 -123
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValueCore/decryptTransactionChangesAndMeta.d.ts +3 -0
  12. package/dist/coValueCore/decryptTransactionChangesAndMeta.d.ts.map +1 -0
  13. package/dist/coValueCore/decryptTransactionChangesAndMeta.js +34 -0
  14. package/dist/coValueCore/decryptTransactionChangesAndMeta.js.map +1 -0
  15. package/dist/localNode.js +1 -1
  16. package/dist/localNode.js.map +1 -1
  17. package/dist/permissions.d.ts.map +1 -1
  18. package/dist/permissions.js +18 -20
  19. package/dist/permissions.js.map +1 -1
  20. package/dist/sync.js +2 -2
  21. package/dist/sync.js.map +1 -1
  22. package/dist/tests/branching.test.js +237 -28
  23. package/dist/tests/branching.test.js.map +1 -1
  24. package/dist/tests/coValueCore.loadFromStorage.test.d.ts +2 -0
  25. package/dist/tests/coValueCore.loadFromStorage.test.d.ts.map +1 -0
  26. package/dist/tests/coValueCore.loadFromStorage.test.js +395 -0
  27. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -0
  28. package/dist/tests/coValueCore.loadingState.test.d.ts +2 -0
  29. package/dist/tests/coValueCore.loadingState.test.d.ts.map +1 -0
  30. package/dist/tests/{coValueCoreLoadingState.test.js → coValueCore.loadingState.test.js} +4 -12
  31. package/dist/tests/coValueCore.loadingState.test.js.map +1 -0
  32. package/dist/tests/coValueCore.test.js +2 -5
  33. package/dist/tests/coValueCore.test.js.map +1 -1
  34. package/dist/tests/sync.mesh.test.js +4 -34
  35. package/dist/tests/sync.mesh.test.js.map +1 -1
  36. package/dist/tests/testUtils.d.ts +7 -1
  37. package/dist/tests/testUtils.d.ts.map +1 -1
  38. package/dist/tests/testUtils.js +12 -0
  39. package/dist/tests/testUtils.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/coValueCore/branching.ts +28 -3
  42. package/src/coValueCore/coValueCore.ts +215 -167
  43. package/src/coValueCore/decryptTransactionChangesAndMeta.ts +56 -0
  44. package/src/localNode.ts +1 -1
  45. package/src/permissions.ts +21 -19
  46. package/src/sync.ts +2 -2
  47. package/src/tests/branching.test.ts +392 -45
  48. package/src/tests/coValueCore.loadFromStorage.test.ts +540 -0
  49. package/src/tests/{coValueCoreLoadingState.test.ts → coValueCore.loadingState.test.ts} +3 -16
  50. package/src/tests/coValueCore.test.ts +2 -5
  51. package/src/tests/sync.mesh.test.ts +11 -38
  52. package/src/tests/testUtils.ts +21 -0
  53. package/dist/coValueCore/decodeTransactionChangesAndMeta.d.ts +0 -3
  54. package/dist/coValueCore/decodeTransactionChangesAndMeta.d.ts.map +0 -1
  55. package/dist/coValueCore/decodeTransactionChangesAndMeta.js +0 -59
  56. package/dist/coValueCore/decodeTransactionChangesAndMeta.js.map +0 -1
  57. package/dist/tests/coValueCoreLoadingState.test.d.ts +0 -2
  58. package/dist/tests/coValueCoreLoadingState.test.d.ts.map +0 -1
  59. package/dist/tests/coValueCoreLoadingState.test.js.map +0 -1
  60. package/src/coValueCore/decodeTransactionChangesAndMeta.ts +0 -81
@@ -115,9 +115,9 @@ export function determineValidTransactions(coValue: CoValueCore) {
115
115
  }
116
116
 
117
117
  tx.isValidated = true;
118
- const wasValid = tx.isValid;
119
-
120
- const groupAtTime = groupContent.atTime(tx.madeAt);
118
+ // We use the original made at to get the group at the original time when the transaction was made
119
+ // madeAt might be changed by the meta field (e.g. merged transactions), and so can't be used for permissions checks
120
+ const groupAtTime = groupContent.atTime(tx.currentMadeAt);
121
121
  const effectiveTransactor = agentInAccountOrMemberInGroup(
122
122
  tx.author,
123
123
  groupAtTime,
@@ -131,6 +131,21 @@ export function determineValidTransactions(coValue: CoValueCore) {
131
131
  const transactorRoleAtTxTime =
132
132
  groupAtTime.roleOfInternal(effectiveTransactor);
133
133
 
134
+ if (
135
+ transactorRoleAtTxTime === "reader" &&
136
+ tx.meta?.branch &&
137
+ tx.meta?.ownerId
138
+ ) {
139
+ // Force the changes and meta to only contain the branch pointer information
140
+ tx.meta = {
141
+ branch: tx.meta.branch,
142
+ ownerId: tx.meta.ownerId,
143
+ };
144
+ tx.changes = [];
145
+ tx.isValid = true;
146
+ continue;
147
+ }
148
+
134
149
  if (
135
150
  transactorRoleAtTxTime !== "admin" &&
136
151
  transactorRoleAtTxTime !== "writer" &&
@@ -217,13 +232,10 @@ function determineValidTransactionsForGroup(
217
232
  initialAdmin: RawAccountID | AgentID,
218
233
  extendChain?: Set<CoValueCore["id"]>,
219
234
  ): { memberState: MemberState } {
220
- coValue.verifiedTransactions.sort((a, b) => {
221
- return a.madeAt - b.madeAt;
222
- });
235
+ coValue.verifiedTransactions.sort(coValue.compareTransactions);
223
236
 
224
237
  const memberState: MemberState = {};
225
238
  const writeOnlyKeys: Record<RawAccountID | AgentID, KeyID> = {};
226
- const validTransactions: ValidTransactionsResult[] = [];
227
239
 
228
240
  const writeKeys = new Set<string>();
229
241
 
@@ -246,20 +258,10 @@ function determineValidTransactionsForGroup(
246
258
  }
247
259
  }
248
260
 
249
- let changes = transaction.changes;
261
+ const changes = transaction.changes;
250
262
 
251
263
  if (!changes) {
252
- try {
253
- changes = parseJSON(tx.changes);
254
- transaction.changes = changes;
255
- } catch (e) {
256
- logPermissionError("Invalid JSON in transaction", {
257
- id: coValue.id,
258
- tx,
259
- });
260
- transaction.hasInvalidChanges = true;
261
- continue;
262
- }
264
+ continue;
263
265
  }
264
266
 
265
267
  const change = changes[0] as
package/src/sync.ts CHANGED
@@ -740,11 +740,11 @@ export class SyncManager {
740
740
  peer.role === "server" &&
741
741
  !peer.loadRequestSent.has(coValue.id)
742
742
  ) {
743
- const state = coValue.getStateForPeer(peer.id)?.type;
743
+ const state = coValue.getLoadingStateForPeer(peer.id);
744
744
 
745
745
  // Check if there is a inflight load operation and we
746
746
  // are waiting for other peers to send the load request
747
- if (state === "unknown" || state === undefined) {
747
+ if (state === "unknown") {
748
748
  // Sending a load message to the peer to get to know how much content is missing
749
749
  // before sending the new content
750
750
  this.trySendToPeer(peer, {
@@ -1,11 +1,21 @@
1
- import { assert, beforeEach, describe, expect, test } from "vitest";
1
+ import {
2
+ assert,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ onTestFinished,
7
+ test,
8
+ vi,
9
+ } from "vitest";
2
10
  import {
3
11
  createTestNode,
4
12
  setupTestNode,
5
13
  loadCoValueOrFail,
14
+ setupTestAccount,
15
+ waitFor,
6
16
  } from "./testUtils.js";
7
17
  import { expectList, expectMap, expectPlainText } from "../coValue.js";
8
- import { RawCoMap } from "../exports.js";
18
+ import { RawAccount, RawCoMap } from "../exports.js";
9
19
 
10
20
  let jazzCloud: ReturnType<typeof setupTestNode>;
11
21
 
@@ -237,6 +247,7 @@ describe("Branching Logic", () => {
237
247
  loadedBranch2.core.mergeBranch();
238
248
 
239
249
  await loadedBranch2.core.waitForSync();
250
+ await new Promise((resolve) => setTimeout(resolve, 5));
240
251
 
241
252
  branch1.core.mergeBranch();
242
253
 
@@ -289,6 +300,385 @@ describe("Branching Logic", () => {
289
300
 
290
301
  expect(plainText.toString()).toEqual("hello world people");
291
302
  });
303
+
304
+ test("should preserve the conflict resolution that was applied to the branch", async () => {
305
+ const dateNowMock = vi.spyOn(Date, "now");
306
+
307
+ dateNowMock.mockReturnValue(1);
308
+
309
+ const client = setupTestNode({
310
+ connected: true,
311
+ });
312
+ const group = client.node.createGroup();
313
+ const map = group.createMap();
314
+ const branchName = "feature-branch";
315
+
316
+ onTestFinished(() => {
317
+ dateNowMock.mockRestore();
318
+ });
319
+
320
+ // Add initial transactions to original map
321
+ map.set("value", 1, "trusting");
322
+
323
+ // Create branch from original map
324
+ const branch = expectMap(
325
+ map.core.createBranch(branchName, group.id).getCurrentContent(),
326
+ );
327
+
328
+ dateNowMock.mockReturnValue(2);
329
+
330
+ // Add new transaction to branch
331
+ branch.set("value", 2, "trusting");
332
+
333
+ const newSession = client.spawnNewSession();
334
+
335
+ const loadedBranch = await loadCoValueOrFail(newSession.node, branch.id);
336
+
337
+ dateNowMock.mockReturnValue(3);
338
+
339
+ loadedBranch.set("value", 3, "trusting");
340
+
341
+ expect(loadedBranch.get("value")).toBe(3);
342
+
343
+ await loadedBranch.core.waitForSync();
344
+
345
+ // Push back the change, so it doesn't win the conflict
346
+ dateNowMock.mockReturnValue(1);
347
+
348
+ branch.set("value", 4, "trusting");
349
+
350
+ expect(branch.get("value")).toBe(3);
351
+
352
+ dateNowMock.mockReturnValue(4);
353
+ branch.core.mergeBranch();
354
+
355
+ // The conflict resolution should be preserved, so we should have 3 and not 4
356
+ expect(map.get("value")).toBe(3);
357
+ });
358
+
359
+ test("should preserve the original madeAt of the branch", async () => {
360
+ const client = setupTestNode();
361
+ const group = client.node.createGroup();
362
+ const map = group.createMap();
363
+
364
+ // Add initial transactions to original map
365
+ map.set("value", 1, "trusting");
366
+
367
+ // Create branch from original map
368
+ const branch = expectMap(
369
+ map.core.createBranch("feature-branch", group.id).getCurrentContent(),
370
+ );
371
+
372
+ // Add new transaction to branch
373
+ branch.set("value", 2, "trusting");
374
+
375
+ await new Promise((resolve) => setTimeout(resolve, 5));
376
+
377
+ const result = branch.core.mergeBranch();
378
+
379
+ // The merge should be successful
380
+ expect(map.get("value")).toBe(2);
381
+
382
+ const lastBranchTransaction = branch.core
383
+ .getValidSortedTransactions()
384
+ .at(-1);
385
+ const lastMapTransaction = result
386
+ .getValidSortedTransactions()
387
+ .findLast((tx) => tx.txID.branch === branch.id);
388
+
389
+ expect(lastMapTransaction?.currentMadeAt).not.toBe(
390
+ lastMapTransaction?.madeAt,
391
+ );
392
+
393
+ expect(lastBranchTransaction?.madeAt).toBe(lastMapTransaction?.madeAt);
394
+ });
395
+
396
+ test("should not load the merged transactions into the branch (regression test)", async () => {
397
+ const client = setupTestNode();
398
+ const group = client.node.createGroup();
399
+ const map = group.createMap();
400
+
401
+ // Add initial transactions to original map
402
+ map.set("value", 1, "trusting");
403
+
404
+ // Create branch from original map
405
+ const branch = expectMap(
406
+ map.core.createBranch("feature-branch", group.id).getCurrentContent(),
407
+ );
408
+
409
+ await new Promise((resolve) => setTimeout(resolve, 5));
410
+
411
+ // Add new transaction to branch
412
+ branch.set("value", 2, "trusting");
413
+
414
+ await new Promise((resolve) => setTimeout(resolve, 5));
415
+
416
+ const result = branch.core.mergeBranch();
417
+
418
+ // The merge should be successful
419
+ expect(map.get("value")).toBe(2);
420
+
421
+ const lastBranchTransaction = branch.core
422
+ .getValidSortedTransactions()
423
+ .at(-1);
424
+ expect(lastBranchTransaction?.madeAt).toBe(
425
+ lastBranchTransaction?.currentMadeAt,
426
+ );
427
+
428
+ const lastMapTransaction = result
429
+ .getValidSortedTransactions()
430
+ .findLast((tx) => tx.txID.branch === branch.id);
431
+ expect(lastMapTransaction?.madeAt).not.toBe(
432
+ lastMapTransaction?.currentMadeAt,
433
+ );
434
+ });
435
+
436
+ test("write permissions should be validated against the time of the merge and not the original madeAt", async () => {
437
+ const alice = setupTestNode({
438
+ connected: true,
439
+ });
440
+ const bob = await setupTestAccount({
441
+ connected: true,
442
+ });
443
+ const group = alice.node.createGroup();
444
+ const map = group.createMap();
445
+ const branchName = "feature-branch";
446
+
447
+ map.set("value", 1, "trusting");
448
+
449
+ const branch = expectMap(
450
+ map.core.createBranch(branchName, group.id).getCurrentContent(),
451
+ );
452
+
453
+ branch.set("value", 2, "trusting");
454
+
455
+ await new Promise((resolve) => setTimeout(resolve, 5));
456
+
457
+ // Grant writer rights to bob after the changes inside the branche
458
+ group.addMember(
459
+ await loadCoValueOrFail(alice.node, bob.accountID),
460
+ "writer",
461
+ );
462
+
463
+ const loadedBranch = await loadCoValueOrFail(bob.node, branch.id);
464
+
465
+ // Bob merges the branch
466
+ const mergeResult = loadedBranch.core.mergeBranch();
467
+
468
+ // The merge should be successful
469
+ expect(expectMap(mergeResult.getCurrentContent()).get("value")).toBe(2);
470
+ });
471
+
472
+ test("should reject edits from kicked out member even with timestamp manipulation", async () => {
473
+ const alice = setupTestNode({
474
+ connected: true,
475
+ });
476
+ const bob = await setupTestAccount({
477
+ connected: true,
478
+ });
479
+ const group = alice.node.createGroup();
480
+ const map = group.createMap();
481
+
482
+ group.addMember(
483
+ await loadCoValueOrFail(alice.node, bob.accountID),
484
+ "writer",
485
+ );
486
+ const timeOnInvitation = Date.now();
487
+
488
+ const bobMap = await loadCoValueOrFail(bob.node, map.id);
489
+
490
+ bobMap.set("value", 1, "trusting");
491
+
492
+ await bobMap.core.waitForSync();
493
+
494
+ const bobGroup = bob.node.createGroup();
495
+ const branch = expectMap(
496
+ bobMap.core
497
+ .createBranch("feature-branch", bobGroup.id)
498
+ .getCurrentContent(),
499
+ );
500
+
501
+ await new Promise((resolve) => setTimeout(resolve, 5));
502
+
503
+ // Alice sets value to 2 and downgrade bob to reader
504
+ map.set("value", 2, "trusting");
505
+ group.addMember(
506
+ await loadCoValueOrFail(alice.node, bob.accountID),
507
+ "reader",
508
+ );
509
+
510
+ await new Promise((resolve) => setTimeout(resolve, 5));
511
+
512
+ branch.set("value", 3, "trusting");
513
+
514
+ // Bob attempts to make an edit after being kicked out by modifying the merge time
515
+ const dateNowMock = vi.spyOn(Date, "now");
516
+ dateNowMock.mockReturnValue(timeOnInvitation + 1);
517
+
518
+ const mergeResult = branch.core.mergeBranch();
519
+
520
+ dateNowMock.mockRestore();
521
+
522
+ // Wait for the full sync to complete
523
+ await waitFor(() => {
524
+ expect(mergeResult.knownState().sessions).toEqual(
525
+ map.core.knownState().sessions,
526
+ );
527
+ });
528
+
529
+ expect(expectMap(map.core.getCurrentContent()).get("value")).toBe(2);
530
+ });
531
+
532
+ test("should alias the txID when a transaction comes from a merge", async () => {
533
+ const client = setupTestNode({
534
+ connected: true,
535
+ });
536
+ const group = client.node.createGroup();
537
+ const map = group.createMap();
538
+
539
+ map.set("key", "value");
540
+
541
+ const branch = map.core
542
+ .createBranch("feature-branch", group.id)
543
+ .getCurrentContent() as RawCoMap;
544
+ branch.set("branchKey", "branchValue");
545
+
546
+ const originalTxID = branch.core
547
+ .getValidTransactions({
548
+ skipBranchSource: true,
549
+ ignorePrivateTransactions: false,
550
+ })
551
+ .at(-1)?.txID;
552
+
553
+ branch.core.mergeBranch();
554
+
555
+ map.set("key2", "value2");
556
+
557
+ const validSortedTransactions = map.core.getValidSortedTransactions();
558
+
559
+ // Only the merged transaction should have the txId changed
560
+ const mergedTransactionIdx = validSortedTransactions.findIndex(
561
+ (tx) => tx.txID.branch,
562
+ );
563
+
564
+ expect(
565
+ validSortedTransactions[mergedTransactionIdx - 1]?.txID.branch,
566
+ ).toBe(undefined);
567
+ expect(validSortedTransactions[mergedTransactionIdx]?.txID).toEqual(
568
+ originalTxID,
569
+ );
570
+ expect(
571
+ validSortedTransactions[mergedTransactionIdx + 1]?.txID.branch,
572
+ ).toBe(undefined);
573
+ });
574
+ });
575
+
576
+ describe("Branching permissions", () => {
577
+ test("should allow the creation of private branches to accounts with read access to the source group", async () => {
578
+ const alice = setupTestNode({
579
+ connected: true,
580
+ });
581
+ const bob = await setupTestAccount({
582
+ connected: true,
583
+ });
584
+ const group = alice.node.createGroup();
585
+ group.addMember(
586
+ await loadCoValueOrFail(alice.node, bob.accountID),
587
+ "reader",
588
+ );
589
+ const map = group.createMap();
590
+ map.set("key", "alice");
591
+
592
+ const bobGroup = bob.node.createGroup();
593
+
594
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
595
+
596
+ const branch = expectMap(
597
+ mapOnBob.core
598
+ .createBranch("feature-branch", bobGroup.id)
599
+ .getCurrentContent(),
600
+ );
601
+
602
+ expect(mapOnBob.core.branches).toEqual([
603
+ {
604
+ branch: "feature-branch",
605
+ ownerId: bobGroup.id,
606
+ },
607
+ ]);
608
+
609
+ expect(branch.id).not.toBe(map.id);
610
+ expect(branch.get("key")).toBe("alice");
611
+ expect(branch.core.getGroup().id).toBe(bobGroup.id);
612
+ expect(branch.core.getGroup().myRole()).toBe("admin");
613
+ branch.set("key", "bob");
614
+
615
+ expect(branch.get("key")).toBe("bob");
616
+ });
617
+
618
+ test("an account with write access to the source and read access to the branch should be able to merge a branch created by a reader", async () => {
619
+ const alice = await setupTestAccount({
620
+ connected: true,
621
+ });
622
+ const bob = await setupTestAccount({
623
+ connected: true,
624
+ });
625
+ const group = alice.node.createGroup();
626
+ group.addMember(
627
+ await loadCoValueOrFail(alice.node, bob.accountID),
628
+ "reader",
629
+ );
630
+ const map = group.createMap();
631
+ map.set("key", "alice");
632
+
633
+ const bobGroup = bob.node.createGroup();
634
+ bobGroup.addMember(
635
+ await loadCoValueOrFail(bob.node, alice.accountID),
636
+ "reader",
637
+ );
638
+
639
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
640
+
641
+ const branch = expectMap(
642
+ mapOnBob.core
643
+ .createBranch("feature-branch", bobGroup.id)
644
+ .getCurrentContent(),
645
+ );
646
+
647
+ branch.set("key", "bob");
648
+
649
+ expect(branch.get("key")).toBe("bob");
650
+
651
+ const branchOnAlice = await loadCoValueOrFail(alice.node, branch.id);
652
+ branchOnAlice.core.mergeBranch();
653
+
654
+ expect(map.get("key")).toBe("bob");
655
+ });
656
+
657
+ test("should not allow the creation of public branches to accounts with read access", async () => {
658
+ const alice = setupTestNode({
659
+ connected: true,
660
+ });
661
+ const bob = await setupTestAccount({
662
+ connected: true,
663
+ });
664
+ const group = alice.node.createGroup();
665
+ group.addMember(
666
+ await loadCoValueOrFail(alice.node, bob.accountID),
667
+ "reader",
668
+ );
669
+ const map = group.createMap();
670
+ map.set("key", "alice");
671
+
672
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
673
+
674
+ const branch = expectMap(
675
+ mapOnBob.core.createBranch("feature-branch").getCurrentContent(),
676
+ );
677
+
678
+ expect(mapOnBob.core.branches).toEqual([]);
679
+
680
+ expect(branch.id).toBe(map.id);
681
+ });
292
682
  });
293
683
 
294
684
  describe("Branch Loading and Checkout", () => {
@@ -560,49 +950,6 @@ describe("Branching Logic", () => {
560
950
  });
561
951
  });
562
952
 
563
- test("should alias the txID when a transaction comes from a merge", async () => {
564
- const client = setupTestNode({
565
- connected: true,
566
- });
567
- const group = client.node.createGroup();
568
- const map = group.createMap();
569
-
570
- map.set("key", "value");
571
-
572
- const branch = map.core
573
- .createBranch("feature-branch", group.id)
574
- .getCurrentContent() as RawCoMap;
575
- branch.set("branchKey", "branchValue");
576
-
577
- const originalTxID = branch.core
578
- .getValidTransactions({
579
- skipBranchSource: true,
580
- ignorePrivateTransactions: false,
581
- })
582
- .at(-1)?.txID;
583
-
584
- branch.core.mergeBranch();
585
-
586
- map.set("key2", "value2");
587
-
588
- const validSortedTransactions = map.core.getValidSortedTransactions();
589
-
590
- // Only the merged transaction should have the txId changed
591
- const mergedTransactionIdx = validSortedTransactions.findIndex(
592
- (tx) => tx.txID.branch,
593
- );
594
-
595
- expect(validSortedTransactions[mergedTransactionIdx - 1]?.txID.branch).toBe(
596
- undefined,
597
- );
598
- expect(validSortedTransactions[mergedTransactionIdx]?.txID).toEqual(
599
- originalTxID,
600
- );
601
- expect(validSortedTransactions[mergedTransactionIdx + 1]?.txID.branch).toBe(
602
- undefined,
603
- );
604
- });
605
-
606
953
  describe("hasBranch", () => {
607
954
  test("should work when the branch owner is the source owner", () => {
608
955
  const client = setupTestNode({