@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.
Files changed (41) hide show
  1. package/dist/src/Console.d.ts +24 -46
  2. package/dist/src/Console.d.ts.map +1 -1
  3. package/dist/src/Console.js +34 -29
  4. package/dist/src/Crypto.d.ts +21 -3
  5. package/dist/src/Crypto.d.ts.map +1 -1
  6. package/dist/src/Crypto.js +18 -2
  7. package/dist/src/Error.d.ts +10 -6
  8. package/dist/src/Error.d.ts.map +1 -1
  9. package/dist/src/Error.js +52 -20
  10. package/dist/src/Sqlite.d.ts.map +1 -1
  11. package/dist/src/Sqlite.js +16 -12
  12. package/dist/src/Task.d.ts +3 -1
  13. package/dist/src/Task.d.ts.map +1 -1
  14. package/dist/src/Task.js +15 -13
  15. package/dist/src/Worker.d.ts +3 -0
  16. package/dist/src/Worker.d.ts.map +1 -1
  17. package/dist/src/Worker.js +6 -1
  18. package/dist/src/local-first/Db.d.ts.map +1 -1
  19. package/dist/src/local-first/Db.js +7 -7
  20. package/dist/src/local-first/Owner.d.ts +20 -8
  21. package/dist/src/local-first/Owner.d.ts.map +1 -1
  22. package/dist/src/local-first/Owner.js +23 -10
  23. package/dist/src/local-first/Schema.d.ts +1 -1
  24. package/dist/src/local-first/Shared.d.ts +55 -39
  25. package/dist/src/local-first/Shared.d.ts.map +1 -1
  26. package/dist/src/local-first/Shared.js +264 -318
  27. package/package.json +1 -1
  28. package/src/Console.test.ts +75 -247
  29. package/src/Console.ts +68 -110
  30. package/src/Crypto.ts +23 -3
  31. package/src/Error.test.ts +85 -0
  32. package/src/Error.ts +48 -22
  33. package/src/Sqlite.ts +19 -11
  34. package/src/Task.test.ts +46 -0
  35. package/src/Task.ts +17 -13
  36. package/src/Worker.ts +6 -1
  37. package/src/local-first/Db.ts +12 -8
  38. package/src/local-first/Owner.test.ts +52 -1
  39. package/src/local-first/Owner.ts +26 -7
  40. package/src/local-first/Shared.test.ts +414 -39
  41. 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 = (): RunDefaultDeps => {
2142
- const console = createConsole();
2143
- return {
2144
- console,
2145
- leakDetector: isDev ? createLeakDetector({ console }) : noopLeakDetector,
2146
- nativeFetch: globalThis.fetch.bind(globalThis),
2147
- randomBytes: createRandomBytes(),
2148
- random: createRandom(),
2149
- reportDefect: reportDefectAfterMicrotask,
2150
- time: createTime(),
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: channel.postMessage,
673
+ postMessage: (message) => {
674
+ channel.postMessage(structuredClone(message));
675
+ },
671
676
  get onMessage() {
672
677
  return channel.onMessage;
673
678
  },
@@ -137,7 +137,10 @@ import type {
137
137
  EvoluInput,
138
138
  SharedWorkerId,
139
139
  } from "./Shared.ts";
140
- import { consoleEntryOrErrorBroadcastChannelName } from "./Shared.ts";
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 { assertEqual, assertFalse, assertNotUndefined } from "../Assert.ts";
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);
@@ -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
- * 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
179
+ * BIP-39 entropy used to derive {@link Owner} keys.
176
180
  *
177
- * Can be created using {@link createOwnerSecret} or converted from a
178
- * {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
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("OwnerSecret", Entropy32);
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 an {@link OwnerSecret}.
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) as OwnerSecret;
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