cojson 0.19.7 → 0.19.10

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.
@@ -379,11 +379,6 @@ export class CoValueCore {
379
379
 
380
380
  this.counter.add(-1, { state: this.loadingState });
381
381
 
382
- if (this.groupInvalidationSubscription) {
383
- this.groupInvalidationSubscription();
384
- this.groupInvalidationSubscription = undefined;
385
- }
386
-
387
382
  this.node.internalDeleteCoValue(this.id);
388
383
 
389
384
  return true;
@@ -517,38 +512,6 @@ export class CoValueCore {
517
512
  this.scheduleNotifyUpdate();
518
513
  }
519
514
 
520
- groupInvalidationSubscription?: () => void;
521
-
522
- subscribeToGroupInvalidation() {
523
- if (!this.verified) {
524
- return;
525
- }
526
-
527
- if (this.groupInvalidationSubscription) {
528
- return;
529
- }
530
-
531
- const header = this.verified.header;
532
-
533
- if (header.ruleset.type == "ownedByGroup") {
534
- const groupId = header.ruleset.group;
535
- const entry = this.node.getCoValue(groupId);
536
-
537
- if (entry.isAvailable()) {
538
- this.groupInvalidationSubscription = entry.subscribe((_groupUpdate) => {
539
- // When the group is updated, we need to reset the cached content because the transactions validity might have changed
540
- this.resetParsedTransactions();
541
- this.scheduleNotifyUpdate();
542
- }, false);
543
- } else {
544
- logger.error("CoValueCore: Owner group not available", {
545
- id: this.id,
546
- groupId,
547
- });
548
- }
549
- }
550
- }
551
-
552
515
  contentInClonedNodeWithDifferentAccount(account: ControlledAccountOrAgent) {
553
516
  return this.node
554
517
  .loadCoValueAsDifferentAgent(this.id, account.agentSecret, account.id)
@@ -656,11 +619,34 @@ export class CoValueCore {
656
619
 
657
620
  this.processNewTransactions();
658
621
  this.scheduleNotifyUpdate();
622
+ this.invalidateDependants();
659
623
  } catch (e) {
660
624
  return { type: "InvalidSignature", id: this.id, error: e } as const;
661
625
  }
662
626
  }
663
627
 
628
+ notifyDependants() {
629
+ if (!this.isGroup()) {
630
+ return;
631
+ }
632
+
633
+ for (const dependency of this.dependant) {
634
+ this.node.getCoValue(dependency).scheduleNotifyUpdate();
635
+ this.node.getCoValue(dependency).notifyDependants();
636
+ }
637
+ }
638
+
639
+ invalidateDependants() {
640
+ if (!this.isGroup()) {
641
+ return;
642
+ }
643
+
644
+ for (const dependency of this.dependant) {
645
+ this.node.getCoValue(dependency).resetParsedTransactions();
646
+ this.node.getCoValue(dependency).invalidateDependants();
647
+ }
648
+ }
649
+
664
650
  private processNewTransactions() {
665
651
  if (this._cachedContent) {
666
652
  this._cachedContent.processNewTransactions();
@@ -807,6 +793,19 @@ export class CoValueCore {
807
793
  this.notifyUpdate();
808
794
  this.node.syncManager.syncLocalTransaction(this.verified, knownStateBefore);
809
795
 
796
+ if (madeAt === undefined) {
797
+ // We don't revalidate the dependants transactions because we assume that transactions that you are
798
+ // creating "now" on groups don't affect the validity of transactions you already have in memory.
799
+ // For validity I mean:
800
+ // - ability to decrypt a transaction
801
+ // - that the account that made the transaction had enough rights to do so
802
+ this.notifyDependants();
803
+ } else {
804
+ // If the transaction is not made "now", we need to revalidate the dependants transactions
805
+ // because the new transaction might affect the validity of the dependants transactions
806
+ this.invalidateDependants();
807
+ }
808
+
810
809
  return true;
811
810
  }
812
811
 
@@ -833,8 +832,6 @@ export class CoValueCore {
833
832
 
834
833
  const newContent = coreToCoValue(this as AvailableCoValueCore, options);
835
834
 
836
- this.subscribeToGroupInvalidation();
837
-
838
835
  if (!options?.ignorePrivateTransactions) {
839
836
  this._cachedContent = newContent;
840
837
  }
@@ -853,19 +850,41 @@ export class CoValueCore {
853
850
 
854
851
  // Reset the parsed transactions and branches, to validate them again from scratch when the group is updated
855
852
  resetParsedTransactions() {
853
+ const verifiedTransactions = this.verifiedTransactions;
854
+
855
+ if (verifiedTransactions.length === 0) {
856
+ return;
857
+ }
858
+
856
859
  this.branchStart = undefined;
857
860
  this.mergeCommits = [];
858
861
 
859
- for (const transaction of this.verifiedTransactions) {
862
+ // Store the validity of the transactions before resetting the parsed transactions
863
+ const validityBeforeReset = new Array<boolean>(verifiedTransactions.length);
864
+ this.verifiedTransactions.forEach((transaction, index) => {
860
865
  transaction.isValidated = false;
861
- }
866
+ validityBeforeReset[index] = transaction.isValidTransactionWithChanges();
867
+ });
862
868
 
863
- this.toValidateTransactions = this.verifiedTransactions.slice();
869
+ this.toValidateTransactions = verifiedTransactions.slice();
864
870
  this.toProcessTransactions = [];
865
871
  this.toDecryptTransactions = [];
866
872
  this.toParseMetaTransactions = [];
867
873
 
868
- this._cachedContent?.rebuildFromCore();
874
+ this.parseNewTransactions(false);
875
+
876
+ // Check if the validity of the transactions has changed after resetting the parsed transactions
877
+ // If it has, we need to rebuild the content to reflect the new validity
878
+ const sameAsBefore = validityBeforeReset.every(
879
+ (valid, index) =>
880
+ valid === verifiedTransactions[index]?.isValidTransactionWithChanges(),
881
+ );
882
+
883
+ if (!sameAsBefore) {
884
+ this._cachedContent?.rebuildFromCore();
885
+ }
886
+
887
+ this.scheduleNotifyUpdate();
869
888
  }
870
889
 
871
890
  verifiedTransactions: VerifiedTransaction[] = [];
@@ -0,0 +1,307 @@
1
+ import {
2
+ JsonValue,
3
+ RawCoID,
4
+ SessionID,
5
+ Stringified,
6
+ base64URLtoBytes,
7
+ bytesToBase64url,
8
+ } from "../exports.js";
9
+ import { CojsonInternalTypes } from "../exports.js";
10
+ import { TransactionID } from "../ids.js";
11
+ import { stableStringify } from "../jsonStringify.js";
12
+ import { JsonObject } from "../jsonValue.js";
13
+ import { logger } from "../logger.js";
14
+ import { ControlledAccountOrAgent } from "../coValues/account.js";
15
+ import {
16
+ PrivateTransaction,
17
+ Transaction,
18
+ TrustingTransaction,
19
+ } from "../coValueCore/verifiedState.js";
20
+ import {
21
+ CryptoProvider,
22
+ KeyID,
23
+ KeySecret,
24
+ Sealed,
25
+ SealerID,
26
+ SealerSecret,
27
+ SessionLogImpl,
28
+ Signature,
29
+ SignerID,
30
+ SignerSecret,
31
+ textDecoder,
32
+ textEncoder,
33
+ } from "./crypto.js";
34
+ import {
35
+ blake3HashOnce,
36
+ blake3HashOnceWithContext,
37
+ verify,
38
+ encrypt,
39
+ decrypt,
40
+ newEd25519SigningKey,
41
+ newX25519PrivateKey,
42
+ getSealerId,
43
+ getSignerId,
44
+ sign,
45
+ seal,
46
+ unseal,
47
+ Blake3Hasher,
48
+ SessionLog,
49
+ } from "cojson-core-rn";
50
+
51
+ type Blake3State = Blake3Hasher;
52
+
53
+ /**
54
+ *
55
+ * @param view - The Uint8Array to convert to an ArrayBuffer.
56
+ * @returns The ArrayBuffer.
57
+ */
58
+ function toArrayBuffer(view: Uint8Array): ArrayBuffer {
59
+ if (
60
+ view.byteOffset === 0 &&
61
+ view.byteLength === view.buffer.byteLength &&
62
+ view.buffer instanceof ArrayBuffer
63
+ ) {
64
+ return view.buffer;
65
+ }
66
+ const buffer = new ArrayBuffer(view.byteLength);
67
+ new Uint8Array(buffer).set(view);
68
+ return buffer;
69
+ }
70
+
71
+ export class RNCrypto extends CryptoProvider<Blake3State> {
72
+ private constructor() {
73
+ super();
74
+ }
75
+
76
+ getSignerID(secret: SignerSecret): SignerID {
77
+ return getSignerId(secret) as SignerID;
78
+ }
79
+ newX25519StaticSecret(): Uint8Array {
80
+ return new Uint8Array(newX25519PrivateKey());
81
+ }
82
+ getSealerID(secret: SealerSecret): SealerID {
83
+ return getSealerId(secret) as SealerID;
84
+ }
85
+ blake3HashOnce(data: Uint8Array): Uint8Array {
86
+ return new Uint8Array(blake3HashOnce(toArrayBuffer(data)));
87
+ }
88
+ blake3HashOnceWithContext(
89
+ data: Uint8Array,
90
+ { context }: { context: Uint8Array },
91
+ ): Uint8Array {
92
+ return new Uint8Array(
93
+ blake3HashOnceWithContext(toArrayBuffer(data), toArrayBuffer(context)),
94
+ );
95
+ }
96
+ seal<T extends JsonValue>({
97
+ message,
98
+ from,
99
+ to,
100
+ nOnceMaterial,
101
+ }: {
102
+ message: T;
103
+ from: SealerSecret;
104
+ to: SealerID;
105
+ nOnceMaterial: { in: RawCoID; tx: TransactionID };
106
+ }): Sealed<T> {
107
+ const messageBuffer = toArrayBuffer(
108
+ textEncoder.encode(stableStringify(message)),
109
+ );
110
+ const nOnceBuffer = toArrayBuffer(
111
+ textEncoder.encode(stableStringify(nOnceMaterial)),
112
+ );
113
+
114
+ return `sealed_U${bytesToBase64url(
115
+ new Uint8Array(seal(messageBuffer, from, to, nOnceBuffer)),
116
+ )}` as Sealed<T>;
117
+ }
118
+ unseal<T extends JsonValue>(
119
+ sealed: Sealed<T>,
120
+ sealer: SealerSecret,
121
+ from: SealerID,
122
+ nOnceMaterial: { in: RawCoID; tx: TransactionID },
123
+ ): T | undefined {
124
+ const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
125
+ const nonceBuffer = toArrayBuffer(
126
+ textEncoder.encode(stableStringify(nOnceMaterial)),
127
+ );
128
+
129
+ const plaintext = textDecoder.decode(
130
+ unseal(toArrayBuffer(sealedBytes), sealer, from, nonceBuffer),
131
+ );
132
+ try {
133
+ return JSON.parse(plaintext) as T;
134
+ } catch (e) {
135
+ logger.error("Failed to decrypt/parse sealed message", { err: e });
136
+ return undefined;
137
+ }
138
+ }
139
+ createSessionLog(
140
+ coID: RawCoID,
141
+ sessionID: SessionID,
142
+ signerID?: SignerID,
143
+ ): SessionLogImpl {
144
+ return new SessionLogAdapter(new SessionLog(coID, sessionID, signerID));
145
+ }
146
+
147
+ static async create(): Promise<RNCrypto> {
148
+ return new RNCrypto();
149
+ }
150
+
151
+ newEd25519SigningKey(): Uint8Array {
152
+ return new Uint8Array(newEd25519SigningKey());
153
+ }
154
+
155
+ sign(
156
+ secret: CojsonInternalTypes.SignerSecret,
157
+ message: JsonValue,
158
+ ): CojsonInternalTypes.Signature {
159
+ return sign(
160
+ toArrayBuffer(textEncoder.encode(stableStringify(message))),
161
+ secret,
162
+ ) as CojsonInternalTypes.Signature;
163
+ }
164
+
165
+ verify(
166
+ signature: CojsonInternalTypes.Signature,
167
+ message: JsonValue,
168
+ id: CojsonInternalTypes.SignerID,
169
+ ): boolean {
170
+ const result = verify(
171
+ signature,
172
+ toArrayBuffer(textEncoder.encode(stableStringify(message))),
173
+ id,
174
+ );
175
+
176
+ return result;
177
+ }
178
+
179
+ encrypt<T extends JsonValue, N extends JsonValue>(
180
+ value: T,
181
+ keySecret: CojsonInternalTypes.KeySecret,
182
+ nOnceMaterial: N,
183
+ ): CojsonInternalTypes.Encrypted<T, N> {
184
+ const valueBytes = toArrayBuffer(
185
+ textEncoder.encode(stableStringify(value)),
186
+ );
187
+ const nOnceBytes = toArrayBuffer(
188
+ textEncoder.encode(stableStringify(nOnceMaterial)),
189
+ );
190
+
191
+ const encrypted = `encrypted_U${bytesToBase64url(
192
+ new Uint8Array(encrypt(valueBytes, keySecret, nOnceBytes)),
193
+ )}` as CojsonInternalTypes.Encrypted<T, N>;
194
+ return encrypted;
195
+ }
196
+
197
+ decryptRaw<T extends JsonValue, N extends JsonValue>(
198
+ encrypted: CojsonInternalTypes.Encrypted<T, N>,
199
+ keySecret: CojsonInternalTypes.KeySecret,
200
+ nOnceMaterial: N,
201
+ ): Stringified<T> {
202
+ const buffer = base64URLtoBytes(encrypted.substring("encrypted_U".length));
203
+
204
+ const decrypted = textDecoder.decode(
205
+ decrypt(
206
+ toArrayBuffer(buffer),
207
+ keySecret,
208
+ toArrayBuffer(textEncoder.encode(stableStringify(nOnceMaterial))),
209
+ ),
210
+ ) as Stringified<T>;
211
+
212
+ return decrypted;
213
+ }
214
+ }
215
+
216
+ class SessionLogAdapter implements SessionLogImpl {
217
+ constructor(private readonly sessionLog: SessionLog) {}
218
+
219
+ tryAdd(
220
+ transactions: Transaction[],
221
+ newSignature: Signature,
222
+ skipVerify: boolean,
223
+ ): void {
224
+ this.sessionLog.tryAdd(
225
+ transactions.map((tx) => stableStringify(tx)),
226
+ newSignature,
227
+ skipVerify,
228
+ );
229
+ }
230
+
231
+ addNewPrivateTransaction(
232
+ signerAgent: ControlledAccountOrAgent,
233
+ changes: JsonValue[],
234
+ keyID: KeyID,
235
+ keySecret: KeySecret,
236
+ madeAt: number,
237
+ meta: JsonObject | undefined,
238
+ ) {
239
+ const output = this.sessionLog.addNewPrivateTransaction(
240
+ stableStringify(changes),
241
+ signerAgent.currentSignerSecret(),
242
+ keySecret,
243
+ keyID,
244
+ madeAt,
245
+ meta ? stableStringify(meta) : undefined,
246
+ );
247
+ const parsedOutput = JSON.parse(output);
248
+ const transaction: PrivateTransaction = {
249
+ privacy: "private",
250
+ madeAt,
251
+ encryptedChanges: parsedOutput.encrypted_changes,
252
+ keyUsed: keyID,
253
+ meta: parsedOutput.meta,
254
+ };
255
+ return { signature: parsedOutput.signature as Signature, transaction };
256
+ }
257
+
258
+ addNewTrustingTransaction(
259
+ signerAgent: ControlledAccountOrAgent,
260
+ changes: JsonValue[],
261
+ madeAt: number,
262
+ meta: JsonObject | undefined,
263
+ ) {
264
+ const stringifiedChanges = stableStringify(changes);
265
+ const stringifiedMeta = meta ? stableStringify(meta) : undefined;
266
+ const output = this.sessionLog.addNewTrustingTransaction(
267
+ stringifiedChanges,
268
+ signerAgent.currentSignerSecret(),
269
+ madeAt,
270
+ stringifiedMeta,
271
+ );
272
+ const transaction: TrustingTransaction = {
273
+ privacy: "trusting",
274
+ madeAt,
275
+ changes: stringifiedChanges,
276
+ meta: stringifiedMeta,
277
+ };
278
+ return { signature: output as Signature, transaction };
279
+ }
280
+
281
+ decryptNextTransactionChangesJson(
282
+ txIndex: number,
283
+ keySecret: KeySecret,
284
+ ): string {
285
+ return this.sessionLog.decryptNextTransactionChangesJson(
286
+ txIndex,
287
+ keySecret,
288
+ );
289
+ }
290
+
291
+ decryptNextTransactionMetaJson(
292
+ txIndex: number,
293
+ keySecret: KeySecret,
294
+ ): string | undefined {
295
+ return this.sessionLog.decryptNextTransactionMetaJson(txIndex, keySecret);
296
+ }
297
+
298
+ free(): void {
299
+ this.sessionLog.uniffiDestroy();
300
+ }
301
+
302
+ clone(): SessionLogImpl {
303
+ return new SessionLogAdapter(
304
+ this.sessionLog.cloneSessionLog() as SessionLog,
305
+ );
306
+ }
307
+ }
package/src/exports.ts CHANGED
@@ -6,7 +6,10 @@ import {
6
6
  enablePermissionErrors,
7
7
  type AvailableCoValueCore,
8
8
  } from "./coValueCore/coValueCore.js";
9
- import { CoValueUniqueness } from "./coValueCore/verifiedState.js";
9
+ import {
10
+ CoValueHeader,
11
+ CoValueUniqueness,
12
+ } from "./coValueCore/verifiedState.js";
10
13
  import {
11
14
  ControlledAccount,
12
15
  ControlledAgent,
@@ -78,7 +81,9 @@ import { getDependedOnCoValuesFromRawData } from "./coValueCore/utils.js";
78
81
  import {
79
82
  CO_VALUE_LOADING_CONFIG,
80
83
  TRANSACTION_CONFIG,
84
+ setCoValueLoadingMaxRetries,
81
85
  setCoValueLoadingRetryDelay,
86
+ setCoValueLoadingTimeout,
82
87
  setIncomingMessagesTimeBudget,
83
88
  setMaxRecommendedTxSize,
84
89
  } from "./config.js";
@@ -118,6 +123,8 @@ export const cojsonInternals = {
118
123
  CO_VALUE_PRIORITY,
119
124
  setIncomingMessagesTimeBudget,
120
125
  setCoValueLoadingRetryDelay,
126
+ setCoValueLoadingMaxRetries,
127
+ setCoValueLoadingTimeout,
121
128
  ConnectedPeerChannel,
122
129
  textEncoder,
123
130
  textDecoder,
@@ -186,6 +193,7 @@ export type {
186
193
  AccountRole,
187
194
  AvailableCoValueCore,
188
195
  PeerState,
196
+ CoValueHeader,
189
197
  };
190
198
 
191
199
  export * from "./storage/index.js";
package/src/localNode.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  } from "./coValues/group.js";
32
32
  import { CO_VALUE_LOADING_CONFIG } from "./config.js";
33
33
  import { AgentSecret, CryptoProvider } from "./crypto/crypto.js";
34
- import { AgentID, RawCoID, SessionID, isAgentID } from "./ids.js";
34
+ import { AgentID, RawCoID, SessionID, isAgentID, isRawCoID } from "./ids.js";
35
35
  import { logger } from "./logger.js";
36
36
  import { StorageAPI } from "./storage/index.js";
37
37
  import { Peer, PeerID, SyncManager } from "./sync.js";
@@ -387,13 +387,17 @@ export class LocalNode {
387
387
  return coValue;
388
388
  }
389
389
 
390
+ hasLoadingSources(id: RawCoID) {
391
+ return this.storage || this.syncManager.getServerPeers(id).length > 0;
392
+ }
393
+
390
394
  /** @internal */
391
395
  async loadCoValueCore(
392
396
  id: RawCoID,
393
397
  skipLoadingFromPeer?: PeerID,
394
398
  skipRetry?: boolean,
395
399
  ): Promise<CoValueCore> {
396
- if (typeof id !== "string" || !id.startsWith("co_z")) {
400
+ if (!isRawCoID(id)) {
397
401
  throw new TypeError(
398
402
  `Trying to load CoValue with invalid id ${Array.isArray(id) ? JSON.stringify(id) : id}`,
399
403
  );
@@ -421,6 +425,8 @@ export class LocalNode {
421
425
  const peers = this.syncManager.getServerPeers(id, skipLoadingFromPeer);
422
426
 
423
427
  if (!this.storage && peers.length === 0) {
428
+ // Flags the coValue as unavailable
429
+ coValue.markNotFoundInPeer("storage");
424
430
  return coValue;
425
431
  }
426
432