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
@@ -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;
@@ -492,6 +546,15 @@ export class CoValueCore {
492
546
  skipVerify?: boolean,
493
547
  ) {
494
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
+
495
558
  const expectedId = idforHeader(header, this.node.crypto);
496
559
 
497
560
  if (this.id !== expectedId) {
@@ -559,6 +622,33 @@ export class CoValueCore {
559
622
  return this.verified?.immutableKnownState() ?? emptyKnownState(this.id);
560
623
  }
561
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
+
562
652
  get meta(): JsonValue {
563
653
  return this.verified?.header.meta ?? null;
564
654
  }
@@ -593,6 +683,107 @@ export class CoValueCore {
593
683
  }
594
684
  }
595
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
+
596
787
  /**
597
788
  * Apply new transactions that were not generated by the current node to the CoValue
598
789
  */
@@ -602,8 +793,22 @@ export class CoValueCore {
602
793
  newSignature: Signature,
603
794
  skipVerify: boolean = false,
604
795
  ) {
796
+ if (newTransactions.length === 0) {
797
+ return;
798
+ }
799
+
605
800
  let signerID: SignerID | undefined;
606
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
+
607
812
  if (!skipVerify) {
608
813
  const result = this.node.resolveAccountAgent(
609
814
  accountOrAgentIDfromSessionID(sessionID),
@@ -629,6 +834,16 @@ export class CoValueCore {
629
834
  };
630
835
  }
631
836
 
837
+ const isDeleteTransaction = this.#isDeleteTransaction(
838
+ sessionID,
839
+ newTransactions,
840
+ skipVerify,
841
+ );
842
+
843
+ if (isDeleteTransaction.err) {
844
+ return isDeleteTransaction.err;
845
+ }
846
+
632
847
  try {
633
848
  this.verified.tryAddTransactions(
634
849
  sessionID,
@@ -638,6 +853,13 @@ export class CoValueCore {
638
853
  skipVerify,
639
854
  );
640
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
+
641
863
  this.processNewTransactions();
642
864
  this.scheduleNotifyUpdate();
643
865
  this.invalidateDependants();
@@ -646,6 +868,78 @@ export class CoValueCore {
646
868
  }
647
869
  }
648
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
+
649
943
  notifyDependants() {
650
944
  if (!this.isGroup()) {
651
945
  return;
@@ -743,6 +1037,70 @@ export class CoValueCore {
743
1037
  };
744
1038
  }
745
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
+
746
1104
  /**
747
1105
  * Creates a new transaction with local changes and syncs it to all peers
748
1106
  */
@@ -757,11 +1115,17 @@ export class CoValueCore {
757
1115
  "CoValueCore: makeTransaction called on coValue without verified state",
758
1116
  );
759
1117
  }
1118
+ const isDeleteTransaction = Boolean(meta?.deleted);
760
1119
 
761
- 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
+ }
762
1126
 
763
1127
  // This is an ugly hack to get a unique but stable session ID for editing the current account
764
- const sessionID =
1128
+ let sessionID =
765
1129
  this.verified.header.meta?.type === "account"
766
1130
  ? (this.node.currentSessionID.replace(
767
1131
  this.node.getCurrentAgent().id,
@@ -769,6 +1133,12 @@ export class CoValueCore {
769
1133
  ) as SessionID)
770
1134
  : this.node.currentSessionID;
771
1135
 
1136
+ if (isDeleteTransaction) {
1137
+ sessionID = this.crypto.newDeleteSessionID(
1138
+ this.node.getCurrentAccountOrAgentID(),
1139
+ );
1140
+ }
1141
+
772
1142
  const signerAgent = this.node.getCurrentAgent();
773
1143
 
774
1144
  let result: { signature: Signature; transaction: Transaction };
@@ -801,6 +1171,10 @@ export class CoValueCore {
801
1171
  );
802
1172
  }
803
1173
 
1174
+ if (isDeleteTransaction) {
1175
+ this.#markAsDeleted();
1176
+ }
1177
+
804
1178
  const { transaction } = result;
805
1179
 
806
1180
  // Assign pre-parsed meta and changes to skip the parse/decrypt operation when loading
@@ -1266,6 +1640,14 @@ export class CoValueCore {
1266
1640
  this.dependant.add(dependant);
1267
1641
  }
1268
1642
 
1643
+ isGroupOrAccount() {
1644
+ if (!this.verified) {
1645
+ return false;
1646
+ }
1647
+
1648
+ return this.verified.header.ruleset.type === "group";
1649
+ }
1650
+
1269
1651
  isGroup() {
1270
1652
  if (!this.verified) {
1271
1653
  return false;
@@ -1645,9 +2027,19 @@ export type TriedToAddTransactionsWithoutSignerIDError = {
1645
2027
  sessionID: SessionID;
1646
2028
  };
1647
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
+
1648
2039
  export type TryAddTransactionsError =
1649
2040
  | TriedToAddTransactionsWithoutVerifiedStateErrpr
1650
2041
  | TriedToAddTransactionsWithoutSignerIDError
1651
2042
  | ResolveAccountAgentError
1652
2043
  | InvalidHashError
1653
- | 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
  /**
package/src/config.ts CHANGED
@@ -7,21 +7,12 @@
7
7
  **/
8
8
  export const TRANSACTION_CONFIG = {
9
9
  MAX_RECOMMENDED_TX_SIZE: 100 * 1024,
10
- /**
11
- * Messages larger than this will be rejected when creating a transaction.
12
- * The current limit is set at 1MB because that's the limit imposed by Cloudflare to Websocket messages.
13
- */
14
- MAX_TX_SIZE_BYTES: 1 * 1024 * 1024,
15
10
  };
16
11
 
17
12
  export function setMaxRecommendedTxSize(size: number) {
18
13
  TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE = size;
19
14
  }
20
15
 
21
- export function setMaxTxSizeBytes(size: number) {
22
- TRANSACTION_CONFIG.MAX_TX_SIZE_BYTES = size;
23
- }
24
-
25
16
  export const CO_VALUE_LOADING_CONFIG = {
26
17
  MAX_RETRIES: 1,
27
18
  TIMEOUT: 30_000,