cojson 0.19.22 → 0.20.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 (194) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +54 -0
  3. package/dist/coValueContentMessage.d.ts +0 -2
  4. package/dist/coValueContentMessage.d.ts.map +1 -1
  5. package/dist/coValueContentMessage.js +0 -8
  6. package/dist/coValueContentMessage.js.map +1 -1
  7. package/dist/coValueCore/SessionMap.d.ts +4 -2
  8. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  9. package/dist/coValueCore/SessionMap.js +30 -0
  10. package/dist/coValueCore/SessionMap.js.map +1 -1
  11. package/dist/coValueCore/coValueCore.d.ts +67 -3
  12. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  13. package/dist/coValueCore/coValueCore.js +289 -12
  14. package/dist/coValueCore/coValueCore.js.map +1 -1
  15. package/dist/coValueCore/verifiedState.d.ts +6 -1
  16. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  17. package/dist/coValueCore/verifiedState.js +9 -0
  18. package/dist/coValueCore/verifiedState.js.map +1 -1
  19. package/dist/coValues/coList.d.ts +3 -2
  20. package/dist/coValues/coList.d.ts.map +1 -1
  21. package/dist/coValues/coList.js.map +1 -1
  22. package/dist/coValues/group.d.ts.map +1 -1
  23. package/dist/coValues/group.js +3 -6
  24. package/dist/coValues/group.js.map +1 -1
  25. package/dist/config.d.ts +0 -6
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +0 -8
  28. package/dist/config.js.map +1 -1
  29. package/dist/crypto/NapiCrypto.d.ts +1 -2
  30. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  31. package/dist/crypto/NapiCrypto.js +19 -4
  32. package/dist/crypto/NapiCrypto.js.map +1 -1
  33. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  34. package/dist/crypto/RNCrypto.js +19 -4
  35. package/dist/crypto/RNCrypto.js.map +1 -1
  36. package/dist/crypto/WasmCrypto.d.ts +11 -4
  37. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  38. package/dist/crypto/WasmCrypto.js +52 -10
  39. package/dist/crypto/WasmCrypto.js.map +1 -1
  40. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  41. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  42. package/dist/crypto/WasmCryptoEdge.js +4 -1
  43. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  44. package/dist/crypto/crypto.d.ts +3 -3
  45. package/dist/crypto/crypto.d.ts.map +1 -1
  46. package/dist/crypto/crypto.js +6 -1
  47. package/dist/crypto/crypto.js.map +1 -1
  48. package/dist/exports.d.ts +2 -2
  49. package/dist/exports.d.ts.map +1 -1
  50. package/dist/exports.js +2 -1
  51. package/dist/exports.js.map +1 -1
  52. package/dist/ids.d.ts +4 -1
  53. package/dist/ids.d.ts.map +1 -1
  54. package/dist/ids.js +4 -0
  55. package/dist/ids.js.map +1 -1
  56. package/dist/knownState.d.ts +2 -0
  57. package/dist/knownState.d.ts.map +1 -1
  58. package/dist/localNode.d.ts +12 -0
  59. package/dist/localNode.d.ts.map +1 -1
  60. package/dist/localNode.js +14 -0
  61. package/dist/localNode.js.map +1 -1
  62. package/dist/platformUtils.d.ts +3 -0
  63. package/dist/platformUtils.d.ts.map +1 -0
  64. package/dist/platformUtils.js +24 -0
  65. package/dist/platformUtils.js.map +1 -0
  66. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  67. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  68. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  69. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  70. package/dist/storage/sqlite/client.d.ts +3 -0
  71. package/dist/storage/sqlite/client.d.ts.map +1 -1
  72. package/dist/storage/sqlite/client.js +44 -0
  73. package/dist/storage/sqlite/client.js.map +1 -1
  74. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  75. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  76. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  77. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  78. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  79. package/dist/storage/sqliteAsync/client.js +42 -0
  80. package/dist/storage/sqliteAsync/client.js.map +1 -1
  81. package/dist/storage/storageAsync.d.ts +7 -0
  82. package/dist/storage/storageAsync.d.ts.map +1 -1
  83. package/dist/storage/storageAsync.js +48 -0
  84. package/dist/storage/storageAsync.js.map +1 -1
  85. package/dist/storage/storageSync.d.ts +6 -0
  86. package/dist/storage/storageSync.d.ts.map +1 -1
  87. package/dist/storage/storageSync.js +42 -0
  88. package/dist/storage/storageSync.js.map +1 -1
  89. package/dist/storage/types.d.ts +59 -0
  90. package/dist/storage/types.d.ts.map +1 -1
  91. package/dist/storage/types.js +12 -1
  92. package/dist/storage/types.js.map +1 -1
  93. package/dist/sync.d.ts.map +1 -1
  94. package/dist/sync.js +44 -11
  95. package/dist/sync.js.map +1 -1
  96. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  97. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  98. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  99. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  100. package/dist/tests/GarbageCollector.test.js +5 -6
  101. package/dist/tests/GarbageCollector.test.js.map +1 -1
  102. package/dist/tests/StorageApiAsync.test.js +484 -152
  103. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  104. package/dist/tests/StorageApiSync.test.js +505 -136
  105. package/dist/tests/StorageApiSync.test.js.map +1 -1
  106. package/dist/tests/WasmCrypto.test.js +6 -3
  107. package/dist/tests/WasmCrypto.test.js.map +1 -1
  108. package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
  109. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  110. package/dist/tests/coValueCore.test.js +34 -13
  111. package/dist/tests/coValueCore.test.js.map +1 -1
  112. package/dist/tests/coreWasm.test.js +127 -4
  113. package/dist/tests/coreWasm.test.js.map +1 -1
  114. package/dist/tests/crypto.test.js +89 -93
  115. package/dist/tests/crypto.test.js.map +1 -1
  116. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  117. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  118. package/dist/tests/deleteCoValue.test.js +313 -0
  119. package/dist/tests/deleteCoValue.test.js.map +1 -0
  120. package/dist/tests/group.removeMember.test.js +18 -30
  121. package/dist/tests/group.removeMember.test.js.map +1 -1
  122. package/dist/tests/knownState.lazyLoading.test.js +3 -0
  123. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  124. package/dist/tests/sync.deleted.test.d.ts +2 -0
  125. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  126. package/dist/tests/sync.deleted.test.js +214 -0
  127. package/dist/tests/sync.deleted.test.js.map +1 -0
  128. package/dist/tests/sync.mesh.test.js +3 -2
  129. package/dist/tests/sync.mesh.test.js.map +1 -1
  130. package/dist/tests/sync.storage.test.js +3 -2
  131. package/dist/tests/sync.storage.test.js.map +1 -1
  132. package/dist/tests/sync.test.js +3 -2
  133. package/dist/tests/sync.test.js.map +1 -1
  134. package/dist/tests/testStorage.d.ts +3 -0
  135. package/dist/tests/testStorage.d.ts.map +1 -1
  136. package/dist/tests/testStorage.js +14 -0
  137. package/dist/tests/testStorage.js.map +1 -1
  138. package/dist/tests/testUtils.d.ts +6 -3
  139. package/dist/tests/testUtils.d.ts.map +1 -1
  140. package/dist/tests/testUtils.js +17 -3
  141. package/dist/tests/testUtils.js.map +1 -1
  142. package/package.json +6 -16
  143. package/src/coValueContentMessage.ts +0 -14
  144. package/src/coValueCore/SessionMap.ts +43 -1
  145. package/src/coValueCore/coValueCore.ts +400 -8
  146. package/src/coValueCore/verifiedState.ts +26 -3
  147. package/src/coValues/coList.ts +5 -3
  148. package/src/coValues/group.ts +5 -6
  149. package/src/config.ts +0 -9
  150. package/src/crypto/NapiCrypto.ts +29 -13
  151. package/src/crypto/RNCrypto.ts +29 -11
  152. package/src/crypto/WasmCrypto.ts +67 -20
  153. package/src/crypto/WasmCryptoEdge.ts +5 -1
  154. package/src/crypto/crypto.ts +16 -4
  155. package/src/exports.ts +2 -0
  156. package/src/ids.ts +11 -1
  157. package/src/localNode.ts +15 -0
  158. package/src/platformUtils.ts +26 -0
  159. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  160. package/src/storage/sqlite/client.ts +77 -0
  161. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  162. package/src/storage/sqliteAsync/client.ts +75 -0
  163. package/src/storage/storageAsync.ts +62 -0
  164. package/src/storage/storageSync.ts +58 -0
  165. package/src/storage/types.ts +69 -0
  166. package/src/sync.ts +51 -11
  167. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  168. package/src/tests/GarbageCollector.test.ts +6 -10
  169. package/src/tests/StorageApiAsync.test.ts +572 -162
  170. package/src/tests/StorageApiSync.test.ts +580 -143
  171. package/src/tests/WasmCrypto.test.ts +8 -3
  172. package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
  173. package/src/tests/coValueCore.test.ts +49 -14
  174. package/src/tests/coreWasm.test.ts +319 -10
  175. package/src/tests/crypto.test.ts +141 -150
  176. package/src/tests/deleteCoValue.test.ts +528 -0
  177. package/src/tests/group.removeMember.test.ts +35 -35
  178. package/src/tests/knownState.lazyLoading.test.ts +6 -0
  179. package/src/tests/sync.deleted.test.ts +294 -0
  180. package/src/tests/sync.mesh.test.ts +5 -2
  181. package/src/tests/sync.storage.test.ts +5 -2
  182. package/src/tests/sync.test.ts +5 -2
  183. package/src/tests/testStorage.ts +28 -1
  184. package/src/tests/testUtils.ts +28 -9
  185. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  186. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  187. package/dist/crypto/PureJSCrypto.js +0 -236
  188. package/dist/crypto/PureJSCrypto.js.map +0 -1
  189. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  190. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  191. package/dist/tests/PureJSCrypto.test.js +0 -145
  192. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  193. package/src/crypto/PureJSCrypto.ts +0 -429
  194. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -0,0 +1,528 @@
1
+ import { assert, beforeEach, expect, test } from "vitest";
2
+ import { WasmCrypto } from "../crypto/WasmCrypto.js";
3
+ import { type SessionID, isDeleteSessionID } from "../ids.js";
4
+ import type { CoValueCore } from "../exports.js";
5
+ import {
6
+ fillCoMapWithLargeData,
7
+ importContentIntoNode,
8
+ setupTestAccount,
9
+ setupTestNode,
10
+ loadCoValueOrFail,
11
+ nodeWithRandomAgentAndSessionID,
12
+ hotSleep,
13
+ waitFor,
14
+ } from "./testUtils.js";
15
+ import { CO_VALUE_PRIORITY } from "../priority.js";
16
+
17
+ const Crypto = await WasmCrypto.create();
18
+
19
+ function makeDeleteMarkerTransaction(core: CoValueCore, madeAt?: number) {
20
+ core.makeTransaction([], "trusting", { deleted: core.id }, madeAt);
21
+ const deleteSessionID = Object.keys(core.knownState().sessions).find(
22
+ (sessionID) => isDeleteSessionID(sessionID as SessionID),
23
+ ) as SessionID;
24
+ assert(deleteSessionID);
25
+ const log = core.verified?.sessions.get(deleteSessionID);
26
+ assert(log?.lastSignature);
27
+ const tx = log.transactions.at(-1);
28
+ assert(tx);
29
+ return { tx, signature: log.lastSignature, deleteSessionID };
30
+ }
31
+
32
+ let jazzCloud: ReturnType<typeof setupTestNode>;
33
+ beforeEach(() => {
34
+ jazzCloud = setupTestNode({ isSyncServer: true });
35
+ });
36
+
37
+ test("deleteCoValue is blocked for Account and Group CoValues", async () => {
38
+ const client = await setupTestAccount();
39
+
40
+ const account = client.node.expectCurrentAccount("to test deleteCoValue");
41
+ expect(() => account.core.deleteCoValue()).toThrow(
42
+ /Cannot delete Group or Account coValues/,
43
+ );
44
+
45
+ const group = client.node.createGroup();
46
+ expect(() => group.core.deleteCoValue()).toThrow(
47
+ /Cannot delete Group or Account coValues/,
48
+ );
49
+ });
50
+
51
+ test("deleteCoValue throws when called by a non-admin on a group-owned CoValue", async () => {
52
+ const alice = await setupTestAccount({ connected: true });
53
+ const bob = await setupTestAccount({ connected: true });
54
+
55
+ const bobAccountOnAlice = await loadCoValueOrFail(alice.node, bob.accountID);
56
+
57
+ const group = alice.node.createGroup();
58
+ group.addMember(bobAccountOnAlice, "writer");
59
+
60
+ const map = group.createMap();
61
+
62
+ // Give sync a moment to propagate the group ownership + membership
63
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
64
+
65
+ await waitFor(() => {
66
+ expect(mapOnBob.core.safeGetGroup()?.myRole()).toBe("writer");
67
+ });
68
+
69
+ expect(() => mapOnBob.core.deleteCoValue()).toThrow(
70
+ /The current account lacks admin permissions to delete this coValue/,
71
+ );
72
+ });
73
+
74
+ test("deleteCoValue creates a trusting {deleted:id} tombstone tx, marks the session, and flips core.isDeleted", async () => {
75
+ const alice = await setupTestAccount({ connected: true });
76
+
77
+ const group = alice.node.createGroup();
78
+ const map = group.createMap();
79
+
80
+ expect(map.core.isDeleted).toBe(false);
81
+
82
+ map.core.deleteCoValue();
83
+
84
+ expect(map.core.isDeleted).toBe(true);
85
+
86
+ const txs = map.core.getValidSortedTransactions();
87
+ const last = txs.at(-1);
88
+ expect(last).toBeTruthy();
89
+
90
+ expect(last!.tx.privacy).toBe("trusting");
91
+ expect(last!.changes).toEqual([]);
92
+ expect(last!.meta).toMatchObject({ deleted: map.id });
93
+ expect(last!.txID.sessionID).toMatch(/_session_d[1-9A-HJ-NP-Za-km-z]+\$$/); // Delete session format
94
+ });
95
+
96
+ test("rejects delete marker ingestion from non-admin (ownedByGroup, skipVerify=false)", async () => {
97
+ const alice = await setupTestAccount({ connected: true });
98
+ const bob = await setupTestAccount({ connected: true });
99
+
100
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
101
+ await loadCoValueOrFail(bob.node, alice.accountID);
102
+
103
+ const group = alice.node.createGroup();
104
+ group.addMember(bobAccount, "writer");
105
+ await group.core.waitForSync();
106
+
107
+ const map = group.createMap();
108
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
109
+
110
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
111
+ mapOnBob.core,
112
+ );
113
+
114
+ const error = map.core.tryAddTransactions(
115
+ deleteSessionID,
116
+ [tx],
117
+ signature,
118
+ false,
119
+ );
120
+
121
+ expect(error).toMatchObject({
122
+ type: "DeleteTransactionRejected",
123
+ reason: "NotAdmin",
124
+ });
125
+ expect(map.core.isDeleted).toBe(false);
126
+ expect(map.core.verified?.sessions.get(deleteSessionID)).toBeUndefined();
127
+ });
128
+
129
+ test("accepts delete marker ingestion from admin (ownedByGroup, skipVerify=false) and marks deleted", async () => {
130
+ const alice = await setupTestAccount({ connected: true });
131
+ const bob = await setupTestAccount({ connected: true });
132
+
133
+ await loadCoValueOrFail(alice.node, bob.accountID);
134
+ await loadCoValueOrFail(bob.node, alice.accountID);
135
+
136
+ const group = alice.node.createGroup();
137
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
138
+ group.addMember(bobAccount, "writer");
139
+ await group.core.waitForSync();
140
+
141
+ const map = group.createMap();
142
+ await loadCoValueOrFail(bob.node, group.id);
143
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
144
+
145
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
146
+ map.core,
147
+ );
148
+
149
+ const error = mapOnBob.core.tryAddTransactions(
150
+ deleteSessionID,
151
+ [tx],
152
+ signature,
153
+ false,
154
+ );
155
+
156
+ expect(error).toBeUndefined();
157
+ expect(mapOnBob.core.isDeleted).toBe(true);
158
+ });
159
+
160
+ test("rejects delete session ingestion when attempting to append a second transaction (txCount > 0)", async () => {
161
+ const alice = await setupTestAccount({ connected: true });
162
+ const bob = await setupTestAccount({ connected: true });
163
+
164
+ await loadCoValueOrFail(alice.node, bob.accountID);
165
+ await loadCoValueOrFail(bob.node, alice.accountID);
166
+
167
+ const group = alice.node.createGroup();
168
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
169
+ group.addMember(bobAccount, "writer");
170
+ await group.core.waitForSync();
171
+
172
+ const map = group.createMap();
173
+ await loadCoValueOrFail(bob.node, group.id);
174
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
175
+
176
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
177
+ map.core,
178
+ );
179
+
180
+ const first = mapOnBob.core.tryAddTransactions(
181
+ deleteSessionID,
182
+ [tx],
183
+ signature,
184
+ false,
185
+ );
186
+
187
+ expect(first).toBeUndefined();
188
+ expect(mapOnBob.core.isDeleted).toBe(true);
189
+ expect(
190
+ mapOnBob.core.verified?.sessions.get(deleteSessionID)?.transactions,
191
+ ).toHaveLength(1);
192
+
193
+ const second = mapOnBob.core.tryAddTransactions(
194
+ deleteSessionID,
195
+ [tx],
196
+ signature,
197
+ false,
198
+ );
199
+
200
+ expect(second).toMatchObject({
201
+ type: "DeleteTransactionRejected",
202
+ reason: "InvalidDeleteTransaction",
203
+ });
204
+ expect(second && "error" in second).toBe(true);
205
+ const secondErr = (second as { error: unknown }).error;
206
+ expect(secondErr).toBeInstanceOf(Error);
207
+ if (secondErr instanceof Error) {
208
+ expect(secondErr.message).toMatch(
209
+ /Delete transaction must be the only transaction in the session/,
210
+ );
211
+ }
212
+ expect(
213
+ mapOnBob.core.verified?.sessions.get(deleteSessionID)?.transactions,
214
+ ).toHaveLength(1);
215
+ });
216
+
217
+ test("rejects delete session ingestion when attempting to add multiple delete transactions", async () => {
218
+ const alice = await setupTestAccount({ connected: true });
219
+ const bob = await setupTestAccount({ connected: true });
220
+
221
+ await loadCoValueOrFail(alice.node, bob.accountID);
222
+ await loadCoValueOrFail(bob.node, alice.accountID);
223
+
224
+ const group = alice.node.createGroup();
225
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
226
+ group.addMember(bobAccount, "writer");
227
+ await group.core.waitForSync();
228
+
229
+ const map = group.createMap();
230
+ await loadCoValueOrFail(bob.node, group.id);
231
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
232
+
233
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
234
+ map.core,
235
+ );
236
+
237
+ const first = mapOnBob.core.tryAddTransactions(
238
+ deleteSessionID,
239
+ [tx, tx],
240
+ signature,
241
+ false,
242
+ );
243
+
244
+ expect(first).toMatchObject({
245
+ type: "DeleteTransactionRejected",
246
+ reason: "InvalidDeleteTransaction",
247
+ });
248
+ expect(first && "error" in first).toBe(true);
249
+ const err = (first as { error: unknown }).error;
250
+ expect(err).toBeInstanceOf(Error);
251
+ if (err instanceof Error) {
252
+ expect(err.message).toMatch(
253
+ /Delete transaction must be the only transaction in the session/,
254
+ );
255
+ }
256
+ expect(mapOnBob.core.verified?.sessions.get(deleteSessionID)).toBeUndefined();
257
+ });
258
+
259
+ test("skipVerify=true ingestion marks deleted even for non-admin delete marker", async () => {
260
+ const alice = await setupTestAccount({ connected: true });
261
+ const bob = await setupTestAccount({ connected: true });
262
+
263
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
264
+ await loadCoValueOrFail(bob.node, alice.accountID);
265
+
266
+ const group = alice.node.createGroup();
267
+ group.addMember(bobAccount, "writer");
268
+ await group.core.waitForSync();
269
+
270
+ const map = group.createMap();
271
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
272
+
273
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
274
+ mapOnBob.core,
275
+ );
276
+
277
+ const error = map.core.tryAddTransactions(
278
+ deleteSessionID,
279
+ [tx],
280
+ signature,
281
+ true,
282
+ );
283
+
284
+ expect(error).toBeUndefined();
285
+ expect(map.core.isDeleted).toBe(true);
286
+ });
287
+
288
+ test("rejects delete marker ingestion when tx.madeAt predates admin rights (time travel permission check)", async () => {
289
+ const alice = await setupTestAccount({ connected: true });
290
+ const bob = await setupTestAccount({ connected: true });
291
+
292
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
293
+ await loadCoValueOrFail(bob.node, alice.accountID);
294
+
295
+ const group = alice.node.createGroup();
296
+ group.addMember(bobAccount, "writer");
297
+ await group.core.waitForSync();
298
+
299
+ const map = group.createMap();
300
+ const mapOnBob = await loadCoValueOrFail(bob.node, map.id);
301
+
302
+ // Ensure Bob is still a writer at tx creation time
303
+ await waitFor(() => {
304
+ expect(mapOnBob.core.safeGetGroup()?.myRole()).toBe("writer");
305
+ });
306
+
307
+ await new Promise((resolve) => setTimeout(resolve, 10));
308
+
309
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
310
+ mapOnBob.core,
311
+ );
312
+
313
+ // Later, Bob gets admin rights...
314
+ await new Promise((resolve) => setTimeout(resolve, 10));
315
+
316
+ group.addMember(bobAccount, "admin");
317
+ await group.core.waitForSync();
318
+
319
+ // ...but ingestion should still validate permissions at tx.madeAt (writer), not "now" (admin).
320
+ expect(group.roleOf(bob.accountID)).toBe("admin");
321
+
322
+ const error = map.core.tryAddTransactions(
323
+ deleteSessionID,
324
+ [tx],
325
+ signature,
326
+ false,
327
+ );
328
+
329
+ expect(error).toMatchObject({
330
+ type: "DeleteTransactionRejected",
331
+ reason: "NotAdmin",
332
+ });
333
+ expect(map.core.isDeleted).toBe(false);
334
+ });
335
+
336
+ test("rejects delete marker ingestion for non-owned covalue when verifying (skipVerify=false)", () => {
337
+ const node = nodeWithRandomAgentAndSessionID();
338
+
339
+ const coValue = node.createCoValue({
340
+ type: "costream",
341
+ ruleset: { type: "unsafeAllowAll" },
342
+ meta: null,
343
+ ...Crypto.createdNowUnique(),
344
+ });
345
+
346
+ const { tx, signature, deleteSessionID } =
347
+ makeDeleteMarkerTransaction(coValue);
348
+
349
+ node.internalDeleteCoValue(coValue.id);
350
+ node.syncManager.handleNewContent(
351
+ {
352
+ action: "content",
353
+ id: coValue.id,
354
+ header: coValue.verified!.header,
355
+ priority: CO_VALUE_PRIORITY.LOW,
356
+ new: {},
357
+ },
358
+ "import",
359
+ );
360
+
361
+ const newEntry = node.getCoValue(coValue.id);
362
+ const error = newEntry.tryAddTransactions(
363
+ deleteSessionID,
364
+ [tx],
365
+ signature,
366
+ false,
367
+ );
368
+
369
+ expect(error).toMatchObject({
370
+ type: "DeleteTransactionRejected",
371
+ reason: "CannotVerifyPermissions",
372
+ });
373
+ expect(newEntry.isDeleted).toBe(false);
374
+ });
375
+
376
+ test("deleted coValues return only the deleted session/transaction on the knownState", async () => {
377
+ const client = setupTestNode({
378
+ connected: true,
379
+ });
380
+ const group = client.node.createGroup();
381
+ const map = group.createMap();
382
+ map.set("hello", "world", "trusting");
383
+ map.core.deleteCoValue();
384
+
385
+ const knownState = map.core.knownState();
386
+ expect(
387
+ Object.keys(knownState.sessions).every((sessionID) =>
388
+ isDeleteSessionID(sessionID as SessionID),
389
+ ),
390
+ ).toBe(true);
391
+ expect(Object.keys(knownState.sessions)).toHaveLength(1);
392
+ });
393
+
394
+ test("deleted coValues return only the deleted session/transaction on the knownStateWithStreaming", async () => {
395
+ const streamingClient = setupTestNode();
396
+ const client = await setupTestAccount({ connected: true });
397
+ const group = streamingClient.node.createGroup();
398
+
399
+ group.addMemberInternal(client.account, "admin");
400
+
401
+ // Import the group content into the client
402
+ importContentIntoNode(group.core, client.node);
403
+
404
+ const map = group.createMap();
405
+ fillCoMapWithLargeData(map);
406
+
407
+ // Import only partially the map content into the client, to keep it in streaming state
408
+ importContentIntoNode(map.core, client.node, 1);
409
+
410
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
411
+
412
+ expect(mapOnClient.core.isStreaming()).toBe(true);
413
+
414
+ mapOnClient.core.deleteCoValue();
415
+
416
+ const streamingSessions = mapOnClient.core.knownStateWithStreaming().sessions;
417
+
418
+ expect(
419
+ Object.keys(streamingSessions).every((sessionID) =>
420
+ isDeleteSessionID(sessionID as SessionID),
421
+ ),
422
+ ).toBe(true);
423
+ expect(Object.keys(streamingSessions)).toHaveLength(1);
424
+ });
425
+
426
+ test("waitForSync should wait only for the delete session/transaction", async () => {
427
+ const client = setupTestNode({
428
+ connected: true,
429
+ });
430
+ const group = client.node.createGroup();
431
+ const map = group.createMap();
432
+ map.set("hello", "world", "trusting");
433
+ map.core.deleteCoValue();
434
+
435
+ await map.core.waitForSync();
436
+
437
+ expect(jazzCloud.node.expectCoValueLoaded(map.id).isDeleted).toBe(true);
438
+ });
439
+
440
+ test("waitForSync should wait only for the delete session/transaction even if the coValue loading was in streaming", async () => {
441
+ const streamingClient = setupTestNode();
442
+ const client = await setupTestAccount({ connected: true });
443
+ const group = streamingClient.node.createGroup();
444
+
445
+ group.addMemberInternal(client.account, "admin");
446
+
447
+ // Import the group content into the client
448
+ importContentIntoNode(group.core, client.node);
449
+
450
+ const map = group.createMap();
451
+ fillCoMapWithLargeData(map);
452
+
453
+ // Import only partially the map content into the client, to keep it in streaming state
454
+ importContentIntoNode(map.core, client.node, 1);
455
+
456
+ const mapOnClient = await loadCoValueOrFail(client.node, map.id);
457
+
458
+ expect(mapOnClient.core.isStreaming()).toBe(true);
459
+
460
+ mapOnClient.core.deleteCoValue();
461
+
462
+ await mapOnClient.core.waitForSync();
463
+
464
+ const mapOnSyncServer = jazzCloud.node.expectCoValueLoaded(map.id);
465
+
466
+ expect(
467
+ Object.keys(mapOnSyncServer.knownState().sessions).every((sessionID) =>
468
+ isDeleteSessionID(sessionID as SessionID),
469
+ ),
470
+ ).toBe(true);
471
+ expect(jazzCloud.node.expectCoValueLoaded(map.id).isDeleted).toBe(true);
472
+ });
473
+
474
+ test("rejects delete transaction with mismatched coValueId", async () => {
475
+ const alice = await setupTestAccount({ connected: true });
476
+ const bob = await setupTestAccount({ connected: true });
477
+
478
+ await loadCoValueOrFail(alice.node, bob.accountID);
479
+ await loadCoValueOrFail(bob.node, alice.accountID);
480
+
481
+ const group = alice.node.createGroup();
482
+ const bobAccount = await loadCoValueOrFail(alice.node, bob.accountID);
483
+ group.addMember(bobAccount, "admin");
484
+ await group.core.waitForSync();
485
+
486
+ // Create two maps owned by the same group
487
+ const mapA = group.createMap();
488
+ const mapB = group.createMap();
489
+
490
+ await loadCoValueOrFail(bob.node, group.id);
491
+ const mapAOnBob = await loadCoValueOrFail(bob.node, mapA.id);
492
+ const mapBOnBob = await loadCoValueOrFail(bob.node, mapB.id);
493
+
494
+ // Create a delete transaction for mapA
495
+ const { tx, signature, deleteSessionID } = makeDeleteMarkerTransaction(
496
+ mapA.core,
497
+ );
498
+
499
+ // Try to apply mapA's delete transaction to mapB - should be rejected due to ID mismatch
500
+ const error = mapBOnBob.core.tryAddTransactions(
501
+ deleteSessionID,
502
+ [tx],
503
+ signature,
504
+ false,
505
+ );
506
+
507
+ expect(error).toMatchObject({
508
+ type: "DeleteTransactionRejected",
509
+ reason: "InvalidDeleteTransaction",
510
+ });
511
+ expect(error && "error" in error).toBe(true);
512
+ const err = (error as { error: unknown }).error;
513
+ expect(err).toBeInstanceOf(Error);
514
+ if (err instanceof Error) {
515
+ expect(err.message).toMatch(/Delete transaction ID mismatch/);
516
+ }
517
+ expect(mapBOnBob.core.isDeleted).toBe(false);
518
+
519
+ // Verify mapA's delete transaction can still be applied correctly to mapA
520
+ const successError = mapAOnBob.core.tryAddTransactions(
521
+ deleteSessionID,
522
+ [tx],
523
+ signature,
524
+ false,
525
+ );
526
+ expect(successError).toBeUndefined();
527
+ expect(mapAOnBob.core.isDeleted).toBe(true);
528
+ });
@@ -168,45 +168,45 @@ describe("Group.removeMember", () => {
168
168
 
169
169
  const loadedGroup = await loadCoValueOrFail(client.node, group.id);
170
170
 
171
- // expect(async () => {
172
- loadedGroup.removeMember(
173
- await loadCoValueOrFail(client.node, reader.accountID),
171
+ expect(async () => {
172
+ loadedGroup.removeMember(
173
+ await loadCoValueOrFail(client.node, reader.accountID),
174
+ );
175
+ }).rejects.toThrow(
176
+ `Failed to revoke role to ${reader.accountID} (role of current account is ${member})`,
174
177
  );
175
- // }).rejects.toThrow(
176
- // `Failed to revoke role to ${reader.accountID} (role of current account is ${member})`,
177
- // );
178
178
 
179
- // expect(async () => {
180
- loadedGroup.removeMember(
181
- await loadCoValueOrFail(client.node, writeOnly.accountID),
179
+ expect(async () => {
180
+ loadedGroup.removeMember(
181
+ await loadCoValueOrFail(client.node, writeOnly.accountID),
182
+ );
183
+ }).rejects.toThrow(
184
+ `Failed to revoke role to ${writeOnly.accountID} (role of current account is ${member})`,
182
185
  );
183
- // }).rejects.toThrow(
184
- // `Failed to revoke role to ${writeOnly.accountID} (role of current account is ${member})`,
185
- // );
186
186
 
187
- // expect(async () => {
188
- loadedGroup.removeMember(
189
- await loadCoValueOrFail(client.node, writer.accountID),
187
+ expect(async () => {
188
+ loadedGroup.removeMember(
189
+ await loadCoValueOrFail(client.node, writer.accountID),
190
+ );
191
+ }).rejects.toThrow(
192
+ `Failed to revoke role to ${writer.accountID} (role of current account is ${member})`,
190
193
  );
191
- // }).rejects.toThrow(
192
- // `Failed to revoke role to ${writer.accountID} (role of current account is ${member})`,
193
- // );
194
194
 
195
- // expect(async () => {
196
- loadedGroup.removeMember(
197
- await loadCoValueOrFail(client.node, admin.accountID),
195
+ expect(async () => {
196
+ loadedGroup.removeMember(
197
+ await loadCoValueOrFail(client.node, admin.accountID),
198
+ );
199
+ }).rejects.toThrow(
200
+ `Failed to revoke role to ${admin.accountID} (role of current account is ${member})`,
198
201
  );
199
- // }).rejects.toThrow(
200
- // `Failed to revoke role to ${admin.accountID} (role of current account is ${member})`,
201
- // );
202
202
 
203
- // expect(async () => {
204
- loadedGroup.removeMember(
205
- await loadCoValueOrFail(client.node, manager.accountID),
203
+ expect(async () => {
204
+ loadedGroup.removeMember(
205
+ await loadCoValueOrFail(client.node, manager.accountID),
206
+ );
207
+ }).rejects.toThrow(
208
+ `Failed to revoke role to ${manager.accountID} (role of current account is ${member})`,
206
209
  );
207
- // }).rejects.toThrow(
208
- // `Failed to revoke role to ${manager.accountID} (role of current account is ${member})`,
209
- // );
210
210
  expect(loadedGroup.roleOf(reader.accountID)).toEqual("reader");
211
211
  expect(loadedGroup.roleOf(writer.accountID)).toEqual("writer");
212
212
  expect(loadedGroup.roleOf(writeOnly.accountID)).toEqual("writeOnly");
@@ -254,11 +254,11 @@ describe("Group.removeMember", () => {
254
254
  admin.accountID,
255
255
  );
256
256
 
257
- // expect(() => {
258
- loadedGroup.removeMember(adminOnClientNode);
259
- // }).toThrow(
260
- // `Failed to revoke role to ${admin.accountID} (role of current account is admin)`,
261
- // );
257
+ expect(() => {
258
+ loadedGroup.removeMember(adminOnClientNode);
259
+ }).toThrow(
260
+ `Failed to revoke role to ${admin.accountID} (role of current account is admin)`,
261
+ );
262
262
 
263
263
  expect(loadedGroup.roleOf(admin.accountID)).toEqual("admin");
264
264
 
@@ -25,9 +25,15 @@ function createMockStorage(
25
25
  stopTrackingSyncState?: (id: RawCoID) => void;
26
26
  onCoValueUnmounted?: (id: RawCoID) => void;
27
27
  close?: () => Promise<unknown> | undefined;
28
+ markDeleteAsValid?: (id: RawCoID) => void;
29
+ enableDeletedCoValuesErasure?: () => void;
30
+ eraseAllDeletedCoValues?: () => Promise<void>;
28
31
  } = {},
29
32
  ): StorageAPI {
30
33
  return {
34
+ markDeleteAsValid: opts.markDeleteAsValid || vi.fn(),
35
+ enableDeletedCoValuesErasure: opts.enableDeletedCoValuesErasure || vi.fn(),
36
+ eraseAllDeletedCoValues: opts.eraseAllDeletedCoValues || vi.fn(),
31
37
  load: opts.load || vi.fn(),
32
38
  store: opts.store || vi.fn(),
33
39
  getKnownState: opts.getKnownState || vi.fn(),