cojson 0.19.21 → 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 (254) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +67 -0
  3. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts +42 -0
  4. package/dist/CojsonMessageChannel/CojsonMessageChannel.d.ts.map +1 -0
  5. package/dist/CojsonMessageChannel/CojsonMessageChannel.js +261 -0
  6. package/dist/CojsonMessageChannel/CojsonMessageChannel.js.map +1 -0
  7. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts +18 -0
  8. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.d.ts.map +1 -0
  9. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js +37 -0
  10. package/dist/CojsonMessageChannel/MessagePortOutgoingChannel.js.map +1 -0
  11. package/dist/CojsonMessageChannel/index.d.ts +3 -0
  12. package/dist/CojsonMessageChannel/index.d.ts.map +1 -0
  13. package/dist/CojsonMessageChannel/index.js +2 -0
  14. package/dist/CojsonMessageChannel/index.js.map +1 -0
  15. package/dist/CojsonMessageChannel/types.d.ts +149 -0
  16. package/dist/CojsonMessageChannel/types.d.ts.map +1 -0
  17. package/dist/CojsonMessageChannel/types.js +36 -0
  18. package/dist/CojsonMessageChannel/types.js.map +1 -0
  19. package/dist/GarbageCollector.d.ts +4 -2
  20. package/dist/GarbageCollector.d.ts.map +1 -1
  21. package/dist/GarbageCollector.js +5 -3
  22. package/dist/GarbageCollector.js.map +1 -1
  23. package/dist/SyncStateManager.d.ts +3 -3
  24. package/dist/SyncStateManager.d.ts.map +1 -1
  25. package/dist/SyncStateManager.js +4 -4
  26. package/dist/SyncStateManager.js.map +1 -1
  27. package/dist/coValueContentMessage.d.ts +0 -2
  28. package/dist/coValueContentMessage.d.ts.map +1 -1
  29. package/dist/coValueContentMessage.js +0 -8
  30. package/dist/coValueContentMessage.js.map +1 -1
  31. package/dist/coValueCore/SessionMap.d.ts +4 -2
  32. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  33. package/dist/coValueCore/SessionMap.js +30 -0
  34. package/dist/coValueCore/SessionMap.js.map +1 -1
  35. package/dist/coValueCore/coValueCore.d.ts +86 -4
  36. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  37. package/dist/coValueCore/coValueCore.js +318 -17
  38. package/dist/coValueCore/coValueCore.js.map +1 -1
  39. package/dist/coValueCore/verifiedState.d.ts +6 -1
  40. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  41. package/dist/coValueCore/verifiedState.js +9 -0
  42. package/dist/coValueCore/verifiedState.js.map +1 -1
  43. package/dist/coValues/coList.d.ts +3 -2
  44. package/dist/coValues/coList.d.ts.map +1 -1
  45. package/dist/coValues/coList.js.map +1 -1
  46. package/dist/coValues/group.d.ts.map +1 -1
  47. package/dist/coValues/group.js +3 -6
  48. package/dist/coValues/group.js.map +1 -1
  49. package/dist/config.d.ts +0 -6
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -8
  52. package/dist/config.js.map +1 -1
  53. package/dist/crypto/NapiCrypto.d.ts +1 -2
  54. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  55. package/dist/crypto/NapiCrypto.js +19 -4
  56. package/dist/crypto/NapiCrypto.js.map +1 -1
  57. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  58. package/dist/crypto/RNCrypto.js +19 -4
  59. package/dist/crypto/RNCrypto.js.map +1 -1
  60. package/dist/crypto/WasmCrypto.d.ts +11 -4
  61. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  62. package/dist/crypto/WasmCrypto.js +52 -10
  63. package/dist/crypto/WasmCrypto.js.map +1 -1
  64. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  65. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  66. package/dist/crypto/WasmCryptoEdge.js +4 -1
  67. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  68. package/dist/crypto/crypto.d.ts +3 -3
  69. package/dist/crypto/crypto.d.ts.map +1 -1
  70. package/dist/crypto/crypto.js +6 -1
  71. package/dist/crypto/crypto.js.map +1 -1
  72. package/dist/exports.d.ts +3 -2
  73. package/dist/exports.d.ts.map +1 -1
  74. package/dist/exports.js +3 -1
  75. package/dist/exports.js.map +1 -1
  76. package/dist/ids.d.ts +4 -1
  77. package/dist/ids.d.ts.map +1 -1
  78. package/dist/ids.js +4 -0
  79. package/dist/ids.js.map +1 -1
  80. package/dist/knownState.d.ts +2 -0
  81. package/dist/knownState.d.ts.map +1 -1
  82. package/dist/localNode.d.ts +13 -3
  83. package/dist/localNode.d.ts.map +1 -1
  84. package/dist/localNode.js +17 -2
  85. package/dist/localNode.js.map +1 -1
  86. package/dist/platformUtils.d.ts +3 -0
  87. package/dist/platformUtils.d.ts.map +1 -0
  88. package/dist/platformUtils.js +24 -0
  89. package/dist/platformUtils.js.map +1 -0
  90. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  91. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  92. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  93. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  94. package/dist/storage/sqlite/client.d.ts +3 -0
  95. package/dist/storage/sqlite/client.d.ts.map +1 -1
  96. package/dist/storage/sqlite/client.js +44 -0
  97. package/dist/storage/sqlite/client.js.map +1 -1
  98. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  99. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  100. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  101. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  102. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  103. package/dist/storage/sqliteAsync/client.js +42 -0
  104. package/dist/storage/sqliteAsync/client.js.map +1 -1
  105. package/dist/storage/storageAsync.d.ts +15 -3
  106. package/dist/storage/storageAsync.d.ts.map +1 -1
  107. package/dist/storage/storageAsync.js +60 -3
  108. package/dist/storage/storageAsync.js.map +1 -1
  109. package/dist/storage/storageSync.d.ts +14 -3
  110. package/dist/storage/storageSync.d.ts.map +1 -1
  111. package/dist/storage/storageSync.js +54 -3
  112. package/dist/storage/storageSync.js.map +1 -1
  113. package/dist/storage/types.d.ts +64 -0
  114. package/dist/storage/types.d.ts.map +1 -1
  115. package/dist/storage/types.js +12 -1
  116. package/dist/storage/types.js.map +1 -1
  117. package/dist/sync.d.ts +6 -0
  118. package/dist/sync.d.ts.map +1 -1
  119. package/dist/sync.js +69 -15
  120. package/dist/sync.js.map +1 -1
  121. package/dist/tests/CojsonMessageChannel.test.d.ts +2 -0
  122. package/dist/tests/CojsonMessageChannel.test.d.ts.map +1 -0
  123. package/dist/tests/CojsonMessageChannel.test.js +236 -0
  124. package/dist/tests/CojsonMessageChannel.test.js.map +1 -0
  125. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  126. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  127. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  128. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  129. package/dist/tests/GarbageCollector.test.js +91 -18
  130. package/dist/tests/GarbageCollector.test.js.map +1 -1
  131. package/dist/tests/StorageApiAsync.test.js +510 -146
  132. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  133. package/dist/tests/StorageApiSync.test.js +531 -130
  134. package/dist/tests/StorageApiSync.test.js.map +1 -1
  135. package/dist/tests/SyncManager.processQueues.test.js +1 -1
  136. package/dist/tests/SyncManager.processQueues.test.js.map +1 -1
  137. package/dist/tests/SyncStateManager.test.js +1 -1
  138. package/dist/tests/SyncStateManager.test.js.map +1 -1
  139. package/dist/tests/WasmCrypto.test.js +6 -3
  140. package/dist/tests/WasmCrypto.test.js.map +1 -1
  141. package/dist/tests/coPlainText.test.js +1 -1
  142. package/dist/tests/coPlainText.test.js.map +1 -1
  143. package/dist/tests/coValueCore.loadFromStorage.test.js +4 -0
  144. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  145. package/dist/tests/coValueCore.test.js +34 -13
  146. package/dist/tests/coValueCore.test.js.map +1 -1
  147. package/dist/tests/coreWasm.test.js +127 -4
  148. package/dist/tests/coreWasm.test.js.map +1 -1
  149. package/dist/tests/crypto.test.js +89 -93
  150. package/dist/tests/crypto.test.js.map +1 -1
  151. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  152. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  153. package/dist/tests/deleteCoValue.test.js +313 -0
  154. package/dist/tests/deleteCoValue.test.js.map +1 -0
  155. package/dist/tests/group.removeMember.test.js +18 -30
  156. package/dist/tests/group.removeMember.test.js.map +1 -1
  157. package/dist/tests/knownState.lazyLoading.test.js +4 -0
  158. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  159. package/dist/tests/sync.deleted.test.d.ts +2 -0
  160. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  161. package/dist/tests/sync.deleted.test.js +214 -0
  162. package/dist/tests/sync.deleted.test.js.map +1 -0
  163. package/dist/tests/sync.garbageCollection.test.js +56 -32
  164. package/dist/tests/sync.garbageCollection.test.js.map +1 -1
  165. package/dist/tests/sync.load.test.js +3 -5
  166. package/dist/tests/sync.load.test.js.map +1 -1
  167. package/dist/tests/sync.mesh.test.js +4 -3
  168. package/dist/tests/sync.mesh.test.js.map +1 -1
  169. package/dist/tests/sync.peerReconciliation.test.js +3 -3
  170. package/dist/tests/sync.peerReconciliation.test.js.map +1 -1
  171. package/dist/tests/sync.storage.test.js +12 -11
  172. package/dist/tests/sync.storage.test.js.map +1 -1
  173. package/dist/tests/sync.storageAsync.test.js +7 -7
  174. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  175. package/dist/tests/sync.test.js +3 -2
  176. package/dist/tests/sync.test.js.map +1 -1
  177. package/dist/tests/sync.tracking.test.js +35 -4
  178. package/dist/tests/sync.tracking.test.js.map +1 -1
  179. package/dist/tests/testStorage.d.ts +3 -0
  180. package/dist/tests/testStorage.d.ts.map +1 -1
  181. package/dist/tests/testStorage.js +16 -2
  182. package/dist/tests/testStorage.js.map +1 -1
  183. package/dist/tests/testUtils.d.ts +29 -4
  184. package/dist/tests/testUtils.d.ts.map +1 -1
  185. package/dist/tests/testUtils.js +84 -9
  186. package/dist/tests/testUtils.js.map +1 -1
  187. package/package.json +6 -16
  188. package/src/CojsonMessageChannel/CojsonMessageChannel.ts +332 -0
  189. package/src/CojsonMessageChannel/MessagePortOutgoingChannel.ts +52 -0
  190. package/src/CojsonMessageChannel/index.ts +9 -0
  191. package/src/CojsonMessageChannel/types.ts +200 -0
  192. package/src/GarbageCollector.ts +5 -5
  193. package/src/SyncStateManager.ts +6 -6
  194. package/src/coValueContentMessage.ts +0 -14
  195. package/src/coValueCore/SessionMap.ts +43 -1
  196. package/src/coValueCore/coValueCore.ts +430 -15
  197. package/src/coValueCore/verifiedState.ts +26 -3
  198. package/src/coValues/coList.ts +5 -3
  199. package/src/coValues/group.ts +5 -6
  200. package/src/config.ts +0 -9
  201. package/src/crypto/NapiCrypto.ts +29 -13
  202. package/src/crypto/RNCrypto.ts +29 -11
  203. package/src/crypto/WasmCrypto.ts +67 -20
  204. package/src/crypto/WasmCryptoEdge.ts +5 -1
  205. package/src/crypto/crypto.ts +16 -4
  206. package/src/exports.ts +3 -0
  207. package/src/ids.ts +11 -1
  208. package/src/localNode.ts +18 -5
  209. package/src/platformUtils.ts +26 -0
  210. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  211. package/src/storage/sqlite/client.ts +77 -0
  212. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  213. package/src/storage/sqliteAsync/client.ts +75 -0
  214. package/src/storage/storageAsync.ts +77 -4
  215. package/src/storage/storageSync.ts +73 -4
  216. package/src/storage/types.ts +75 -0
  217. package/src/sync.ts +84 -15
  218. package/src/tests/CojsonMessageChannel.test.ts +306 -0
  219. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  220. package/src/tests/GarbageCollector.test.ts +119 -22
  221. package/src/tests/StorageApiAsync.test.ts +615 -156
  222. package/src/tests/StorageApiSync.test.ts +623 -137
  223. package/src/tests/SyncManager.processQueues.test.ts +1 -1
  224. package/src/tests/SyncStateManager.test.ts +1 -1
  225. package/src/tests/WasmCrypto.test.ts +8 -3
  226. package/src/tests/coPlainText.test.ts +1 -1
  227. package/src/tests/coValueCore.loadFromStorage.test.ts +8 -0
  228. package/src/tests/coValueCore.test.ts +49 -14
  229. package/src/tests/coreWasm.test.ts +319 -10
  230. package/src/tests/crypto.test.ts +141 -150
  231. package/src/tests/deleteCoValue.test.ts +528 -0
  232. package/src/tests/group.removeMember.test.ts +35 -35
  233. package/src/tests/knownState.lazyLoading.test.ts +8 -0
  234. package/src/tests/sync.deleted.test.ts +294 -0
  235. package/src/tests/sync.garbageCollection.test.ts +69 -36
  236. package/src/tests/sync.load.test.ts +3 -5
  237. package/src/tests/sync.mesh.test.ts +6 -3
  238. package/src/tests/sync.peerReconciliation.test.ts +3 -3
  239. package/src/tests/sync.storage.test.ts +14 -11
  240. package/src/tests/sync.storageAsync.test.ts +7 -7
  241. package/src/tests/sync.test.ts +5 -2
  242. package/src/tests/sync.tracking.test.ts +54 -4
  243. package/src/tests/testStorage.ts +30 -3
  244. package/src/tests/testUtils.ts +113 -15
  245. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  246. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  247. package/dist/crypto/PureJSCrypto.js +0 -236
  248. package/dist/crypto/PureJSCrypto.js.map +0 -1
  249. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  250. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  251. package/dist/tests/PureJSCrypto.test.js +0 -145
  252. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  253. package/src/crypto/PureJSCrypto.ts +0 -429
  254. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -1,10 +1,12 @@
1
1
  import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
2
2
  import type { PeerState } from "../PeerState.js";
3
3
  import type { RawCoValue } from "../coValue.js";
4
- import type { ControlledAccountOrAgent } from "../coValues/account.js";
4
+ import {
5
+ RawAccount,
6
+ type ControlledAccountOrAgent,
7
+ } from "../coValues/account.js";
5
8
  import type { RawGroup } from "../coValues/group.js";
6
9
  import { CO_VALUE_LOADING_CONFIG } from "../config.js";
7
- import { validateTxSizeLimitInBytes } from "../coValueContentMessage.js";
8
10
  import { coreToCoValue } from "../coreToCoValue.js";
9
11
  import {
10
12
  CryptoProvider,
@@ -14,12 +16,18 @@ import {
14
16
  Signature,
15
17
  SignerID,
16
18
  } from "../crypto/crypto.js";
17
- import { AgentID, RawCoID, SessionID, TransactionID } from "../ids.js";
19
+ import {
20
+ AgentID,
21
+ isDeleteSessionID,
22
+ RawCoID,
23
+ SessionID,
24
+ TransactionID,
25
+ } from "../ids.js";
18
26
  import { JsonObject, JsonValue } from "../jsonValue.js";
19
27
  import { LocalNode, ResolveAccountAgentError } from "../localNode.js";
20
28
  import { logger } from "../logger.js";
21
29
  import { determineValidTransactions } from "../permissions.js";
22
- import { NewContentMessage, PeerID } from "../sync.js";
30
+ import { KnownStateMessage, NewContentMessage, PeerID } from "../sync.js";
23
31
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
24
32
  import { expectGroup } from "../typeUtils/expectGroup.js";
25
33
  import {
@@ -27,7 +35,12 @@ import {
27
35
  getDependenciesFromGroupRawTransactions,
28
36
  getDependenciesFromHeader,
29
37
  } from "./utils.js";
30
- import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
38
+ import {
39
+ CoValueHeader,
40
+ Transaction,
41
+ Uniqueness,
42
+ VerifiedState,
43
+ } from "./verifiedState.js";
31
44
  import { SessionMap } from "./SessionMap.js";
32
45
  import {
33
46
  MergeCommit,
@@ -51,6 +64,45 @@ import {
51
64
  } from "../knownState.js";
52
65
  import { safeParseJSON } from "../jsonStringify.js";
53
66
 
67
+ export type ValidationValue =
68
+ | { isOk: true }
69
+ | {
70
+ isOk: false;
71
+ message: string;
72
+ };
73
+
74
+ function validateUniqueness(uniqueness: Uniqueness): ValidationValue {
75
+ if (typeof uniqueness === "number" && !Number.isInteger(uniqueness)) {
76
+ return {
77
+ isOk: false,
78
+ message: "Uniqueness cannot be a non-integer number, got " + uniqueness,
79
+ };
80
+ }
81
+
82
+ if (Array.isArray(uniqueness)) {
83
+ return {
84
+ isOk: false,
85
+ message: "Uniqueness cannot be an array, got " + uniqueness,
86
+ };
87
+ }
88
+
89
+ if (typeof uniqueness === "object" && uniqueness !== null) {
90
+ for (let [key, value] of Object.entries(uniqueness)) {
91
+ if (typeof value !== "string") {
92
+ return {
93
+ isOk: false,
94
+ message:
95
+ "Uniqueness object values must be a string, got " +
96
+ value +
97
+ " for key " +
98
+ key,
99
+ };
100
+ }
101
+ }
102
+ }
103
+ return { isOk: true };
104
+ }
105
+
54
106
  export function idforHeader(
55
107
  header: CoValueHeader,
56
108
  crypto: CryptoProvider,
@@ -197,6 +249,8 @@ export class CoValueCore {
197
249
  // context
198
250
  readonly node: LocalNode;
199
251
  private readonly crypto: CryptoProvider;
252
+ // Whether the coValue is deleted
253
+ public isDeleted: boolean = false;
200
254
 
201
255
  // state
202
256
  id: RawCoID;
@@ -373,16 +427,26 @@ export class CoValueCore {
373
427
  }
374
428
  }
375
429
 
376
- unmount(garbageCollectGroups = false) {
377
- if (
378
- !garbageCollectGroups &&
379
- this.verified?.header.ruleset.type === "group"
380
- ) {
430
+ /**
431
+ * Removes the CoValue from memory.
432
+ *
433
+ * @returns true if the coValue was successfully unmounted, false otherwise
434
+ */
435
+ unmount(): boolean {
436
+ if (this.listeners.size > 0) {
437
+ // The coValue is still in use
381
438
  return false;
382
439
  }
383
440
 
384
- if (this.listeners.size > 0) {
385
- return false; // The coValue is still in use
441
+ for (const dependant of this.dependant) {
442
+ if (this.node.hasCoValue(dependant)) {
443
+ // Another in-memory coValue depends on this coValue
444
+ return false;
445
+ }
446
+ }
447
+
448
+ if (!this.node.syncManager.isSyncedToServerPeers(this.id)) {
449
+ return false;
386
450
  }
387
451
 
388
452
  this.counter.add(-1, { state: this.loadingState });
@@ -482,6 +546,15 @@ export class CoValueCore {
482
546
  skipVerify?: boolean,
483
547
  ) {
484
548
  if (!skipVerify) {
549
+ const validation = validateUniqueness(header.uniqueness);
550
+ if (!validation.isOk) {
551
+ logger.error("Invalid uniqueness", {
552
+ header,
553
+ errorMessage: validation.message,
554
+ });
555
+ return false;
556
+ }
557
+
485
558
  const expectedId = idforHeader(header, this.node.crypto);
486
559
 
487
560
  if (this.id !== expectedId) {
@@ -549,6 +622,33 @@ export class CoValueCore {
549
622
  return this.verified?.immutableKnownState() ?? emptyKnownState(this.id);
550
623
  }
551
624
 
625
+ /**
626
+ * Returns a known state message to signal to the peer that the coValue doesn't need to be synced anymore
627
+ *
628
+ * Implemented to be backward compatible with clients that don't support deleted coValues
629
+ */
630
+ stopSyncingKnownStateMessage(
631
+ peerKnownState: CoValueKnownState | undefined,
632
+ ): KnownStateMessage {
633
+ if (!peerKnownState) {
634
+ return {
635
+ action: "known",
636
+ ...this.knownState(),
637
+ };
638
+ }
639
+
640
+ const knownState = cloneKnownState(this.knownState());
641
+
642
+ // We combine everything for backward compatibility with clients that don't support deleted coValues
643
+ // This way they won't try to sync their own content that we have discarded because the coValue is deleted
644
+ combineKnownStateSessions(knownState.sessions, peerKnownState.sessions);
645
+
646
+ return {
647
+ action: "known",
648
+ ...knownState,
649
+ };
650
+ }
651
+
552
652
  get meta(): JsonValue {
553
653
  return this.verified?.header.meta ?? null;
554
654
  }
@@ -583,6 +683,107 @@ export class CoValueCore {
583
683
  }
584
684
  }
585
685
 
686
+ #isDeleteTransaction(
687
+ sessionID: SessionID,
688
+ newTransactions: Transaction[],
689
+ skipVerify: boolean,
690
+ ) {
691
+ if (!this.verified) {
692
+ return {
693
+ value: false,
694
+ };
695
+ }
696
+
697
+ // Detect + validate delete transactions during ingestion
698
+ // Delete transactions are:
699
+ // - in a delete session (sessionID ends with `$`)
700
+ // - trusting (unencrypted)
701
+ // - have meta `{ deleted: true }`
702
+ let deleteTransaction: Transaction | undefined = undefined;
703
+
704
+ if (isDeleteSessionID(sessionID)) {
705
+ const txCount =
706
+ this.verified.sessions.get(sessionID)?.transactions.length ?? 0;
707
+ if (txCount > 0 || newTransactions.length > 1) {
708
+ return {
709
+ value: true,
710
+ err: {
711
+ type: "DeleteTransactionRejected",
712
+ id: this.id,
713
+ sessionID,
714
+ reason: "InvalidDeleteTransaction",
715
+ error: new Error(
716
+ "Delete transaction must be the only transaction in the session",
717
+ ),
718
+ } as const,
719
+ };
720
+ }
721
+
722
+ const firstTransaction = newTransactions[0];
723
+ const deleteMarker =
724
+ firstTransaction && this.#getDeleteMarker(firstTransaction);
725
+
726
+ if (deleteMarker) {
727
+ deleteTransaction = firstTransaction;
728
+
729
+ if (deleteMarker.deleted !== this.id) {
730
+ return {
731
+ value: true,
732
+ err: {
733
+ type: "DeleteTransactionRejected",
734
+ id: this.id,
735
+ sessionID,
736
+ reason: "InvalidDeleteTransaction",
737
+ error: new Error(
738
+ `Delete transaction ID mismatch: expected ${this.id}, got ${deleteMarker.deleted}`,
739
+ ),
740
+ } as const,
741
+ };
742
+ }
743
+ }
744
+
745
+ if (this.isGroupOrAccount()) {
746
+ return {
747
+ value: true,
748
+ err: {
749
+ type: "DeleteTransactionRejected",
750
+ id: this.id,
751
+ sessionID,
752
+ reason: "CoValueNotDeletable",
753
+ error: new Error("Cannot delete Group or Account coValues"),
754
+ },
755
+ } as const;
756
+ }
757
+ }
758
+
759
+ if (!skipVerify && deleteTransaction) {
760
+ const author = accountOrAgentIDfromSessionID(sessionID);
761
+
762
+ const permission = this.#canAuthorDeleteCoValueAtTime(
763
+ author,
764
+ deleteTransaction.madeAt,
765
+ );
766
+
767
+ if (!permission.ok) {
768
+ return {
769
+ value: true,
770
+ err: {
771
+ type: "DeleteTransactionRejected",
772
+ id: this.id,
773
+ sessionID,
774
+ author,
775
+ reason: permission.reason,
776
+ error: new Error(permission.message),
777
+ },
778
+ } as const;
779
+ }
780
+ }
781
+
782
+ return {
783
+ value: Boolean(deleteTransaction),
784
+ };
785
+ }
786
+
586
787
  /**
587
788
  * Apply new transactions that were not generated by the current node to the CoValue
588
789
  */
@@ -592,8 +793,22 @@ export class CoValueCore {
592
793
  newSignature: Signature,
593
794
  skipVerify: boolean = false,
594
795
  ) {
796
+ if (newTransactions.length === 0) {
797
+ return;
798
+ }
799
+
595
800
  let signerID: SignerID | undefined;
596
801
 
802
+ // sync should never try to add transactions to a deleted coValue
803
+ // this can only happen if `tryAddTransactions` is called directly, without going through `handleNewContent`
804
+ if (this.isDeleted && !isDeleteSessionID(sessionID)) {
805
+ return {
806
+ type: "CoValueDeleted",
807
+ id: this.id,
808
+ error: new Error("Cannot add transactions to a deleted coValue"),
809
+ } as const;
810
+ }
811
+
597
812
  if (!skipVerify) {
598
813
  const result = this.node.resolveAccountAgent(
599
814
  accountOrAgentIDfromSessionID(sessionID),
@@ -619,6 +834,16 @@ export class CoValueCore {
619
834
  };
620
835
  }
621
836
 
837
+ const isDeleteTransaction = this.#isDeleteTransaction(
838
+ sessionID,
839
+ newTransactions,
840
+ skipVerify,
841
+ );
842
+
843
+ if (isDeleteTransaction.err) {
844
+ return isDeleteTransaction.err;
845
+ }
846
+
622
847
  try {
623
848
  this.verified.tryAddTransactions(
624
849
  sessionID,
@@ -628,6 +853,13 @@ export class CoValueCore {
628
853
  skipVerify,
629
854
  );
630
855
 
856
+ // Mark deleted state when a delete marker transaction is accepted.
857
+ // - In skipVerify mode (storage shards), we accept + mark without permission checks.
858
+ // - In verify mode, we only reach here if the delete permission check passed.
859
+ if (isDeleteTransaction.value) {
860
+ this.#markAsDeleted();
861
+ }
862
+
631
863
  this.processNewTransactions();
632
864
  this.scheduleNotifyUpdate();
633
865
  this.invalidateDependants();
@@ -636,6 +868,78 @@ export class CoValueCore {
636
868
  }
637
869
  }
638
870
 
871
+ #markAsDeleted() {
872
+ this.isDeleted = true;
873
+ this.verified?.markAsDeleted();
874
+ }
875
+
876
+ #getDeleteMarker(tx: Transaction): { deleted: RawCoID } | undefined {
877
+ if (tx.privacy !== "trusting") {
878
+ return;
879
+ }
880
+ if (!tx.meta) {
881
+ return;
882
+ }
883
+ const meta = safeParseJSON(tx.meta);
884
+
885
+ return meta && typeof meta.deleted === "string"
886
+ ? (meta as { deleted: RawCoID })
887
+ : undefined;
888
+ }
889
+
890
+ #canAuthorDeleteCoValueAtTime(
891
+ author: RawAccountID | AgentID,
892
+ madeAt: number,
893
+ ):
894
+ | { ok: true }
895
+ | {
896
+ ok: false;
897
+ reason: DeleteTransactionRejectedError["reason"];
898
+ message: string;
899
+ } {
900
+ if (!this.verified) {
901
+ return {
902
+ ok: false,
903
+ reason: "CannotVerifyPermissions",
904
+ message: "Cannot verify delete permissions without verified state",
905
+ };
906
+ }
907
+
908
+ if (this.isGroupOrAccount()) {
909
+ return {
910
+ ok: false,
911
+ reason: "CoValueNotDeletable",
912
+ message: "Cannot delete Group or Account coValues",
913
+ };
914
+ }
915
+
916
+ const group = this.safeGetGroup();
917
+
918
+ // Today, delete permission is defined in terms of group-admin on the owning group.
919
+ // If we cannot derive that (non-owned coValues), we reject the delete when verification is required.
920
+ if (!group) {
921
+ return {
922
+ ok: false,
923
+ reason: "CannotVerifyPermissions",
924
+ message:
925
+ "Cannot verify delete permissions for coValues not owned by a group",
926
+ };
927
+ }
928
+
929
+ const groupAtTime = group.atTime(madeAt);
930
+ const role = groupAtTime.roleOfInternal(author);
931
+
932
+ if (role !== "admin") {
933
+ return {
934
+ ok: false,
935
+ reason: "NotAdmin",
936
+ message: "Delete transaction rejected: author is not an admin",
937
+ };
938
+ }
939
+
940
+ return { ok: true };
941
+ }
942
+
639
943
  notifyDependants() {
640
944
  if (!this.isGroup()) {
641
945
  return;
@@ -733,6 +1037,70 @@ export class CoValueCore {
733
1037
  };
734
1038
  }
735
1039
 
1040
+ validateDeletePermissions() {
1041
+ if (!this.verified) {
1042
+ return {
1043
+ ok: false,
1044
+ reason: "CannotVerifyPermissions",
1045
+ message: "Cannot verify delete permissions without verified state",
1046
+ };
1047
+ }
1048
+
1049
+ if (this.isGroupOrAccount()) {
1050
+ return {
1051
+ ok: false,
1052
+ reason: "CoValueNotDeletable",
1053
+ message: "Cannot delete Group or Account coValues",
1054
+ };
1055
+ }
1056
+
1057
+ const group = this.safeGetGroup();
1058
+ if (!group) {
1059
+ return {
1060
+ ok: false,
1061
+ reason: "CannotVerifyPermissions",
1062
+ message:
1063
+ "Cannot verify delete permissions for coValues not owned by a group",
1064
+ };
1065
+ }
1066
+
1067
+ const role = group.myRole();
1068
+ if (role !== "admin") {
1069
+ return {
1070
+ ok: false,
1071
+ reason: "NotAdmin",
1072
+ message:
1073
+ "The current account lacks admin permissions to delete this coValue",
1074
+ };
1075
+ }
1076
+
1077
+ return { ok: true };
1078
+ }
1079
+
1080
+ /**
1081
+ * Creates a delete marker transaction for this CoValue and sets the coValue as deleted
1082
+ *
1083
+ * Constraints:
1084
+ * - Account and Group CoValues cannot be deleted.
1085
+ * - Only admins can delete a coValue.
1086
+ */
1087
+ deleteCoValue() {
1088
+ if (this.isDeleted) {
1089
+ return;
1090
+ }
1091
+
1092
+ const result = this.validateDeletePermissions();
1093
+ if (!result.ok) {
1094
+ throw new Error(result.message);
1095
+ }
1096
+
1097
+ this.makeTransaction(
1098
+ [], // Empty changes array
1099
+ "trusting", // Unencrypted
1100
+ { deleted: this.id }, // Delete metadata
1101
+ );
1102
+ }
1103
+
736
1104
  /**
737
1105
  * Creates a new transaction with local changes and syncs it to all peers
738
1106
  */
@@ -747,11 +1115,17 @@ export class CoValueCore {
747
1115
  "CoValueCore: makeTransaction called on coValue without verified state",
748
1116
  );
749
1117
  }
1118
+ const isDeleteTransaction = Boolean(meta?.deleted);
750
1119
 
751
- validateTxSizeLimitInBytes(changes);
1120
+ if (this.isDeleted && !isDeleteTransaction) {
1121
+ logger.error("Cannot make transaction on a deleted coValue", {
1122
+ id: this.id,
1123
+ });
1124
+ return false;
1125
+ }
752
1126
 
753
1127
  // This is an ugly hack to get a unique but stable session ID for editing the current account
754
- const sessionID =
1128
+ let sessionID =
755
1129
  this.verified.header.meta?.type === "account"
756
1130
  ? (this.node.currentSessionID.replace(
757
1131
  this.node.getCurrentAgent().id,
@@ -759,6 +1133,12 @@ export class CoValueCore {
759
1133
  ) as SessionID)
760
1134
  : this.node.currentSessionID;
761
1135
 
1136
+ if (isDeleteTransaction) {
1137
+ sessionID = this.crypto.newDeleteSessionID(
1138
+ this.node.getCurrentAccountOrAgentID(),
1139
+ );
1140
+ }
1141
+
762
1142
  const signerAgent = this.node.getCurrentAgent();
763
1143
 
764
1144
  let result: { signature: Signature; transaction: Transaction };
@@ -791,6 +1171,10 @@ export class CoValueCore {
791
1171
  );
792
1172
  }
793
1173
 
1174
+ if (isDeleteTransaction) {
1175
+ this.#markAsDeleted();
1176
+ }
1177
+
794
1178
  const { transaction } = result;
795
1179
 
796
1180
  // Assign pre-parsed meta and changes to skip the parse/decrypt operation when loading
@@ -1193,6 +1577,15 @@ export class CoValueCore {
1193
1577
  return matchingTransactions;
1194
1578
  }
1195
1579
 
1580
+ /**
1581
+ * The CoValues that this CoValue depends on.
1582
+ * We currently track dependencies for:
1583
+ * - Ownership (a CoValue depends on its account/group owner)
1584
+ * - Group membership (a group depends on its direct account/group members)
1585
+ * - Sessions (a CoValue depends on Accounts that made changes to it)
1586
+ * - Branches (a branched CoValue depends on its branch source)
1587
+ * See {@link dependant} for the CoValues that depend on this CoValue.
1588
+ */
1196
1589
  dependencies: Set<RawCoID> = new Set();
1197
1590
  incompleteDependencies: Set<RawCoID> = new Set();
1198
1591
  private addDependency(dependency: RawCoID) {
@@ -1238,11 +1631,23 @@ export class CoValueCore {
1238
1631
  }
1239
1632
  }
1240
1633
 
1634
+ /**
1635
+ * The CoValues that depend on this CoValue.
1636
+ * This is the inverse relationship of {@link dependencies}.
1637
+ */
1241
1638
  dependant: Set<RawCoID> = new Set();
1242
1639
  private addDependant(dependant: RawCoID) {
1243
1640
  this.dependant.add(dependant);
1244
1641
  }
1245
1642
 
1643
+ isGroupOrAccount() {
1644
+ if (!this.verified) {
1645
+ return false;
1646
+ }
1647
+
1648
+ return this.verified.header.ruleset.type === "group";
1649
+ }
1650
+
1246
1651
  isGroup() {
1247
1652
  if (!this.verified) {
1248
1653
  return false;
@@ -1622,9 +2027,19 @@ export type TriedToAddTransactionsWithoutSignerIDError = {
1622
2027
  sessionID: SessionID;
1623
2028
  };
1624
2029
 
2030
+ export type DeleteTransactionRejectedError = {
2031
+ type: "DeleteTransactionRejected";
2032
+ id: RawCoID;
2033
+ sessionID: SessionID;
2034
+ author: RawAccountID | AgentID;
2035
+ reason: "NotAdmin" | "CoValueNotDeletable" | "CannotVerifyPermissions";
2036
+ error: Error;
2037
+ };
2038
+
1625
2039
  export type TryAddTransactionsError =
1626
2040
  | TriedToAddTransactionsWithoutVerifiedStateErrpr
1627
2041
  | TriedToAddTransactionsWithoutSignerIDError
1628
2042
  | ResolveAccountAgentError
1629
2043
  | InvalidHashError
1630
- | InvalidSignatureError;
2044
+ | InvalidSignatureError
2045
+ | DeleteTransactionRejectedError;
@@ -14,12 +14,16 @@ import {
14
14
  Signature,
15
15
  SignerID,
16
16
  } from "../crypto/crypto.js";
17
- import { RawCoID, SessionID, TransactionID } from "../ids.js";
17
+ import {
18
+ isDeleteSessionID,
19
+ RawCoID,
20
+ SessionID,
21
+ TransactionID,
22
+ } from "../ids.js";
18
23
  import { Stringified } from "../jsonStringify.js";
19
24
  import { JsonObject, JsonValue } from "../jsonValue.js";
20
25
  import { PermissionsDef as RulesetDef } from "../permissions.js";
21
26
  import { NewContentMessage } from "../sync.js";
22
- import { TryAddTransactionsError } from "./coValueCore.js";
23
27
  import { SessionMap } from "./SessionMap.js";
24
28
  import { ControlledAccountOrAgent } from "../coValues/account.js";
25
29
  import {
@@ -35,10 +39,19 @@ export type CoValueHeader = {
35
39
  } & CoValueUniqueness;
36
40
 
37
41
  export type CoValueUniqueness = {
38
- uniqueness: JsonValue;
42
+ uniqueness: Uniqueness;
39
43
  createdAt?: `2${string}` | null;
40
44
  };
41
45
 
46
+ export type Uniqueness =
47
+ | string
48
+ | boolean
49
+ | null
50
+ | undefined
51
+ | {
52
+ [key: string]: string;
53
+ };
54
+
42
55
  export type PrivateTransaction = {
43
56
  privacy: "private";
44
57
  madeAt: number;
@@ -63,6 +76,7 @@ export class VerifiedState {
63
76
  public lastAccessed: number | undefined;
64
77
  public branchSourceId?: RawCoID;
65
78
  public branchName?: string;
79
+ private isDeleted: boolean = false;
66
80
 
67
81
  constructor(
68
82
  id: RawCoID,
@@ -87,6 +101,11 @@ export class VerifiedState {
87
101
  );
88
102
  }
89
103
 
104
+ markAsDeleted() {
105
+ this.isDeleted = true;
106
+ this.sessions.markAsDeleted();
107
+ }
108
+
90
109
  tryAddTransactions(
91
110
  sessionID: SessionID,
92
111
  signerID: SignerID | undefined,
@@ -199,6 +218,10 @@ export class VerifiedState {
199
218
  const sessionSent = knownState?.sessions;
200
219
 
201
220
  for (const [sessionID, log] of this.sessions.sessions) {
221
+ if (this.isDeleted && !isDeleteSessionID(sessionID)) {
222
+ continue;
223
+ }
224
+
202
225
  const startFrom = sessionSent?.[sessionID] ?? 0;
203
226
 
204
227
  let currentSessionSize = 0;
@@ -59,14 +59,14 @@ export class RawCoList<
59
59
  afterStart: OpID[] = [];
60
60
  beforeEnd: OpID[] = [];
61
61
  insertions: {
62
- [sessionID: SessionID]: {
62
+ [sessionID: SessionIndex]: {
63
63
  [txIdx: number]: {
64
64
  [changeIdx: number]: InsertionEntry<Item>;
65
65
  };
66
66
  };
67
67
  } = {};
68
68
  deletionsByInsertion: {
69
- [deletedSessionID: SessionID]: {
69
+ [deletedSessionID: SessionIndex]: {
70
70
  [deletedTxIdx: number]: {
71
71
  [deletedChangeIdx: number]: DeletionEntry[];
72
72
  };
@@ -700,7 +700,9 @@ export class RawCoList<
700
700
  }
701
701
  }
702
702
 
703
- function getSessionIndex(txID: TransactionID): SessionID {
703
+ type SessionIndex = SessionID | `${SessionID}_branch_${RawCoID}`;
704
+
705
+ function getSessionIndex(txID: TransactionID): SessionIndex {
704
706
  if (txID.branch) {
705
707
  return `${txID.sessionID}_branch_${txID.branch}`;
706
708
  }
@@ -1203,12 +1203,11 @@ export class RawGroup<
1203
1203
 
1204
1204
  this.set(memberKey, "revoked", "trusting");
1205
1205
 
1206
- // TODO: removeMember fails silently. Uncomment this will be a breaking change
1207
- // if (this.get(memberKey) !== "revoked") {
1208
- // throw new Error(
1209
- // `Failed to revoke role to ${memberKey} (role of current account is ${this.myRole()})`,
1210
- // );
1211
- // }
1206
+ if (this.get(memberKey) !== "revoked") {
1207
+ throw new Error(
1208
+ `Failed to revoke role to ${memberKey} (role of current account is ${this.myRole()})`,
1209
+ );
1210
+ }
1212
1211
  }
1213
1212
 
1214
1213
  /**