cojson 0.17.10 → 0.17.11

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 (59) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/dist/coValueCore/SessionMap.d.ts +3 -2
  4. package/dist/coValueCore/SessionMap.d.ts.map +1 -1
  5. package/dist/coValueCore/SessionMap.js.map +1 -1
  6. package/dist/coValueCore/coValueCore.d.ts +9 -4
  7. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  8. package/dist/coValueCore/coValueCore.js +16 -8
  9. package/dist/coValueCore/coValueCore.js.map +1 -1
  10. package/dist/coValueCore/verifiedState.d.ts +2 -2
  11. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  12. package/dist/coValueCore/verifiedState.js +1 -1
  13. package/dist/coValueCore/verifiedState.js.map +1 -1
  14. package/dist/coValues/group.d.ts.map +1 -1
  15. package/dist/coValues/group.js +6 -2
  16. package/dist/coValues/group.js.map +1 -1
  17. package/dist/crypto/PureJSCrypto.d.ts +2 -2
  18. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  19. package/dist/crypto/PureJSCrypto.js +3 -0
  20. package/dist/crypto/PureJSCrypto.js.map +1 -1
  21. package/dist/crypto/WasmCrypto.d.ts +1 -1
  22. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  23. package/dist/crypto/WasmCrypto.js.map +1 -1
  24. package/dist/crypto/crypto.d.ts +1 -1
  25. package/dist/crypto/crypto.d.ts.map +1 -1
  26. package/dist/permissions.d.ts +17 -1
  27. package/dist/permissions.d.ts.map +1 -1
  28. package/dist/permissions.js.map +1 -1
  29. package/dist/sync.d.ts.map +1 -1
  30. package/dist/sync.js +55 -49
  31. package/dist/sync.js.map +1 -1
  32. package/dist/tests/PureJSCrypto.test.js +15 -1
  33. package/dist/tests/PureJSCrypto.test.js.map +1 -1
  34. package/dist/tests/WasmCrypto.test.js +1 -1
  35. package/dist/tests/WasmCrypto.test.js.map +1 -1
  36. package/dist/tests/coValueCore.test.js +2 -2
  37. package/dist/tests/coValueCore.test.js.map +1 -1
  38. package/dist/tests/group.addMember.test.js +6 -11
  39. package/dist/tests/group.addMember.test.js.map +1 -1
  40. package/dist/tests/sync.load.test.js +40 -1
  41. package/dist/tests/sync.load.test.js.map +1 -1
  42. package/dist/tests/sync.test.js +1 -1
  43. package/dist/tests/sync.test.js.map +1 -1
  44. package/package.json +2 -2
  45. package/src/coValueCore/SessionMap.ts +4 -5
  46. package/src/coValueCore/coValueCore.ts +42 -32
  47. package/src/coValueCore/verifiedState.ts +1 -3
  48. package/src/coValues/group.ts +10 -2
  49. package/src/crypto/PureJSCrypto.ts +6 -2
  50. package/src/crypto/WasmCrypto.ts +1 -1
  51. package/src/crypto/crypto.ts +1 -1
  52. package/src/permissions.ts +17 -1
  53. package/src/sync.ts +63 -59
  54. package/src/tests/PureJSCrypto.test.ts +25 -2
  55. package/src/tests/WasmCrypto.test.ts +0 -2
  56. package/src/tests/coValueCore.test.ts +0 -4
  57. package/src/tests/group.addMember.test.ts +69 -63
  58. package/src/tests/sync.load.test.ts +52 -0
  59. package/src/tests/sync.test.ts +0 -2
@@ -1,5 +1,5 @@
1
1
  import { UpDownCounter, ValueType, metrics } from "@opentelemetry/api";
2
- import { Result, err } from "neverthrow";
2
+ import { Result, err, ok } from "neverthrow";
3
3
  import type { PeerState } from "../PeerState.js";
4
4
  import type { RawCoValue } from "../coValue.js";
5
5
  import type { ControlledAccountOrAgent } from "../coValues/account.js";
@@ -431,43 +431,46 @@ export class CoValueCore {
431
431
  tryAddTransactions(
432
432
  sessionID: SessionID,
433
433
  newTransactions: Transaction[],
434
- givenExpectedNewHash: Hash | undefined,
435
434
  newSignature: Signature,
436
- notifyMode: "immediate" | "deferred",
437
435
  skipVerify: boolean = false,
438
- givenNewStreamingHash?: StreamingHash,
439
436
  ): Result<true, TryAddTransactionsError> {
440
- return this.node
441
- .resolveAccountAgent(
442
- accountOrAgentIDfromSessionID(sessionID),
443
- "Expected to know signer of transaction",
444
- )
445
- .andThen((agent) => {
446
- if (!this.verified) {
447
- return err({
448
- type: "TriedToAddTransactionsWithoutVerifiedState",
449
- id: this.id,
450
- } satisfies TriedToAddTransactionsWithoutVerifiedStateErrpr);
451
- }
437
+ let result: Result<SignerID | undefined, TryAddTransactionsError>;
452
438
 
453
- const signerID = this.crypto.getAgentSignerID(agent);
439
+ if (skipVerify) {
440
+ result = ok(undefined);
441
+ } else {
442
+ result = this.node
443
+ .resolveAccountAgent(
444
+ accountOrAgentIDfromSessionID(sessionID),
445
+ "Expected to know signer of transaction",
446
+ )
447
+ .andThen((agent) => {
448
+ return ok(this.crypto.getAgentSignerID(agent));
449
+ });
450
+ }
454
451
 
455
- const result = this.verified.tryAddTransactions(
456
- sessionID,
457
- signerID,
458
- newTransactions,
459
- givenExpectedNewHash,
460
- newSignature,
461
- skipVerify,
462
- givenNewStreamingHash,
463
- );
452
+ return result.andThen((signerID) => {
453
+ if (!this.verified) {
454
+ return err({
455
+ type: "TriedToAddTransactionsWithoutVerifiedState",
456
+ id: this.id,
457
+ } satisfies TriedToAddTransactionsWithoutVerifiedStateErrpr);
458
+ }
464
459
 
465
- if (result.isOk()) {
466
- this.updateContentAndNotifyUpdate(notifyMode);
467
- }
460
+ const result = this.verified.tryAddTransactions(
461
+ sessionID,
462
+ signerID,
463
+ newTransactions,
464
+ newSignature,
465
+ skipVerify,
466
+ );
468
467
 
469
- return result;
470
- });
468
+ if (result.isOk()) {
469
+ this.updateContentAndNotifyUpdate("immediate");
470
+ }
471
+
472
+ return result;
473
+ });
471
474
  }
472
475
 
473
476
  deferredUpdates = 0;
@@ -973,7 +976,7 @@ export type InvalidSignatureError = {
973
976
  id: RawCoID;
974
977
  newSignature: Signature;
975
978
  sessionID: SessionID;
976
- signerID: SignerID;
979
+ signerID: SignerID | undefined;
977
980
  };
978
981
 
979
982
  export type TriedToAddTransactionsWithoutVerifiedStateErrpr = {
@@ -981,8 +984,15 @@ export type TriedToAddTransactionsWithoutVerifiedStateErrpr = {
981
984
  id: RawCoID;
982
985
  };
983
986
 
987
+ export type TriedToAddTransactionsWithoutSignerIDError = {
988
+ type: "TriedToAddTransactionsWithoutSignerID";
989
+ id: RawCoID;
990
+ sessionID: SessionID;
991
+ };
992
+
984
993
  export type TryAddTransactionsError =
985
994
  | TriedToAddTransactionsWithoutVerifiedStateErrpr
995
+ | TriedToAddTransactionsWithoutSignerIDError
986
996
  | ResolveAccountAgentError
987
997
  | InvalidHashError
988
998
  | InvalidSignatureError;
@@ -89,12 +89,10 @@ export class VerifiedState {
89
89
 
90
90
  tryAddTransactions(
91
91
  sessionID: SessionID,
92
- signerID: SignerID,
92
+ signerID: SignerID | undefined,
93
93
  newTransactions: Transaction[],
94
- givenExpectedNewHash: Hash | undefined,
95
94
  newSignature: Signature,
96
95
  skipVerify: boolean = false,
97
- givenNewStreamingHash?: StreamingHash,
98
96
  ): Result<true, TryAddTransactionsError> {
99
97
  const result = this.sessions.addTransaction(
100
98
  sessionID,
@@ -370,16 +370,24 @@ export class RawGroup<
370
370
  if (role === "writeOnly" || role === "writeOnlyInvite") {
371
371
  const previousRole = this.get(memberKey);
372
372
 
373
- this.set(memberKey, role, "trusting");
373
+ if (
374
+ previousRole === "admin" &&
375
+ memberKey !== this.core.node.getCurrentAgent().id
376
+ ) {
377
+ throw new Error(
378
+ "Administrators cannot demote other administrators in a group",
379
+ );
380
+ }
374
381
 
375
382
  if (
376
383
  previousRole === "reader" ||
377
384
  previousRole === "writer" ||
378
385
  previousRole === "admin"
379
386
  ) {
380
- this.rotateReadKey();
387
+ this.rotateReadKey(memberKey);
381
388
  }
382
389
 
390
+ this.set(memberKey, role, "trusting");
383
391
  this.internalCreateWriteOnlyKeyForMember(memberKey, agent);
384
392
  } else {
385
393
  const currentReadKey = this.getCurrentReadKey();
@@ -212,7 +212,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
212
212
  createSessionLog(
213
213
  coID: RawCoID,
214
214
  sessionID: SessionID,
215
- signerID: SignerID,
215
+ signerID?: SignerID,
216
216
  ): SessionLogImpl {
217
217
  return new PureJSSessionLog(coID, sessionID, signerID, this);
218
218
  }
@@ -226,7 +226,7 @@ export class PureJSSessionLog implements SessionLogImpl {
226
226
  constructor(
227
227
  private readonly coID: RawCoID,
228
228
  private readonly sessionID: SessionID,
229
- private readonly signerID: SignerID,
229
+ private readonly signerID: SignerID | undefined,
230
230
  private readonly crypto: PureJSCrypto,
231
231
  ) {
232
232
  this.streamingHash = this.crypto.emptyBlake3State();
@@ -263,6 +263,10 @@ export class PureJSSessionLog implements SessionLogImpl {
263
263
  skipVerify: boolean,
264
264
  ) {
265
265
  if (!skipVerify) {
266
+ if (!this.signerID) {
267
+ throw new Error("Tried to add transactions without signer ID");
268
+ }
269
+
266
270
  const checkHasher = this.crypto.cloneBlake3State(this.streamingHash);
267
271
 
268
272
  for (const tx of transactions) {
@@ -205,7 +205,7 @@ export class WasmCrypto extends CryptoProvider<Blake3State> {
205
205
  }
206
206
  }
207
207
 
208
- createSessionLog(coID: RawCoID, sessionID: SessionID, signerID: SignerID) {
208
+ createSessionLog(coID: RawCoID, sessionID: SessionID, signerID?: SignerID) {
209
209
  return new SessionLogAdapter(new SessionLog(coID, sessionID, signerID));
210
210
  }
211
211
  }
@@ -306,7 +306,7 @@ export abstract class CryptoProvider<Blake3State = any> {
306
306
  abstract createSessionLog(
307
307
  coID: RawCoID,
308
308
  sessionID: SessionID,
309
- signerID: SignerID,
309
+ signerID?: SignerID,
310
310
  ): SessionLogImpl;
311
311
  }
312
312
 
@@ -31,7 +31,23 @@ export type PermissionsDef =
31
31
  | { type: "ownedByGroup"; group: RawCoID }
32
32
  | { type: "unsafeAllowAll" };
33
33
 
34
- export type AccountRole = "reader" | "writer" | "admin" | "writeOnly";
34
+ export type AccountRole =
35
+ /**
36
+ * Can read the group's CoValues
37
+ */
38
+ | "reader"
39
+ /**
40
+ * Can read and write to the group's CoValues
41
+ */
42
+ | "writer"
43
+ /**
44
+ * Can read and write to the group, and change group member roles
45
+ */
46
+ | "admin"
47
+ /**
48
+ * Can only write to the group's CoValues and read their own changes
49
+ */
50
+ | "writeOnly";
35
51
 
36
52
  export type Role =
37
53
  | AccountRole
package/src/sync.ts CHANGED
@@ -511,28 +511,31 @@ export class SyncManager {
511
511
  (content) => content.newTransactions,
512
512
  );
513
513
 
514
- for (const dependency of getDependedOnCoValuesFromRawData(
515
- msg.id,
516
- msg.header,
517
- sessionIDs,
518
- transactions,
519
- )) {
520
- const dependencyCoValue = this.local.getCoValue(dependency);
514
+ // If we'll be performing transaction verification, ensure all the dependencies available.
515
+ if (!this.skipVerify) {
516
+ for (const dependency of getDependedOnCoValuesFromRawData(
517
+ msg.id,
518
+ msg.header,
519
+ sessionIDs,
520
+ transactions,
521
+ )) {
522
+ const dependencyCoValue = this.local.getCoValue(dependency);
523
+
524
+ if (!dependencyCoValue.hasVerifiedContent()) {
525
+ coValue.markMissingDependency(dependency);
521
526
 
522
- if (!dependencyCoValue.hasVerifiedContent()) {
523
- coValue.markMissingDependency(dependency);
527
+ const peers = this.getServerPeers();
524
528
 
525
- const peers = this.getServerPeers();
529
+ // if the peer that sent the content is a client, we add it to the list of peers
530
+ // to also ask them for the dependency
531
+ if (peer?.role === "client") {
532
+ peers.push(peer);
533
+ }
526
534
 
527
- // if the peer that sent the content is a client, we add it to the list of peers
528
- // to also ask them for the dependency
529
- if (peer?.role === "client") {
530
- peers.push(peer);
535
+ dependencyCoValue.load(peers);
536
+ } else if (!dependencyCoValue.isAvailable()) {
537
+ coValue.markMissingDependency(dependency);
531
538
  }
532
-
533
- dependencyCoValue.load(peers);
534
- } else if (!dependencyCoValue.isAvailable()) {
535
- coValue.markMissingDependency(dependency);
536
539
  }
537
540
  }
538
541
 
@@ -592,60 +595,61 @@ export class SyncManager {
592
595
  continue;
593
596
  }
594
597
 
595
- const accountId = accountOrAgentIDfromSessionID(sessionID);
598
+ // If we'll be performing transaction verification, ensure the account is available.
599
+ if (!this.skipVerify) {
600
+ const accountId = accountOrAgentIDfromSessionID(sessionID);
596
601
 
597
- if (isAccountID(accountId)) {
598
- const account = this.local.getCoValue(accountId);
602
+ if (isAccountID(accountId)) {
603
+ const account = this.local.getCoValue(accountId);
599
604
 
600
- // We can't verify the transaction without the account, so we delay the session content handling until the account is available
601
- if (!account.isAvailable()) {
602
- // This covers the case where we are getting a new session on an already loaded coValue
603
- // where we need to load the account to get their public key
604
- if (!coValue.missingDependencies.has(accountId)) {
605
- const peers = this.getServerPeers();
605
+ // We can't verify the transaction without the account, so we delay the session content handling until the account is available
606
+ if (!account.isAvailable()) {
607
+ // This covers the case where we are getting a new session on an already loaded coValue
608
+ // where we need to load the account to get their public key
609
+ if (!coValue.missingDependencies.has(accountId)) {
610
+ const peers = this.getServerPeers();
606
611
 
607
- if (peer?.role === "client") {
608
- // if the peer that sent the content is a client, we add it to the list of peers
609
- // to also ask them for the dependency
610
- peers.push(peer);
611
- }
612
+ if (peer?.role === "client") {
613
+ // if the peer that sent the content is a client, we add it to the list of peers
614
+ // to also ask them for the dependency
615
+ peers.push(peer);
616
+ }
612
617
 
613
- account.load(peers);
614
- }
618
+ account.load(peers);
619
+ }
615
620
 
616
- // We need to wait for the account to be available before we can verify the transaction
617
- // Currently doing this by delaying the handleNewContent for the session to when we have the account
618
- //
619
- // This is not the best solution, because the knownState is not updated and the ACK response will be given
620
- // by excluding the session.
621
- // This is good enough implementation for now because the only case for the account to be missing are out-of-order
622
- // dependencies push, so the gap should be short lived.
623
- //
624
- // When we are going to have sharded-peers we should revisit this, and store unverified sessions that are considered as part of the
625
- // knwonState, but not actively used until they can be verified.
626
- void account.waitForAvailable().then(() => {
627
- this.handleNewContent(
628
- {
629
- action: "content",
630
- id: coValue.id,
631
- new: {
632
- [sessionID]: newContentForSession,
621
+ // We need to wait for the account to be available before we can verify the transaction
622
+ // Currently doing this by delaying the handleNewContent for the session to when we have the account
623
+ //
624
+ // This is not the best solution, because the knownState is not updated and the ACK response will be given
625
+ // by excluding the session.
626
+ // This is good enough implementation for now because the only case for the account to be missing are out-of-order
627
+ // dependencies push, so the gap should be short lived.
628
+ //
629
+ // When we are going to have sharded-peers we should revisit this, and store unverified sessions that are considered as part of the
630
+ // knwonState, but not actively used until they can be verified.
631
+ void account.waitForAvailable().then(() => {
632
+ this.handleNewContent(
633
+ {
634
+ action: "content",
635
+ id: coValue.id,
636
+ new: {
637
+ [sessionID]: newContentForSession,
638
+ },
639
+ priority: msg.priority,
633
640
  },
634
- priority: msg.priority,
635
- },
636
- from,
637
- );
638
- });
639
- continue;
641
+ from,
642
+ );
643
+ });
644
+ continue;
645
+ }
640
646
  }
641
647
  }
642
648
 
643
649
  const result = coValue.tryAddTransactions(
644
650
  sessionID,
645
651
  newTransactions,
646
- undefined,
647
652
  newContentForSession.lastSignature,
648
- "immediate",
649
653
  this.skipVerify,
650
654
  );
651
655
 
@@ -4,6 +4,7 @@ import {
4
4
  setCurrentTestCryptoProvider,
5
5
  setupTestNode,
6
6
  setupTestAccount,
7
+ randomAgentAndSessionID,
7
8
  } from "./testUtils";
8
9
  import { PureJSCrypto } from "../crypto/PureJSCrypto";
9
10
  import { stableStringify } from "../jsonStringify";
@@ -113,9 +114,7 @@ describe("PureJSCrypto", () => {
113
114
  madeAt: Date.now(),
114
115
  },
115
116
  ],
116
- "hash_z12345678",
117
117
  "signature_z12345678",
118
- "immediate",
119
118
  true,
120
119
  );
121
120
 
@@ -128,3 +127,27 @@ describe("PureJSCrypto", () => {
128
127
  expect(map.get("count")).toEqual(0);
129
128
  });
130
129
  });
130
+
131
+ describe("PureJSSessionLog", () => {
132
+ it("fails to verify signatures without a signer ID", async () => {
133
+ const agentSecret = jsCrypto.newRandomAgentSecret();
134
+ const sessionID = jsCrypto.newRandomSessionID(
135
+ jsCrypto.getAgentID(agentSecret),
136
+ );
137
+
138
+ const sessionLog = jsCrypto.createSessionLog("co_z12345678", sessionID);
139
+ expect(() =>
140
+ sessionLog.tryAdd(
141
+ [
142
+ {
143
+ privacy: "trusting",
144
+ changes: stableStringify([{ op: "set", key: "count", value: 1 }]),
145
+ madeAt: Date.now(),
146
+ },
147
+ ],
148
+ "signature_z12345678",
149
+ false,
150
+ ),
151
+ ).toThrow("Tried to add transactions without signer ID");
152
+ });
153
+ });
@@ -113,9 +113,7 @@ describe("WasmCrypto", () => {
113
113
  madeAt: Date.now(),
114
114
  },
115
115
  ],
116
- "hash_z12345678",
117
116
  "signature_z12345678",
118
- "immediate",
119
117
  true,
120
118
  );
121
119
 
@@ -76,9 +76,7 @@ test("transactions with wrong signature are rejected", () => {
76
76
  const result = newEntry.tryAddTransactions(
77
77
  node.currentSessionID,
78
78
  [transaction],
79
- undefined,
80
79
  signature,
81
- "immediate",
82
80
  );
83
81
 
84
82
  expect(result.isErr()).toBe(true);
@@ -301,9 +299,7 @@ test("getValidTransactions should skip private transactions with invalid JSON",
301
299
  .tryAddTransactions(
302
300
  fixtures.session,
303
301
  [fixtures.transaction],
304
- undefined,
305
302
  fixtures.signature,
306
- "immediate",
307
303
  )
308
304
  ._unsafeUnwrap();
309
305
 
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, test } from "vitest";
2
2
  import { expectMap } from "../coValue.js";
3
3
  import {
4
4
  SyncMessagesLog,
5
- TEST_NODE_CONFIG,
6
5
  loadCoValueOrFail,
7
6
  setupTestAccount,
8
7
  setupTestNode,
@@ -220,68 +219,75 @@ describe("Group.addMember", () => {
220
219
  expect(personOnReaderNode.get("name")).toEqual(undefined);
221
220
  });
222
221
 
223
- test("an admin should not be able downgrade an admin", async () => {
224
- const admin = await setupTestAccount({
225
- connected: true,
226
- });
227
-
228
- const otherAdmin = await setupTestAccount({
229
- connected: true,
230
- });
231
-
232
- const group = admin.node.createGroup();
233
- const person = group.createMap({
234
- name: "John Doe",
235
- });
236
-
237
- const otherAdminOnAdminNode = await loadCoValueOrFail(
238
- admin.node,
239
- otherAdmin.accountID,
240
- );
241
- group.addMember(otherAdminOnAdminNode, "admin");
242
-
243
- // Try to downgrade other admin
244
- try {
245
- group.addMember(otherAdminOnAdminNode, "writer");
246
- } catch (e) {
247
- expect(e).toBeDefined();
248
- }
249
-
250
- expect(group.roleOf(otherAdmin.accountID)).toEqual("admin");
251
-
252
- // Verify other admin still has admin access by adding a new member
253
- const reader = await setupTestAccount({
254
- connected: true,
255
- });
256
-
257
- const readerOnOtherAdminNode = await loadCoValueOrFail(
258
- otherAdmin.node,
259
- reader.accountID,
260
- );
261
- group.addMember(readerOnOtherAdminNode, "reader");
262
-
263
- const personOnReaderNode = await loadCoValueOrFail(reader.node, person.id);
264
-
265
- await waitFor(() => {
266
- expect(
267
- expectMap(personOnReaderNode.core.getCurrentContent()).get("name"),
268
- ).toEqual("John Doe");
269
- });
270
- });
271
-
272
- test("an admin should be able downgrade themselves", async () => {
273
- const admin = await setupTestAccount({
274
- connected: true,
275
- });
276
-
277
- const group = admin.node.createGroup();
278
-
279
- const account = await loadCoValueOrFail(admin.node, admin.accountID);
280
-
281
- // Downgrade self to writer
282
- group.addMember(account, "writer");
283
- expect(group.roleOf(admin.accountID)).toEqual("writer");
284
- });
222
+ test.each(["writer", "reader", "writeOnly"] as const)(
223
+ "an admin should not be able to downgrade an admin to %s",
224
+ async (targetRole) => {
225
+ const admin = await setupTestAccount({
226
+ connected: true,
227
+ });
228
+
229
+ const otherAdmin = await setupTestAccount({
230
+ connected: true,
231
+ });
232
+
233
+ const group = admin.node.createGroup();
234
+ const person = group.createMap({
235
+ name: "John Doe",
236
+ });
237
+
238
+ const otherAdminOnAdminNode = await loadCoValueOrFail(
239
+ admin.node,
240
+ otherAdmin.accountID,
241
+ );
242
+ group.addMember(otherAdminOnAdminNode, "admin");
243
+
244
+ // Try to downgrade other admin
245
+ expect(() => group.addMember(otherAdminOnAdminNode, targetRole)).toThrow(
246
+ "Administrators cannot demote other administrators in a group",
247
+ );
248
+
249
+ expect(group.roleOf(otherAdmin.accountID)).toEqual("admin");
250
+
251
+ // Verify other admin still has admin access by adding a new member
252
+ const reader = await setupTestAccount({
253
+ connected: true,
254
+ });
255
+
256
+ const readerOnOtherAdminNode = await loadCoValueOrFail(
257
+ otherAdmin.node,
258
+ reader.accountID,
259
+ );
260
+ group.addMember(readerOnOtherAdminNode, "reader");
261
+
262
+ const personOnReaderNode = await loadCoValueOrFail(
263
+ reader.node,
264
+ person.id,
265
+ );
266
+
267
+ await waitFor(() => {
268
+ expect(
269
+ expectMap(personOnReaderNode.core.getCurrentContent()).get("name"),
270
+ ).toEqual("John Doe");
271
+ });
272
+ },
273
+ );
274
+
275
+ test.each(["writer", "reader", "writeOnly"] as const)(
276
+ "an admin should be able downgrade themselves to %s",
277
+ async (targetRole) => {
278
+ const admin = await setupTestAccount({
279
+ connected: true,
280
+ });
281
+
282
+ const group = admin.node.createGroup();
283
+
284
+ const account = await loadCoValueOrFail(admin.node, admin.accountID);
285
+
286
+ // Downgrade self to target role
287
+ group.addMember(account, targetRole);
288
+ expect(group.roleOf(admin.accountID)).toEqual(targetRole);
289
+ },
290
+ );
285
291
 
286
292
  test("an admin should be able downgrade a writeOnly to reader", async () => {
287
293
  const admin = await setupTestAccount({
@@ -9,6 +9,7 @@ import {
9
9
  SyncMessagesLog,
10
10
  TEST_NODE_CONFIG,
11
11
  blockMessageTypeOnOutgoingPeer,
12
+ getSyncServerConnectedPeer,
12
13
  loadCoValueOrFail,
13
14
  setupTestAccount,
14
15
  setupTestNode,
@@ -1034,4 +1035,55 @@ describe("loading coValues from server", () => {
1034
1035
 
1035
1036
  vi.useRealTimers();
1036
1037
  });
1038
+
1039
+ test("should not request dependencies if transaction verification is disabled", async () => {
1040
+ // Create a disconnected client
1041
+ const { node: client, accountID } = await setupTestAccount({
1042
+ connected: false,
1043
+ });
1044
+ const account = client.expectCurrentAccount(accountID);
1045
+
1046
+ // Prepare a group -- this will be a non-account dependency of a forthcoming map.
1047
+ const group = client.createGroup();
1048
+ group.addMember("everyone", "writer");
1049
+
1050
+ // Create a sync server and disable transaction verification
1051
+ const syncServer = await setupTestAccount({ isSyncServer: true });
1052
+ syncServer.node.syncManager.disableTransactionVerification();
1053
+
1054
+ // Connect the client, but don't setup syncing just yet...
1055
+ const { peer } = getSyncServerConnectedPeer({
1056
+ peerId: client.getCurrentAgent().id,
1057
+ syncServer: syncServer.node,
1058
+ });
1059
+
1060
+ // Disable reconciliation while we setup syncing because we don't want the
1061
+ // server to know about our forthcoming map's dependencies (group + account).
1062
+ const blocker = blockMessageTypeOnOutgoingPeer(peer, "load", {});
1063
+ client.syncManager.addPeer(peer);
1064
+ blocker.unblock();
1065
+
1066
+ // Create a map and set a value on it.
1067
+ // If transaction verification were enabled, this would trigger LOAD messages
1068
+ // from the server to the client asking for the group and account. However, we
1069
+ // don't expect to see those messages since we disabled transaction verification.
1070
+ const map = group.createMap();
1071
+ map.set("hello", "world");
1072
+ await map.core.waitForSync();
1073
+
1074
+ const syncMessages = SyncMessagesLog.getMessages({
1075
+ Account: account.core,
1076
+ Group: group.core,
1077
+ Map: map.core,
1078
+ });
1079
+ expect(
1080
+ syncMessages.some(
1081
+ (msg) => msg.includes("LOAD Account") || msg.includes("LOAD Group"),
1082
+ ),
1083
+ ).toBe(false);
1084
+
1085
+ // Verify the map is available on the server (transaction was accepted)
1086
+ const mapOnServerCore = await syncServer.node.loadCoValueCore(map.core.id);
1087
+ expect(mapOnServerCore.isAvailable()).toBe(true);
1088
+ });
1037
1089
  });