cojson 0.7.33 → 0.7.34-neverthrow.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ import {
7
7
  StreamingHash,
8
8
  KeyID,
9
9
  CryptoProvider,
10
+ SignerID,
10
11
  } from "./crypto/crypto.js";
11
12
  import { JsonObject, JsonValue } from "./jsonValue.js";
12
13
  import { base58 } from "@scure/base";
@@ -16,7 +17,7 @@ import {
16
17
  isKeyForKeyField,
17
18
  } from "./permissions.js";
18
19
  import { RawGroup } from "./coValues/group.js";
19
- import { LocalNode } from "./localNode.js";
20
+ import { LocalNode, ResolveAccountAgentError } from "./localNode.js";
20
21
  import { CoValueKnownState, NewContentMessage } from "./sync.js";
21
22
  import { AgentID, RawCoID, SessionID, TransactionID } from "./ids.js";
22
23
  import { AccountID, ControlledAccountOrAgent } from "./coValues/account.js";
@@ -25,6 +26,7 @@ import { coreToCoValue } from "./coreToCoValue.js";
25
26
  import { expectGroup } from "./typeUtils/expectGroup.js";
26
27
  import { isAccountID } from "./typeUtils/isAccountID.js";
27
28
  import { accountOrAgentIDfromSessionID } from "./typeUtils/accountOrAgentIDfromSessionID.js";
29
+ import { err, ok, Result, ResultAsync } from "neverthrow";
28
30
 
29
31
  /**
30
32
  In order to not block other concurrently syncing CoValues we introduce a maximum size of transactions,
@@ -103,10 +105,10 @@ export class CoValueCore {
103
105
  _decryptionCache: {
104
106
  [key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined;
105
107
  } = {};
106
- currentlyAsyncApplyingTxDone?: Promise<void>;
107
108
  _cachedKnownState?: CoValueKnownState;
108
109
  _cachedDependentOn?: RawCoID[];
109
110
  _cachedNewContentSinceEmpty?: NewContentMessage[] | undefined;
111
+ _currentAsyncAddTransaction?: Promise<void>;
110
112
 
111
113
  constructor(
112
114
  header: CoValueHeader,
@@ -182,7 +184,9 @@ export class CoValueCore {
182
184
  this.header.meta?.type === "account"
183
185
  ? (this.node.currentSessionID.replace(
184
186
  this.node.account.id,
185
- this.node.account.currentAgentID(),
187
+ this.node.account
188
+ .currentAgentID()
189
+ ._unsafeUnwrap({ withStackTrace: true }),
186
190
  ) as SessionID)
187
191
  : this.node.currentSessionID;
188
192
 
@@ -197,166 +201,197 @@ export class CoValueCore {
197
201
  newTransactions: Transaction[],
198
202
  givenExpectedNewHash: Hash | undefined,
199
203
  newSignature: Signature,
200
- ): boolean {
201
- const signerID = this.crypto.getAgentSignerID(
202
- this.node.resolveAccountAgent(
204
+ ): Result<true, TryAddTransactionsError> {
205
+ return this.node
206
+ .resolveAccountAgent(
203
207
  accountOrAgentIDfromSessionID(sessionID),
204
208
  "Expected to know signer of transaction",
205
- ),
206
- );
209
+ )
210
+ .andThen((agent) => {
211
+ const signerID = this.crypto.getAgentSignerID(agent);
212
+
213
+ // const beforeHash = performance.now();
214
+ const { expectedNewHash, newStreamingHash } =
215
+ this.expectedNewHashAfter(sessionID, newTransactions);
216
+ // const afterHash = performance.now();
217
+ // console.log(
218
+ // "Hashing took",
219
+ // afterHash - beforeHash
220
+ // );
221
+
222
+ if (
223
+ givenExpectedNewHash &&
224
+ givenExpectedNewHash !== expectedNewHash
225
+ ) {
226
+ return err({
227
+ type: "InvalidHash",
228
+ id: this.id,
229
+ expectedNewHash,
230
+ givenExpectedNewHash,
231
+ } satisfies InvalidHashError);
232
+ }
207
233
 
208
- if (!signerID) {
209
- console.warn(
210
- "Unknown agent",
211
- accountOrAgentIDfromSessionID(sessionID),
212
- );
213
- return false;
214
- }
234
+ // const beforeVerify = performance.now();
235
+ if (
236
+ !this.crypto.verify(newSignature, expectedNewHash, signerID)
237
+ ) {
238
+ return err({
239
+ type: "InvalidSignature",
240
+ id: this.id,
241
+ newSignature,
242
+ sessionID,
243
+ signerID,
244
+ } satisfies InvalidSignatureError);
245
+ }
246
+ // const afterVerify = performance.now();
247
+ // console.log(
248
+ // "Verify took",
249
+ // afterVerify - beforeVerify
250
+ // );
251
+
252
+ this.doAddTransactions(
253
+ sessionID,
254
+ newTransactions,
255
+ newSignature,
256
+ expectedNewHash,
257
+ newStreamingHash,
258
+ "immediate",
259
+ );
215
260
 
216
- // const beforeHash = performance.now();
217
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
218
- sessionID,
219
- newTransactions,
220
- );
221
- // const afterHash = performance.now();
222
- // console.log(
223
- // "Hashing took",
224
- // afterHash - beforeHash
225
- // );
226
-
227
- if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
228
- console.warn("Invalid hash", {
229
- expectedNewHash,
230
- givenExpectedNewHash,
261
+ return ok(true as const);
231
262
  });
232
- return false;
233
- }
234
-
235
- // const beforeVerify = performance.now();
236
- if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
237
- console.warn(
238
- "Invalid signature in",
239
- this.id,
240
- newSignature,
241
- expectedNewHash,
242
- signerID,
243
- );
244
- return false;
245
- }
246
- // const afterVerify = performance.now();
247
- // console.log(
248
- // "Verify took",
249
- // afterVerify - beforeVerify
250
- // );
251
-
252
- this.doAddTransactions(
253
- sessionID,
254
- newTransactions,
255
- newSignature,
256
- expectedNewHash,
257
- newStreamingHash,
258
- "immediate",
259
- );
260
-
261
- return true;
262
263
  }
263
264
 
264
- async tryAddTransactionsAsync(
265
+ tryAddTransactionsAsync(
265
266
  sessionID: SessionID,
266
267
  newTransactions: Transaction[],
267
268
  givenExpectedNewHash: Hash | undefined,
268
269
  newSignature: Signature,
269
- ): Promise<boolean> {
270
- if (this.currentlyAsyncApplyingTxDone) {
271
- await this.currentlyAsyncApplyingTxDone;
272
- }
273
- let resolveDone!: () => void;
274
-
275
- this.currentlyAsyncApplyingTxDone = new Promise((resolve) => {
276
- resolveDone = resolve;
277
- });
278
-
279
- const signerID = this.crypto.getAgentSignerID(
280
- await this.node.resolveAccountAgentAsync(
281
- accountOrAgentIDfromSessionID(sessionID),
282
- "Expected to know signer of transaction",
283
- ),
284
- );
285
-
286
- if (!signerID) {
287
- console.warn(
288
- "Unknown agent",
289
- accountOrAgentIDfromSessionID(sessionID),
270
+ ): ResultAsync<true, TryAddTransactionsError> {
271
+ const currentAsyncAddTransaction = this._currentAsyncAddTransaction;
272
+ let maybeAwaitPrevious:
273
+ | ResultAsync<void, TryAddTransactionsError>
274
+ | undefined;
275
+ let thisDone = () => {};
276
+
277
+ if (currentAsyncAddTransaction) {
278
+ // eslint-disable-next-line neverthrow/must-use-result
279
+ maybeAwaitPrevious = ResultAsync.fromSafePromise(
280
+ currentAsyncAddTransaction,
290
281
  );
291
- resolveDone();
292
- return false;
293
- }
294
-
295
- const nTxBefore =
296
- this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
297
-
298
- // const beforeHash = performance.now();
299
- const { expectedNewHash, newStreamingHash } =
300
- await this.expectedNewHashAfterAsync(sessionID, newTransactions);
301
- // const afterHash = performance.now();
302
- // console.log(
303
- // "Hashing took",
304
- // afterHash - beforeHash
305
- // );
306
-
307
- const nTxAfter =
308
- this.sessionLogs.get(sessionID)?.transactions.length ?? 0;
309
-
310
- if (nTxAfter !== nTxBefore) {
311
- const newTransactionLengthBefore = newTransactions.length;
312
- newTransactions = newTransactions.slice(nTxAfter - nTxBefore);
313
- console.warn("Transactions changed while async hashing", {
314
- nTxBefore,
315
- nTxAfter,
316
- newTransactionLengthBefore,
317
- remainingNewTransactions: newTransactions.length,
282
+ } else {
283
+ // eslint-disable-next-line neverthrow/must-use-result
284
+ maybeAwaitPrevious = ResultAsync.fromSafePromise(Promise.resolve());
285
+ this._currentAsyncAddTransaction = new Promise((resolve) => {
286
+ thisDone = resolve;
318
287
  });
319
288
  }
320
289
 
321
- if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
322
- console.warn("Invalid hash", {
323
- expectedNewHash,
324
- givenExpectedNewHash,
325
- });
326
- resolveDone();
327
- return false;
328
- }
290
+ return maybeAwaitPrevious
291
+ .andThen((_previousDone) =>
292
+ this.node
293
+ .resolveAccountAgentAsync(
294
+ accountOrAgentIDfromSessionID(sessionID),
295
+ "Expected to know signer of transaction",
296
+ )
297
+ .andThen((agent) => {
298
+ const signerID = this.crypto.getAgentSignerID(agent);
299
+
300
+ const nTxBefore =
301
+ this.sessionLogs.get(sessionID)?.transactions
302
+ .length ?? 0;
303
+
304
+ // const beforeHash = performance.now();
305
+ return ResultAsync.fromSafePromise(
306
+ this.expectedNewHashAfterAsync(
307
+ sessionID,
308
+ newTransactions,
309
+ ),
310
+ ).andThen(({ expectedNewHash, newStreamingHash }) => {
311
+ // const afterHash = performance.now();
312
+ // console.log(
313
+ // "Hashing took",
314
+ // afterHash - beforeHash
315
+ // );
316
+
317
+ const nTxAfter =
318
+ this.sessionLogs.get(sessionID)?.transactions
319
+ .length ?? 0;
320
+
321
+ if (nTxAfter !== nTxBefore) {
322
+ const newTransactionLengthBefore =
323
+ newTransactions.length;
324
+ newTransactions = newTransactions.slice(
325
+ nTxAfter - nTxBefore,
326
+ );
327
+ console.warn(
328
+ "Transactions changed while async hashing",
329
+ {
330
+ nTxBefore,
331
+ nTxAfter,
332
+ newTransactionLengthBefore,
333
+ remainingNewTransactions:
334
+ newTransactions.length,
335
+ },
336
+ );
337
+ }
329
338
 
330
- performance.mark("verifyStart" + this.id);
331
- if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
332
- console.warn(
333
- "Invalid signature in",
334
- this.id,
335
- newSignature,
336
- expectedNewHash,
337
- signerID,
338
- );
339
- resolveDone();
340
- return false;
341
- }
342
- performance.mark("verifyEnd" + this.id);
343
- performance.measure(
344
- "verify" + this.id,
345
- "verifyStart" + this.id,
346
- "verifyEnd" + this.id,
347
- );
339
+ if (
340
+ givenExpectedNewHash &&
341
+ givenExpectedNewHash !== expectedNewHash
342
+ ) {
343
+ return err({
344
+ type: "InvalidHash",
345
+ id: this.id,
346
+ expectedNewHash,
347
+ givenExpectedNewHash,
348
+ } satisfies InvalidHashError);
349
+ }
348
350
 
349
- this.doAddTransactions(
350
- sessionID,
351
- newTransactions,
352
- newSignature,
353
- expectedNewHash,
354
- newStreamingHash,
355
- "deferred",
356
- );
351
+ performance.mark("verifyStart" + this.id);
352
+ if (
353
+ !this.crypto.verify(
354
+ newSignature,
355
+ expectedNewHash,
356
+ signerID,
357
+ )
358
+ ) {
359
+ return err({
360
+ type: "InvalidSignature",
361
+ id: this.id,
362
+ newSignature,
363
+ sessionID,
364
+ signerID,
365
+ } satisfies InvalidSignatureError);
366
+ }
367
+ performance.mark("verifyEnd" + this.id);
368
+ performance.measure(
369
+ "verify" + this.id,
370
+ "verifyStart" + this.id,
371
+ "verifyEnd" + this.id,
372
+ );
357
373
 
358
- resolveDone();
359
- return true;
374
+ this.doAddTransactions(
375
+ sessionID,
376
+ newTransactions,
377
+ newSignature,
378
+ expectedNewHash,
379
+ newStreamingHash,
380
+ "deferred",
381
+ );
382
+
383
+ return ok(true as const);
384
+ });
385
+ }),
386
+ )
387
+ .map((trueResult) => {
388
+ thisDone();
389
+ return trueResult;
390
+ })
391
+ .mapErr((err) => {
392
+ thisDone();
393
+ return err;
394
+ });
360
395
  }
361
396
 
362
397
  private doAddTransactions(
@@ -545,7 +580,9 @@ export class CoValueCore {
545
580
  this.header.meta?.type === "account"
546
581
  ? (this.node.currentSessionID.replace(
547
582
  this.node.account.id,
548
- this.node.account.currentAgentID(),
583
+ this.node.account
584
+ .currentAgentID()
585
+ ._unsafeUnwrap({ withStackTrace: true }),
549
586
  ) as SessionID)
550
587
  : this.node.currentSessionID;
551
588
 
@@ -563,7 +600,7 @@ export class CoValueCore {
563
600
  [transaction],
564
601
  expectedNewHash,
565
602
  signature,
566
- );
603
+ )._unsafeUnwrap({ withStackTrace: true });
567
604
 
568
605
  if (success) {
569
606
  void this.node.syncManager.syncCoValue(this);
@@ -709,7 +746,9 @@ export class CoValueCore {
709
746
  // Try to find key revelation for us
710
747
  const lookupAccountOrAgentID =
711
748
  this.header.meta?.type === "account"
712
- ? this.node.account.currentAgentID()
749
+ ? this.node.account
750
+ .currentAgentID()
751
+ ._unsafeUnwrap({ withStackTrace: true })
713
752
  : this.node.account.id;
714
753
 
715
754
  const lastReadyKeyEdit = content.lastEditAt(
@@ -718,10 +757,9 @@ export class CoValueCore {
718
757
 
719
758
  if (lastReadyKeyEdit?.value) {
720
759
  const revealer = lastReadyKeyEdit.by;
721
- const revealerAgent = this.node.resolveAccountAgent(
722
- revealer,
723
- "Expected to know revealer",
724
- );
760
+ const revealerAgent = this.node
761
+ .resolveAccountAgent(revealer, "Expected to know revealer")
762
+ ._unsafeUnwrap({ withStackTrace: true });
725
763
 
726
764
  const secret = this.crypto.unseal(
727
765
  lastReadyKeyEdit.value,
@@ -986,3 +1024,23 @@ function getNextKnownSignatureIdx(
986
1024
  idx >= (sentStateForSessionID ?? knownStateForSessionID ?? -1),
987
1025
  );
988
1026
  }
1027
+
1028
+ export type InvalidHashError = {
1029
+ type: "InvalidHash";
1030
+ id: RawCoID;
1031
+ expectedNewHash: Hash;
1032
+ givenExpectedNewHash: Hash;
1033
+ };
1034
+
1035
+ export type InvalidSignatureError = {
1036
+ type: "InvalidSignature";
1037
+ id: RawCoID;
1038
+ newSignature: Signature;
1039
+ sessionID: SessionID;
1040
+ signerID: SignerID;
1041
+ };
1042
+
1043
+ export type TryAddTransactionsError =
1044
+ | ResolveAccountAgentError
1045
+ | InvalidHashError
1046
+ | InvalidSignatureError;
@@ -13,6 +13,7 @@ import { RawCoMap } from "./coMap.js";
13
13
  import { RawGroup, InviteSecret } from "./group.js";
14
14
  import { LocalNode } from "../index.js";
15
15
  import { JsonObject } from "../jsonValue.js";
16
+ import { err, ok, Result } from "neverthrow";
16
17
 
17
18
  export function accountHeaderForInitialAgentSecret(
18
19
  agentSecret: AgentSecret,
@@ -30,28 +31,34 @@ export function accountHeaderForInitialAgentSecret(
30
31
  };
31
32
  }
32
33
 
34
+ export type InvalidAccountAgentIDError = {
35
+ type: "InvalidAccountAgentID";
36
+ reason: string;
37
+ };
38
+
33
39
  export class RawAccount<
34
40
  Meta extends AccountMeta = AccountMeta,
35
41
  > extends RawGroup<Meta> {
36
42
  _cachedCurrentAgentID: AgentID | undefined;
37
43
 
38
- currentAgentID(): AgentID {
44
+ currentAgentID(): Result<AgentID, InvalidAccountAgentIDError> {
39
45
  if (this._cachedCurrentAgentID) {
40
- return this._cachedCurrentAgentID;
46
+ return ok(this._cachedCurrentAgentID);
41
47
  }
42
48
  const agents = this.keys().filter((k): k is AgentID =>
43
49
  k.startsWith("sealer_"),
44
50
  );
45
51
 
46
52
  if (agents.length !== 1) {
47
- throw new Error(
48
- "Expected exactly one agent in account, got " + agents.length,
49
- );
53
+ return err({
54
+ type: "InvalidAccountAgentID",
55
+ reason: "Account has " + agents.length + " agents",
56
+ });
50
57
  }
51
58
 
52
59
  this._cachedCurrentAgentID = agents[0];
53
60
 
54
- return agents[0]!;
61
+ return ok(agents[0]!);
55
62
  }
56
63
  }
57
64
 
@@ -59,10 +66,10 @@ export interface ControlledAccountOrAgent {
59
66
  id: AccountID | AgentID;
60
67
  agentSecret: AgentSecret;
61
68
 
62
- currentAgentID: () => AgentID;
63
- currentSignerID: () => SignerID;
69
+ currentAgentID: () => Result<AgentID, InvalidAccountAgentIDError>;
70
+ currentSignerID: () => Result<SignerID, InvalidAccountAgentIDError>;
64
71
  currentSignerSecret: () => SignerSecret;
65
- currentSealerID: () => SealerID;
72
+ currentSealerID: () => Result<SealerID, InvalidAccountAgentIDError>;
66
73
  currentSealerSecret: () => SealerSecret;
67
74
  }
68
75
 
@@ -96,25 +103,29 @@ export class RawControlledAccount<Meta extends AccountMeta = AccountMeta>
96
103
  return this.core.node.acceptInvite(groupOrOwnedValueID, inviteSecret);
97
104
  }
98
105
 
99
- currentAgentID(): AgentID {
106
+ currentAgentID(): Result<AgentID, InvalidAccountAgentIDError> {
100
107
  if (this._cachedCurrentAgentID) {
101
- return this._cachedCurrentAgentID;
108
+ return ok(this._cachedCurrentAgentID);
102
109
  }
103
110
  const agentID = this.crypto.getAgentID(this.agentSecret);
104
111
  this._cachedCurrentAgentID = agentID;
105
- return agentID;
112
+ return ok(agentID);
106
113
  }
107
114
 
108
- currentSignerID(): SignerID {
109
- return this.crypto.getAgentSignerID(this.currentAgentID());
115
+ currentSignerID() {
116
+ return this.currentAgentID().map((id) =>
117
+ this.crypto.getAgentSignerID(id),
118
+ );
110
119
  }
111
120
 
112
121
  currentSignerSecret(): SignerSecret {
113
122
  return this.crypto.getAgentSignerSecret(this.agentSecret);
114
123
  }
115
124
 
116
- currentSealerID(): SealerID {
117
- return this.crypto.getAgentSealerID(this.currentAgentID());
125
+ currentSealerID() {
126
+ return this.currentAgentID().map((id) =>
127
+ this.crypto.getAgentSealerID(id),
128
+ );
118
129
  }
119
130
 
120
131
  currentSealerSecret(): SealerSecret {
@@ -133,20 +144,24 @@ export class ControlledAgent implements ControlledAccountOrAgent {
133
144
  return this.crypto.getAgentID(this.agentSecret);
134
145
  }
135
146
 
136
- currentAgentID(): AgentID {
137
- return this.crypto.getAgentID(this.agentSecret);
147
+ currentAgentID() {
148
+ return ok(this.crypto.getAgentID(this.agentSecret));
138
149
  }
139
150
 
140
- currentSignerID(): SignerID {
141
- return this.crypto.getAgentSignerID(this.currentAgentID());
151
+ currentSignerID() {
152
+ return this.currentAgentID().map((id) =>
153
+ this.crypto.getAgentSignerID(id),
154
+ );
142
155
  }
143
156
 
144
157
  currentSignerSecret(): SignerSecret {
145
158
  return this.crypto.getAgentSignerSecret(this.agentSecret);
146
159
  }
147
160
 
148
- currentSealerID(): SealerID {
149
- return this.crypto.getAgentSealerID(this.currentAgentID());
161
+ currentSealerID() {
162
+ return this.currentAgentID().map((id) =>
163
+ this.crypto.getAgentSealerID(id),
164
+ );
150
165
  }
151
166
 
152
167
  currentSealerSecret(): SealerSecret {
@@ -120,7 +120,9 @@ export class RawGroup<
120
120
  const agent =
121
121
  typeof account === "string"
122
122
  ? account
123
- : account.currentAgentID();
123
+ : account
124
+ .currentAgentID()
125
+ ._unsafeUnwrap({ withStackTrace: true });
124
126
  this.set(memberKey, role, "trusting");
125
127
 
126
128
  if (this.get(memberKey) !== role) {
@@ -175,7 +177,7 @@ export class RawGroup<
175
177
  const reader = this.core.node.resolveAccountAgent(
176
178
  readerID,
177
179
  "Expected to know currently permitted reader",
178
- );
180
+ )._unsafeUnwrap({ withStackTrace: true });
179
181
 
180
182
  this.set(
181
183
  `${newReadKey.id}_for_${readerID}`,