cojson 0.1.10 → 0.1.12
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.
- package/dist/base64url.d.ts +2 -0
- package/dist/base64url.js +59 -0
- package/dist/base64url.js.map +1 -0
- package/dist/coValueCore.d.ts +8 -0
- package/dist/coValueCore.js +110 -11
- package/dist/coValueCore.js.map +1 -1
- package/dist/coValues/coStream.d.ts +1 -1
- package/dist/coValues/coStream.js +18 -11
- package/dist/coValues/coStream.js.map +1 -1
- package/dist/crypto.d.ts +3 -3
- package/dist/crypto.js +48 -19
- package/dist/crypto.js.map +1 -1
- package/dist/fastJsonStableStringify.d.ts +1 -0
- package/dist/fastJsonStableStringify.js +53 -0
- package/dist/fastJsonStableStringify.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/sync.js +8 -1
- package/dist/sync.js.map +1 -1
- package/package.json +3 -4
- package/src/account.test.ts +5 -0
- package/src/base64url.test.ts +32 -0
- package/src/base64url.ts +68 -0
- package/src/coValue.test.ts +28 -0
- package/src/coValueCore.test.ts +5 -0
- package/src/coValueCore.ts +158 -15
- package/src/coValues/coStream.ts +31 -16
- package/src/crypto.test.ts +5 -0
- package/src/crypto.ts +59 -22
- package/src/fastJsonStableStringify.ts +54 -0
- package/src/group.test.ts +5 -1
- package/src/index.ts +5 -0
- package/src/permissions.test.ts +5 -1
- package/src/sync.test.ts +6 -6
- package/src/sync.ts +16 -1
package/src/coValueCore.ts
CHANGED
|
@@ -100,6 +100,7 @@ export class CoValueCore {
|
|
|
100
100
|
_sessions: { [key: SessionID]: SessionLog };
|
|
101
101
|
_cachedContent?: CoValueImpl;
|
|
102
102
|
listeners: Set<(content?: CoValueImpl) => void> = new Set();
|
|
103
|
+
_decryptionCache: {[key: Encrypted<JsonValue[], JsonValue>]: JsonValue[] | undefined} = {}
|
|
103
104
|
|
|
104
105
|
constructor(
|
|
105
106
|
header: CoValueHeader,
|
|
@@ -186,10 +187,16 @@ export class CoValueCore {
|
|
|
186
187
|
return false;
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
// const beforeHash = performance.now();
|
|
189
191
|
const { expectedNewHash, newStreamingHash } = this.expectedNewHashAfter(
|
|
190
192
|
sessionID,
|
|
191
193
|
newTransactions
|
|
192
194
|
);
|
|
195
|
+
// const afterHash = performance.now();
|
|
196
|
+
// console.log(
|
|
197
|
+
// "Hashing took",
|
|
198
|
+
// afterHash - beforeHash
|
|
199
|
+
// );
|
|
193
200
|
|
|
194
201
|
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
|
195
202
|
console.warn("Invalid hash", {
|
|
@@ -199,6 +206,7 @@ export class CoValueCore {
|
|
|
199
206
|
return false;
|
|
200
207
|
}
|
|
201
208
|
|
|
209
|
+
// const beforeVerify = performance.now();
|
|
202
210
|
if (!verify(newSignature, expectedNewHash, signerID)) {
|
|
203
211
|
console.warn(
|
|
204
212
|
"Invalid signature",
|
|
@@ -208,6 +216,11 @@ export class CoValueCore {
|
|
|
208
216
|
);
|
|
209
217
|
return false;
|
|
210
218
|
}
|
|
219
|
+
// const afterVerify = performance.now();
|
|
220
|
+
// console.log(
|
|
221
|
+
// "Verify took",
|
|
222
|
+
// afterVerify - beforeVerify
|
|
223
|
+
// );
|
|
211
224
|
|
|
212
225
|
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
|
213
226
|
|
|
@@ -222,10 +235,105 @@ export class CoValueCore {
|
|
|
222
235
|
|
|
223
236
|
this._cachedContent = undefined;
|
|
224
237
|
|
|
225
|
-
|
|
238
|
+
if (this.listeners.size > 0) {
|
|
239
|
+
const content = this.getCurrentContent();
|
|
240
|
+
for (const listener of this.listeners) {
|
|
241
|
+
listener(content);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
async tryAddTransactionsAsync(
|
|
249
|
+
sessionID: SessionID,
|
|
250
|
+
newTransactions: Transaction[],
|
|
251
|
+
givenExpectedNewHash: Hash | undefined,
|
|
252
|
+
newSignature: Signature
|
|
253
|
+
): Promise<boolean> {
|
|
254
|
+
const signerID = getAgentSignerID(
|
|
255
|
+
this.node.resolveAccountAgent(
|
|
256
|
+
accountOrAgentIDfromSessionID(sessionID),
|
|
257
|
+
"Expected to know signer of transaction"
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!signerID) {
|
|
262
|
+
console.warn(
|
|
263
|
+
"Unknown agent",
|
|
264
|
+
accountOrAgentIDfromSessionID(sessionID)
|
|
265
|
+
);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const nTxBefore = this.sessions[sessionID]?.transactions.length ?? 0;
|
|
270
|
+
|
|
271
|
+
// const beforeHash = performance.now();
|
|
272
|
+
const { expectedNewHash, newStreamingHash } = await this.expectedNewHashAfterAsync(
|
|
273
|
+
sessionID,
|
|
274
|
+
newTransactions
|
|
275
|
+
);
|
|
276
|
+
// const afterHash = performance.now();
|
|
277
|
+
// console.log(
|
|
278
|
+
// "Hashing took",
|
|
279
|
+
// afterHash - beforeHash
|
|
280
|
+
// );
|
|
281
|
+
|
|
282
|
+
const nTxAfter = this.sessions[sessionID]?.transactions.length ?? 0;
|
|
283
|
+
|
|
284
|
+
if (nTxAfter !== nTxBefore) {
|
|
285
|
+
const newTransactionLengthBefore = newTransactions.length;
|
|
286
|
+
newTransactions = newTransactions.slice((nTxAfter - nTxBefore));
|
|
287
|
+
console.warn("Transactions changed while async hashing", {
|
|
288
|
+
nTxBefore,
|
|
289
|
+
nTxAfter,
|
|
290
|
+
newTransactionLengthBefore,
|
|
291
|
+
remainingNewTransactions: newTransactions.length,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (givenExpectedNewHash && givenExpectedNewHash !== expectedNewHash) {
|
|
296
|
+
console.warn("Invalid hash", {
|
|
297
|
+
expectedNewHash,
|
|
298
|
+
givenExpectedNewHash,
|
|
299
|
+
});
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// const beforeVerify = performance.now();
|
|
304
|
+
if (!verify(newSignature, expectedNewHash, signerID)) {
|
|
305
|
+
console.warn(
|
|
306
|
+
"Invalid signature",
|
|
307
|
+
newSignature,
|
|
308
|
+
expectedNewHash,
|
|
309
|
+
signerID
|
|
310
|
+
);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
// const afterVerify = performance.now();
|
|
314
|
+
// console.log(
|
|
315
|
+
// "Verify took",
|
|
316
|
+
// afterVerify - beforeVerify
|
|
317
|
+
// );
|
|
318
|
+
|
|
319
|
+
const transactions = this.sessions[sessionID]?.transactions ?? [];
|
|
320
|
+
|
|
321
|
+
transactions.push(...newTransactions);
|
|
322
|
+
|
|
323
|
+
this._sessions[sessionID] = {
|
|
324
|
+
transactions,
|
|
325
|
+
lastHash: expectedNewHash,
|
|
326
|
+
streamingHash: newStreamingHash,
|
|
327
|
+
lastSignature: newSignature,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
this._cachedContent = undefined;
|
|
331
|
+
|
|
332
|
+
if (this.listeners.size > 0) {
|
|
333
|
+
const content = this.getCurrentContent();
|
|
334
|
+
for (const listener of this.listeners) {
|
|
335
|
+
listener(content);
|
|
336
|
+
}
|
|
229
337
|
}
|
|
230
338
|
|
|
231
339
|
return true;
|
|
@@ -259,6 +367,32 @@ export class CoValueCore {
|
|
|
259
367
|
};
|
|
260
368
|
}
|
|
261
369
|
|
|
370
|
+
async expectedNewHashAfterAsync(
|
|
371
|
+
sessionID: SessionID,
|
|
372
|
+
newTransactions: Transaction[]
|
|
373
|
+
): Promise<{ expectedNewHash: Hash; newStreamingHash: StreamingHash }> {
|
|
374
|
+
const streamingHash =
|
|
375
|
+
this.sessions[sessionID]?.streamingHash.clone() ??
|
|
376
|
+
new StreamingHash();
|
|
377
|
+
let before = performance.now();
|
|
378
|
+
for (const transaction of newTransactions) {
|
|
379
|
+
streamingHash.update(transaction)
|
|
380
|
+
const after = performance.now();
|
|
381
|
+
if (after - before > 1) {
|
|
382
|
+
console.log("Hashing blocked for", after - before);
|
|
383
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
384
|
+
before = performance.now();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const newStreamingHash = streamingHash.clone();
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
expectedNewHash: streamingHash.digest(),
|
|
392
|
+
newStreamingHash,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
262
396
|
makeTransaction(
|
|
263
397
|
changes: JsonValue[],
|
|
264
398
|
privacy: "private" | "trusting"
|
|
@@ -276,14 +410,18 @@ export class CoValueCore {
|
|
|
276
410
|
);
|
|
277
411
|
}
|
|
278
412
|
|
|
413
|
+
const encrypted = encryptForTransaction(changes, keySecret, {
|
|
414
|
+
in: this.id,
|
|
415
|
+
tx: this.nextTransactionID(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
this._decryptionCache[encrypted] = changes;
|
|
419
|
+
|
|
279
420
|
transaction = {
|
|
280
421
|
privacy: "private",
|
|
281
422
|
madeAt,
|
|
282
423
|
keyUsed: keyID,
|
|
283
|
-
encryptedChanges:
|
|
284
|
-
in: this.id,
|
|
285
|
-
tx: this.nextTransactionID(),
|
|
286
|
-
}),
|
|
424
|
+
encryptedChanges: encrypted,
|
|
287
425
|
};
|
|
288
426
|
} else {
|
|
289
427
|
transaction = {
|
|
@@ -359,14 +497,19 @@ export class CoValueCore {
|
|
|
359
497
|
if (!readKey) {
|
|
360
498
|
return undefined;
|
|
361
499
|
} else {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
500
|
+
let decrytedChanges = this._decryptionCache[tx.encryptedChanges];
|
|
501
|
+
|
|
502
|
+
if (!decrytedChanges) {
|
|
503
|
+
decrytedChanges = decryptForTransaction(
|
|
504
|
+
tx.encryptedChanges,
|
|
505
|
+
readKey,
|
|
506
|
+
{
|
|
507
|
+
in: this.id,
|
|
508
|
+
tx: txID,
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
this._decryptionCache[tx.encryptedChanges] = decrytedChanges;
|
|
512
|
+
}
|
|
370
513
|
|
|
371
514
|
if (!decrytedChanges) {
|
|
372
515
|
console.error(
|
package/src/coValues/coStream.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CoID, ReadableCoValue, WriteableCoValue } from "../coValue.js";
|
|
|
3
3
|
import { CoValueCore } from "../coValueCore.js";
|
|
4
4
|
import { Group } from "../group.js";
|
|
5
5
|
import { SessionID } from "../ids.js";
|
|
6
|
-
import {
|
|
6
|
+
import { base64URLtoBytes, bytesToBase64url } from "../base64url.js";
|
|
7
7
|
|
|
8
8
|
export type BinaryChunkInfo = {
|
|
9
9
|
mimeType: string;
|
|
@@ -17,7 +17,7 @@ export type BinaryStreamStart = {
|
|
|
17
17
|
|
|
18
18
|
export type BinaryStreamChunk = {
|
|
19
19
|
type: "chunk";
|
|
20
|
-
chunk: `
|
|
20
|
+
chunk: `binary_U${string}`;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export type BinaryStreamEnd = {
|
|
@@ -47,6 +47,7 @@ export class CoStream<
|
|
|
47
47
|
this.id = core.id as CoID<CoStream<T, Meta>>;
|
|
48
48
|
this.core = core;
|
|
49
49
|
this.items = {};
|
|
50
|
+
this.fillFromCoValue();
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
get meta(): Meta {
|
|
@@ -110,6 +111,8 @@ export class CoStream<
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
const binary_U_prefixLength = 8; // "binary_U".length;
|
|
115
|
+
|
|
113
116
|
export class BinaryCoStream<
|
|
114
117
|
Meta extends BinaryCoStreamMeta = { type: "binary" }
|
|
115
118
|
>
|
|
@@ -121,6 +124,7 @@ export class BinaryCoStream<
|
|
|
121
124
|
getBinaryChunks():
|
|
122
125
|
| (BinaryChunkInfo & { chunks: Uint8Array[]; finished: boolean })
|
|
123
126
|
| undefined {
|
|
127
|
+
const before = performance.now();
|
|
124
128
|
const items = this.getSingleStream();
|
|
125
129
|
|
|
126
130
|
if (!items) return;
|
|
@@ -134,15 +138,13 @@ export class BinaryCoStream<
|
|
|
134
138
|
|
|
135
139
|
const chunks: Uint8Array[] = [];
|
|
136
140
|
|
|
141
|
+
let finished = false;
|
|
142
|
+
let totalLength = 0;
|
|
143
|
+
|
|
137
144
|
for (const item of items.slice(1)) {
|
|
138
145
|
if (item.type === "end") {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
fileName: start.fileName,
|
|
142
|
-
totalSizeBytes: start.totalSizeBytes,
|
|
143
|
-
chunks,
|
|
144
|
-
finished: true,
|
|
145
|
-
};
|
|
146
|
+
finished = true;
|
|
147
|
+
break;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
if (item.type !== "chunk") {
|
|
@@ -150,15 +152,25 @@ export class BinaryCoStream<
|
|
|
150
152
|
return undefined;
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
|
|
155
|
+
const chunk = base64URLtoBytes(
|
|
156
|
+
item.chunk.slice(binary_U_prefixLength)
|
|
157
|
+
);
|
|
158
|
+
totalLength += chunk.length;
|
|
159
|
+
chunks.push(chunk);
|
|
154
160
|
}
|
|
155
161
|
|
|
162
|
+
const after = performance.now();
|
|
163
|
+
console.log(
|
|
164
|
+
"getBinaryChunks bandwidth in MB/s",
|
|
165
|
+
(1000 * totalLength) / (after - before) / (1024 * 1024)
|
|
166
|
+
);
|
|
167
|
+
|
|
156
168
|
return {
|
|
157
169
|
mimeType: start.mimeType,
|
|
158
170
|
fileName: start.fileName,
|
|
159
171
|
totalSizeBytes: start.totalSizeBytes,
|
|
160
172
|
chunks,
|
|
161
|
-
finished
|
|
173
|
+
finished,
|
|
162
174
|
};
|
|
163
175
|
}
|
|
164
176
|
|
|
@@ -205,10 +217,7 @@ export class WriteableBinaryCoStream<
|
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
/** @internal */
|
|
208
|
-
push(
|
|
209
|
-
item: BinaryStreamItem,
|
|
210
|
-
privacy: "private" | "trusting" = "private"
|
|
211
|
-
) {
|
|
220
|
+
push(item: BinaryStreamItem, privacy: "private" | "trusting" = "private") {
|
|
212
221
|
WriteableCoStream.prototype.push.call(this, item, privacy);
|
|
213
222
|
}
|
|
214
223
|
|
|
@@ -229,13 +238,19 @@ export class WriteableBinaryCoStream<
|
|
|
229
238
|
chunk: Uint8Array,
|
|
230
239
|
privacy: "private" | "trusting" = "private"
|
|
231
240
|
) {
|
|
241
|
+
const before = performance.now();
|
|
232
242
|
this.push(
|
|
233
243
|
{
|
|
234
244
|
type: "chunk",
|
|
235
|
-
chunk: `
|
|
245
|
+
chunk: `binary_U${bytesToBase64url(chunk)}`,
|
|
236
246
|
} satisfies BinaryStreamChunk,
|
|
237
247
|
privacy
|
|
238
248
|
);
|
|
249
|
+
const after = performance.now();
|
|
250
|
+
console.log(
|
|
251
|
+
"pushBinaryStreamChunk bandwidth in MB/s",
|
|
252
|
+
(1000 * chunk.length) / (after - before) / (1024 * 1024)
|
|
253
|
+
);
|
|
239
254
|
}
|
|
240
255
|
|
|
241
256
|
endBinaryStream(privacy: "private" | "trusting" = "private") {
|
package/src/crypto.test.ts
CHANGED
|
@@ -21,6 +21,11 @@ import { xsalsa20_poly1305 } from "@noble/ciphers/salsa";
|
|
|
21
21
|
import { blake3 } from "@noble/hashes/blake3";
|
|
22
22
|
import stableStringify from "fast-json-stable-stringify";
|
|
23
23
|
import { SessionID } from './ids.js';
|
|
24
|
+
import { cojsonReady } from './index.js';
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
await cojsonReady;
|
|
28
|
+
});
|
|
24
29
|
|
|
25
30
|
test("Signatures round-trip and use stable stringify", () => {
|
|
26
31
|
const data = { b: "world", a: "hello" };
|
package/src/crypto.ts
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { ed25519, x25519 } from "@noble/curves/ed25519";
|
|
2
2
|
import { xsalsa20_poly1305, xsalsa20 } from "@noble/ciphers/salsa";
|
|
3
3
|
import { JsonValue } from "./jsonValue.js";
|
|
4
|
-
import { base58
|
|
5
|
-
import stableStringify from "fast-json-stable-stringify";
|
|
6
|
-
import { blake3 } from "@noble/hashes/blake3";
|
|
4
|
+
import { base58 } from "@scure/base";
|
|
7
5
|
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
|
8
6
|
import { AgentID, RawCoID, TransactionID } from "./ids.js";
|
|
7
|
+
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
|
8
|
+
|
|
9
|
+
import { createBLAKE3 } from 'hash-wasm';
|
|
10
|
+
import { stableStringify } from "./fastJsonStableStringify.js";
|
|
11
|
+
|
|
12
|
+
let blake3Instance: Awaited<ReturnType<typeof createBLAKE3>>;
|
|
13
|
+
let blake3HashOnce: (data: Uint8Array) => Uint8Array;
|
|
14
|
+
let blake3HashOnceWithContext: (data: Uint8Array, {context}: {context: Uint8Array}) => Uint8Array;
|
|
15
|
+
let blake3incrementalUpdateSLOW_WITH_DEVTOOLS: (state: Uint8Array, data: Uint8Array) => Uint8Array;
|
|
16
|
+
let blake3digestForState: (state: Uint8Array) => Uint8Array;
|
|
17
|
+
|
|
18
|
+
export const cryptoReady = new Promise<void>((resolve) => {
|
|
19
|
+
createBLAKE3().then(bl3 => {
|
|
20
|
+
blake3Instance = bl3;
|
|
21
|
+
blake3HashOnce = (data) => {
|
|
22
|
+
return bl3.init().update(data).digest('binary');
|
|
23
|
+
}
|
|
24
|
+
blake3HashOnceWithContext = (data, {context}) => {
|
|
25
|
+
return bl3.init().update(context).update(data).digest('binary');
|
|
26
|
+
}
|
|
27
|
+
blake3incrementalUpdateSLOW_WITH_DEVTOOLS = (state, data) => {
|
|
28
|
+
bl3.load(state).update(data);
|
|
29
|
+
return bl3.save();
|
|
30
|
+
}
|
|
31
|
+
blake3digestForState = (state) => {
|
|
32
|
+
return bl3.load(state).digest('binary');
|
|
33
|
+
}
|
|
34
|
+
resolve();
|
|
35
|
+
})
|
|
36
|
+
});
|
|
9
37
|
|
|
10
38
|
export type SignerSecret = `signerSecret_z${string}`;
|
|
11
39
|
export type SignerID = `signer_z${string}`;
|
|
@@ -127,7 +155,7 @@ export function seal<T extends JsonValue>(
|
|
|
127
155
|
to: SealerID,
|
|
128
156
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
|
129
157
|
): Sealed<T> {
|
|
130
|
-
const nOnce =
|
|
158
|
+
const nOnce = blake3HashOnce(
|
|
131
159
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
132
160
|
).slice(0, 24);
|
|
133
161
|
|
|
@@ -143,7 +171,7 @@ export function seal<T extends JsonValue>(
|
|
|
143
171
|
plaintext
|
|
144
172
|
);
|
|
145
173
|
|
|
146
|
-
return `sealed_U${
|
|
174
|
+
return `sealed_U${bytesToBase64url(sealedBytes)}` as Sealed<T>;
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
export function unseal<T extends JsonValue>(
|
|
@@ -152,7 +180,7 @@ export function unseal<T extends JsonValue>(
|
|
|
152
180
|
from: SealerID,
|
|
153
181
|
nOnceMaterial: { in: RawCoID; tx: TransactionID }
|
|
154
182
|
): T | undefined {
|
|
155
|
-
const nOnce =
|
|
183
|
+
const nOnce = blake3HashOnce(
|
|
156
184
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
157
185
|
).slice(0, 24);
|
|
158
186
|
|
|
@@ -160,7 +188,7 @@ export function unseal<T extends JsonValue>(
|
|
|
160
188
|
|
|
161
189
|
const senderPub = base58.decode(from.substring("sealer_z".length));
|
|
162
190
|
|
|
163
|
-
const sealedBytes =
|
|
191
|
+
const sealedBytes = base64URLtoBytes(sealed.substring("sealed_U".length));
|
|
164
192
|
|
|
165
193
|
const sharedSecret = x25519.getSharedSecret(sealerPriv, senderPub);
|
|
166
194
|
|
|
@@ -180,28 +208,32 @@ export type Hash = `hash_z${string}`;
|
|
|
180
208
|
|
|
181
209
|
export function secureHash(value: JsonValue): Hash {
|
|
182
210
|
return `hash_z${base58.encode(
|
|
183
|
-
|
|
211
|
+
blake3HashOnce(textEncoder.encode(stableStringify(value)))
|
|
184
212
|
)}`;
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
export class StreamingHash {
|
|
188
|
-
state:
|
|
216
|
+
state: Uint8Array;
|
|
189
217
|
|
|
190
|
-
constructor(fromClone?:
|
|
191
|
-
this.state = fromClone ||
|
|
218
|
+
constructor(fromClone?: Uint8Array) {
|
|
219
|
+
this.state = fromClone || blake3Instance.init().save();
|
|
192
220
|
}
|
|
193
221
|
|
|
194
222
|
update(value: JsonValue) {
|
|
195
|
-
|
|
223
|
+
const encoded = textEncoder.encode(stableStringify(value))
|
|
224
|
+
// const before = performance.now();
|
|
225
|
+
this.state = blake3incrementalUpdateSLOW_WITH_DEVTOOLS(this.state, encoded);
|
|
226
|
+
// const after = performance.now();
|
|
227
|
+
// console.log(`Hashing throughput in MB/s`, 1000 * (encoded.length / (after - before)) / (1024 * 1024));
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
digest(): Hash {
|
|
199
|
-
const hash = this.state
|
|
231
|
+
const hash = blake3digestForState(this.state);
|
|
200
232
|
return `hash_z${base58.encode(hash)}`;
|
|
201
233
|
}
|
|
202
234
|
|
|
203
235
|
clone(): StreamingHash {
|
|
204
|
-
return new StreamingHash(this.state
|
|
236
|
+
return new StreamingHash(new Uint8Array(this.state));
|
|
205
237
|
}
|
|
206
238
|
}
|
|
207
239
|
|
|
@@ -210,7 +242,10 @@ export const shortHashLength = 19;
|
|
|
210
242
|
|
|
211
243
|
export function shortHash(value: JsonValue): ShortHash {
|
|
212
244
|
return `shortHash_z${base58.encode(
|
|
213
|
-
|
|
245
|
+
blake3HashOnce(textEncoder.encode(stableStringify(value))).slice(
|
|
246
|
+
0,
|
|
247
|
+
shortHashLength
|
|
248
|
+
)
|
|
214
249
|
)}`;
|
|
215
250
|
}
|
|
216
251
|
|
|
@@ -237,13 +272,13 @@ function encrypt<T extends JsonValue, N extends JsonValue>(
|
|
|
237
272
|
const keySecretBytes = base58.decode(
|
|
238
273
|
keySecret.substring("keySecret_z".length)
|
|
239
274
|
);
|
|
240
|
-
const nOnce =
|
|
275
|
+
const nOnce = blake3HashOnce(
|
|
241
276
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
242
277
|
).slice(0, 24);
|
|
243
278
|
|
|
244
279
|
const plaintext = textEncoder.encode(stableStringify(value));
|
|
245
280
|
const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext);
|
|
246
|
-
return `encrypted_U${
|
|
281
|
+
return `encrypted_U${bytesToBase64url(ciphertext)}` as Encrypted<T, N>;
|
|
247
282
|
}
|
|
248
283
|
|
|
249
284
|
export function encryptForTransaction<T extends JsonValue>(
|
|
@@ -289,11 +324,11 @@ function decrypt<T extends JsonValue, N extends JsonValue>(
|
|
|
289
324
|
const keySecretBytes = base58.decode(
|
|
290
325
|
keySecret.substring("keySecret_z".length)
|
|
291
326
|
);
|
|
292
|
-
const nOnce =
|
|
327
|
+
const nOnce = blake3HashOnce(
|
|
293
328
|
textEncoder.encode(stableStringify(nOnceMaterial))
|
|
294
329
|
).slice(0, 24);
|
|
295
330
|
|
|
296
|
-
const ciphertext =
|
|
331
|
+
const ciphertext = base64URLtoBytes(
|
|
297
332
|
encrypted.substring("encrypted_U".length)
|
|
298
333
|
);
|
|
299
334
|
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
|
|
@@ -355,15 +390,17 @@ export function newRandomSecretSeed(): Uint8Array {
|
|
|
355
390
|
|
|
356
391
|
export function agentSecretFromSecretSeed(secretSeed: Uint8Array): AgentSecret {
|
|
357
392
|
if (secretSeed.length !== secretSeedLength) {
|
|
358
|
-
throw new Error(
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Secret seed needs to be ${secretSeedLength} bytes long`
|
|
395
|
+
);
|
|
359
396
|
}
|
|
360
397
|
|
|
361
398
|
return `sealerSecret_z${base58.encode(
|
|
362
|
-
|
|
399
|
+
blake3HashOnceWithContext(secretSeed, {
|
|
363
400
|
context: textEncoder.encode("seal"),
|
|
364
401
|
})
|
|
365
402
|
)}/signerSecret_z${base58.encode(
|
|
366
|
-
|
|
403
|
+
blake3HashOnceWithContext(secretSeed, {
|
|
367
404
|
context: textEncoder.encode("sign"),
|
|
368
405
|
})
|
|
369
406
|
)}`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// adapted from fast-json-stable-stringify (https://github.com/epoberezkin/fast-json-stable-stringify)
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
export function stableStringify(data: any): string | undefined {
|
|
5
|
+
const cycles = false;
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
const seen: any[] = [];
|
|
9
|
+
let node = data;
|
|
10
|
+
|
|
11
|
+
if (node && node.toJSON && typeof node.toJSON === "function") {
|
|
12
|
+
node = node.toJSON();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (node === undefined) return;
|
|
16
|
+
if (typeof node == "number") return isFinite(node) ? "" + node : "null";
|
|
17
|
+
if (typeof node !== "object") {
|
|
18
|
+
if (typeof node === "string" && (node.startsWith("encrypted_U") || node.startsWith("binary_U"))) {
|
|
19
|
+
return `"${node}"`;
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify(node);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let i, out;
|
|
25
|
+
if (Array.isArray(node)) {
|
|
26
|
+
out = "[";
|
|
27
|
+
for (i = 0; i < node.length; i++) {
|
|
28
|
+
if (i) out += ",";
|
|
29
|
+
out += stableStringify(node[i]) || "null";
|
|
30
|
+
}
|
|
31
|
+
return out + "]";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (node === null) return "null";
|
|
35
|
+
|
|
36
|
+
if (seen.indexOf(node) !== -1) {
|
|
37
|
+
if (cycles) return JSON.stringify("__cycle__");
|
|
38
|
+
throw new TypeError("Converting circular structure to JSON");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const seenIndex = seen.push(node) - 1;
|
|
42
|
+
const keys = Object.keys(node).sort();
|
|
43
|
+
out = "";
|
|
44
|
+
for (i = 0; i < keys.length; i++) {
|
|
45
|
+
const key = keys[i]!;
|
|
46
|
+
const value = stableStringify(node[key]);
|
|
47
|
+
|
|
48
|
+
if (!value) continue;
|
|
49
|
+
if (out) out += ",";
|
|
50
|
+
out += JSON.stringify(key) + ":" + value;
|
|
51
|
+
}
|
|
52
|
+
seen.splice(seenIndex, 1);
|
|
53
|
+
return "{" + out + "}";
|
|
54
|
+
}
|
package/src/group.test.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream } from "./index";
|
|
1
|
+
import { LocalNode, CoMap, CoList, CoStream, BinaryCoStream, cojsonReady } from "./index";
|
|
2
2
|
import { randomAnonymousAccountAndSessionID } from "./testUtils";
|
|
3
3
|
|
|
4
|
+
beforeEach(async () => {
|
|
5
|
+
await cojsonReady;
|
|
6
|
+
});
|
|
7
|
+
|
|
4
8
|
test("Can create a CoMap in a group", () => {
|
|
5
9
|
const node = new LocalNode(...randomAnonymousAccountAndSessionID());
|
|
6
10
|
|
package/src/index.ts
CHANGED
|
@@ -18,11 +18,13 @@ import {
|
|
|
18
18
|
agentSecretFromSecretSeed,
|
|
19
19
|
secretSeedLength,
|
|
20
20
|
shortHashLength,
|
|
21
|
+
cryptoReady
|
|
21
22
|
} from "./crypto.js";
|
|
22
23
|
import { connectedPeers } from "./streamUtils.js";
|
|
23
24
|
import { AnonymousControlledAccount, ControlledAccount } from "./account.js";
|
|
24
25
|
import { rawCoIDtoBytes, rawCoIDfromBytes } from "./ids.js";
|
|
25
26
|
import { Group, expectGroupContent } from "./group.js";
|
|
27
|
+
import { base64URLtoBytes, bytesToBase64url } from "./base64url.js";
|
|
26
28
|
|
|
27
29
|
import type { SessionID, AgentID } from "./ids.js";
|
|
28
30
|
import type { CoID, CoValueImpl } from "./coValue.js";
|
|
@@ -50,6 +52,8 @@ export const cojsonInternals = {
|
|
|
50
52
|
secretSeedLength,
|
|
51
53
|
shortHashLength,
|
|
52
54
|
expectGroupContent,
|
|
55
|
+
base64URLtoBytes,
|
|
56
|
+
bytesToBase64url
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
export {
|
|
@@ -66,6 +70,7 @@ export {
|
|
|
66
70
|
CoValueCore,
|
|
67
71
|
AnonymousControlledAccount,
|
|
68
72
|
ControlledAccount,
|
|
73
|
+
cryptoReady as cojsonReady,
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
export type {
|
package/src/permissions.test.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
groupWithTwoAdmins,
|
|
18
18
|
groupWithTwoAdminsHighLevel,
|
|
19
19
|
} from "./testUtils.js";
|
|
20
|
-
import { AnonymousControlledAccount } from "./index.js";
|
|
20
|
+
import { AnonymousControlledAccount, cojsonReady } from "./index.js";
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await cojsonReady;
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
test("Initial admin can add another admin to a group", () => {
|
|
23
27
|
groupWithTwoAdmins();
|
package/src/sync.test.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { newRandomSessionID } from "./coValueCore.js";
|
|
2
2
|
import { LocalNode } from "./node.js";
|
|
3
|
-
import {
|
|
3
|
+
import { SyncMessage } from "./sync.js";
|
|
4
4
|
import { expectMap } from "./coValue.js";
|
|
5
5
|
import { MapOpPayload } from "./coValues/coMap.js";
|
|
6
6
|
import { Group } from "./group.js";
|
|
7
|
-
import {
|
|
8
|
-
ReadableStream,
|
|
9
|
-
WritableStream,
|
|
10
|
-
TransformStream,
|
|
11
|
-
} from "isomorphic-streams";
|
|
12
7
|
import {
|
|
13
8
|
randomAnonymousAccountAndSessionID,
|
|
14
9
|
shouldNotResolve,
|
|
@@ -18,6 +13,11 @@ import {
|
|
|
18
13
|
newStreamPair
|
|
19
14
|
} from "./streamUtils.js";
|
|
20
15
|
import { AccountID } from "./account.js";
|
|
16
|
+
import { cojsonReady } from "./index.js";
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
await cojsonReady;
|
|
20
|
+
});
|
|
21
21
|
|
|
22
22
|
test("Node replies with initial tx and header to empty subscribe", async () => {
|
|
23
23
|
const [admin, session] = randomAnonymousAccountAndSessionID();
|