cojson 0.16.3 → 0.16.5

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 (138) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/coValue.d.ts +1 -1
  4. package/dist/coValue.d.ts.map +1 -1
  5. package/dist/coValue.js.map +1 -1
  6. package/dist/coValueContentMessage.d.ts +10 -0
  7. package/dist/coValueContentMessage.d.ts.map +1 -0
  8. package/dist/coValueContentMessage.js +46 -0
  9. package/dist/coValueContentMessage.js.map +1 -0
  10. package/dist/coValueCore/coValueCore.d.ts +6 -10
  11. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  12. package/dist/coValueCore/coValueCore.js +20 -125
  13. package/dist/coValueCore/coValueCore.js.map +1 -1
  14. package/dist/coValueCore/verifiedState.d.ts +1 -0
  15. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  16. package/dist/coValueCore/verifiedState.js +14 -27
  17. package/dist/coValueCore/verifiedState.js.map +1 -1
  18. package/dist/coValues/group.d.ts +18 -10
  19. package/dist/coValues/group.d.ts.map +1 -1
  20. package/dist/coValues/group.js +237 -67
  21. package/dist/coValues/group.js.map +1 -1
  22. package/dist/ids.d.ts +3 -3
  23. package/dist/ids.d.ts.map +1 -1
  24. package/dist/ids.js.map +1 -1
  25. package/dist/localNode.d.ts +11 -6
  26. package/dist/localNode.d.ts.map +1 -1
  27. package/dist/localNode.js +7 -2
  28. package/dist/localNode.js.map +1 -1
  29. package/dist/queue/LocalTransactionsSyncQueue.d.ts +24 -0
  30. package/dist/queue/LocalTransactionsSyncQueue.d.ts.map +1 -0
  31. package/dist/queue/LocalTransactionsSyncQueue.js +55 -0
  32. package/dist/queue/LocalTransactionsSyncQueue.js.map +1 -0
  33. package/dist/queue/StoreQueue.d.ts +9 -6
  34. package/dist/queue/StoreQueue.d.ts.map +1 -1
  35. package/dist/queue/StoreQueue.js +10 -2
  36. package/dist/queue/StoreQueue.js.map +1 -1
  37. package/dist/storage/storageAsync.d.ts +11 -3
  38. package/dist/storage/storageAsync.d.ts.map +1 -1
  39. package/dist/storage/storageAsync.js +59 -46
  40. package/dist/storage/storageAsync.js.map +1 -1
  41. package/dist/storage/storageSync.d.ts +9 -3
  42. package/dist/storage/storageSync.d.ts.map +1 -1
  43. package/dist/storage/storageSync.js +48 -35
  44. package/dist/storage/storageSync.js.map +1 -1
  45. package/dist/storage/syncUtils.d.ts +2 -1
  46. package/dist/storage/syncUtils.d.ts.map +1 -1
  47. package/dist/storage/syncUtils.js +4 -0
  48. package/dist/storage/syncUtils.js.map +1 -1
  49. package/dist/storage/types.d.ts +3 -2
  50. package/dist/storage/types.d.ts.map +1 -1
  51. package/dist/sync.d.ts +6 -6
  52. package/dist/sync.d.ts.map +1 -1
  53. package/dist/sync.js +33 -56
  54. package/dist/sync.js.map +1 -1
  55. package/dist/tests/StorageApiAsync.test.d.ts +2 -0
  56. package/dist/tests/StorageApiAsync.test.d.ts.map +1 -0
  57. package/dist/tests/StorageApiAsync.test.js +574 -0
  58. package/dist/tests/StorageApiAsync.test.js.map +1 -0
  59. package/dist/tests/StorageApiSync.test.d.ts +2 -0
  60. package/dist/tests/StorageApiSync.test.d.ts.map +1 -0
  61. package/dist/tests/StorageApiSync.test.js +426 -0
  62. package/dist/tests/StorageApiSync.test.js.map +1 -0
  63. package/dist/tests/StoreQueue.test.js +9 -21
  64. package/dist/tests/StoreQueue.test.js.map +1 -1
  65. package/dist/tests/SyncStateManager.test.js +18 -8
  66. package/dist/tests/SyncStateManager.test.js.map +1 -1
  67. package/dist/tests/group.inheritance.test.js +274 -2
  68. package/dist/tests/group.inheritance.test.js.map +1 -1
  69. package/dist/tests/group.removeMember.test.js +152 -1
  70. package/dist/tests/group.removeMember.test.js.map +1 -1
  71. package/dist/tests/group.roleOf.test.js +2 -2
  72. package/dist/tests/group.roleOf.test.js.map +1 -1
  73. package/dist/tests/group.test.js +81 -3
  74. package/dist/tests/group.test.js.map +1 -1
  75. package/dist/tests/sync.auth.test.js +22 -10
  76. package/dist/tests/sync.auth.test.js.map +1 -1
  77. package/dist/tests/sync.load.test.js +30 -25
  78. package/dist/tests/sync.load.test.js.map +1 -1
  79. package/dist/tests/sync.mesh.test.js +12 -6
  80. package/dist/tests/sync.mesh.test.js.map +1 -1
  81. package/dist/tests/sync.peerReconciliation.test.js +6 -4
  82. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  83. package/dist/tests/sync.storage.test.js +8 -14
  84. package/dist/tests/sync.storage.test.js.map +1 -1
  85. package/dist/tests/sync.storageAsync.test.js +31 -14
  86. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  87. package/dist/tests/sync.test.js +5 -9
  88. package/dist/tests/sync.test.js.map +1 -1
  89. package/dist/tests/sync.upload.test.js +31 -1
  90. package/dist/tests/sync.upload.test.js.map +1 -1
  91. package/dist/tests/testStorage.d.ts +2 -3
  92. package/dist/tests/testStorage.d.ts.map +1 -1
  93. package/dist/tests/testStorage.js +16 -8
  94. package/dist/tests/testStorage.js.map +1 -1
  95. package/dist/tests/testUtils.d.ts +4 -0
  96. package/dist/tests/testUtils.d.ts.map +1 -1
  97. package/dist/tests/testUtils.js +22 -4
  98. package/dist/tests/testUtils.js.map +1 -1
  99. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts +2 -2
  100. package/dist/typeUtils/accountOrAgentIDfromSessionID.d.ts.map +1 -1
  101. package/dist/typeUtils/expectGroup.d.ts.map +1 -1
  102. package/dist/typeUtils/expectGroup.js +6 -5
  103. package/dist/typeUtils/expectGroup.js.map +1 -1
  104. package/package.json +1 -1
  105. package/src/coValue.ts +1 -4
  106. package/src/coValueContentMessage.ts +73 -0
  107. package/src/coValueCore/coValueCore.ts +36 -192
  108. package/src/coValueCore/verifiedState.ts +28 -35
  109. package/src/coValues/group.ts +329 -99
  110. package/src/ids.ts +3 -3
  111. package/src/localNode.ts +15 -10
  112. package/src/queue/LocalTransactionsSyncQueue.ts +96 -0
  113. package/src/queue/StoreQueue.ts +22 -12
  114. package/src/storage/storageAsync.ts +78 -56
  115. package/src/storage/storageSync.ts +66 -45
  116. package/src/storage/syncUtils.ts +9 -1
  117. package/src/storage/types.ts +6 -5
  118. package/src/sync.ts +47 -67
  119. package/src/tests/StorageApiAsync.test.ts +829 -0
  120. package/src/tests/StorageApiSync.test.ts +628 -0
  121. package/src/tests/StoreQueue.test.ts +10 -24
  122. package/src/tests/SyncStateManager.test.ts +22 -21
  123. package/src/tests/group.inheritance.test.ts +415 -1
  124. package/src/tests/group.removeMember.test.ts +244 -1
  125. package/src/tests/group.roleOf.test.ts +2 -2
  126. package/src/tests/group.test.ts +105 -5
  127. package/src/tests/sync.auth.test.ts +22 -10
  128. package/src/tests/sync.load.test.ts +32 -26
  129. package/src/tests/sync.mesh.test.ts +12 -6
  130. package/src/tests/sync.peerReconciliation.test.ts +6 -4
  131. package/src/tests/sync.storage.test.ts +8 -14
  132. package/src/tests/sync.storageAsync.test.ts +39 -14
  133. package/src/tests/sync.test.ts +6 -14
  134. package/src/tests/sync.upload.test.ts +38 -1
  135. package/src/tests/testStorage.ts +19 -13
  136. package/src/tests/testUtils.ts +29 -5
  137. package/src/typeUtils/accountOrAgentIDfromSessionID.ts +2 -2
  138. package/src/typeUtils/expectGroup.ts +8 -5
@@ -38,14 +38,6 @@ describe("SyncStateManager", () => {
38
38
  const updateSpy: GlobalSyncStateListenerCallback = vi.fn();
39
39
  const unsubscribe = subscriptionManager.subscribeToUpdates(updateSpy);
40
40
 
41
- await client.node.syncManager.syncCoValue(map.core);
42
-
43
- expect(updateSpy).toHaveBeenCalledWith(
44
- peerState.id,
45
- emptyKnownState(map.core.id),
46
- { uploaded: false },
47
- );
48
-
49
41
  await waitFor(() => {
50
42
  return subscriptionManager.getCurrentSyncState(peerState.id, map.core.id)
51
43
  .uploaded;
@@ -98,13 +90,6 @@ describe("SyncStateManager", () => {
98
90
  unsubscribe2();
99
91
  });
100
92
 
101
- await client.node.syncManager.syncCoValue(map.core);
102
-
103
- expect(updateToJazzCloudSpy).toHaveBeenCalledWith(
104
- emptyKnownState(map.core.id),
105
- { uploaded: false },
106
- );
107
-
108
93
  await waitFor(() => {
109
94
  return subscriptionManager.getCurrentSyncState(peerState.id, map.core.id)
110
95
  .uploaded;
@@ -117,7 +102,7 @@ describe("SyncStateManager", () => {
117
102
  { uploaded: true },
118
103
  );
119
104
 
120
- expect(updateToStorageSpy).toHaveBeenLastCalledWith(
105
+ expect(updateToStorageSpy).toHaveBeenCalledWith(
121
106
  emptyKnownState(group.core.id),
122
107
  { uploaded: false },
123
108
  );
@@ -133,8 +118,6 @@ describe("SyncStateManager", () => {
133
118
  const map = group.createMap();
134
119
  map.set("key1", "value1", "trusting");
135
120
 
136
- await client.node.syncManager.syncCoValue(map.core);
137
-
138
121
  const subscriptionManager = client.node.syncManager.syncState;
139
122
 
140
123
  expect(
@@ -174,8 +157,6 @@ describe("SyncStateManager", () => {
174
157
  unsubscribe1();
175
158
  unsubscribe2();
176
159
 
177
- await client.node.syncManager.syncCoValue(map.core);
178
-
179
160
  anyUpdateSpy.mockClear();
180
161
 
181
162
  await waitFor(() => {
@@ -336,6 +317,26 @@ describe("SyncStateManager", () => {
336
317
 
337
318
  await map.core.waitForSync();
338
319
 
339
- expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
320
+ expect(client.node.getCoValue(map.id).hasVerifiedContent()).toBe(true);
321
+
322
+ // Since only the map is subscribed, the dependencies are pushed after the client requests them
323
+ await waitFor(() => {
324
+ expect(client.node.getCoValue(map.id).isAvailable()).toBe(true);
325
+ });
326
+
327
+ expect(
328
+ SyncMessagesLog.getMessages({
329
+ Map: map.core,
330
+ Group: group.core,
331
+ }),
332
+ ).toMatchInlineSnapshot(`
333
+ [
334
+ "server -> client | CONTENT Map header: true new: After: 0 New: 1",
335
+ "client -> server | LOAD Group sessions: empty",
336
+ "client -> server | KNOWN Map sessions: header/1",
337
+ "server -> client | CONTENT Group header: true new: After: 0 New: 3",
338
+ "client -> server | KNOWN Group sessions: header/3",
339
+ ]
340
+ `);
340
341
  });
341
342
  });
@@ -1,10 +1,21 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { beforeEach, describe, expect, test } from "vitest";
2
+ import type { CoID, RawGroup } from "../exports";
3
+ import { NewContentMessage } from "../sync";
2
4
  import {
5
+ SyncMessagesLog,
3
6
  createThreeConnectedNodes,
4
7
  createTwoConnectedNodes,
5
8
  loadCoValueOrFail,
9
+ setupTestNode,
6
10
  } from "./testUtils";
7
11
 
12
+ let jazzCloud: ReturnType<typeof setupTestNode>;
13
+
14
+ beforeEach(async () => {
15
+ SyncMessagesLog.clear();
16
+ jazzCloud = setupTestNode({ isSyncServer: true });
17
+ });
18
+
8
19
  describe("extend", () => {
9
20
  test("inherited writer roles should work correctly", async () => {
10
21
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
@@ -87,6 +98,32 @@ describe("extend", () => {
87
98
  expect(mapOnNode2.get("test")).toEqual("Written from node2");
88
99
  });
89
100
 
101
+ test("inherited everyone roles should work correctly", async () => {
102
+ const { node1, node2 } = await createTwoConnectedNodes("server", "server");
103
+
104
+ const group = node1.node.createGroup();
105
+ group.addMember("everyone", "writer");
106
+
107
+ const childGroup = node1.node.createGroup();
108
+ childGroup.extend(group);
109
+
110
+ expect(childGroup.roleOf("everyone")).toEqual("writer");
111
+
112
+ const map = childGroup.createMap();
113
+ map.set("test", "Written from the admin");
114
+
115
+ await map.core.waitForSync();
116
+
117
+ const mapOnNode2 = await loadCoValueOrFail(node2.node, map.id);
118
+
119
+ // The writer role should be able to see the edits from the admin
120
+ expect(mapOnNode2.get("test")).toEqual("Written from the admin");
121
+
122
+ mapOnNode2.set("hello", "from node 2");
123
+
124
+ expect(mapOnNode2.get("hello")).toEqual("from node 2");
125
+ });
126
+
90
127
  test("a user should be able to extend a group when his role on the parent group is writeOnly", async () => {
91
128
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
92
129
 
@@ -143,6 +180,132 @@ describe("extend", () => {
143
180
  expect(map.get("test")).toEqual("Hello!");
144
181
  });
145
182
 
183
+ test("should not break when checking for cycles on a loaded group", async () => {
184
+ const clientSession1 = setupTestNode({
185
+ connected: true,
186
+ });
187
+ const clientSession2 = clientSession1.spawnNewSession();
188
+
189
+ const group = clientSession1.node.createGroup();
190
+ const childGroup = clientSession1.node.createGroup();
191
+ const group2 = clientSession1.node.createGroup();
192
+ const group3 = clientSession1.node.createGroup();
193
+
194
+ childGroup.extend(group);
195
+ group.extend(group2);
196
+ group2.extend(group3);
197
+
198
+ await group.core.waitForSync();
199
+ await childGroup.core.waitForSync();
200
+ await group2.core.waitForSync();
201
+ await group3.core.waitForSync();
202
+
203
+ const groupOnClientSession2 = await loadCoValueOrFail(
204
+ clientSession2.node,
205
+ group.id,
206
+ );
207
+ const group3OnClientSession2 = await loadCoValueOrFail(
208
+ clientSession2.node,
209
+ group3.id,
210
+ );
211
+
212
+ expect(group3OnClientSession2.isSelfExtension(groupOnClientSession2)).toBe(
213
+ true,
214
+ );
215
+
216
+ // Child groups are not loaded as dependencies, and we want to make sure having a missing child doesn't break the extension
217
+ expect(clientSession2.node.getCoValue(childGroup.id).isAvailable()).toEqual(
218
+ false,
219
+ );
220
+
221
+ group3OnClientSession2.extend(groupOnClientSession2);
222
+
223
+ expect(group3OnClientSession2.getParentGroups()).toEqual([]);
224
+
225
+ const map = group3OnClientSession2.createMap();
226
+ map.set("test", "Hello!");
227
+
228
+ expect(map.get("test")).toEqual("Hello!");
229
+ });
230
+
231
+ test("should extend groups when loaded from a different session", async () => {
232
+ const clientSession1 = setupTestNode({
233
+ connected: true,
234
+ });
235
+ const clientSession2 = clientSession1.spawnNewSession();
236
+
237
+ const group = clientSession1.node.createGroup();
238
+ const group2 = clientSession1.node.createGroup();
239
+
240
+ await group.core.waitForSync();
241
+ await group2.core.waitForSync();
242
+
243
+ const groupOnClientSession2 = await loadCoValueOrFail(
244
+ clientSession2.node,
245
+ group.id,
246
+ );
247
+ const group2OnClientSession2 = await loadCoValueOrFail(
248
+ clientSession2.node,
249
+ group2.id,
250
+ );
251
+
252
+ group2OnClientSession2.extend(groupOnClientSession2);
253
+
254
+ expect(group2OnClientSession2.getParentGroups()).toEqual([
255
+ groupOnClientSession2,
256
+ ]);
257
+
258
+ const map = group2OnClientSession2.createMap();
259
+ map.set("test", "Hello!");
260
+
261
+ expect(map.get("test")).toEqual("Hello!");
262
+ });
263
+
264
+ test("should extend groups when there is a cycle in the parent groups", async () => {
265
+ const clientSession1 = setupTestNode({
266
+ connected: true,
267
+ });
268
+ const clientSession2 = clientSession1.spawnNewSession();
269
+
270
+ const group = clientSession1.node.createGroup();
271
+ const group2 = clientSession1.node.createGroup();
272
+
273
+ await group.core.waitForSync();
274
+ await group2.core.waitForSync();
275
+
276
+ const groupOnClientSession2 = await loadCoValueOrFail(
277
+ clientSession2.node,
278
+ group.id,
279
+ );
280
+ const group2OnClientSession2 = await loadCoValueOrFail(
281
+ clientSession2.node,
282
+ group2.id,
283
+ );
284
+
285
+ group.extend(group2);
286
+ group2OnClientSession2.extend(groupOnClientSession2);
287
+
288
+ expect(group.getParentGroups()).toEqual([group2]);
289
+
290
+ expect(group2OnClientSession2.getParentGroups()).toEqual([
291
+ groupOnClientSession2,
292
+ ]);
293
+
294
+ await group.core.waitForSync();
295
+ await group2OnClientSession2.core.waitForSync();
296
+
297
+ const group3 = clientSession1.node.createGroup();
298
+
299
+ group3.extend(group2);
300
+
301
+ expect(group3.getParentGroups()).toEqual([group2]);
302
+
303
+ const map = group3.createMap();
304
+ map.set("test", "Hello!");
305
+
306
+ expect(map.get("test")).toEqual("Hello!");
307
+ });
308
+
146
309
  test("a writerInvite role should not be inherited", async () => {
147
310
  const { node1, node2 } = await createTwoConnectedNodes("server", "server");
148
311
 
@@ -180,6 +343,257 @@ describe("extend", () => {
180
343
 
181
344
  expect(childGroup.roleOf(alice.id)).toBe("writer");
182
345
  });
346
+
347
+ test("should be possible to extend a group after getting revoked from the parent group", async () => {
348
+ const { node1, node2, node3 } = await createThreeConnectedNodes(
349
+ "server",
350
+ "server",
351
+ "server",
352
+ );
353
+
354
+ const parentGroup = node1.node.createGroup();
355
+
356
+ const alice = await loadCoValueOrFail(node1.node, node3.accountID);
357
+ const bob = await loadCoValueOrFail(node1.node, node2.accountID);
358
+ parentGroup.addMember(alice, "writer");
359
+ parentGroup.addMember(bob, "reader");
360
+ parentGroup.removeMember(bob);
361
+
362
+ const parentGroupOnNode2 = await loadCoValueOrFail(
363
+ node2.node,
364
+ parentGroup.id,
365
+ );
366
+
367
+ const childGroup = node2.node.createGroup();
368
+ childGroup.extend(parentGroupOnNode2);
369
+
370
+ expect(childGroup.roleOf(alice.id)).toBe("writer");
371
+ });
372
+
373
+ test("should be possible to extend when access is everyone reader and the account is revoked from the parent group", async () => {
374
+ const { node1, node2, node3 } = await createThreeConnectedNodes(
375
+ "server",
376
+ "server",
377
+ "server",
378
+ );
379
+
380
+ const parentGroup = node1.node.createGroup();
381
+ parentGroup.addMember("everyone", "reader");
382
+ const alice = await loadCoValueOrFail(node1.node, node3.accountID);
383
+ const bob = await loadCoValueOrFail(node1.node, node2.accountID);
384
+ parentGroup.addMember(alice, "writer");
385
+ parentGroup.addMember(bob, "reader");
386
+ parentGroup.removeMember(bob);
387
+
388
+ const parentGroupOnNode2 = await loadCoValueOrFail(
389
+ node2.node,
390
+ parentGroup.id,
391
+ );
392
+
393
+ const childGroup = node2.node.createGroup();
394
+ childGroup.extend(parentGroupOnNode2);
395
+
396
+ expect(childGroup.roleOf(alice.id)).toBe("writer");
397
+ });
398
+
399
+ test("should be able to extend when the last read key is healed", async () => {
400
+ const clientWithAccess = setupTestNode({
401
+ secret:
402
+ "sealerSecret_zBTPp7U58Fzq9o7EvJpu4KEziepi8QVf2Xaxuy5xmmXFx/signerSecret_z62DuviZdXCjz4EZWofvr9vaLYFXDeTaC9KWhoQiQjzKk",
403
+ connected: true,
404
+ });
405
+ const clientWithoutAccess = setupTestNode({
406
+ connected: true,
407
+ });
408
+
409
+ const brokenGroupContent = {
410
+ action: "content",
411
+ id: "co_zW7F36Nnop9A7Er4gUzBcUXnZCK",
412
+ header: {
413
+ type: "comap",
414
+ ruleset: {
415
+ type: "group",
416
+ initialAdmin:
417
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv",
418
+ },
419
+ meta: null,
420
+ createdAt: "2025-08-06T10:14:39.617Z",
421
+ uniqueness: "z3LJjnuPiPJaf5Qb9A",
422
+ },
423
+ priority: 0,
424
+ new: {
425
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv_session_zYLsz2CiW9pW":
426
+ {
427
+ after: 0,
428
+ newTransactions: [
429
+ {
430
+ privacy: "trusting",
431
+ madeAt: 1754475279619,
432
+ changes:
433
+ '[{"key":"sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"admin"}]',
434
+ },
435
+ {
436
+ privacy: "trusting",
437
+ madeAt: 1754475279621,
438
+ changes:
439
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UCg4UkytXF-W8PaIvaDffO3pZ3d9hdXUuNkQQEikPTAuOD9us92Pqb5Vgu7lx1Fpb0X8V5BJ2yxz6_D5WOzK3qjWBSsc7J1xDJA=="}]',
440
+ },
441
+ {
442
+ privacy: "trusting",
443
+ madeAt: 1754475279621,
444
+ changes:
445
+ '[{"key":"readKey","op":"set","value":"key_z5CVahfMkEWPj1B3zH"}]',
446
+ },
447
+ {
448
+ privacy: "trusting",
449
+ madeAt: 1754475279622,
450
+ changes: '[{"key":"everyone","op":"set","value":"reader"}]',
451
+ },
452
+ {
453
+ privacy: "trusting",
454
+ madeAt: 1754475279623,
455
+ changes:
456
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_everyone","op":"set","value":"keySecret_z9U9gzkahQXCxDoSw7isiUnbobXwuLdcSkL9Ci6ZEEkaL"}]',
457
+ },
458
+ {
459
+ privacy: "trusting",
460
+ madeAt: 1754475279623,
461
+ changes:
462
+ '[{"key":"key_z4Fi7hZNBx7XoVAKkP_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UuCBBfZkTnRTrGraqWWlzm9JE-VFduhsfu7WaZjpCbJYOTXpPhSNOnzGeS8qVuIsG6dORbME22lc5ltLxPjRqofQdDCNGQehCeQ=="}]',
463
+ },
464
+ {
465
+ privacy: "trusting",
466
+ madeAt: 1754475279624,
467
+ changes:
468
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_key_z4Fi7hZNBx7XoVAKkP","op":"set","value":"encrypted_USTrBuobwTCORwy5yHxy4sFZ7swfrafP6k5ZwcTf76f0MBu9Ie-JmsX3mNXad4mluI47gvGXzi8I_"}]',
469
+ },
470
+ {
471
+ privacy: "trusting",
472
+ madeAt: 1754475279624,
473
+ changes:
474
+ '[{"key":"readKey","op":"set","value":"key_z4Fi7hZNBx7XoVAKkP"}]',
475
+ },
476
+ ],
477
+ lastSignature:
478
+ "signature_z3tsE7U1JaeNeUmZ4EY3Xq5uQ9jq9jDi6Rkhdt7T7b7z4NCnpMgB4bo8TwLXYVCrRdBm6PoyyPdK8fYFzHJUh5EzA",
479
+ },
480
+ },
481
+ } as unknown as NewContentMessage;
482
+
483
+ clientWithAccess.node.syncManager.handleNewContent(
484
+ brokenGroupContent,
485
+ "import",
486
+ );
487
+
488
+ // Load the CoValue to recover the key_for_everyone
489
+ await loadCoValueOrFail(
490
+ clientWithAccess.node,
491
+ brokenGroupContent.id as CoID<RawGroup>,
492
+ );
493
+
494
+ const group = await loadCoValueOrFail(
495
+ clientWithoutAccess.node,
496
+ brokenGroupContent.id as CoID<RawGroup>,
497
+ );
498
+ const childGroup = clientWithoutAccess.node.createGroup();
499
+ childGroup.extend(group);
500
+
501
+ expect(childGroup.getParentGroups()).toEqual([group]);
502
+ });
503
+
504
+ test("should be able to extend when the last read key is missing", async () => {
505
+ const clientWithoutAccess = setupTestNode({
506
+ connected: true,
507
+ });
508
+
509
+ const brokenGroupContent = {
510
+ action: "content",
511
+ id: "co_zW7F36Nnop9A7Er4gUzBcUXnZCK",
512
+ header: {
513
+ type: "comap",
514
+ ruleset: {
515
+ type: "group",
516
+ initialAdmin:
517
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv",
518
+ },
519
+ meta: null,
520
+ createdAt: "2025-08-06T10:14:39.617Z",
521
+ uniqueness: "z3LJjnuPiPJaf5Qb9A",
522
+ },
523
+ priority: 0,
524
+ new: {
525
+ "sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv_session_zYLsz2CiW9pW":
526
+ {
527
+ after: 0,
528
+ newTransactions: [
529
+ {
530
+ privacy: "trusting",
531
+ madeAt: 1754475279619,
532
+ changes:
533
+ '[{"key":"sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"admin"}]',
534
+ },
535
+ {
536
+ privacy: "trusting",
537
+ madeAt: 1754475279621,
538
+ changes:
539
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UCg4UkytXF-W8PaIvaDffO3pZ3d9hdXUuNkQQEikPTAuOD9us92Pqb5Vgu7lx1Fpb0X8V5BJ2yxz6_D5WOzK3qjWBSsc7J1xDJA=="}]',
540
+ },
541
+ {
542
+ privacy: "trusting",
543
+ madeAt: 1754475279621,
544
+ changes:
545
+ '[{"key":"readKey","op":"set","value":"key_z5CVahfMkEWPj1B3zH"}]',
546
+ },
547
+ {
548
+ privacy: "trusting",
549
+ madeAt: 1754475279622,
550
+ changes: '[{"key":"everyone","op":"set","value":"reader"}]',
551
+ },
552
+ {
553
+ privacy: "trusting",
554
+ madeAt: 1754475279623,
555
+ changes:
556
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_everyone","op":"set","value":"keySecret_z9U9gzkahQXCxDoSw7isiUnbobXwuLdcSkL9Ci6ZEEkaL"}]',
557
+ },
558
+ {
559
+ privacy: "trusting",
560
+ madeAt: 1754475279623,
561
+ changes:
562
+ '[{"key":"key_z4Fi7hZNBx7XoVAKkP_for_sealer_z12QDazYB3ygPZtBV7sMm7iYKMRnNZ6Aaj1dfLXR7LSBm/signer_z2AskZQbc82qxo7iA3oiXoNExHLsAEXC2pHbwJzRnATWv","op":"set","value":"sealed_UuCBBfZkTnRTrGraqWWlzm9JE-VFduhsfu7WaZjpCbJYOTXpPhSNOnzGeS8qVuIsG6dORbME22lc5ltLxPjRqofQdDCNGQehCeQ=="}]',
563
+ },
564
+ {
565
+ privacy: "trusting",
566
+ madeAt: 1754475279624,
567
+ changes:
568
+ '[{"key":"key_z5CVahfMkEWPj1B3zH_for_key_z4Fi7hZNBx7XoVAKkP","op":"set","value":"encrypted_USTrBuobwTCORwy5yHxy4sFZ7swfrafP6k5ZwcTf76f0MBu9Ie-JmsX3mNXad4mluI47gvGXzi8I_"}]',
569
+ },
570
+ {
571
+ privacy: "trusting",
572
+ madeAt: 1754475279624,
573
+ changes:
574
+ '[{"key":"readKey","op":"set","value":"key_z4Fi7hZNBx7XoVAKkP"}]',
575
+ },
576
+ ],
577
+ lastSignature:
578
+ "signature_z3tsE7U1JaeNeUmZ4EY3Xq5uQ9jq9jDi6Rkhdt7T7b7z4NCnpMgB4bo8TwLXYVCrRdBm6PoyyPdK8fYFzHJUh5EzA",
579
+ },
580
+ },
581
+ } as unknown as NewContentMessage;
582
+
583
+ clientWithoutAccess.node.syncManager.handleNewContent(
584
+ brokenGroupContent,
585
+ "import",
586
+ );
587
+
588
+ const group = await loadCoValueOrFail(
589
+ clientWithoutAccess.node,
590
+ brokenGroupContent.id as CoID<RawGroup>,
591
+ );
592
+ const childGroup = clientWithoutAccess.node.createGroup();
593
+ childGroup.extend(group);
594
+
595
+ expect(childGroup.getParentGroups()).toEqual([group]);
596
+ });
183
597
  });
184
598
 
185
599
  describe("unextend", () => {