@evolu/common 8.15.1 → 8.17.0
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/src/Console.d.ts +24 -46
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +34 -29
- package/dist/src/Crypto.d.ts +21 -3
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +18 -2
- package/dist/src/Error.d.ts +10 -6
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +52 -20
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +16 -12
- package/dist/src/Task.d.ts +3 -1
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +15 -13
- package/dist/src/Worker.d.ts +3 -0
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +6 -1
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +7 -7
- package/dist/src/local-first/Owner.d.ts +20 -8
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +23 -10
- package/dist/src/local-first/Schema.d.ts +1 -1
- package/dist/src/local-first/Shared.d.ts +55 -39
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +264 -318
- package/package.json +1 -1
- package/src/Console.test.ts +75 -247
- package/src/Console.ts +68 -110
- package/src/Crypto.ts +23 -3
- package/src/Error.test.ts +85 -0
- package/src/Error.ts +48 -22
- package/src/Sqlite.ts +19 -11
- package/src/Task.test.ts +46 -0
- package/src/Task.ts +17 -13
- package/src/Worker.ts +6 -1
- package/src/local-first/Db.ts +12 -8
- package/src/local-first/Owner.test.ts +52 -1
- package/src/local-first/Owner.ts +26 -7
- package/src/local-first/Shared.test.ts +414 -39
- package/src/local-first/Shared.ts +386 -438
package/src/Task.ts
CHANGED
|
@@ -2136,20 +2136,21 @@ export type RunDefaultDeps = ConsoleDep &
|
|
|
2136
2136
|
/**
|
|
2137
2137
|
* Creates {@link RunDefaultDeps}.
|
|
2138
2138
|
*
|
|
2139
|
+
* The leak detector reports to the provided console, or to a default one.
|
|
2140
|
+
*
|
|
2139
2141
|
* @group Run
|
|
2140
2142
|
*/
|
|
2141
|
-
export const createRunDefaultDeps = (
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
};
|
|
2143
|
+
export const createRunDefaultDeps = ({
|
|
2144
|
+
console = createConsole(),
|
|
2145
|
+
}: Partial<ConsoleDep> = {}): RunDefaultDeps => ({
|
|
2146
|
+
console,
|
|
2147
|
+
leakDetector: isDev ? createLeakDetector({ console }) : noopLeakDetector,
|
|
2148
|
+
nativeFetch: globalThis.fetch.bind(globalThis),
|
|
2149
|
+
randomBytes: createRandomBytes(),
|
|
2150
|
+
random: createRandom(),
|
|
2151
|
+
reportDefect: reportDefectAfterMicrotask,
|
|
2152
|
+
time: createTime(),
|
|
2153
|
+
});
|
|
2153
2154
|
|
|
2154
2155
|
/**
|
|
2155
2156
|
* Factory type for creating root {@link DisposableRun} instances.
|
|
@@ -2198,7 +2199,7 @@ export const createRun: CreateRun = <D extends object>(
|
|
|
2198
2199
|
deps?: RunCustomDeps<D>,
|
|
2199
2200
|
): DisposableRun<D> =>
|
|
2200
2201
|
createRunInternal<D>({
|
|
2201
|
-
...createRunDefaultDeps(),
|
|
2202
|
+
...createRunDefaultDeps(deps),
|
|
2202
2203
|
...deps,
|
|
2203
2204
|
} as RunDefaultDeps & D);
|
|
2204
2205
|
|
|
@@ -2406,8 +2407,11 @@ export function testCreateRun<D extends object>(
|
|
|
2406
2407
|
export function testCreateRun<D extends object>(
|
|
2407
2408
|
deps?: TestRunDefaultDeps | RunCustomDeps<D>,
|
|
2408
2409
|
): DisposableRun<TestRunDefaultDeps & D> {
|
|
2410
|
+
// As in createRun, the default leak detector reports to a passed console.
|
|
2411
|
+
const { console }: Partial<ConsoleDep> = deps ?? {};
|
|
2409
2412
|
return createRunInternal<TestRunDefaultDeps & D>({
|
|
2410
2413
|
...testCreateDeps(),
|
|
2414
|
+
...(console && { leakDetector: testCreateLeakDetector({ console }) }),
|
|
2411
2415
|
...deps,
|
|
2412
2416
|
} as TestRunDefaultDeps & D);
|
|
2413
2417
|
}
|
package/src/Worker.ts
CHANGED
|
@@ -656,6 +656,9 @@ export const testCreateMessagePort: CreateMessagePort = <Input, Output = never>(
|
|
|
656
656
|
/**
|
|
657
657
|
* Creates an in-memory {@link BroadcastChannel} for testing.
|
|
658
658
|
*
|
|
659
|
+
* Like a native channel, it structured-clones each message, so posting a value
|
|
660
|
+
* that cannot be cloned throws.
|
|
661
|
+
*
|
|
659
662
|
* @group Testing
|
|
660
663
|
*/
|
|
661
664
|
export const testCreateBroadcastChannel = <Input, Output = Input>(
|
|
@@ -667,7 +670,9 @@ export const testCreateBroadcastChannel = <Input, Output = Input>(
|
|
|
667
670
|
const disposables = disposer.move();
|
|
668
671
|
|
|
669
672
|
return {
|
|
670
|
-
postMessage:
|
|
673
|
+
postMessage: (message) => {
|
|
674
|
+
channel.postMessage(structuredClone(message));
|
|
675
|
+
},
|
|
671
676
|
get onMessage() {
|
|
672
677
|
return channel.onMessage;
|
|
673
678
|
},
|
package/src/local-first/Db.ts
CHANGED
|
@@ -137,7 +137,10 @@ import type {
|
|
|
137
137
|
EvoluInput,
|
|
138
138
|
SharedWorkerId,
|
|
139
139
|
} from "./Shared.ts";
|
|
140
|
-
import {
|
|
140
|
+
import {
|
|
141
|
+
consoleEntryOrErrorBroadcastChannelName,
|
|
142
|
+
postConsoleEntry,
|
|
143
|
+
} from "./Shared.ts";
|
|
141
144
|
import {
|
|
142
145
|
createBaseSqliteStorage,
|
|
143
146
|
createBaseSqliteStorageTables,
|
|
@@ -229,6 +232,13 @@ export const startDbWorker =
|
|
|
229
232
|
self.onMessage = (message) => resolve(ok(message));
|
|
230
233
|
}),
|
|
231
234
|
);
|
|
235
|
+
// Tabs report the errors this worker logs as an UnknownError, so it
|
|
236
|
+
// forwards them even for a silent app, whose tabs do not print them.
|
|
237
|
+
deps.console.setLevel(
|
|
238
|
+
initMessage.consoleLevel === "silent"
|
|
239
|
+
? "error"
|
|
240
|
+
: initMessage.consoleLevel,
|
|
241
|
+
);
|
|
232
242
|
|
|
233
243
|
const port = disposer.use(
|
|
234
244
|
deps.createMessagePort<DbWorkerOutput, DbWorkerInput>(initMessage.port),
|
|
@@ -242,11 +252,7 @@ export const startDbWorker =
|
|
|
242
252
|
disposer.defer(
|
|
243
253
|
deps.consoleStoreOutputEntry.subscribe(() => {
|
|
244
254
|
const entry = deps.consoleStoreOutputEntry.get();
|
|
245
|
-
if (entry)
|
|
246
|
-
consoleEntryOrErrorBroadcastChannel.postMessage({
|
|
247
|
-
type: "ConsoleEntry",
|
|
248
|
-
entry,
|
|
249
|
-
});
|
|
255
|
+
if (entry) postConsoleEntry(consoleEntryOrErrorBroadcastChannel, entry);
|
|
250
256
|
}),
|
|
251
257
|
);
|
|
252
258
|
|
|
@@ -310,7 +316,6 @@ export const startDbWorker =
|
|
|
310
316
|
// waits on. The tab leader lock is unaffected.
|
|
311
317
|
port.postMessage({
|
|
312
318
|
type: "LeaderRefused",
|
|
313
|
-
name: initMessage.name,
|
|
314
319
|
error: startup.error,
|
|
315
320
|
});
|
|
316
321
|
return ok();
|
|
@@ -322,7 +327,6 @@ export const startDbWorker =
|
|
|
322
327
|
|
|
323
328
|
port.postMessage({
|
|
324
329
|
type: "LeaderAcquired",
|
|
325
|
-
name: initMessage.name,
|
|
326
330
|
clock: initialClock,
|
|
327
331
|
});
|
|
328
332
|
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
2
|
+
import * as bip39 from "@scure/bip39";
|
|
3
|
+
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
1
4
|
import { eqData } from "../Eq.ts";
|
|
2
5
|
import { test } from "node:test";
|
|
3
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
assertEqual,
|
|
8
|
+
assertFalse,
|
|
9
|
+
assertNotUndefined,
|
|
10
|
+
assertTrue,
|
|
11
|
+
} from "../Assert.ts";
|
|
4
12
|
|
|
5
13
|
import {
|
|
6
14
|
createAppOwner,
|
|
7
15
|
createOwnerSecret,
|
|
8
16
|
deriveShardOwner,
|
|
9
17
|
mnemonicToOwnerSecret,
|
|
18
|
+
OwnerSecret,
|
|
10
19
|
ownerIdBytesToOwnerId,
|
|
11
20
|
ownerIdToOwnerIdBytes,
|
|
12
21
|
ownerSecretToMnemonic,
|
|
@@ -14,6 +23,7 @@ import {
|
|
|
14
23
|
testOwnerSecret,
|
|
15
24
|
} from "./Owner.ts";
|
|
16
25
|
import { testCreateDeps } from "../Task.ts";
|
|
26
|
+
import { Mnemonic } from "../Type.ts";
|
|
17
27
|
|
|
18
28
|
const testOwnerSecret2 = createOwnerSecret(testCreateDeps({ seed: "owner-2" }));
|
|
19
29
|
|
|
@@ -31,6 +41,47 @@ test("ownerSecretToMnemonic and mnemonicToOwnerSecret are inverses", () => {
|
|
|
31
41
|
assertEqual(backToSecret, secret);
|
|
32
42
|
});
|
|
33
43
|
|
|
44
|
+
test("mnemonicToOwnerSecret converts every BIP-39 mnemonic length", () => {
|
|
45
|
+
for (const [words, bytes] of [
|
|
46
|
+
[12, 16],
|
|
47
|
+
[15, 20],
|
|
48
|
+
[18, 24],
|
|
49
|
+
[21, 28],
|
|
50
|
+
[24, 32],
|
|
51
|
+
] as const) {
|
|
52
|
+
const entropy = new Uint8Array(bytes).fill(words);
|
|
53
|
+
const mnemonic = Mnemonic.orThrow(
|
|
54
|
+
bip39.entropyToMnemonic(entropy, wordlist),
|
|
55
|
+
);
|
|
56
|
+
const secret = mnemonicToOwnerSecret(mnemonic);
|
|
57
|
+
|
|
58
|
+
assertEqual(mnemonic.split(" ").length, words);
|
|
59
|
+
assertTrue(OwnerSecret.is(secret));
|
|
60
|
+
assertEqual(secret, entropy);
|
|
61
|
+
assertEqual(ownerSecretToMnemonic(secret), mnemonic);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("OwnerSecret rejects lengths that no mnemonic holds", () => {
|
|
66
|
+
assertFalse(OwnerSecret.is(new Uint8Array(17)));
|
|
67
|
+
assertFalse(OwnerSecret.is(new Uint8Array(64)));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("createAppOwner derives fixed keys from a 12-word mnemonic", () => {
|
|
71
|
+
const owner = createAppOwner(
|
|
72
|
+
mnemonicToOwnerSecret(
|
|
73
|
+
Mnemonic.orThrow("all all all all all all all all all all all all"),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
assertEqual(owner.id, "njGMKFwCtldekpIYmB-VKA");
|
|
78
|
+
assertEqual(
|
|
79
|
+
bytesToHex(owner.encryptionKey),
|
|
80
|
+
"d9cae8d1e141e9f4824ba9b56b9b991d242c1a37f48a146053c3d69f432c5599",
|
|
81
|
+
);
|
|
82
|
+
assertEqual(bytesToHex(owner.writeKey), "8d77d62517c4d525223647a189261ab2");
|
|
83
|
+
});
|
|
84
|
+
|
|
34
85
|
test("createAppOwner is deterministic", () => {
|
|
35
86
|
const owner1 = createAppOwner(testOwnerSecret);
|
|
36
87
|
const owner2 = createAppOwner(testOwnerSecret);
|
package/src/local-first/Owner.ts
CHANGED
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
createSlip21,
|
|
49
49
|
EncryptionKey,
|
|
50
50
|
Entropy16,
|
|
51
|
+
Entropy20,
|
|
52
|
+
Entropy24,
|
|
53
|
+
Entropy28,
|
|
51
54
|
Entropy32,
|
|
52
55
|
testCreateRandomBytes,
|
|
53
56
|
} from "../Crypto.ts";
|
|
@@ -62,6 +65,7 @@ import {
|
|
|
62
65
|
Mnemonic,
|
|
63
66
|
NonNegativeInt,
|
|
64
67
|
type Typed,
|
|
68
|
+
union,
|
|
65
69
|
} from "../Type.ts";
|
|
66
70
|
import type { EncryptedDbChange, Storage } from "./Storage.ts";
|
|
67
71
|
import { TimestampBytes } from "./Timestamp.ts";
|
|
@@ -172,18 +176,33 @@ export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
|
|
|
172
176
|
deps.randomBytes.create(16) as OwnerWriteKey;
|
|
173
177
|
|
|
174
178
|
/**
|
|
175
|
-
*
|
|
179
|
+
* BIP-39 entropy used to derive {@link Owner} keys.
|
|
176
180
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
181
|
+
* A {@link Mnemonic} represents an OwnerSecret, so each valid mnemonic converts
|
|
182
|
+
* to exactly one secret with {@link mnemonicToOwnerSecret} and back with
|
|
183
|
+
* {@link ownerSecretToMnemonic}. A 24-word mnemonic holds 32 bytes, a 12-word
|
|
184
|
+
* mnemonic holds 16 bytes, and 15, 18, and 21 words hold 20, 24, and 28 bytes.
|
|
185
|
+
*
|
|
186
|
+
* {@link createOwnerSecret} generates 32 random bytes, which keep the owner
|
|
187
|
+
* post-quantum safe. The {@link OwnerId} is public and derived from the secret,
|
|
188
|
+
* so anyone who stores an owner's encrypted data can test guesses of the secret
|
|
189
|
+
* offline, and a quantum computer needs only about the square root of the
|
|
190
|
+
* guesses. 32 random bytes leave about 128 bits of security, while 16 bytes
|
|
191
|
+
* leave about 64. A secret derived from a smaller root, such as a 20-word
|
|
192
|
+
* SLIP-39 share, is no stronger than that root, whatever its length. Shorter
|
|
193
|
+
* secrets are accepted for owners created outside Evolu, typically from 12-word
|
|
194
|
+
* mnemonics.
|
|
179
195
|
*
|
|
180
196
|
* @group Core
|
|
181
197
|
*/
|
|
182
|
-
export const OwnerSecret = /*#__PURE__*/ brand(
|
|
198
|
+
export const OwnerSecret = /*#__PURE__*/ brand(
|
|
199
|
+
"OwnerSecret",
|
|
200
|
+
/*#__PURE__*/ union(Entropy16, Entropy20, Entropy24, Entropy28, Entropy32),
|
|
201
|
+
);
|
|
183
202
|
export type OwnerSecret = typeof OwnerSecret.Output;
|
|
184
203
|
|
|
185
204
|
/**
|
|
186
|
-
* Creates a cryptographically random {@link OwnerSecret}.
|
|
205
|
+
* Creates a cryptographically random 32-byte {@link OwnerSecret}.
|
|
187
206
|
*
|
|
188
207
|
* @group Core
|
|
189
208
|
*/
|
|
@@ -210,12 +229,12 @@ export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
|
|
|
210
229
|
bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
|
|
211
230
|
|
|
212
231
|
/**
|
|
213
|
-
* Converts a {@link Mnemonic} to
|
|
232
|
+
* Converts a {@link Mnemonic} to the {@link OwnerSecret} it represents.
|
|
214
233
|
*
|
|
215
234
|
* @group Core
|
|
216
235
|
*/
|
|
217
236
|
export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
|
|
218
|
-
bip39.mnemonicToEntropy(mnemonic, wordlist)
|
|
237
|
+
OwnerSecret.orThrow(bip39.mnemonicToEntropy(mnemonic, wordlist));
|
|
219
238
|
|
|
220
239
|
/**
|
|
221
240
|
* Creates an {@link Owner} from a {@link OwnerSecret} using SLIP-21 key
|