cojson 0.17.12 → 0.17.14

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 (90) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +16 -0
  3. package/dist/coValueCore/SessionMap.d.ts +4 -3
  4. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  5. package/dist/coValueCore/SessionMap.js +15 -4
  6. package/dist/coValueCore/SessionMap.js.map +1 -1
  7. package/dist/coValueCore/coValueCore.d.ts +2 -2
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +33 -32
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValueCore/utils.d.ts.map +1 -1
  12. package/dist/coValueCore/utils.js.map +1 -1
  13. package/dist/coValueCore/verifiedState.d.ts +8 -2
  14. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  15. package/dist/coValueCore/verifiedState.js +7 -4
  16. package/dist/coValueCore/verifiedState.js.map +1 -1
  17. package/dist/coValues/account.d.ts +6 -2
  18. package/dist/coValues/account.d.ts.map +1 -1
  19. package/dist/coValues/account.js +12 -0
  20. package/dist/coValues/account.js.map +1 -1
  21. package/dist/crypto/PureJSCrypto.d.ts +4 -3
  22. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  23. package/dist/crypto/PureJSCrypto.js +34 -2
  24. package/dist/crypto/PureJSCrypto.js.map +1 -1
  25. package/dist/crypto/WasmCrypto.d.ts +4 -3
  26. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  27. package/dist/crypto/WasmCrypto.js +10 -4
  28. package/dist/crypto/WasmCrypto.js.map +1 -1
  29. package/dist/crypto/crypto.d.ts +4 -3
  30. package/dist/crypto/crypto.d.ts.map +1 -1
  31. package/dist/sync.d.ts +14 -5
  32. package/dist/sync.d.ts.map +1 -1
  33. package/dist/sync.js +44 -13
  34. package/dist/sync.js.map +1 -1
  35. package/dist/tests/PureJSCrypto.test.js +43 -0
  36. package/dist/tests/PureJSCrypto.test.js.map +1 -1
  37. package/dist/tests/WasmCrypto.test.js +55 -0
  38. package/dist/tests/WasmCrypto.test.js.map +1 -1
  39. package/dist/tests/account.test.js +40 -0
  40. package/dist/tests/account.test.js.map +1 -1
  41. package/dist/tests/coList.test.js +13 -0
  42. package/dist/tests/coList.test.js.map +1 -1
  43. package/dist/tests/coMap.test.js +14 -0
  44. package/dist/tests/coMap.test.js.map +1 -1
  45. package/dist/tests/coPlainText.test.js +13 -0
  46. package/dist/tests/coPlainText.test.js.map +1 -1
  47. package/dist/tests/coStream.test.js +25 -0
  48. package/dist/tests/coStream.test.js.map +1 -1
  49. package/dist/tests/coValueCore.test.js +28 -2
  50. package/dist/tests/coValueCore.test.js.map +1 -1
  51. package/dist/tests/coreWasm.test.js +1 -1
  52. package/dist/tests/coreWasm.test.js.map +1 -1
  53. package/dist/tests/sync.sharding.test.js +104 -1
  54. package/dist/tests/sync.sharding.test.js.map +1 -1
  55. package/dist/tests/sync.storage.test.js +31 -1
  56. package/dist/tests/sync.storage.test.js.map +1 -1
  57. package/dist/tests/sync.storageAsync.test.js +1 -1
  58. package/dist/tests/sync.storageAsync.test.js.map +1 -1
  59. package/dist/tests/sync.test.js +2 -2
  60. package/dist/tests/sync.test.js.map +1 -1
  61. package/dist/tests/sync.upload.test.js +26 -0
  62. package/dist/tests/sync.upload.test.js.map +1 -1
  63. package/dist/tests/testUtils.d.ts.map +1 -1
  64. package/dist/tests/testUtils.js +3 -1
  65. package/dist/tests/testUtils.js.map +1 -1
  66. package/package.json +2 -2
  67. package/src/coValueCore/SessionMap.ts +24 -1
  68. package/src/coValueCore/coValueCore.ts +46 -41
  69. package/src/coValueCore/utils.ts +0 -1
  70. package/src/coValueCore/verifiedState.ts +14 -3
  71. package/src/coValues/account.ts +27 -3
  72. package/src/crypto/PureJSCrypto.ts +49 -1
  73. package/src/crypto/WasmCrypto.ts +15 -1
  74. package/src/crypto/crypto.ts +7 -1
  75. package/src/sync.ts +53 -19
  76. package/src/tests/PureJSCrypto.test.ts +66 -0
  77. package/src/tests/WasmCrypto.test.ts +88 -0
  78. package/src/tests/account.test.ts +56 -0
  79. package/src/tests/coList.test.ts +19 -0
  80. package/src/tests/coMap.test.ts +21 -0
  81. package/src/tests/coPlainText.test.ts +18 -0
  82. package/src/tests/coStream.test.ts +36 -0
  83. package/src/tests/coValueCore.test.ts +49 -0
  84. package/src/tests/coreWasm.test.ts +1 -0
  85. package/src/tests/sync.sharding.test.ts +156 -6
  86. package/src/tests/sync.storage.test.ts +43 -1
  87. package/src/tests/sync.storageAsync.test.ts +1 -1
  88. package/src/tests/sync.test.ts +6 -2
  89. package/src/tests/sync.upload.test.ts +35 -0
  90. package/src/tests/testUtils.ts +3 -1
@@ -8,19 +8,16 @@ import {
8
8
  import {
9
9
  CryptoProvider,
10
10
  Encrypted,
11
- Hash,
12
11
  KeyID,
13
12
  KeySecret,
14
13
  Signature,
15
14
  SignerID,
16
- StreamingHash,
17
15
  } from "../crypto/crypto.js";
18
16
  import { RawCoID, SessionID, TransactionID } from "../ids.js";
19
17
  import { Stringified } from "../jsonStringify.js";
20
18
  import { JsonObject, JsonValue } from "../jsonValue.js";
21
19
  import { PermissionsDef as RulesetDef } from "../permissions.js";
22
20
  import { CoValueKnownState, NewContentMessage } from "../sync.js";
23
- import { InvalidHashError, InvalidSignatureError } from "./coValueCore.js";
24
21
  import { TryAddTransactionsError } from "./coValueCore.js";
25
22
  import { SessionLog, SessionMap } from "./SessionMap.js";
26
23
  import { ControlledAccountOrAgent } from "../coValues/account.js";
@@ -41,12 +38,14 @@ export type PrivateTransaction = {
41
38
  madeAt: number;
42
39
  keyUsed: KeyID;
43
40
  encryptedChanges: Encrypted<JsonValue[], { in: RawCoID; tx: TransactionID }>;
41
+ meta?: Encrypted<JsonObject, { in: RawCoID; tx: TransactionID }>;
44
42
  };
45
43
 
46
44
  export type TrustingTransaction = {
47
45
  privacy: "trusting";
48
46
  madeAt: number;
49
47
  changes: Stringified<JsonValue[]>;
48
+ meta?: Stringified<JsonObject>;
50
49
  };
51
50
 
52
51
  export type Transaction = PrivateTransaction | TrustingTransaction;
@@ -114,11 +113,13 @@ export class VerifiedState {
114
113
  sessionID: SessionID,
115
114
  signerAgent: ControlledAccountOrAgent,
116
115
  changes: JsonValue[],
116
+ meta: JsonObject | undefined,
117
117
  ) {
118
118
  const result = this.sessions.makeNewTrustingTransaction(
119
119
  sessionID,
120
120
  signerAgent,
121
121
  changes,
122
+ meta,
122
123
  );
123
124
 
124
125
  this._cachedNewContentSinceEmpty = undefined;
@@ -133,6 +134,7 @@ export class VerifiedState {
133
134
  changes: JsonValue[],
134
135
  keyID: KeyID,
135
136
  keySecret: KeySecret,
137
+ meta: JsonObject | undefined,
136
138
  ) {
137
139
  const result = this.sessions.makeNewPrivateTransaction(
138
140
  sessionID,
@@ -140,6 +142,7 @@ export class VerifiedState {
140
142
  changes,
141
143
  keyID,
142
144
  keySecret,
145
+ meta,
143
146
  );
144
147
 
145
148
  this._cachedNewContentSinceEmpty = undefined;
@@ -353,6 +356,14 @@ export class VerifiedState {
353
356
  ): JsonValue[] | undefined {
354
357
  return this.sessions.decryptTransaction(sessionID, txIndex, keySecret);
355
358
  }
359
+
360
+ decryptTransactionMeta(
361
+ sessionID: SessionID,
362
+ txIndex: number,
363
+ keySecret: KeySecret,
364
+ ): JsonObject | undefined {
365
+ return this.sessions.decryptTransactionMeta(sessionID, txIndex, keySecret);
366
+ }
356
367
  }
357
368
 
358
369
  function getNextKnownSignatureIdx(
@@ -19,9 +19,9 @@ import { AgentID } from "../ids.js";
19
19
  import { JsonObject } from "../jsonValue.js";
20
20
  import { LocalNode } from "../localNode.js";
21
21
  import { logger } from "../logger.js";
22
- import type { AccountRole } from "../permissions.js";
22
+ import type { AccountRole, Role } from "../permissions.js";
23
23
  import { RawCoMap } from "./coMap.js";
24
- import { InviteSecret, RawGroup } from "./group.js";
24
+ import { Everyone, InviteSecret, RawGroup } from "./group.js";
25
25
 
26
26
  export function accountHeaderForInitialAgentSecret(
27
27
  agentSecret: AgentSecret,
@@ -71,9 +71,33 @@ export class RawAccount<
71
71
  return agents[0]!;
72
72
  }
73
73
 
74
- createInvite(_: AccountRole): InviteSecret {
74
+ override createInvite(_: AccountRole): InviteSecret {
75
75
  throw new Error("Cannot create invite from an account");
76
76
  }
77
+
78
+ override addMember(
79
+ account: RawAccount | ControlledAccountOrAgent | Everyone,
80
+ role: Role,
81
+ ) {
82
+ throw new Error("Cannot add a member to an account");
83
+ }
84
+
85
+ override removeMember(
86
+ account: RawAccount | ControlledAccountOrAgent | Everyone,
87
+ ) {
88
+ throw new Error("Cannot remove a member from an account");
89
+ }
90
+
91
+ override extend(
92
+ parent: RawGroup,
93
+ role: "reader" | "writer" | "admin" | "inherit" = "inherit",
94
+ ) {
95
+ throw new Error("Cannot extend an account");
96
+ }
97
+
98
+ override revokeExtend(parent: RawGroup) {
99
+ throw new Error("Cannot unextend an account");
100
+ }
77
101
  }
78
102
 
79
103
  export interface ControlledAccountOrAgent {
@@ -10,7 +10,7 @@ import {
10
10
  } from "../coValueCore/verifiedState.js";
11
11
  import { RawCoID, SessionID, TransactionID } from "../ids.js";
12
12
  import { Stringified, stableStringify } from "../jsonStringify.js";
13
- import { JsonValue } from "../jsonValue.js";
13
+ import { JsonObject, JsonValue } from "../jsonValue.js";
14
14
  import { logger } from "../logger.js";
15
15
  import {
16
16
  CryptoProvider,
@@ -328,16 +328,26 @@ export class PureJSSessionLog implements SessionLogImpl {
328
328
  keyID: KeyID,
329
329
  keySecret: KeySecret,
330
330
  madeAt: number,
331
+ meta: JsonObject | undefined,
331
332
  ): { signature: Signature; transaction: PrivateTransaction } {
332
333
  const encryptedChanges = this.crypto.encrypt(changes, keySecret, {
333
334
  in: this.coID,
334
335
  tx: { sessionID: this.sessionID, txIndex: this.transactions.length },
335
336
  });
337
+
338
+ const encryptedMeta = meta
339
+ ? this.crypto.encrypt(meta, keySecret, {
340
+ in: this.coID,
341
+ tx: { sessionID: this.sessionID, txIndex: this.transactions.length },
342
+ })
343
+ : undefined;
344
+
336
345
  const tx = {
337
346
  encryptedChanges: encryptedChanges,
338
347
  madeAt: madeAt,
339
348
  privacy: "private",
340
349
  keyUsed: keyID,
350
+ meta: encryptedMeta,
341
351
  } satisfies Transaction;
342
352
  const signature = this.internalAddNewTransaction(
343
353
  stableStringify(tx),
@@ -353,11 +363,13 @@ export class PureJSSessionLog implements SessionLogImpl {
353
363
  signerAgent: ControlledAccountOrAgent,
354
364
  changes: JsonValue[],
355
365
  madeAt: number,
366
+ meta: JsonObject | undefined,
356
367
  ): { signature: Signature; transaction: TrustingTransaction } {
357
368
  const tx = {
358
369
  changes: stableStringify(changes),
359
370
  madeAt: madeAt,
360
371
  privacy: "trusting",
372
+ meta: meta ? stableStringify(meta) : undefined,
361
373
  } satisfies Transaction;
362
374
  const signature = this.internalAddNewTransaction(
363
375
  stableStringify(tx),
@@ -400,6 +412,42 @@ export class PureJSSessionLog implements SessionLogImpl {
400
412
  }
401
413
  }
402
414
 
415
+ decryptNextTransactionMetaJson(
416
+ txIndex: number,
417
+ keySecret: KeySecret,
418
+ ): string | undefined {
419
+ const txJson = this.transactions[txIndex];
420
+ if (!txJson) {
421
+ throw new Error("Transaction not found");
422
+ }
423
+ const tx = JSON.parse(txJson) as Transaction;
424
+
425
+ if (!tx.meta) {
426
+ return undefined;
427
+ }
428
+
429
+ if (tx.privacy === "private") {
430
+ const nOnceMaterial = {
431
+ in: this.coID,
432
+ tx: { sessionID: this.sessionID, txIndex: txIndex },
433
+ };
434
+
435
+ const nOnce = this.crypto.generateJsonNonce(nOnceMaterial);
436
+
437
+ const ciphertext = base64URLtoBytes(
438
+ tx.meta.substring("encrypted_U".length),
439
+ );
440
+ const keySecretBytes = base58.decode(
441
+ keySecret.substring("keySecret_z".length),
442
+ );
443
+ const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
444
+
445
+ return textDecoder.decode(plaintext);
446
+ } else {
447
+ return tx.meta;
448
+ }
449
+ }
450
+
403
451
  free(): void {
404
452
  // no-op
405
453
  }
@@ -19,7 +19,7 @@ import {
19
19
  import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
20
20
  import { RawCoID, SessionID, TransactionID } from "../ids.js";
21
21
  import { Stringified, stableStringify } from "../jsonStringify.js";
22
- import { JsonValue } from "../jsonValue.js";
22
+ import { JsonObject, JsonValue } from "../jsonValue.js";
23
23
  import { logger } from "../logger.js";
24
24
  import { PureJSCrypto } from "./PureJSCrypto.js";
25
25
  import {
@@ -231,6 +231,7 @@ class SessionLogAdapter {
231
231
  keyID: KeyID,
232
232
  keySecret: KeySecret,
233
233
  madeAt: number,
234
+ meta: JsonObject | undefined,
234
235
  ): { signature: Signature; transaction: PrivateTransaction } {
235
236
  const output = this.sessionLog.addNewPrivateTransaction(
236
237
  stableStringify(changes),
@@ -238,6 +239,7 @@ class SessionLogAdapter {
238
239
  keySecret,
239
240
  keyID,
240
241
  madeAt,
242
+ meta ? stableStringify(meta) : undefined,
241
243
  );
242
244
  const parsedOutput = JSON.parse(output);
243
245
  const transaction: PrivateTransaction = {
@@ -245,6 +247,7 @@ class SessionLogAdapter {
245
247
  madeAt,
246
248
  encryptedChanges: parsedOutput.encrypted_changes,
247
249
  keyUsed: keyID,
250
+ meta: parsedOutput.meta,
248
251
  };
249
252
  return { signature: parsedOutput.signature, transaction };
250
253
  }
@@ -253,17 +256,21 @@ class SessionLogAdapter {
253
256
  signerAgent: ControlledAccountOrAgent,
254
257
  changes: JsonValue[],
255
258
  madeAt: number,
259
+ meta: JsonObject | undefined,
256
260
  ): { signature: Signature; transaction: TrustingTransaction } {
257
261
  const stringifiedChanges = stableStringify(changes);
262
+ const stringifiedMeta = meta ? stableStringify(meta) : undefined;
258
263
  const output = this.sessionLog.addNewTrustingTransaction(
259
264
  stringifiedChanges,
260
265
  signerAgent.currentSignerSecret(),
261
266
  madeAt,
267
+ stringifiedMeta,
262
268
  );
263
269
  const transaction: TrustingTransaction = {
264
270
  privacy: "trusting",
265
271
  madeAt,
266
272
  changes: stringifiedChanges,
273
+ meta: stringifiedMeta,
267
274
  };
268
275
  return { signature: output as Signature, transaction };
269
276
  }
@@ -279,6 +286,13 @@ class SessionLogAdapter {
279
286
  return output;
280
287
  }
281
288
 
289
+ decryptNextTransactionMetaJson(
290
+ txIndex: number,
291
+ keySecret: KeySecret,
292
+ ): string | undefined {
293
+ return this.sessionLog.decryptNextTransactionMetaJson(txIndex, keySecret);
294
+ }
295
+
282
296
  free() {
283
297
  this.sessionLog.free();
284
298
  }
@@ -3,7 +3,7 @@ import { ControlledAccountOrAgent, RawAccountID } from "../coValues/account.js";
3
3
  import { AgentID, RawCoID, TransactionID } from "../ids.js";
4
4
  import { SessionID } from "../ids.js";
5
5
  import { Stringified, parseJSON, stableStringify } from "../jsonStringify.js";
6
- import { JsonValue } from "../jsonValue.js";
6
+ import { JsonObject, JsonValue } from "../jsonValue.js";
7
7
  import { logger } from "../logger.js";
8
8
  import {
9
9
  PrivateTransaction,
@@ -366,15 +366,21 @@ export interface SessionLogImpl {
366
366
  keyID: KeyID,
367
367
  keySecret: KeySecret,
368
368
  madeAt: number,
369
+ meta: JsonObject | undefined,
369
370
  ): { signature: Signature; transaction: PrivateTransaction };
370
371
  addNewTrustingTransaction(
371
372
  signerAgent: ControlledAccountOrAgent,
372
373
  changes: JsonValue[],
373
374
  madeAt: number,
375
+ meta: JsonObject | undefined,
374
376
  ): { signature: Signature; transaction: TrustingTransaction };
375
377
  decryptNextTransactionChangesJson(
376
378
  tx_index: number,
377
379
  key_secret: KeySecret,
378
380
  ): string;
379
381
  free(): void;
382
+ decryptNextTransactionMetaJson(
383
+ tx_index: number,
384
+ key_secret: KeySecret,
385
+ ): string | undefined;
380
386
  }
package/src/sync.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { md5 } from "@noble/hashes/legacy";
1
2
  import { Histogram, ValueType, metrics } from "@opentelemetry/api";
2
3
  import { PeerState } from "./PeerState.js";
3
4
  import { SyncStateManager } from "./SyncStateManager.js";
@@ -8,11 +9,7 @@ import {
8
9
  } from "./coValueContentMessage.js";
9
10
  import { CoValueCore } from "./coValueCore/coValueCore.js";
10
11
  import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
11
- import {
12
- CoValueHeader,
13
- Transaction,
14
- VerifiedState,
15
- } from "./coValueCore/verifiedState.js";
12
+ import { CoValueHeader, Transaction } from "./coValueCore/verifiedState.js";
16
13
  import { Signature } from "./crypto/crypto.js";
17
14
  import { RawCoID, SessionID, isRawCoID } from "./ids.js";
18
15
  import { LocalNode } from "./localNode.js";
@@ -170,21 +167,16 @@ export class SyncManager {
170
167
  this.skipVerify = true;
171
168
  }
172
169
 
173
- peersInPriorityOrder(): PeerState[] {
174
- return Object.values(this.peers).sort((a, b) => {
175
- const aPriority = a.priority || 0;
176
- const bPriority = b.priority || 0;
177
-
178
- return bPriority - aPriority;
179
- });
170
+ getPeers(id: RawCoID): PeerState[] {
171
+ return this.getServerPeers(id).concat(this.getClientPeers());
180
172
  }
181
173
 
182
- getPeers(): PeerState[] {
183
- return Object.values(this.peers);
174
+ getClientPeers(): PeerState[] {
175
+ return Object.values(this.peers).filter((peer) => peer.role === "client");
184
176
  }
185
177
 
186
178
  getServerPeers(id: RawCoID, excludePeerId?: PeerID): PeerState[] {
187
- const serverPeers = this.getPeers().filter(
179
+ const serverPeers = Object.values(this.peers).filter(
188
180
  (peer) => peer.role === "server" && peer.id !== excludePeerId,
189
181
  );
190
182
  return this.serverPeerSelector
@@ -760,7 +752,7 @@ export class SyncManager {
760
752
  this.storeContent(contentToStore);
761
753
  }
762
754
 
763
- for (const peer of this.peersInPriorityOrder()) {
755
+ for (const peer of this.getPeers(coValue.id)) {
764
756
  /**
765
757
  * We sync the content against the source peer if it is a client or server peers
766
758
  * to upload any content that is available on the current node and not on the source peer.
@@ -818,7 +810,7 @@ export class SyncManager {
818
810
 
819
811
  const contentKnownState = knownStateFromContent(content);
820
812
 
821
- for (const peer of this.peersInPriorityOrder()) {
813
+ for (const peer of this.getPeers(coValue.id)) {
822
814
  if (peer.closed) continue;
823
815
  if (coValue.isErroredInPeer(peer.id)) continue;
824
816
 
@@ -837,7 +829,7 @@ export class SyncManager {
837
829
  peer.trackToldKnownState(coValue.id);
838
830
  }
839
831
 
840
- for (const peer of this.getPeers()) {
832
+ for (const peer of this.getPeers(coValue.id)) {
841
833
  this.syncState.triggerUpdate(peer.id, coValue.id);
842
834
  }
843
835
  }
@@ -918,7 +910,7 @@ export class SyncManager {
918
910
  }
919
911
 
920
912
  waitForSync(id: RawCoID, timeout = 60_000) {
921
- const peers = this.getPeers();
913
+ const peers = this.getPeers(id);
922
914
 
923
915
  return Promise.all(
924
916
  peers
@@ -954,3 +946,45 @@ function knownStateIn(msg: LoadMessage | KnownStateMessage) {
954
946
  sessions: msg.sessions,
955
947
  };
956
948
  }
949
+
950
+ /**
951
+ * Returns a ServerPeerSelector that implements the Highest Weighted Random (HWR) algorithm.
952
+ *
953
+ * The HWR algorithm deterministically selects the top `n` peers for a given CoValue ID by assigning
954
+ * each peer a "weight" based on the MD5 hash of the concatenation of the CoValue ID and the peer's ID.
955
+ * The first 4 bytes of the hash are interpreted as a 32-bit unsigned integer, which serves as the peer's weight.
956
+ * Peers are then sorted in descending order of weight, and the top `n` are selected.
957
+ */
958
+ export function hwrServerPeerSelector(n: number): ServerPeerSelector {
959
+ if (n === 0) {
960
+ throw new Error("n must be greater than 0");
961
+ }
962
+
963
+ const enc = new TextEncoder();
964
+
965
+ // Take the md5 hash of the peer ID and CoValue ID and convert the first 4 bytes to a 32-bit unsigned integer
966
+ const getWeight = (id: RawCoID, peer: PeerState): number => {
967
+ const hash = md5(enc.encode(id + peer.id));
968
+ return (
969
+ ((hash[0]! << 24) | (hash[1]! << 16) | (hash[2]! << 8) | hash[3]!) >>> 0
970
+ );
971
+ };
972
+
973
+ return (id, serverPeers) => {
974
+ if (serverPeers.length <= n) {
975
+ return serverPeers;
976
+ }
977
+
978
+ const weightedPeers = serverPeers.map((peer) => {
979
+ return {
980
+ peer,
981
+ weight: getWeight(id, peer),
982
+ };
983
+ });
984
+
985
+ return weightedPeers
986
+ .sort((a, b) => b.weight - a.weight)
987
+ .slice(0, n)
988
+ .map((wp) => wp.peer);
989
+ };
990
+ }
@@ -126,6 +126,72 @@ describe("PureJSCrypto", () => {
126
126
 
127
127
  expect(map.get("count")).toEqual(0);
128
128
  });
129
+
130
+ it("can add a meta to a private transaction", async () => {
131
+ const client = setupTestNode({
132
+ connected: true,
133
+ });
134
+
135
+ const group = client.node.createGroup();
136
+ const map = group.createMap();
137
+
138
+ map.core.makeTransaction([], "private", {
139
+ meta: {
140
+ count: 1,
141
+ },
142
+ });
143
+
144
+ await map.core.waitForSync();
145
+
146
+ const session2 = client.spawnNewSession();
147
+
148
+ const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
149
+
150
+ const decryptedMeta =
151
+ mapInOtherSession.core.verified.decryptTransactionMeta(
152
+ client.node.currentSessionID,
153
+ 0,
154
+ map.core.getCurrentReadKey().secret!,
155
+ );
156
+
157
+ expect(decryptedMeta).toEqual({
158
+ meta: {
159
+ count: 1,
160
+ },
161
+ });
162
+ });
163
+
164
+ it("can add a meta to a trusting transaction", async () => {
165
+ const client = setupTestNode({
166
+ connected: true,
167
+ });
168
+
169
+ const group = client.node.createGroup();
170
+ const map = group.createMap();
171
+
172
+ map.core.makeTransaction([], "trusting", {
173
+ meta: {
174
+ count: 1,
175
+ },
176
+ });
177
+
178
+ await map.core.waitForSync();
179
+
180
+ const session2 = client.spawnNewSession();
181
+
182
+ const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
183
+
184
+ const transferredMeta = JSON.parse(
185
+ mapInOtherSession.core.verified.sessions.get(client.node.currentSessionID)
186
+ ?.transactions[0]?.meta!,
187
+ );
188
+
189
+ expect(transferredMeta).toEqual({
190
+ meta: {
191
+ count: 1,
192
+ },
193
+ });
194
+ });
129
195
  });
130
196
 
131
197
  describe("PureJSSessionLog", () => {
@@ -125,4 +125,92 @@ describe("WasmCrypto", () => {
125
125
 
126
126
  expect(map.get("count")).toEqual(0);
127
127
  });
128
+
129
+ it("can add a meta to a private transaction", async () => {
130
+ const client = setupTestNode({
131
+ connected: true,
132
+ });
133
+
134
+ const group = client.node.createGroup();
135
+ const map = group.createMap();
136
+
137
+ map.core.makeTransaction([], "private", {
138
+ meta: {
139
+ count: 1,
140
+ },
141
+ });
142
+
143
+ await map.core.waitForSync();
144
+
145
+ const session2 = client.spawnNewSession();
146
+
147
+ const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
148
+
149
+ const decryptedMeta =
150
+ mapInOtherSession.core.verified.decryptTransactionMeta(
151
+ client.node.currentSessionID,
152
+ 0,
153
+ mapInOtherSession.core.getCurrentReadKey().secret!,
154
+ );
155
+
156
+ expect(decryptedMeta).toEqual({
157
+ meta: {
158
+ count: 1,
159
+ },
160
+ });
161
+ });
162
+
163
+ it("can add a meta to a trusting transaction", async () => {
164
+ const client = setupTestNode({
165
+ connected: true,
166
+ });
167
+
168
+ const group = client.node.createGroup();
169
+ const map = group.createMap();
170
+
171
+ map.core.makeTransaction([], "trusting", {
172
+ meta: {
173
+ count: 1,
174
+ },
175
+ });
176
+
177
+ await map.core.waitForSync();
178
+
179
+ const session2 = client.spawnNewSession();
180
+
181
+ const mapInOtherSession = await loadCoValueOrFail(session2.node, map.id);
182
+
183
+ const transferredMeta = JSON.parse(
184
+ mapInOtherSession.core.verified.sessions.get(client.node.currentSessionID)
185
+ ?.transactions[0]?.meta!,
186
+ );
187
+
188
+ expect(transferredMeta).toEqual({
189
+ meta: {
190
+ count: 1,
191
+ },
192
+ });
193
+ });
194
+
195
+ it("fails to verify signatures without a signer ID", async () => {
196
+ const agentSecret = wasmCrypto.newRandomAgentSecret();
197
+ const sessionID = wasmCrypto.newRandomSessionID(
198
+ wasmCrypto.getAgentID(agentSecret),
199
+ );
200
+
201
+ const sessionLog = wasmCrypto.createSessionLog("co_z12345678", sessionID);
202
+ expect(() =>
203
+ sessionLog.tryAdd(
204
+ [
205
+ {
206
+ privacy: "trusting",
207
+ changes: stableStringify([{ op: "set", key: "count", value: 1 }]),
208
+ madeAt: Date.now(),
209
+ },
210
+ ],
211
+ "signature_z12345678",
212
+ false,
213
+ ),
214
+ ).toThrow(expect.stringContaining("Signature verification failed"));
215
+ });
128
216
  });
@@ -137,6 +137,62 @@ test("Should migrate the root from private to trusting", async () => {
137
137
  expect(account3.ops).toEqual(account2.ops); // No new transactions were made
138
138
  });
139
139
 
140
+ test("throws an error if the user tried to add a member to an account", async () => {
141
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
142
+ creationProps: { name: "Hermes Puggington" },
143
+ crypto: Crypto,
144
+ });
145
+
146
+ const account = await node.load(accountID);
147
+ if (account === "unavailable") throw new Error("Account unavailable");
148
+
149
+ expect(() => account.addMember("everyone", "admin")).toThrow(
150
+ "Cannot add a member to an account",
151
+ );
152
+ });
153
+
154
+ test("throws an error if the user tried to remove a member from an account", async () => {
155
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
156
+ creationProps: { name: "Hermes Puggington" },
157
+ crypto: Crypto,
158
+ });
159
+
160
+ const account = await node.load(accountID);
161
+ if (account === "unavailable") throw new Error("Account unavailable");
162
+
163
+ expect(() => account.removeMember("everyone")).toThrow(
164
+ "Cannot remove a member from an account",
165
+ );
166
+ });
167
+
168
+ test("throws an error if the user tried to extend an account", async () => {
169
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
170
+ creationProps: { name: "Hermes Puggington" },
171
+ crypto: Crypto,
172
+ });
173
+
174
+ const account = await node.load(accountID);
175
+ if (account === "unavailable") throw new Error("Account unavailable");
176
+
177
+ expect(() => account.extend(node.createGroup())).toThrow(
178
+ "Cannot extend an account",
179
+ );
180
+ });
181
+
182
+ test("throws an error if the user tried to revoke extend from an account", async () => {
183
+ const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
184
+ creationProps: { name: "Hermes Puggington" },
185
+ crypto: Crypto,
186
+ });
187
+
188
+ const account = await node.load(accountID);
189
+ if (account === "unavailable") throw new Error("Account unavailable");
190
+
191
+ expect(() => account.revokeExtend(node.createGroup())).toThrow(
192
+ "Cannot unextend an account",
193
+ );
194
+ });
195
+
140
196
  test("throws an error if the user tried to create an invite from an account", async () => {
141
197
  const { node, accountID } = await LocalNode.withNewlyCreatedAccount({
142
198
  creationProps: { name: "Hermes Puggington" },
@@ -409,3 +409,22 @@ test("totalValidTransactions should return the number of valid transactions proc
409
409
  listOnOtherClient.core.getCurrentContent().totalValidTransactions,
410
410
  ).toEqual(2);
411
411
  });
412
+
413
+ test("Should ignore unknown meta transactions", () => {
414
+ const node = nodeWithRandomAgentAndSessionID();
415
+
416
+ const coValue = node.createCoValue({
417
+ type: "colist",
418
+ ruleset: { type: "unsafeAllowAll" },
419
+ meta: null,
420
+ ...Crypto.createdNowUnique(),
421
+ });
422
+
423
+ coValue.makeTransaction([], "trusting", { unknownMeta: 1 });
424
+
425
+ const content = expectList(coValue.getCurrentContent());
426
+
427
+ content.append("first", 0, "trusting");
428
+
429
+ expect(content.toJSON()).toEqual(["first"]);
430
+ });