cojson 0.19.22 → 0.20.1

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 (223) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +66 -0
  3. package/dist/PeerState.d.ts +6 -1
  4. package/dist/PeerState.d.ts.map +1 -1
  5. package/dist/PeerState.js +18 -3
  6. package/dist/PeerState.js.map +1 -1
  7. package/dist/coValueContentMessage.d.ts +0 -2
  8. package/dist/coValueContentMessage.d.ts.map +1 -1
  9. package/dist/coValueContentMessage.js +0 -8
  10. package/dist/coValueContentMessage.js.map +1 -1
  11. package/dist/coValueCore/SessionMap.d.ts +4 -2
  12. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  13. package/dist/coValueCore/SessionMap.js +30 -0
  14. package/dist/coValueCore/SessionMap.js.map +1 -1
  15. package/dist/coValueCore/coValueCore.d.ts +70 -5
  16. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  17. package/dist/coValueCore/coValueCore.js +302 -31
  18. package/dist/coValueCore/coValueCore.js.map +1 -1
  19. package/dist/coValueCore/verifiedState.d.ts +6 -1
  20. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  21. package/dist/coValueCore/verifiedState.js +9 -0
  22. package/dist/coValueCore/verifiedState.js.map +1 -1
  23. package/dist/coValues/coList.d.ts +4 -2
  24. package/dist/coValues/coList.d.ts.map +1 -1
  25. package/dist/coValues/coList.js +3 -0
  26. package/dist/coValues/coList.js.map +1 -1
  27. package/dist/coValues/group.d.ts.map +1 -1
  28. package/dist/coValues/group.js +3 -6
  29. package/dist/coValues/group.js.map +1 -1
  30. package/dist/config.d.ts +2 -8
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/config.js +4 -12
  33. package/dist/config.js.map +1 -1
  34. package/dist/crypto/NapiCrypto.d.ts +1 -2
  35. package/dist/crypto/NapiCrypto.d.ts.map +1 -1
  36. package/dist/crypto/NapiCrypto.js +19 -4
  37. package/dist/crypto/NapiCrypto.js.map +1 -1
  38. package/dist/crypto/RNCrypto.d.ts.map +1 -1
  39. package/dist/crypto/RNCrypto.js +19 -4
  40. package/dist/crypto/RNCrypto.js.map +1 -1
  41. package/dist/crypto/WasmCrypto.d.ts +11 -4
  42. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  43. package/dist/crypto/WasmCrypto.js +52 -10
  44. package/dist/crypto/WasmCrypto.js.map +1 -1
  45. package/dist/crypto/WasmCryptoEdge.d.ts +1 -0
  46. package/dist/crypto/WasmCryptoEdge.d.ts.map +1 -1
  47. package/dist/crypto/WasmCryptoEdge.js +4 -1
  48. package/dist/crypto/WasmCryptoEdge.js.map +1 -1
  49. package/dist/crypto/crypto.d.ts +3 -3
  50. package/dist/crypto/crypto.d.ts.map +1 -1
  51. package/dist/crypto/crypto.js +6 -1
  52. package/dist/crypto/crypto.js.map +1 -1
  53. package/dist/exports.d.ts +5 -5
  54. package/dist/exports.d.ts.map +1 -1
  55. package/dist/exports.js +4 -3
  56. package/dist/exports.js.map +1 -1
  57. package/dist/ids.d.ts +4 -1
  58. package/dist/ids.d.ts.map +1 -1
  59. package/dist/ids.js +4 -0
  60. package/dist/ids.js.map +1 -1
  61. package/dist/knownState.d.ts +2 -0
  62. package/dist/knownState.d.ts.map +1 -1
  63. package/dist/localNode.d.ts +12 -0
  64. package/dist/localNode.d.ts.map +1 -1
  65. package/dist/localNode.js +14 -0
  66. package/dist/localNode.js.map +1 -1
  67. package/dist/platformUtils.d.ts +3 -0
  68. package/dist/platformUtils.d.ts.map +1 -0
  69. package/dist/platformUtils.js +24 -0
  70. package/dist/platformUtils.js.map +1 -0
  71. package/dist/queue/LinkedList.d.ts +9 -3
  72. package/dist/queue/LinkedList.d.ts.map +1 -1
  73. package/dist/queue/LinkedList.js +30 -1
  74. package/dist/queue/LinkedList.js.map +1 -1
  75. package/dist/queue/OutgoingLoadQueue.d.ts +95 -0
  76. package/dist/queue/OutgoingLoadQueue.d.ts.map +1 -0
  77. package/dist/queue/OutgoingLoadQueue.js +240 -0
  78. package/dist/queue/OutgoingLoadQueue.js.map +1 -0
  79. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts +30 -0
  80. package/dist/storage/DeletedCoValuesEraserScheduler.d.ts.map +1 -0
  81. package/dist/storage/DeletedCoValuesEraserScheduler.js +84 -0
  82. package/dist/storage/DeletedCoValuesEraserScheduler.js.map +1 -0
  83. package/dist/storage/sqlite/client.d.ts +3 -0
  84. package/dist/storage/sqlite/client.d.ts.map +1 -1
  85. package/dist/storage/sqlite/client.js +44 -0
  86. package/dist/storage/sqlite/client.js.map +1 -1
  87. package/dist/storage/sqlite/sqliteMigrations.d.ts.map +1 -1
  88. package/dist/storage/sqlite/sqliteMigrations.js +7 -0
  89. package/dist/storage/sqlite/sqliteMigrations.js.map +1 -1
  90. package/dist/storage/sqliteAsync/client.d.ts +3 -0
  91. package/dist/storage/sqliteAsync/client.d.ts.map +1 -1
  92. package/dist/storage/sqliteAsync/client.js +42 -0
  93. package/dist/storage/sqliteAsync/client.js.map +1 -1
  94. package/dist/storage/storageAsync.d.ts +7 -0
  95. package/dist/storage/storageAsync.d.ts.map +1 -1
  96. package/dist/storage/storageAsync.js +48 -0
  97. package/dist/storage/storageAsync.js.map +1 -1
  98. package/dist/storage/storageSync.d.ts +6 -0
  99. package/dist/storage/storageSync.d.ts.map +1 -1
  100. package/dist/storage/storageSync.js +42 -0
  101. package/dist/storage/storageSync.js.map +1 -1
  102. package/dist/storage/types.d.ts +59 -0
  103. package/dist/storage/types.d.ts.map +1 -1
  104. package/dist/storage/types.js +12 -1
  105. package/dist/storage/types.js.map +1 -1
  106. package/dist/sync.d.ts.map +1 -1
  107. package/dist/sync.js +66 -43
  108. package/dist/sync.js.map +1 -1
  109. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts +2 -0
  110. package/dist/tests/DeletedCoValuesEraserScheduler.test.d.ts.map +1 -0
  111. package/dist/tests/DeletedCoValuesEraserScheduler.test.js +149 -0
  112. package/dist/tests/DeletedCoValuesEraserScheduler.test.js.map +1 -0
  113. package/dist/tests/GarbageCollector.test.js +5 -6
  114. package/dist/tests/GarbageCollector.test.js.map +1 -1
  115. package/dist/tests/LinkedList.test.js +90 -0
  116. package/dist/tests/LinkedList.test.js.map +1 -1
  117. package/dist/tests/OutgoingLoadQueue.test.d.ts +2 -0
  118. package/dist/tests/OutgoingLoadQueue.test.d.ts.map +1 -0
  119. package/dist/tests/OutgoingLoadQueue.test.js +814 -0
  120. package/dist/tests/OutgoingLoadQueue.test.js.map +1 -0
  121. package/dist/tests/StorageApiAsync.test.js +484 -152
  122. package/dist/tests/StorageApiAsync.test.js.map +1 -1
  123. package/dist/tests/StorageApiSync.test.js +505 -136
  124. package/dist/tests/StorageApiSync.test.js.map +1 -1
  125. package/dist/tests/WasmCrypto.test.js +6 -3
  126. package/dist/tests/WasmCrypto.test.js.map +1 -1
  127. package/dist/tests/coValueCore.loadFromStorage.test.js +3 -0
  128. package/dist/tests/coValueCore.loadFromStorage.test.js.map +1 -1
  129. package/dist/tests/coValueCore.test.js +34 -13
  130. package/dist/tests/coValueCore.test.js.map +1 -1
  131. package/dist/tests/coreWasm.test.js +127 -4
  132. package/dist/tests/coreWasm.test.js.map +1 -1
  133. package/dist/tests/crypto.test.js +89 -93
  134. package/dist/tests/crypto.test.js.map +1 -1
  135. package/dist/tests/deleteCoValue.test.d.ts +2 -0
  136. package/dist/tests/deleteCoValue.test.d.ts.map +1 -0
  137. package/dist/tests/deleteCoValue.test.js +313 -0
  138. package/dist/tests/deleteCoValue.test.js.map +1 -0
  139. package/dist/tests/group.removeMember.test.js +18 -30
  140. package/dist/tests/group.removeMember.test.js.map +1 -1
  141. package/dist/tests/knownState.lazyLoading.test.js +3 -0
  142. package/dist/tests/knownState.lazyLoading.test.js.map +1 -1
  143. package/dist/tests/sync.concurrentLoad.test.d.ts +2 -0
  144. package/dist/tests/sync.concurrentLoad.test.d.ts.map +1 -0
  145. package/dist/tests/sync.concurrentLoad.test.js +481 -0
  146. package/dist/tests/sync.concurrentLoad.test.js.map +1 -0
  147. package/dist/tests/sync.deleted.test.d.ts +2 -0
  148. package/dist/tests/sync.deleted.test.d.ts.map +1 -0
  149. package/dist/tests/sync.deleted.test.js +214 -0
  150. package/dist/tests/sync.deleted.test.js.map +1 -0
  151. package/dist/tests/sync.mesh.test.js +3 -2
  152. package/dist/tests/sync.mesh.test.js.map +1 -1
  153. package/dist/tests/sync.storage.test.js +4 -3
  154. package/dist/tests/sync.storage.test.js.map +1 -1
  155. package/dist/tests/sync.test.js +3 -2
  156. package/dist/tests/sync.test.js.map +1 -1
  157. package/dist/tests/testStorage.d.ts +3 -0
  158. package/dist/tests/testStorage.d.ts.map +1 -1
  159. package/dist/tests/testStorage.js +17 -1
  160. package/dist/tests/testStorage.js.map +1 -1
  161. package/dist/tests/testUtils.d.ts +7 -3
  162. package/dist/tests/testUtils.d.ts.map +1 -1
  163. package/dist/tests/testUtils.js +19 -4
  164. package/dist/tests/testUtils.js.map +1 -1
  165. package/package.json +6 -16
  166. package/src/PeerState.ts +26 -3
  167. package/src/coValueContentMessage.ts +0 -14
  168. package/src/coValueCore/SessionMap.ts +43 -1
  169. package/src/coValueCore/coValueCore.ts +415 -27
  170. package/src/coValueCore/verifiedState.ts +26 -3
  171. package/src/coValues/coList.ts +9 -3
  172. package/src/coValues/group.ts +5 -6
  173. package/src/config.ts +4 -13
  174. package/src/crypto/NapiCrypto.ts +29 -13
  175. package/src/crypto/RNCrypto.ts +29 -11
  176. package/src/crypto/WasmCrypto.ts +67 -20
  177. package/src/crypto/WasmCryptoEdge.ts +5 -1
  178. package/src/crypto/crypto.ts +16 -4
  179. package/src/exports.ts +4 -2
  180. package/src/ids.ts +11 -1
  181. package/src/localNode.ts +15 -0
  182. package/src/platformUtils.ts +26 -0
  183. package/src/queue/LinkedList.ts +34 -4
  184. package/src/queue/OutgoingLoadQueue.ts +307 -0
  185. package/src/storage/DeletedCoValuesEraserScheduler.ts +124 -0
  186. package/src/storage/sqlite/client.ts +77 -0
  187. package/src/storage/sqlite/sqliteMigrations.ts +7 -0
  188. package/src/storage/sqliteAsync/client.ts +75 -0
  189. package/src/storage/storageAsync.ts +62 -0
  190. package/src/storage/storageSync.ts +58 -0
  191. package/src/storage/types.ts +69 -0
  192. package/src/sync.ts +78 -46
  193. package/src/tests/DeletedCoValuesEraserScheduler.test.ts +185 -0
  194. package/src/tests/GarbageCollector.test.ts +6 -10
  195. package/src/tests/LinkedList.test.ts +111 -0
  196. package/src/tests/OutgoingLoadQueue.test.ts +1129 -0
  197. package/src/tests/StorageApiAsync.test.ts +572 -162
  198. package/src/tests/StorageApiSync.test.ts +580 -143
  199. package/src/tests/WasmCrypto.test.ts +8 -3
  200. package/src/tests/coValueCore.loadFromStorage.test.ts +6 -0
  201. package/src/tests/coValueCore.test.ts +49 -14
  202. package/src/tests/coreWasm.test.ts +319 -10
  203. package/src/tests/crypto.test.ts +141 -150
  204. package/src/tests/deleteCoValue.test.ts +528 -0
  205. package/src/tests/group.removeMember.test.ts +35 -35
  206. package/src/tests/knownState.lazyLoading.test.ts +6 -0
  207. package/src/tests/sync.concurrentLoad.test.ts +650 -0
  208. package/src/tests/sync.deleted.test.ts +294 -0
  209. package/src/tests/sync.mesh.test.ts +5 -2
  210. package/src/tests/sync.storage.test.ts +6 -3
  211. package/src/tests/sync.test.ts +5 -2
  212. package/src/tests/testStorage.ts +31 -2
  213. package/src/tests/testUtils.ts +31 -10
  214. package/dist/crypto/PureJSCrypto.d.ts +0 -77
  215. package/dist/crypto/PureJSCrypto.d.ts.map +0 -1
  216. package/dist/crypto/PureJSCrypto.js +0 -236
  217. package/dist/crypto/PureJSCrypto.js.map +0 -1
  218. package/dist/tests/PureJSCrypto.test.d.ts +0 -2
  219. package/dist/tests/PureJSCrypto.test.d.ts.map +0 -1
  220. package/dist/tests/PureJSCrypto.test.js +0 -145
  221. package/dist/tests/PureJSCrypto.test.js.map +0 -1
  222. package/src/crypto/PureJSCrypto.ts +0 -429
  223. package/src/tests/PureJSCrypto.test.ts +0 -217
@@ -7,7 +7,7 @@ import type {
7
7
  Signature,
8
8
  SignerID,
9
9
  } from "../crypto/crypto.js";
10
- import { RawCoID, SessionID } from "../ids.js";
10
+ import { isDeleteSessionID, RawCoID, SessionID } from "../ids.js";
11
11
  import { parseJSON, Stringified } from "../jsonStringify.js";
12
12
  import { JsonObject, JsonValue } from "../jsonValue.js";
13
13
  import { TryAddTransactionsError } from "./coValueCore.js";
@@ -34,6 +34,7 @@ export type SessionLog = {
34
34
  };
35
35
 
36
36
  export class SessionMap {
37
+ private isDeleted: boolean = false;
37
38
  sessions: Map<SessionID, SessionLog> = new Map();
38
39
 
39
40
  // Known state related properies, mutated when adding transactions to the session map
@@ -60,7 +61,32 @@ export class SessionMap {
60
61
  }
61
62
  }
62
63
 
64
+ markAsDeleted() {
65
+ this.isDeleted = true;
66
+
67
+ // We reset the known state to report only the deleted session/transaction
68
+ this.knownState = { id: this.id, header: true, sessions: {} };
69
+
70
+ // We remove the streaming statuses, because once deleted we don't need
71
+ // to wait for any streaming to be completed
72
+ this.knownStateWithStreaming = undefined;
73
+ this.streamingKnownState = undefined;
74
+ this.invalidateKnownStateCache();
75
+
76
+ for (const [sessionID, sessionLog] of this.sessions.entries()) {
77
+ if (!isDeleteSessionID(sessionID)) {
78
+ continue;
79
+ }
80
+
81
+ this.knownState.sessions[sessionID] = sessionLog.transactions.length;
82
+ }
83
+ }
84
+
63
85
  setStreamingKnownState(streamingKnownState: KnownStateSessions) {
86
+ if (this.isDeleted) {
87
+ return;
88
+ }
89
+
64
90
  // if the streaming known state is a subset of the current known state, we can skip the update
65
91
  if (isKnownStateSubsetOf(streamingKnownState, this.knownState.sessions)) {
66
92
  return;
@@ -148,6 +174,10 @@ export class SessionMap {
148
174
  newSignature: Signature,
149
175
  skipVerify: boolean = false,
150
176
  ) {
177
+ if (this.isDeleted && !isDeleteSessionID(sessionID)) {
178
+ throw new Error("Cannot add transactions to a deleted coValue");
179
+ }
180
+
151
181
  const sessionLog = this.getOrCreateSessionLog(sessionID, signerID);
152
182
 
153
183
  sessionLog.impl.tryAdd(newTransactions, newSignature, skipVerify);
@@ -164,6 +194,12 @@ export class SessionMap {
164
194
  meta: JsonObject | undefined,
165
195
  madeAt: number,
166
196
  ): { signature: Signature; transaction: Transaction } {
197
+ if (this.isDeleted) {
198
+ throw new Error(
199
+ "Cannot make new private transaction on a deleted coValue",
200
+ );
201
+ }
202
+
167
203
  const sessionLog = this.getOrCreateSessionLog(
168
204
  sessionID,
169
205
  signerAgent.currentSignerID(),
@@ -194,6 +230,12 @@ export class SessionMap {
194
230
  meta: JsonObject | undefined,
195
231
  madeAt: number,
196
232
  ): { signature: Signature; transaction: Transaction } {
233
+ if (this.isDeleted) {
234
+ throw new Error(
235
+ "Cannot make new trusting transaction on a deleted coValue",
236
+ );
237
+ }
238
+
197
239
  const sessionLog = this.getOrCreateSessionLog(
198
240
  sessionID,
199
241
  signerAgent.currentSignerID(),
@@ -1,10 +1,13 @@
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 type { LoadMode } from "../queue/OutgoingLoadQueue.js";
5
+ import {
6
+ RawAccount,
7
+ type ControlledAccountOrAgent,
8
+ } from "../coValues/account.js";
5
9
  import type { RawGroup } from "../coValues/group.js";
6
10
  import { CO_VALUE_LOADING_CONFIG } from "../config.js";
7
- import { validateTxSizeLimitInBytes } from "../coValueContentMessage.js";
8
11
  import { coreToCoValue } from "../coreToCoValue.js";
9
12
  import {
10
13
  CryptoProvider,
@@ -14,12 +17,18 @@ import {
14
17
  Signature,
15
18
  SignerID,
16
19
  } from "../crypto/crypto.js";
17
- import { AgentID, RawCoID, SessionID, TransactionID } from "../ids.js";
20
+ import {
21
+ AgentID,
22
+ isDeleteSessionID,
23
+ RawCoID,
24
+ SessionID,
25
+ TransactionID,
26
+ } from "../ids.js";
18
27
  import { JsonObject, JsonValue } from "../jsonValue.js";
19
28
  import { LocalNode, ResolveAccountAgentError } from "../localNode.js";
20
29
  import { logger } from "../logger.js";
21
30
  import { determineValidTransactions } from "../permissions.js";
22
- import { NewContentMessage, PeerID } from "../sync.js";
31
+ import { KnownStateMessage, NewContentMessage, PeerID } from "../sync.js";
23
32
  import { accountOrAgentIDfromSessionID } from "../typeUtils/accountOrAgentIDfromSessionID.js";
24
33
  import { expectGroup } from "../typeUtils/expectGroup.js";
25
34
  import {
@@ -27,7 +36,12 @@ import {
27
36
  getDependenciesFromGroupRawTransactions,
28
37
  getDependenciesFromHeader,
29
38
  } from "./utils.js";
30
- import { CoValueHeader, Transaction, VerifiedState } from "./verifiedState.js";
39
+ import {
40
+ CoValueHeader,
41
+ Transaction,
42
+ Uniqueness,
43
+ VerifiedState,
44
+ } from "./verifiedState.js";
31
45
  import { SessionMap } from "./SessionMap.js";
32
46
  import {
33
47
  MergeCommit,
@@ -51,6 +65,45 @@ import {
51
65
  } from "../knownState.js";
52
66
  import { safeParseJSON } from "../jsonStringify.js";
53
67
 
68
+ export type ValidationValue =
69
+ | { isOk: true }
70
+ | {
71
+ isOk: false;
72
+ message: string;
73
+ };
74
+
75
+ function validateUniqueness(uniqueness: Uniqueness): ValidationValue {
76
+ if (typeof uniqueness === "number" && !Number.isInteger(uniqueness)) {
77
+ return {
78
+ isOk: false,
79
+ message: "Uniqueness cannot be a non-integer number, got " + uniqueness,
80
+ };
81
+ }
82
+
83
+ if (Array.isArray(uniqueness)) {
84
+ return {
85
+ isOk: false,
86
+ message: "Uniqueness cannot be an array, got " + uniqueness,
87
+ };
88
+ }
89
+
90
+ if (typeof uniqueness === "object" && uniqueness !== null) {
91
+ for (let [key, value] of Object.entries(uniqueness)) {
92
+ if (typeof value !== "string") {
93
+ return {
94
+ isOk: false,
95
+ message:
96
+ "Uniqueness object values must be a string, got " +
97
+ value +
98
+ " for key " +
99
+ key,
100
+ };
101
+ }
102
+ }
103
+ }
104
+ return { isOk: true };
105
+ }
106
+
54
107
  export function idforHeader(
55
108
  header: CoValueHeader,
56
109
  crypto: CryptoProvider,
@@ -197,6 +250,8 @@ export class CoValueCore {
197
250
  // context
198
251
  readonly node: LocalNode;
199
252
  private readonly crypto: CryptoProvider;
253
+ // Whether the coValue is deleted
254
+ public isDeleted: boolean = false;
200
255
 
201
256
  // state
202
257
  id: RawCoID;
@@ -492,6 +547,15 @@ export class CoValueCore {
492
547
  skipVerify?: boolean,
493
548
  ) {
494
549
  if (!skipVerify) {
550
+ const validation = validateUniqueness(header.uniqueness);
551
+ if (!validation.isOk) {
552
+ logger.error("Invalid uniqueness", {
553
+ header,
554
+ errorMessage: validation.message,
555
+ });
556
+ return false;
557
+ }
558
+
495
559
  const expectedId = idforHeader(header, this.node.crypto);
496
560
 
497
561
  if (this.id !== expectedId) {
@@ -559,6 +623,33 @@ export class CoValueCore {
559
623
  return this.verified?.immutableKnownState() ?? emptyKnownState(this.id);
560
624
  }
561
625
 
626
+ /**
627
+ * Returns a known state message to signal to the peer that the coValue doesn't need to be synced anymore
628
+ *
629
+ * Implemented to be backward compatible with clients that don't support deleted coValues
630
+ */
631
+ stopSyncingKnownStateMessage(
632
+ peerKnownState: CoValueKnownState | undefined,
633
+ ): KnownStateMessage {
634
+ if (!peerKnownState) {
635
+ return {
636
+ action: "known",
637
+ ...this.knownState(),
638
+ };
639
+ }
640
+
641
+ const knownState = cloneKnownState(this.knownState());
642
+
643
+ // We combine everything for backward compatibility with clients that don't support deleted coValues
644
+ // This way they won't try to sync their own content that we have discarded because the coValue is deleted
645
+ combineKnownStateSessions(knownState.sessions, peerKnownState.sessions);
646
+
647
+ return {
648
+ action: "known",
649
+ ...knownState,
650
+ };
651
+ }
652
+
562
653
  get meta(): JsonValue {
563
654
  return this.verified?.header.meta ?? null;
564
655
  }
@@ -593,6 +684,107 @@ export class CoValueCore {
593
684
  }
594
685
  }
595
686
 
687
+ #isDeleteTransaction(
688
+ sessionID: SessionID,
689
+ newTransactions: Transaction[],
690
+ skipVerify: boolean,
691
+ ) {
692
+ if (!this.verified) {
693
+ return {
694
+ value: false,
695
+ };
696
+ }
697
+
698
+ // Detect + validate delete transactions during ingestion
699
+ // Delete transactions are:
700
+ // - in a delete session (sessionID ends with `$`)
701
+ // - trusting (unencrypted)
702
+ // - have meta `{ deleted: true }`
703
+ let deleteTransaction: Transaction | undefined = undefined;
704
+
705
+ if (isDeleteSessionID(sessionID)) {
706
+ const txCount =
707
+ this.verified.sessions.get(sessionID)?.transactions.length ?? 0;
708
+ if (txCount > 0 || newTransactions.length > 1) {
709
+ return {
710
+ value: true,
711
+ err: {
712
+ type: "DeleteTransactionRejected",
713
+ id: this.id,
714
+ sessionID,
715
+ reason: "InvalidDeleteTransaction",
716
+ error: new Error(
717
+ "Delete transaction must be the only transaction in the session",
718
+ ),
719
+ } as const,
720
+ };
721
+ }
722
+
723
+ const firstTransaction = newTransactions[0];
724
+ const deleteMarker =
725
+ firstTransaction && this.#getDeleteMarker(firstTransaction);
726
+
727
+ if (deleteMarker) {
728
+ deleteTransaction = firstTransaction;
729
+
730
+ if (deleteMarker.deleted !== this.id) {
731
+ return {
732
+ value: true,
733
+ err: {
734
+ type: "DeleteTransactionRejected",
735
+ id: this.id,
736
+ sessionID,
737
+ reason: "InvalidDeleteTransaction",
738
+ error: new Error(
739
+ `Delete transaction ID mismatch: expected ${this.id}, got ${deleteMarker.deleted}`,
740
+ ),
741
+ } as const,
742
+ };
743
+ }
744
+ }
745
+
746
+ if (this.isGroupOrAccount()) {
747
+ return {
748
+ value: true,
749
+ err: {
750
+ type: "DeleteTransactionRejected",
751
+ id: this.id,
752
+ sessionID,
753
+ reason: "CoValueNotDeletable",
754
+ error: new Error("Cannot delete Group or Account coValues"),
755
+ },
756
+ } as const;
757
+ }
758
+ }
759
+
760
+ if (!skipVerify && deleteTransaction) {
761
+ const author = accountOrAgentIDfromSessionID(sessionID);
762
+
763
+ const permission = this.#canAuthorDeleteCoValueAtTime(
764
+ author,
765
+ deleteTransaction.madeAt,
766
+ );
767
+
768
+ if (!permission.ok) {
769
+ return {
770
+ value: true,
771
+ err: {
772
+ type: "DeleteTransactionRejected",
773
+ id: this.id,
774
+ sessionID,
775
+ author,
776
+ reason: permission.reason,
777
+ error: new Error(permission.message),
778
+ },
779
+ } as const;
780
+ }
781
+ }
782
+
783
+ return {
784
+ value: Boolean(deleteTransaction),
785
+ };
786
+ }
787
+
596
788
  /**
597
789
  * Apply new transactions that were not generated by the current node to the CoValue
598
790
  */
@@ -602,8 +794,22 @@ export class CoValueCore {
602
794
  newSignature: Signature,
603
795
  skipVerify: boolean = false,
604
796
  ) {
797
+ if (newTransactions.length === 0) {
798
+ return;
799
+ }
800
+
605
801
  let signerID: SignerID | undefined;
606
802
 
803
+ // sync should never try to add transactions to a deleted coValue
804
+ // this can only happen if `tryAddTransactions` is called directly, without going through `handleNewContent`
805
+ if (this.isDeleted && !isDeleteSessionID(sessionID)) {
806
+ return {
807
+ type: "CoValueDeleted",
808
+ id: this.id,
809
+ error: new Error("Cannot add transactions to a deleted coValue"),
810
+ } as const;
811
+ }
812
+
607
813
  if (!skipVerify) {
608
814
  const result = this.node.resolveAccountAgent(
609
815
  accountOrAgentIDfromSessionID(sessionID),
@@ -629,6 +835,16 @@ export class CoValueCore {
629
835
  };
630
836
  }
631
837
 
838
+ const isDeleteTransaction = this.#isDeleteTransaction(
839
+ sessionID,
840
+ newTransactions,
841
+ skipVerify,
842
+ );
843
+
844
+ if (isDeleteTransaction.err) {
845
+ return isDeleteTransaction.err;
846
+ }
847
+
632
848
  try {
633
849
  this.verified.tryAddTransactions(
634
850
  sessionID,
@@ -638,6 +854,13 @@ export class CoValueCore {
638
854
  skipVerify,
639
855
  );
640
856
 
857
+ // Mark deleted state when a delete marker transaction is accepted.
858
+ // - In skipVerify mode (storage shards), we accept + mark without permission checks.
859
+ // - In verify mode, we only reach here if the delete permission check passed.
860
+ if (isDeleteTransaction.value) {
861
+ this.#markAsDeleted();
862
+ }
863
+
641
864
  this.processNewTransactions();
642
865
  this.scheduleNotifyUpdate();
643
866
  this.invalidateDependants();
@@ -646,6 +869,78 @@ export class CoValueCore {
646
869
  }
647
870
  }
648
871
 
872
+ #markAsDeleted() {
873
+ this.isDeleted = true;
874
+ this.verified?.markAsDeleted();
875
+ }
876
+
877
+ #getDeleteMarker(tx: Transaction): { deleted: RawCoID } | undefined {
878
+ if (tx.privacy !== "trusting") {
879
+ return;
880
+ }
881
+ if (!tx.meta) {
882
+ return;
883
+ }
884
+ const meta = safeParseJSON(tx.meta);
885
+
886
+ return meta && typeof meta.deleted === "string"
887
+ ? (meta as { deleted: RawCoID })
888
+ : undefined;
889
+ }
890
+
891
+ #canAuthorDeleteCoValueAtTime(
892
+ author: RawAccountID | AgentID,
893
+ madeAt: number,
894
+ ):
895
+ | { ok: true }
896
+ | {
897
+ ok: false;
898
+ reason: DeleteTransactionRejectedError["reason"];
899
+ message: string;
900
+ } {
901
+ if (!this.verified) {
902
+ return {
903
+ ok: false,
904
+ reason: "CannotVerifyPermissions",
905
+ message: "Cannot verify delete permissions without verified state",
906
+ };
907
+ }
908
+
909
+ if (this.isGroupOrAccount()) {
910
+ return {
911
+ ok: false,
912
+ reason: "CoValueNotDeletable",
913
+ message: "Cannot delete Group or Account coValues",
914
+ };
915
+ }
916
+
917
+ const group = this.safeGetGroup();
918
+
919
+ // Today, delete permission is defined in terms of group-admin on the owning group.
920
+ // If we cannot derive that (non-owned coValues), we reject the delete when verification is required.
921
+ if (!group) {
922
+ return {
923
+ ok: false,
924
+ reason: "CannotVerifyPermissions",
925
+ message:
926
+ "Cannot verify delete permissions for coValues not owned by a group",
927
+ };
928
+ }
929
+
930
+ const groupAtTime = group.atTime(madeAt);
931
+ const role = groupAtTime.roleOfInternal(author);
932
+
933
+ if (role !== "admin") {
934
+ return {
935
+ ok: false,
936
+ reason: "NotAdmin",
937
+ message: "Delete transaction rejected: author is not an admin",
938
+ };
939
+ }
940
+
941
+ return { ok: true };
942
+ }
943
+
649
944
  notifyDependants() {
650
945
  if (!this.isGroup()) {
651
946
  return;
@@ -743,6 +1038,70 @@ export class CoValueCore {
743
1038
  };
744
1039
  }
745
1040
 
1041
+ validateDeletePermissions() {
1042
+ if (!this.verified) {
1043
+ return {
1044
+ ok: false,
1045
+ reason: "CannotVerifyPermissions",
1046
+ message: "Cannot verify delete permissions without verified state",
1047
+ };
1048
+ }
1049
+
1050
+ if (this.isGroupOrAccount()) {
1051
+ return {
1052
+ ok: false,
1053
+ reason: "CoValueNotDeletable",
1054
+ message: "Cannot delete Group or Account coValues",
1055
+ };
1056
+ }
1057
+
1058
+ const group = this.safeGetGroup();
1059
+ if (!group) {
1060
+ return {
1061
+ ok: false,
1062
+ reason: "CannotVerifyPermissions",
1063
+ message:
1064
+ "Cannot verify delete permissions for coValues not owned by a group",
1065
+ };
1066
+ }
1067
+
1068
+ const role = group.myRole();
1069
+ if (role !== "admin") {
1070
+ return {
1071
+ ok: false,
1072
+ reason: "NotAdmin",
1073
+ message:
1074
+ "The current account lacks admin permissions to delete this coValue",
1075
+ };
1076
+ }
1077
+
1078
+ return { ok: true };
1079
+ }
1080
+
1081
+ /**
1082
+ * Creates a delete marker transaction for this CoValue and sets the coValue as deleted
1083
+ *
1084
+ * Constraints:
1085
+ * - Account and Group CoValues cannot be deleted.
1086
+ * - Only admins can delete a coValue.
1087
+ */
1088
+ deleteCoValue() {
1089
+ if (this.isDeleted) {
1090
+ return;
1091
+ }
1092
+
1093
+ const result = this.validateDeletePermissions();
1094
+ if (!result.ok) {
1095
+ throw new Error(result.message);
1096
+ }
1097
+
1098
+ this.makeTransaction(
1099
+ [], // Empty changes array
1100
+ "trusting", // Unencrypted
1101
+ { deleted: this.id }, // Delete metadata
1102
+ );
1103
+ }
1104
+
746
1105
  /**
747
1106
  * Creates a new transaction with local changes and syncs it to all peers
748
1107
  */
@@ -757,11 +1116,17 @@ export class CoValueCore {
757
1116
  "CoValueCore: makeTransaction called on coValue without verified state",
758
1117
  );
759
1118
  }
1119
+ const isDeleteTransaction = Boolean(meta?.deleted);
760
1120
 
761
- validateTxSizeLimitInBytes(changes);
1121
+ if (this.isDeleted && !isDeleteTransaction) {
1122
+ logger.error("Cannot make transaction on a deleted coValue", {
1123
+ id: this.id,
1124
+ });
1125
+ return false;
1126
+ }
762
1127
 
763
1128
  // This is an ugly hack to get a unique but stable session ID for editing the current account
764
- const sessionID =
1129
+ let sessionID =
765
1130
  this.verified.header.meta?.type === "account"
766
1131
  ? (this.node.currentSessionID.replace(
767
1132
  this.node.getCurrentAgent().id,
@@ -769,6 +1134,12 @@ export class CoValueCore {
769
1134
  ) as SessionID)
770
1135
  : this.node.currentSessionID;
771
1136
 
1137
+ if (isDeleteTransaction) {
1138
+ sessionID = this.crypto.newDeleteSessionID(
1139
+ this.node.getCurrentAccountOrAgentID(),
1140
+ );
1141
+ }
1142
+
772
1143
  const signerAgent = this.node.getCurrentAgent();
773
1144
 
774
1145
  let result: { signature: Signature; transaction: Transaction };
@@ -801,6 +1172,10 @@ export class CoValueCore {
801
1172
  );
802
1173
  }
803
1174
 
1175
+ if (isDeleteTransaction) {
1176
+ this.#markAsDeleted();
1177
+ }
1178
+
804
1179
  const { transaction } = result;
805
1180
 
806
1181
  // Assign pre-parsed meta and changes to skip the parse/decrypt operation when loading
@@ -1266,6 +1641,14 @@ export class CoValueCore {
1266
1641
  this.dependant.add(dependant);
1267
1642
  }
1268
1643
 
1644
+ isGroupOrAccount() {
1645
+ if (!this.verified) {
1646
+ return false;
1647
+ }
1648
+
1649
+ return this.verified.header.ruleset.type === "group";
1650
+ }
1651
+
1269
1652
  isGroup() {
1270
1653
  if (!this.verified) {
1271
1654
  return false;
@@ -1465,11 +1848,11 @@ export class CoValueCore {
1465
1848
  return this.node.syncManager.waitForSync(this.id, options?.timeout);
1466
1849
  }
1467
1850
 
1468
- load(peers: PeerState[]) {
1851
+ load(peers: PeerState[], mode?: LoadMode) {
1469
1852
  this.loadFromStorage((found) => {
1470
1853
  // When found the load is triggered by handleNewContent
1471
1854
  if (!found) {
1472
- this.loadFromPeers(peers);
1855
+ this.loadFromPeers(peers, mode);
1473
1856
  }
1474
1857
  });
1475
1858
  }
@@ -1555,7 +1938,7 @@ export class CoValueCore {
1555
1938
  this.node.storage.loadKnownState(this.id, done);
1556
1939
  }
1557
1940
 
1558
- loadFromPeers(peers: PeerState[]) {
1941
+ loadFromPeers(peers: PeerState[], mode?: LoadMode) {
1559
1942
  if (peers.length === 0) {
1560
1943
  return;
1561
1944
  }
@@ -1565,29 +1948,17 @@ export class CoValueCore {
1565
1948
 
1566
1949
  if (currentState === "unknown" || currentState === "unavailable") {
1567
1950
  this.markPending(peer.id);
1568
- this.internalLoadFromPeer(peer);
1951
+ this.internalLoadFromPeer(peer, mode);
1569
1952
  }
1570
1953
  }
1571
1954
  }
1572
1955
 
1573
- private internalLoadFromPeer(peer: PeerState) {
1956
+ private internalLoadFromPeer(peer: PeerState, mode?: LoadMode) {
1574
1957
  if (peer.closed && !peer.persistent) {
1575
1958
  this.markNotFoundInPeer(peer.id);
1576
1959
  return;
1577
1960
  }
1578
1961
 
1579
- /**
1580
- * On reconnection persistent peers will automatically fire the load request
1581
- * as part of the reconnection process.
1582
- */
1583
- if (!peer.closed) {
1584
- peer.pushOutgoingMessage({
1585
- action: "load",
1586
- ...this.knownState(),
1587
- });
1588
- peer.trackLoadRequestSent(this.id);
1589
- }
1590
-
1591
1962
  const markNotFound = () => {
1592
1963
  if (this.getLoadingStateForPeer(peer.id) === "pending") {
1593
1964
  logger.warn("Timeout waiting for peer to load coValue", {
@@ -1598,11 +1969,19 @@ export class CoValueCore {
1598
1969
  }
1599
1970
  };
1600
1971
 
1601
- const timeout = setTimeout(markNotFound, CO_VALUE_LOADING_CONFIG.TIMEOUT);
1972
+ // Close listener for non-persistent peers
1602
1973
  const removeCloseListener = peer.persistent
1603
1974
  ? undefined
1604
1975
  : peer.addCloseListener(markNotFound);
1605
1976
 
1977
+ /**
1978
+ * On reconnection persistent peers will automatically fire the load request
1979
+ * as part of the reconnection process.
1980
+ */
1981
+ if (!peer.closed) {
1982
+ peer.sendLoadRequest(this, mode);
1983
+ }
1984
+
1606
1985
  this.subscribe((state, unsubscribe) => {
1607
1986
  const peerState = state.getLoadingStateForPeer(peer.id);
1608
1987
  if (
@@ -1613,7 +1992,6 @@ export class CoValueCore {
1613
1992
  ) {
1614
1993
  unsubscribe();
1615
1994
  removeCloseListener?.();
1616
- clearTimeout(timeout);
1617
1995
  }
1618
1996
  }, true);
1619
1997
  }
@@ -1645,9 +2023,19 @@ export type TriedToAddTransactionsWithoutSignerIDError = {
1645
2023
  sessionID: SessionID;
1646
2024
  };
1647
2025
 
2026
+ export type DeleteTransactionRejectedError = {
2027
+ type: "DeleteTransactionRejected";
2028
+ id: RawCoID;
2029
+ sessionID: SessionID;
2030
+ author: RawAccountID | AgentID;
2031
+ reason: "NotAdmin" | "CoValueNotDeletable" | "CannotVerifyPermissions";
2032
+ error: Error;
2033
+ };
2034
+
1648
2035
  export type TryAddTransactionsError =
1649
2036
  | TriedToAddTransactionsWithoutVerifiedStateErrpr
1650
2037
  | TriedToAddTransactionsWithoutSignerIDError
1651
2038
  | ResolveAccountAgentError
1652
2039
  | InvalidHashError
1653
- | InvalidSignatureError;
2040
+ | InvalidSignatureError
2041
+ | DeleteTransactionRejectedError;