cojson 0.17.9 → 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 (86) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/coValueCore/SessionMap.d.ts +45 -0
  4. package/dist/coValueCore/SessionMap.d.ts.map +1 -0
  5. package/dist/coValueCore/SessionMap.js +118 -0
  6. package/dist/coValueCore/SessionMap.js.map +1 -0
  7. package/dist/coValueCore/coValueCore.d.ts +10 -4
  8. package/dist/coValueCore/coValueCore.d.ts.map +1 -1
  9. package/dist/coValueCore/coValueCore.js +55 -68
  10. package/dist/coValueCore/coValueCore.js.map +1 -1
  11. package/dist/coValueCore/verifiedState.d.ts +15 -19
  12. package/dist/coValueCore/verifiedState.d.ts.map +1 -1
  13. package/dist/coValueCore/verifiedState.js +24 -87
  14. package/dist/coValueCore/verifiedState.js.map +1 -1
  15. package/dist/coValues/account.d.ts +4 -0
  16. package/dist/coValues/account.d.ts.map +1 -1
  17. package/dist/coValues/account.js +24 -4
  18. package/dist/coValues/account.js.map +1 -1
  19. package/dist/coValues/group.d.ts.map +1 -1
  20. package/dist/coValues/group.js +6 -2
  21. package/dist/coValues/group.js.map +1 -1
  22. package/dist/crypto/PureJSCrypto.d.ts +31 -3
  23. package/dist/crypto/PureJSCrypto.d.ts.map +1 -1
  24. package/dist/crypto/PureJSCrypto.js +115 -0
  25. package/dist/crypto/PureJSCrypto.js.map +1 -1
  26. package/dist/crypto/WasmCrypto.d.ts +23 -4
  27. package/dist/crypto/WasmCrypto.d.ts.map +1 -1
  28. package/dist/crypto/WasmCrypto.js +44 -2
  29. package/dist/crypto/WasmCrypto.js.map +1 -1
  30. package/dist/crypto/crypto.d.ts +17 -1
  31. package/dist/crypto/crypto.d.ts.map +1 -1
  32. package/dist/crypto/crypto.js.map +1 -1
  33. package/dist/localNode.d.ts +1 -0
  34. package/dist/localNode.d.ts.map +1 -1
  35. package/dist/localNode.js +10 -5
  36. package/dist/localNode.js.map +1 -1
  37. package/dist/permissions.d.ts +17 -1
  38. package/dist/permissions.d.ts.map +1 -1
  39. package/dist/permissions.js.map +1 -1
  40. package/dist/sync.d.ts.map +1 -1
  41. package/dist/sync.js +55 -49
  42. package/dist/sync.js.map +1 -1
  43. package/dist/tests/PureJSCrypto.test.d.ts +2 -0
  44. package/dist/tests/PureJSCrypto.test.d.ts.map +1 -0
  45. package/dist/tests/PureJSCrypto.test.js +102 -0
  46. package/dist/tests/PureJSCrypto.test.js.map +1 -0
  47. package/dist/tests/WasmCrypto.test.d.ts +2 -0
  48. package/dist/tests/WasmCrypto.test.d.ts.map +1 -0
  49. package/dist/tests/WasmCrypto.test.js +88 -0
  50. package/dist/tests/WasmCrypto.test.js.map +1 -0
  51. package/dist/tests/coValueCore.test.js +62 -187
  52. package/dist/tests/coValueCore.test.js.map +1 -1
  53. package/dist/tests/coreWasm.test.d.ts +2 -0
  54. package/dist/tests/coreWasm.test.d.ts.map +1 -0
  55. package/dist/tests/coreWasm.test.js +80 -0
  56. package/dist/tests/coreWasm.test.js.map +1 -0
  57. package/dist/tests/group.addMember.test.js +6 -11
  58. package/dist/tests/group.addMember.test.js.map +1 -1
  59. package/dist/tests/sync.load.test.js +40 -1
  60. package/dist/tests/sync.load.test.js.map +1 -1
  61. package/dist/tests/sync.test.js +1 -1
  62. package/dist/tests/sync.test.js.map +1 -1
  63. package/dist/tests/testUtils.d.ts +3 -0
  64. package/dist/tests/testUtils.d.ts.map +1 -1
  65. package/dist/tests/testUtils.js +4 -1
  66. package/dist/tests/testUtils.js.map +1 -1
  67. package/package.json +3 -3
  68. package/src/coValueCore/SessionMap.ts +229 -0
  69. package/src/coValueCore/coValueCore.ts +106 -121
  70. package/src/coValueCore/verifiedState.ts +61 -132
  71. package/src/coValues/account.ts +28 -4
  72. package/src/coValues/group.ts +10 -2
  73. package/src/crypto/PureJSCrypto.ts +206 -2
  74. package/src/crypto/WasmCrypto.ts +95 -4
  75. package/src/crypto/crypto.ts +38 -1
  76. package/src/localNode.ts +18 -10
  77. package/src/permissions.ts +17 -1
  78. package/src/sync.ts +63 -59
  79. package/src/tests/PureJSCrypto.test.ts +153 -0
  80. package/src/tests/WasmCrypto.test.ts +128 -0
  81. package/src/tests/coValueCore.test.ts +81 -293
  82. package/src/tests/coreWasm.test.ts +142 -0
  83. package/src/tests/group.addMember.test.ts +69 -63
  84. package/src/tests/sync.load.test.ts +52 -0
  85. package/src/tests/sync.test.ts +0 -2
  86. package/src/tests/testUtils.ts +9 -1
@@ -10,6 +10,7 @@ import {
10
10
  Encrypted,
11
11
  Hash,
12
12
  KeyID,
13
+ KeySecret,
13
14
  Signature,
14
15
  SignerID,
15
16
  StreamingHash,
@@ -21,6 +22,8 @@ import { PermissionsDef as RulesetDef } from "../permissions.js";
21
22
  import { CoValueKnownState, NewContentMessage } from "../sync.js";
22
23
  import { InvalidHashError, InvalidSignatureError } from "./coValueCore.js";
23
24
  import { TryAddTransactionsError } from "./coValueCore.js";
25
+ import { SessionLog, SessionMap } from "./SessionMap.js";
26
+ import { ControlledAccountOrAgent } from "../coValues/account.js";
24
27
 
25
28
  export type CoValueHeader = {
26
29
  type: AnyRawCoValue["type"];
@@ -48,20 +51,11 @@ export type TrustingTransaction = {
48
51
 
49
52
  export type Transaction = PrivateTransaction | TrustingTransaction;
50
53
 
51
- type SessionLog = {
52
- readonly transactions: Transaction[];
53
- streamingHash?: StreamingHash;
54
- readonly signatureAfter: { [txIdx: number]: Signature | undefined };
55
- lastSignature: Signature;
56
- };
57
-
58
- export type ValidatedSessions = Map<SessionID, SessionLog>;
59
-
60
54
  export class VerifiedState {
61
55
  readonly id: RawCoID;
62
56
  readonly crypto: CryptoProvider;
63
57
  readonly header: CoValueHeader;
64
- readonly sessions: ValidatedSessions;
58
+ readonly sessions: SessionMap;
65
59
  private _cachedKnownState?: CoValueKnownState;
66
60
  private _cachedNewContentSinceEmpty: NewContentMessage[] | undefined;
67
61
  private streamingKnownState?: CoValueKnownState["sessions"];
@@ -71,171 +65,98 @@ export class VerifiedState {
71
65
  id: RawCoID,
72
66
  crypto: CryptoProvider,
73
67
  header: CoValueHeader,
74
- sessions: ValidatedSessions,
68
+ sessions?: SessionMap,
75
69
  streamingKnownState?: CoValueKnownState["sessions"],
76
70
  ) {
77
71
  this.id = id;
78
72
  this.crypto = crypto;
79
73
  this.header = header;
80
- this.sessions = sessions;
74
+ this.sessions = sessions ?? new SessionMap(id, crypto);
81
75
  this.streamingKnownState = streamingKnownState
82
76
  ? { ...streamingKnownState }
83
77
  : undefined;
84
78
  }
85
79
 
86
80
  clone(): VerifiedState {
87
- // do a deep clone, including the sessions
88
- const clonedSessions = new Map();
89
- for (let [sessionID, sessionLog] of this.sessions) {
90
- clonedSessions.set(sessionID, {
91
- lastSignature: sessionLog.lastSignature,
92
- streamingHash: sessionLog.streamingHash?.clone(),
93
- signatureAfter: { ...sessionLog.signatureAfter },
94
- transactions: sessionLog.transactions.slice(),
95
- } satisfies SessionLog);
96
- }
97
81
  return new VerifiedState(
98
82
  this.id,
99
83
  this.crypto,
100
84
  this.header,
101
- clonedSessions,
102
- this.streamingKnownState,
85
+ this.sessions.clone(),
86
+ this.streamingKnownState ? { ...this.streamingKnownState } : undefined,
103
87
  );
104
88
  }
105
89
 
106
90
  tryAddTransactions(
107
91
  sessionID: SessionID,
108
- signerID: SignerID,
92
+ signerID: SignerID | undefined,
109
93
  newTransactions: Transaction[],
110
- givenExpectedNewHash: Hash | undefined,
111
94
  newSignature: Signature,
112
95
  skipVerify: boolean = false,
113
- givenNewStreamingHash?: StreamingHash,
114
96
  ): Result<true, TryAddTransactionsError> {
115
- if (skipVerify === true) {
116
- this.doAddTransactions(
117
- sessionID,
118
- newTransactions,
119
- newSignature,
120
- givenNewStreamingHash,
121
- );
122
- } else {
123
- const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
124
- sessionID,
125
- newTransactions,
126
- );
127
-
128
- if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
129
- return err({
130
- type: "InvalidHash",
131
- id: this.id,
132
- expectedNewHash,
133
- givenExpectedNewHash,
134
- } satisfies InvalidHashError);
135
- }
136
-
137
- if (!this.crypto.verify(newSignature, expectedNewHash, signerID)) {
138
- return err({
139
- type: "InvalidSignature",
140
- id: this.id,
141
- newSignature,
142
- sessionID,
143
- signerID,
144
- } satisfies InvalidSignatureError);
145
- }
97
+ const result = this.sessions.addTransaction(
98
+ sessionID,
99
+ signerID,
100
+ newTransactions,
101
+ newSignature,
102
+ skipVerify,
103
+ );
146
104
 
147
- this.doAddTransactions(
148
- sessionID,
149
- newTransactions,
150
- newSignature,
151
- newStreamingHash,
152
- );
105
+ if (result.isOk()) {
106
+ this._cachedNewContentSinceEmpty = undefined;
107
+ this._cachedKnownState = undefined;
153
108
  }
154
109
 
155
- return ok(true as const);
110
+ return result;
156
111
  }
157
112
 
158
- getLastSignatureCheckpoint(sessionID: SessionID): number {
159
- const sessionLog = this.sessions.get(sessionID);
113
+ makeNewTrustingTransaction(
114
+ sessionID: SessionID,
115
+ signerAgent: ControlledAccountOrAgent,
116
+ changes: JsonValue[],
117
+ ) {
118
+ const result = this.sessions.makeNewTrustingTransaction(
119
+ sessionID,
120
+ signerAgent,
121
+ changes,
122
+ );
160
123
 
161
- if (!sessionLog?.signatureAfter) return -1;
124
+ this._cachedNewContentSinceEmpty = undefined;
125
+ this._cachedKnownState = undefined;
162
126
 
163
- return Object.keys(sessionLog.signatureAfter).reduce(
164
- (max, idx) => Math.max(max, parseInt(idx)),
165
- -1,
166
- );
127
+ return result;
167
128
  }
168
129
 
169
- private doAddTransactions(
130
+ makeNewPrivateTransaction(
170
131
  sessionID: SessionID,
171
- newTransactions: Transaction[],
172
- newSignature: Signature,
173
- newStreamingHash?: StreamingHash,
132
+ signerAgent: ControlledAccountOrAgent,
133
+ changes: JsonValue[],
134
+ keyID: KeyID,
135
+ keySecret: KeySecret,
174
136
  ) {
175
- const sessionLog = this.sessions.get(sessionID);
176
- const transactions = sessionLog?.transactions ?? [];
177
-
178
- for (const tx of newTransactions) {
179
- transactions.push(tx);
180
- }
181
-
182
- const signatureAfter = sessionLog?.signatureAfter ?? {};
183
- const lastInbetweenSignatureIdx =
184
- this.getLastSignatureCheckpoint(sessionID);
185
-
186
- const sizeOfTxsSinceLastInbetweenSignature = transactions
187
- .slice(lastInbetweenSignatureIdx + 1)
188
- .reduce((sum, tx) => sum + getTransactionSize(tx), 0);
189
-
190
- if (exceedsRecommendedSize(sizeOfTxsSinceLastInbetweenSignature)) {
191
- signatureAfter[transactions.length - 1] = newSignature;
192
- }
193
-
194
- this.sessions.set(sessionID, {
195
- transactions,
196
- streamingHash: newStreamingHash,
197
- lastSignature: newSignature,
198
- signatureAfter: signatureAfter,
199
- });
137
+ const result = this.sessions.makeNewPrivateTransaction(
138
+ sessionID,
139
+ signerAgent,
140
+ changes,
141
+ keyID,
142
+ keySecret,
143
+ );
200
144
 
201
145
  this._cachedNewContentSinceEmpty = undefined;
202
146
  this._cachedKnownState = undefined;
147
+
148
+ return result;
203
149
  }
204
150
 
205
- expectedNewHashAfter(
206
- sessionID: SessionID,
207
- newTransactions: Transaction[],
208
- ): { expectedNewHash: Hash; newStreamingHash: StreamingHash } {
151
+ getLastSignatureCheckpoint(sessionID: SessionID): number {
209
152
  const sessionLog = this.sessions.get(sessionID);
210
153
 
211
- if (!sessionLog?.streamingHash) {
212
- const streamingHash = new StreamingHash(this.crypto);
213
- const oldTransactions = sessionLog?.transactions ?? [];
214
-
215
- for (const transaction of oldTransactions) {
216
- streamingHash.update(transaction);
217
- }
218
-
219
- for (const transaction of newTransactions) {
220
- streamingHash.update(transaction);
221
- }
222
-
223
- return {
224
- expectedNewHash: streamingHash.digest(),
225
- newStreamingHash: streamingHash,
226
- };
227
- }
228
-
229
- const streamingHash = sessionLog.streamingHash.clone();
230
-
231
- for (const transaction of newTransactions) {
232
- streamingHash.update(transaction);
233
- }
154
+ if (!sessionLog?.signatureAfter) return -1;
234
155
 
235
- return {
236
- expectedNewHash: streamingHash.digest(),
237
- newStreamingHash: streamingHash,
238
- };
156
+ return Object.keys(sessionLog.signatureAfter).reduce(
157
+ (max, idx) => Math.max(max, parseInt(idx)),
158
+ -1,
159
+ );
239
160
  }
240
161
 
241
162
  newContentSince(
@@ -424,6 +345,14 @@ export class VerifiedState {
424
345
  sessions,
425
346
  };
426
347
  }
348
+
349
+ decryptTransaction(
350
+ sessionID: SessionID,
351
+ txIndex: number,
352
+ keySecret: KeySecret,
353
+ ): JsonValue[] | undefined {
354
+ return this.sessions.decryptTransaction(sessionID, txIndex, keySecret);
355
+ }
427
356
  }
428
357
 
429
358
  function getNextKnownSignatureIdx(
@@ -92,6 +92,10 @@ export class ControlledAccount implements ControlledAccountOrAgent {
92
92
  account: RawAccount<AccountMeta>;
93
93
  agentSecret: AgentSecret;
94
94
  _cachedCurrentAgentID: AgentID | undefined;
95
+ _cachedCurrentSignerID: SignerID | undefined;
96
+ _cachedCurrentSignerSecret: SignerSecret | undefined;
97
+ _cachedCurrentSealerID: SealerID | undefined;
98
+ _cachedCurrentSealerSecret: SealerSecret | undefined;
95
99
  crypto: CryptoProvider;
96
100
 
97
101
  constructor(account: RawAccount<AccountMeta>, agentSecret: AgentSecret) {
@@ -114,19 +118,39 @@ export class ControlledAccount implements ControlledAccountOrAgent {
114
118
  }
115
119
 
116
120
  currentSignerID() {
117
- return this.crypto.getAgentSignerID(this.currentAgentID());
121
+ if (this._cachedCurrentSignerID) {
122
+ return this._cachedCurrentSignerID;
123
+ }
124
+ const signerID = this.crypto.getAgentSignerID(this.currentAgentID());
125
+ this._cachedCurrentSignerID = signerID;
126
+ return signerID;
118
127
  }
119
128
 
120
129
  currentSignerSecret(): SignerSecret {
121
- return this.crypto.getAgentSignerSecret(this.agentSecret);
130
+ if (this._cachedCurrentSignerSecret) {
131
+ return this._cachedCurrentSignerSecret;
132
+ }
133
+ const signerSecret = this.crypto.getAgentSignerSecret(this.agentSecret);
134
+ this._cachedCurrentSignerSecret = signerSecret;
135
+ return signerSecret;
122
136
  }
123
137
 
124
138
  currentSealerID() {
125
- return this.crypto.getAgentSealerID(this.currentAgentID());
139
+ if (this._cachedCurrentSealerID) {
140
+ return this._cachedCurrentSealerID;
141
+ }
142
+ const sealerID = this.crypto.getAgentSealerID(this.currentAgentID());
143
+ this._cachedCurrentSealerID = sealerID;
144
+ return sealerID;
126
145
  }
127
146
 
128
147
  currentSealerSecret(): SealerSecret {
129
- return this.crypto.getAgentSealerSecret(this.agentSecret);
148
+ if (this._cachedCurrentSealerSecret) {
149
+ return this._cachedCurrentSealerSecret;
150
+ }
151
+ const sealerSecret = this.crypto.getAgentSealerSecret(this.agentSecret);
152
+ this._cachedCurrentSealerSecret = sealerSecret;
153
+ return sealerSecret;
130
154
  }
131
155
  }
132
156
 
@@ -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();
@@ -3,23 +3,32 @@ import { ed25519, x25519 } from "@noble/curves/ed25519";
3
3
  import { blake3 } from "@noble/hashes/blake3";
4
4
  import { base58 } from "@scure/base";
5
5
  import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
6
- import { RawCoID, TransactionID } from "../ids.js";
6
+ import {
7
+ PrivateTransaction,
8
+ Transaction,
9
+ TrustingTransaction,
10
+ } from "../coValueCore/verifiedState.js";
11
+ import { RawCoID, SessionID, TransactionID } from "../ids.js";
7
12
  import { Stringified, stableStringify } from "../jsonStringify.js";
8
13
  import { JsonValue } from "../jsonValue.js";
9
14
  import { logger } from "../logger.js";
10
15
  import {
11
16
  CryptoProvider,
12
17
  Encrypted,
18
+ KeyID,
13
19
  KeySecret,
14
20
  Sealed,
15
21
  SealerID,
16
22
  SealerSecret,
23
+ SessionLogImpl,
17
24
  Signature,
18
25
  SignerID,
19
26
  SignerSecret,
27
+ StreamingHash,
20
28
  textDecoder,
21
29
  textEncoder,
22
30
  } from "./crypto.js";
31
+ import { ControlledAccountOrAgent } from "../coValues/account.js";
23
32
 
24
33
  type Blake3State = ReturnType<typeof blake3.create>;
25
34
 
@@ -67,7 +76,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
67
76
  return this.blake3HashOnce(input).slice(0, 24);
68
77
  }
69
78
 
70
- protected generateJsonNonce(material: JsonValue): Uint8Array {
79
+ generateJsonNonce(material: JsonValue): Uint8Array {
71
80
  return this.generateNonce(textEncoder.encode(stableStringify(material)));
72
81
  }
73
82
 
@@ -199,4 +208,199 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
199
208
  return undefined;
200
209
  }
201
210
  }
211
+
212
+ createSessionLog(
213
+ coID: RawCoID,
214
+ sessionID: SessionID,
215
+ signerID?: SignerID,
216
+ ): SessionLogImpl {
217
+ return new PureJSSessionLog(coID, sessionID, signerID, this);
218
+ }
219
+ }
220
+
221
+ export class PureJSSessionLog implements SessionLogImpl {
222
+ transactions: string[] = [];
223
+ lastSignature: Signature | undefined;
224
+ streamingHash: Blake3State;
225
+
226
+ constructor(
227
+ private readonly coID: RawCoID,
228
+ private readonly sessionID: SessionID,
229
+ private readonly signerID: SignerID | undefined,
230
+ private readonly crypto: PureJSCrypto,
231
+ ) {
232
+ this.streamingHash = this.crypto.emptyBlake3State();
233
+ }
234
+
235
+ clone(): SessionLogImpl {
236
+ const newLog = new PureJSSessionLog(
237
+ this.coID,
238
+ this.sessionID,
239
+ this.signerID,
240
+ this.crypto,
241
+ );
242
+ newLog.transactions = this.transactions.slice();
243
+ newLog.lastSignature = this.lastSignature;
244
+ newLog.streamingHash = this.crypto.cloneBlake3State(this.streamingHash);
245
+ return newLog;
246
+ }
247
+
248
+ tryAdd(
249
+ transactions: Transaction[],
250
+ newSignature: Signature,
251
+ skipVerify: boolean,
252
+ ): void {
253
+ this.internalTryAdd(
254
+ transactions.map((tx) => stableStringify(tx)),
255
+ newSignature,
256
+ skipVerify,
257
+ );
258
+ }
259
+
260
+ internalTryAdd(
261
+ transactions: string[],
262
+ newSignature: Signature,
263
+ skipVerify: boolean,
264
+ ) {
265
+ if (!skipVerify) {
266
+ if (!this.signerID) {
267
+ throw new Error("Tried to add transactions without signer ID");
268
+ }
269
+
270
+ const checkHasher = this.crypto.cloneBlake3State(this.streamingHash);
271
+
272
+ for (const tx of transactions) {
273
+ checkHasher.update(textEncoder.encode(tx));
274
+ }
275
+ const newHash = checkHasher.digest();
276
+ const newHashEncoded = `hash_z${base58.encode(newHash)}`;
277
+
278
+ if (!this.crypto.verify(newSignature, newHashEncoded, this.signerID)) {
279
+ throw new Error("Signature verification failed");
280
+ }
281
+ }
282
+
283
+ for (const tx of transactions) {
284
+ this.crypto.blake3IncrementalUpdate(
285
+ this.streamingHash,
286
+ textEncoder.encode(tx),
287
+ );
288
+ this.transactions.push(tx);
289
+ }
290
+
291
+ this.lastSignature = newSignature;
292
+
293
+ return newSignature;
294
+ }
295
+
296
+ expectedHashAfter(transactionsJson: string[]): string {
297
+ const hasher = this.crypto.cloneBlake3State(this.streamingHash);
298
+ for (const tx of transactionsJson) {
299
+ hasher.update(textEncoder.encode(tx));
300
+ }
301
+ const newHash = hasher.digest();
302
+ return `hash_z${base58.encode(newHash)}`;
303
+ }
304
+
305
+ internalAddNewTransaction(
306
+ transaction: string,
307
+ signerAgent: ControlledAccountOrAgent,
308
+ ) {
309
+ this.crypto.blake3IncrementalUpdate(
310
+ this.streamingHash,
311
+ textEncoder.encode(transaction),
312
+ );
313
+ const newHash = this.crypto.blake3DigestForState(this.streamingHash);
314
+ const newHashEncoded = `hash_z${base58.encode(newHash)}`;
315
+ const signature = this.crypto.sign(
316
+ signerAgent.currentSignerSecret(),
317
+ newHashEncoded,
318
+ );
319
+ this.transactions.push(transaction);
320
+ this.lastSignature = signature;
321
+
322
+ return signature;
323
+ }
324
+
325
+ addNewPrivateTransaction(
326
+ signerAgent: ControlledAccountOrAgent,
327
+ changes: JsonValue[],
328
+ keyID: KeyID,
329
+ keySecret: KeySecret,
330
+ madeAt: number,
331
+ ): { signature: Signature; transaction: PrivateTransaction } {
332
+ const encryptedChanges = this.crypto.encrypt(changes, keySecret, {
333
+ in: this.coID,
334
+ tx: { sessionID: this.sessionID, txIndex: this.transactions.length },
335
+ });
336
+ const tx = {
337
+ encryptedChanges: encryptedChanges,
338
+ madeAt: madeAt,
339
+ privacy: "private",
340
+ keyUsed: keyID,
341
+ } satisfies Transaction;
342
+ const signature = this.internalAddNewTransaction(
343
+ stableStringify(tx),
344
+ signerAgent,
345
+ );
346
+ return {
347
+ signature: signature as Signature,
348
+ transaction: tx,
349
+ };
350
+ }
351
+
352
+ addNewTrustingTransaction(
353
+ signerAgent: ControlledAccountOrAgent,
354
+ changes: JsonValue[],
355
+ madeAt: number,
356
+ ): { signature: Signature; transaction: TrustingTransaction } {
357
+ const tx = {
358
+ changes: stableStringify(changes),
359
+ madeAt: madeAt,
360
+ privacy: "trusting",
361
+ } satisfies Transaction;
362
+ const signature = this.internalAddNewTransaction(
363
+ stableStringify(tx),
364
+ signerAgent,
365
+ );
366
+ return {
367
+ signature: signature as Signature,
368
+ transaction: tx,
369
+ };
370
+ }
371
+
372
+ decryptNextTransactionChangesJson(
373
+ txIndex: number,
374
+ keySecret: KeySecret,
375
+ ): string {
376
+ const txJson = this.transactions[txIndex];
377
+ if (!txJson) {
378
+ throw new Error("Transaction not found");
379
+ }
380
+ const tx = JSON.parse(txJson) as Transaction;
381
+ if (tx.privacy === "private") {
382
+ const nOnceMaterial = {
383
+ in: this.coID,
384
+ tx: { sessionID: this.sessionID, txIndex: txIndex },
385
+ };
386
+
387
+ const nOnce = this.crypto.generateJsonNonce(nOnceMaterial);
388
+
389
+ const ciphertext = base64URLtoBytes(
390
+ tx.encryptedChanges.substring("encrypted_U".length),
391
+ );
392
+ const keySecretBytes = base58.decode(
393
+ keySecret.substring("keySecret_z".length),
394
+ );
395
+ const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
396
+
397
+ return textDecoder.decode(plaintext);
398
+ } else {
399
+ return tx.changes;
400
+ }
401
+ }
402
+
403
+ free(): void {
404
+ // no-op
405
+ }
202
406
  }