@evolu/common 6.0.1-preview.9 → 7.1.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 (195) hide show
  1. package/README.md +27 -29
  2. package/dist/src/Array.d.ts +317 -12
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +219 -9
  5. package/dist/src/Assert.d.ts +0 -13
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +0 -15
  8. package/dist/src/Brand.d.ts +75 -0
  9. package/dist/src/Brand.d.ts.map +1 -0
  10. package/dist/src/Brand.js +1 -0
  11. package/dist/src/Buffer.d.ts +1 -1
  12. package/dist/src/Buffer.d.ts.map +1 -1
  13. package/dist/src/Buffer.js +8 -7
  14. package/dist/src/Cache.d.ts +39 -0
  15. package/dist/src/Cache.d.ts.map +1 -0
  16. package/dist/src/Cache.js +47 -0
  17. package/dist/src/Callbacks.d.ts +45 -12
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Callbacks.js +14 -7
  20. package/dist/src/Console.d.ts +32 -7
  21. package/dist/src/Console.d.ts.map +1 -1
  22. package/dist/src/Console.js +73 -10
  23. package/dist/src/Crypto.d.ts +56 -42
  24. package/dist/src/Crypto.d.ts.map +1 -1
  25. package/dist/src/Crypto.js +45 -54
  26. package/dist/src/Evolu/Db.d.ts +162 -74
  27. package/dist/src/Evolu/Db.d.ts.map +1 -1
  28. package/dist/src/Evolu/Db.js +284 -702
  29. package/dist/src/Evolu/Diff.d.ts +3 -3
  30. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  31. package/dist/src/Evolu/Diff.js +7 -5
  32. package/dist/src/Evolu/Evolu.d.ts +213 -133
  33. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  34. package/dist/src/Evolu/Evolu.js +189 -180
  35. package/dist/src/Evolu/Internal.d.ts +2 -4
  36. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  37. package/dist/src/Evolu/Internal.js +2 -4
  38. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  39. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  40. package/dist/src/Evolu/LocalAuth.js +174 -0
  41. package/dist/src/Evolu/Owner.d.ts +273 -120
  42. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  43. package/dist/src/Evolu/Owner.js +130 -104
  44. package/dist/src/Evolu/Platform.d.ts +9 -7
  45. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  46. package/dist/src/Evolu/Protocol.d.ts +268 -240
  47. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  48. package/dist/src/Evolu/Protocol.js +595 -447
  49. package/dist/src/Evolu/Public.d.ts +7 -9
  50. package/dist/src/Evolu/Public.d.ts.map +1 -1
  51. package/dist/src/Evolu/Public.js +3 -4
  52. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
  53. package/dist/src/Evolu/PublicKysely.js +3 -4
  54. package/dist/src/Evolu/Query.d.ts +2 -1
  55. package/dist/src/Evolu/Query.d.ts.map +1 -1
  56. package/dist/src/Evolu/Query.js +1 -1
  57. package/dist/src/Evolu/Relay.d.ts +91 -8
  58. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  59. package/dist/src/Evolu/Relay.js +214 -88
  60. package/dist/src/Evolu/Schema.d.ts +125 -47
  61. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  62. package/dist/src/Evolu/Schema.js +175 -31
  63. package/dist/src/Evolu/Storage.d.ts +249 -27
  64. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  65. package/dist/src/Evolu/Storage.js +198 -92
  66. package/dist/src/Evolu/Sync.d.ts +68 -13
  67. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  68. package/dist/src/Evolu/Sync.js +469 -20
  69. package/dist/src/Evolu/Timestamp.d.ts +83 -30
  70. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  71. package/dist/src/Evolu/Timestamp.js +79 -34
  72. package/dist/src/Identicon.d.ts +35 -0
  73. package/dist/src/Identicon.d.ts.map +1 -0
  74. package/dist/src/Identicon.js +143 -0
  75. package/dist/src/Instances.d.ts +34 -0
  76. package/dist/src/Instances.d.ts.map +1 -0
  77. package/dist/src/Instances.js +44 -0
  78. package/dist/src/Number.d.ts +4 -3
  79. package/dist/src/Number.d.ts.map +1 -1
  80. package/dist/src/Number.js +5 -4
  81. package/dist/src/Object.d.ts +10 -4
  82. package/dist/src/Object.d.ts.map +1 -1
  83. package/dist/src/Object.js +9 -3
  84. package/dist/src/Order.d.ts +0 -5
  85. package/dist/src/Order.d.ts.map +1 -1
  86. package/dist/src/Order.js +0 -5
  87. package/dist/src/Platform.d.ts +15 -0
  88. package/dist/src/Platform.d.ts.map +1 -0
  89. package/dist/src/Platform.js +17 -0
  90. package/dist/src/Random.d.ts +3 -7
  91. package/dist/src/Random.d.ts.map +1 -1
  92. package/dist/src/Random.js +0 -5
  93. package/dist/src/Ref.d.ts.map +1 -1
  94. package/dist/src/Relation.d.ts +79 -0
  95. package/dist/src/Relation.d.ts.map +1 -0
  96. package/dist/src/Relation.js +127 -0
  97. package/dist/src/Resources.d.ts +118 -0
  98. package/dist/src/Resources.d.ts.map +1 -0
  99. package/dist/src/Resources.js +197 -0
  100. package/dist/src/Result.d.ts +189 -59
  101. package/dist/src/Result.d.ts.map +1 -1
  102. package/dist/src/Result.js +30 -241
  103. package/dist/src/Skiplist.js +2 -1
  104. package/dist/src/Sqlite.d.ts +89 -5
  105. package/dist/src/Sqlite.d.ts.map +1 -1
  106. package/dist/src/Sqlite.js +136 -9
  107. package/dist/src/Store.d.ts +0 -5
  108. package/dist/src/Store.d.ts.map +1 -1
  109. package/dist/src/Store.js +0 -5
  110. package/dist/src/Task.d.ts +580 -0
  111. package/dist/src/Task.d.ts.map +1 -0
  112. package/dist/src/Task.js +464 -0
  113. package/dist/src/Time.d.ts +66 -6
  114. package/dist/src/Time.d.ts.map +1 -1
  115. package/dist/src/Time.js +96 -7
  116. package/dist/src/Type.d.ts +677 -344
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +718 -467
  119. package/dist/src/Types.d.ts +38 -75
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/WebSocket.d.ts +5 -7
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +12 -23
  124. package/dist/src/Worker.d.ts +39 -16
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +22 -9
  127. package/dist/src/index.d.ts +8 -3
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +8 -3
  130. package/package.json +14 -13
  131. package/src/Array.ts +396 -19
  132. package/src/Assert.ts +0 -35
  133. package/src/Brand.ts +75 -0
  134. package/src/Buffer.ts +7 -7
  135. package/src/Cache.ts +79 -0
  136. package/src/Callbacks.ts +62 -22
  137. package/src/Console.ts +92 -12
  138. package/src/Crypto.ts +99 -91
  139. package/src/Evolu/Db.ts +519 -1026
  140. package/src/Evolu/Diff.ts +7 -5
  141. package/src/Evolu/Evolu.ts +476 -360
  142. package/src/Evolu/Internal.ts +2 -4
  143. package/src/Evolu/LocalAuth.ts +463 -0
  144. package/src/Evolu/Owner.ts +355 -228
  145. package/src/Evolu/Platform.ts +9 -9
  146. package/src/Evolu/Protocol.ts +846 -742
  147. package/src/Evolu/Public.ts +8 -15
  148. package/src/Evolu/PublicKysely.ts +4 -5
  149. package/src/Evolu/Query.ts +3 -2
  150. package/src/Evolu/Relay.ts +406 -103
  151. package/src/Evolu/Schema.ts +323 -91
  152. package/src/Evolu/Storage.ts +559 -137
  153. package/src/Evolu/Sync.ts +819 -36
  154. package/src/Evolu/Timestamp.ts +90 -58
  155. package/src/Identicon.ts +197 -0
  156. package/src/Instances.ts +90 -0
  157. package/src/Number.ts +6 -10
  158. package/src/Object.ts +13 -5
  159. package/src/Order.ts +0 -6
  160. package/src/Platform.ts +20 -0
  161. package/src/Random.ts +3 -8
  162. package/src/Ref.ts +0 -5
  163. package/src/Relation.ts +234 -0
  164. package/src/Resources.ts +367 -0
  165. package/src/Result.ts +196 -61
  166. package/src/Skiplist.ts +1 -1
  167. package/src/Sqlite.ts +152 -17
  168. package/src/Store.ts +0 -5
  169. package/src/Task.ts +894 -0
  170. package/src/Time.ts +179 -10
  171. package/src/Type.ts +1136 -731
  172. package/src/Types.ts +43 -77
  173. package/src/WebSocket.ts +27 -31
  174. package/src/Worker.ts +72 -29
  175. package/src/index.ts +8 -3
  176. package/dist/src/Evolu/Config.d.ts +0 -69
  177. package/dist/src/Evolu/Config.d.ts.map +0 -1
  178. package/dist/src/Evolu/Config.js +0 -9
  179. package/dist/src/Evolu/Kysely.d.ts +0 -6
  180. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  181. package/dist/src/Evolu/Kysely.js +0 -21
  182. package/dist/src/ManyToManyMap.d.ts +0 -26
  183. package/dist/src/ManyToManyMap.d.ts.map +0 -1
  184. package/dist/src/ManyToManyMap.js +0 -92
  185. package/dist/src/NanoId.d.ts +0 -27
  186. package/dist/src/NanoId.d.ts.map +0 -1
  187. package/dist/src/NanoId.js +0 -6
  188. package/dist/src/Promise.d.ts +0 -180
  189. package/dist/src/Promise.d.ts.map +0 -1
  190. package/dist/src/Promise.js +0 -176
  191. package/src/Evolu/Config.ts +0 -83
  192. package/src/Evolu/Kysely.ts +0 -38
  193. package/src/ManyToManyMap.ts +0 -140
  194. package/src/NanoId.ts +0 -39
  195. package/src/Promise.ts +0 -295
package/src/Callbacks.ts CHANGED
@@ -1,43 +1,83 @@
1
- import { NanoIdLibDep } from "./NanoId.js";
2
- import { Brand } from "./Types.js";
1
+ import { Brand } from "./Brand.js";
2
+ import { RandomBytesDep } from "./Crypto.js";
3
+ import { Result } from "./Result.js";
4
+ import { createId, Id } from "./Type.js";
3
5
 
4
6
  /**
5
- * Manages one-time callback functions.
7
+ * Request-response correlation for callbacks across boundaries.
6
8
  *
7
- * Allows registering callbacks with a unique ID and executing them once with an
8
- * optional argument. Executed callbacks are automatically removed.
9
+ * Stores callbacks with unique IDs and executes them once with an optional
10
+ * argument. Executed callbacks are automatically removed.
9
11
  *
10
- * This is useful for managing event-driven or asynchronous workflows where
11
- * callbacks need to be invoked only once.
12
+ * This is useful for correlating asynchronous request-response operations
13
+ * across boundaries where callback functions cannot be passed directly (e.g.,
14
+ * web workers, message queues).
15
+ *
16
+ * The `execute` method intentionally does not use try-catch or {@link Result}
17
+ * because it's the callback's responsibility to handle its own errors.
18
+ *
19
+ * ### Example
20
+ *
21
+ * ```ts
22
+ * // No-argument callbacks
23
+ * const callbacks = createCallbacks(deps);
24
+ * const id = callbacks.register(() => console.log("called"));
25
+ * callbacks.execute(id);
26
+ *
27
+ * // With argument callbacks
28
+ * const stringCallbacks = createCallbacks<string>(deps);
29
+ * const id = stringCallbacks.register((value) => {
30
+ * console.log(value);
31
+ * });
32
+ * stringCallbacks.execute(id, "hello");
33
+ *
34
+ * // Promise.withResolvers pattern
35
+ * const promiseCallbacks = createCallbacks<string>(deps);
36
+ * const { promise, resolve } = Promise.withResolvers<string>();
37
+ * const id = promiseCallbacks.register(resolve);
38
+ * promiseCallbacks.execute(id, "resolved value");
39
+ * await promise; // "resolved value"
40
+ * ```
41
+ *
42
+ * @template T - The type of argument passed to callbacks (defaults to undefined
43
+ * for no-argument callbacks)
12
44
  */
13
- export interface Callbacks {
45
+ export interface Callbacks<T = undefined> {
14
46
  /** Registers a callback function and returns a unique ID. */
15
- readonly register: (callback: (arg?: unknown) => void) => CallbackId;
47
+ readonly register: (callback: (arg: T) => void) => CallbackId;
16
48
 
17
49
  /** Executes and removes a callback associated with the given ID. */
18
- readonly execute: (id: CallbackId, arg?: unknown) => void;
50
+ readonly execute: T extends undefined
51
+ ? (id: CallbackId) => undefined
52
+ : (id: CallbackId, arg: T) => undefined;
19
53
  }
20
54
 
21
- export type CallbackId = string & Brand<"CallbackId">;
55
+ /** Unique identifier for a callback in {@link Callbacks}. */
56
+ export type CallbackId = Id & Brand<"Callback">;
22
57
 
23
- export const createCallbacks = (deps: NanoIdLibDep): Callbacks => {
24
- const callbackMap = new Map<CallbackId, (arg?: unknown) => void>();
58
+ /** Creates a new {@link Callbacks}. */
59
+ export const createCallbacks = <T = undefined>(
60
+ deps: RandomBytesDep,
61
+ ): Callbacks<T> => {
62
+ const callbackMap = new Map<CallbackId, (arg: T) => void>();
25
63
 
26
- const callbacks: Callbacks = {
64
+ return {
27
65
  register: (callback) => {
28
- const id = deps.nanoIdLib.nanoid() as CallbackId;
66
+ const id = createId<"Callback">(deps);
29
67
  callbackMap.set(id, callback);
30
68
  return id;
31
69
  },
32
70
 
33
- execute: (id, arg) => {
71
+ execute: (id: CallbackId, ...args: T extends undefined ? [] : [T]) => {
34
72
  const callback = callbackMap.get(id);
35
- if (callback) {
36
- callbackMap.delete(id);
37
- callback(arg);
73
+ if (!callback) return;
74
+ callbackMap.delete(id);
75
+ if (args.length === 0) {
76
+ // Called without argument (undefined case)
77
+ (callback as () => void)();
78
+ } else {
79
+ callback(args[0]);
38
80
  }
39
81
  },
40
- };
41
-
42
- return callbacks;
82
+ } as Callbacks<T>;
43
83
  };
package/src/Console.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 📝 Cross-platform console
2
+ * Cross-platform console
3
3
  *
4
4
  * Console abstraction for Chrome 123+, Firefox 125+, Safari 18.1+, Node.js
5
5
  * 22.x+, and React Native 0.75+. Includes methods guaranteed to be available in
@@ -13,14 +13,28 @@
13
13
  * ### Example
14
14
  *
15
15
  * ```ts
16
- * deps.console.log("[db]", "send data message", protocolMessage);
16
+ * deps.console.log("[evolu]", "createEvoluInstance", { name });
17
17
  * ```
18
18
  *
19
19
  * **Tip**: In browser dev tools, you can filter logs by tag (e.g., `[db]`) to
20
- * quickly find relevant messages.
20
+ * quickly find relevant messages. In Node.js, use `grep` to filter output:
21
21
  *
22
- * **Warning**: If you encounter platform-specific issues or missing methods,
23
- * please contribute a PR with details about the environment and behavior.
22
+ * ```bash
23
+ * node app.js | grep "\[relay\]" # Show only relay logs
24
+ * node app.js | grep -E "\[db\]|\[sql\]" # Show db and sql logs
25
+ * node app.js | grep -v "\[debug\]" # Hide debug logs
26
+ * ```
27
+ *
28
+ * Or add to package.json scripts:
29
+ *
30
+ * ```json
31
+ * {
32
+ * "scripts": {
33
+ * "dev:relay": "node app.js | grep \"\\[relay\\]\"",
34
+ * "dev:db": "node app.js | grep -E \"\\[db\\]|\\[sql\\]\""
35
+ * }
36
+ * }
37
+ * ```
24
38
  *
25
39
  * @module
26
40
  */
@@ -51,6 +65,9 @@ export interface Console {
51
65
  /** Starts a timer with an optional label */
52
66
  time: (label?: string) => void;
53
67
 
68
+ /** Logs the elapsed time for a timer without ending it */
69
+ timeLog: (label?: string, ...data: Array<any>) => void;
70
+
54
71
  /** Ends a timer and logs the elapsed time */
55
72
  timeEnd: (label?: string) => void;
56
73
 
@@ -69,9 +86,6 @@ export interface Console {
69
86
  /** Writes a message if the value is falsy, otherwise does nothing */
70
87
  assert: (value: any, message?: string, ...optionalParams: Array<any>) => void;
71
88
 
72
- /** Logs the elapsed time for a timer without ending it */
73
- timeLog: (label?: string, ...data: Array<any>) => void;
74
-
75
89
  /** Prints a stack trace with an optional message */
76
90
  trace: (message?: any, ...optionalParams: Array<any>) => void;
77
91
  }
@@ -121,6 +135,10 @@ export const createConsole = (config: ConsoleConfig = {}): Console => {
121
135
  // eslint-disable-next-line no-console
122
136
  if (instance.enabled) console.time(label);
123
137
  },
138
+ timeLog: (label, ...data) => {
139
+ // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
140
+ if (instance.enabled) console.timeLog(label, ...data);
141
+ },
124
142
  timeEnd: (label) => {
125
143
  // eslint-disable-next-line no-console
126
144
  if (instance.enabled) console.timeEnd(label);
@@ -145,10 +163,6 @@ export const createConsole = (config: ConsoleConfig = {}): Console => {
145
163
  // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
146
164
  if (instance.enabled) console.assert(value, message, ...optionalParams);
147
165
  },
148
- timeLog: (label, ...data) => {
149
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
150
- if (instance.enabled) console.timeLog(label, ...data);
151
- },
152
166
  trace: (message, ...optionalParams) => {
153
167
  // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
154
168
  if (instance.enabled) console.trace(message, ...optionalParams);
@@ -157,3 +171,69 @@ export const createConsole = (config: ConsoleConfig = {}): Console => {
157
171
 
158
172
  return instance;
159
173
  };
174
+
175
+ export interface ConsoleWithTimeConfig extends ConsoleConfig {
176
+ /**
177
+ * Type of timestamp to prepend to log messages.
178
+ *
179
+ * - 'absolute': Shows actual time (e.g., "14:32:15.234")
180
+ * - 'relative': Shows time since console creation (e.g., "+1.234s")
181
+ */
182
+ readonly timestampType: "absolute" | "relative";
183
+ }
184
+
185
+ /** Creates a console instance with timestamp prefixes. */
186
+ export const createConsoleWithTime = (
187
+ config: ConsoleWithTimeConfig = { timestampType: "relative" },
188
+ ): Console => {
189
+ const console = createConsole(config);
190
+ const startTime = performance.now();
191
+
192
+ const getTimestamp = (): string => {
193
+ if (config.timestampType === "relative") {
194
+ const elapsed = (performance.now() - startTime) / 1000;
195
+
196
+ // Format for better readability at different time scales
197
+ if (elapsed < 60) {
198
+ // Under 1 minute: show seconds with millisecond precision
199
+ return `+${elapsed.toFixed(3)}s`;
200
+ } else if (elapsed < 3600) {
201
+ // 1 minute to 1 hour: show minutes and seconds with millisecond precision
202
+ const minutes = Math.floor(elapsed / 60);
203
+ const seconds = (elapsed % 60).toFixed(3);
204
+ return `+${minutes}m${seconds}s`;
205
+ } else {
206
+ // Over 1 hour: show hours, minutes, and seconds with millisecond precision
207
+ const hours = Math.floor(elapsed / 3600);
208
+ const minutes = Math.floor((elapsed % 3600) / 60);
209
+ const seconds = ((elapsed % 3600) % 60).toFixed(3);
210
+ return `+${hours}h${minutes}m${seconds}s`;
211
+ }
212
+ } else {
213
+ // Absolute time - format as HH:MM:SS.mmm
214
+ const now = new Date();
215
+ const hours = now.getHours().toString().padStart(2, "0");
216
+ const minutes = now.getMinutes().toString().padStart(2, "0");
217
+ const seconds = now.getSeconds().toString().padStart(2, "0");
218
+ const milliseconds = now.getMilliseconds().toString().padStart(3, "0");
219
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
220
+ }
221
+ };
222
+
223
+ const withTimestamp =
224
+ (fn: (...args: Array<any>) => void) =>
225
+ (...args: Array<any>) => {
226
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
227
+ fn(`[${getTimestamp()}]`, ...args);
228
+ };
229
+
230
+ // Override methods that should have timestamps
231
+ console.log = withTimestamp(console.log);
232
+ console.info = withTimestamp(console.info);
233
+ console.warn = withTimestamp(console.warn);
234
+ console.error = withTimestamp(console.error);
235
+ console.debug = withTimestamp(console.debug);
236
+ console.trace = withTimestamp(console.trace);
237
+
238
+ return console;
239
+ };
package/src/Crypto.ts CHANGED
@@ -1,53 +1,71 @@
1
1
  /**
2
- * 🔒
2
+ * Cryptographic utilities
3
+ *
4
+ * Type-safe cryptographic operations including random number generation, SLIP21
5
+ * key derivation, XChaCha20-Poly1305 symmetric encryption, PADMÉ padding, and
6
+ * timing-safe comparisons.
3
7
  *
4
8
  * @module
5
9
  */
6
-
7
- import { xchacha20poly1305 } from "@noble/ciphers/chacha";
8
- import { hmac } from "@noble/hashes/hmac";
9
- import { sha512 } from "@noble/hashes/sha2";
10
- import { randomBytes } from "@noble/hashes/utils";
11
- import * as bip39 from "@scure/bip39";
12
- import { wordlist } from "@scure/bip39/wordlists/english";
13
- import { urlAlphabet } from "nanoid";
14
- import { getOrThrow, Result, trySync } from "./Result.js";
15
- import {
16
- brand,
17
- Id,
18
- length,
19
- Mnemonic,
20
- NonNegativeInt,
21
- Uint8Array,
22
- } from "./Type.js";
23
- import { Brand } from "./Types.js";
24
-
25
- /** `Uint8Array` created by {@link createRandomBytes}. */
26
- export type RandomBytes = Uint8Array & Brand<"RandomBytes">;
27
-
28
- export type CreateRandomBytes = (bytesLength?: number) => RandomBytes;
29
-
30
- export interface CreateRandomBytesDep {
31
- readonly createRandomBytes: CreateRandomBytes;
10
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
11
+ import { hmac } from "@noble/hashes/hmac.js";
12
+ import { sha512 } from "@noble/hashes/sha2.js";
13
+ import { randomBytes, utf8ToBytes } from "@noble/hashes/utils.js";
14
+ import { Result, trySync } from "./Result.js";
15
+ import { brand, length, NonNegativeInt, Uint8Array } from "./Type.js";
16
+
17
+ export interface RandomBytes {
18
+ /**
19
+ * Creates cryptographically secure random bytes with type-safe length
20
+ * branding.
21
+ *
22
+ * Uses the operating system's cryptographically secure random number
23
+ * generator (crypto.getRandomValues) to generate high-quality entropy
24
+ * suitable for cryptographic operations.
25
+ *
26
+ * ### Type Safety
27
+ *
28
+ * Returns specific branded types for common sizes:
29
+ *
30
+ * - `Random16` for 16-byte values (128 bits)
31
+ * - `Random32` for 32-byte values (256 bits)
32
+ * - `Random64` for 64-byte values (512 bits)
33
+ * - `Random` for any other size
34
+ *
35
+ * ### Example
36
+ *
37
+ * ```ts
38
+ * const nonce = randomBytes.create(16); // Type: Random16
39
+ * const key = randomBytes.create(32); // Type: Random32
40
+ * const seed = randomBytes.create(64); // Type: Random64
41
+ * const custom = randomBytes.create(48); // Type: Random
42
+ * ```
43
+ */
44
+ create(bytesLength: 16): Entropy16;
45
+ create(bytesLength: 32): Entropy32;
46
+ create(bytesLength: 64): Entropy64;
47
+ create(bytesLength: number): Entropy;
32
48
  }
33
49
 
34
- /** Cryptographically secure PRNG. Uses internal OS-level crypto.getRandomValues. */
35
- export const createRandomBytes: CreateRandomBytes = (bytesLength = 32) =>
36
- randomBytes(bytesLength) as RandomBytes;
50
+ export interface RandomBytesDep {
51
+ readonly randomBytes: RandomBytes;
52
+ }
37
53
 
38
- export type CreateMnemonic = () => Mnemonic;
54
+ const Entropy = brand("Entropy", Uint8Array);
55
+ type Entropy = typeof Entropy.Type;
39
56
 
40
- export interface CreateMnemonicDep {
41
- readonly createMnemonic: CreateMnemonic;
42
- }
57
+ export const Entropy16 = length(16)(Entropy);
58
+ export type Entropy16 = typeof Entropy16.Type;
43
59
 
44
- export const createEnglishMnemonic: CreateMnemonic = () =>
45
- bip39.generateMnemonic(wordlist, 128) as Mnemonic;
60
+ export const Entropy32 = length(32)(Entropy);
61
+ export type Entropy32 = typeof Entropy32.Type;
46
62
 
47
- export type MnemonicSeed = Uint8Array & Brand<"MnemonicSeed">;
63
+ export const Entropy64 = length(64)(Entropy);
64
+ export type Entropy64 = typeof Entropy64.Type;
48
65
 
49
- export const mnemonicToMnemonicSeed = (mnemonic: Mnemonic): MnemonicSeed =>
50
- bip39.mnemonicToSeedSync(mnemonic) as MnemonicSeed;
66
+ export const createRandomBytes = (): RandomBytes => ({
67
+ create: randomBytes as RandomBytes["create"],
68
+ });
51
69
 
52
70
  /**
53
71
  * SLIP21.
@@ -55,54 +73,43 @@ export const mnemonicToMnemonicSeed = (mnemonic: Mnemonic): MnemonicSeed =>
55
73
  * https://github.com/satoshilabs/slips/blob/master/slip-0021.md
56
74
  */
57
75
  export const createSlip21 = (
58
- seed: MnemonicSeed,
59
- path: ReadonlyArray<string>,
60
- ): Uint8Array => {
61
- let m = hmac(sha512, "Symmetric key seed", seed);
62
- for (const component of path) {
63
- const p = new TextEncoder().encode(component);
64
- const e = new globalThis.Uint8Array(p.byteLength + 1);
65
- e[0] = 0;
66
- e.set(p, 1);
67
- m = hmac(sha512, m.slice(0, 32), e);
76
+ seed: Entropy16 | Entropy32 | Entropy64,
77
+ path: ReadonlyArray<string | number>,
78
+ ): Entropy32 => {
79
+ let currentNode = hmac(
80
+ sha512,
81
+ utf8ToBytes("Symmetric key seed"),
82
+ seed,
83
+ ) as Entropy64;
84
+
85
+ for (const element of path) {
86
+ const label = typeof element === "number" ? element.toString() : element;
87
+ currentNode = deriveSlip21Node(label, currentNode);
68
88
  }
69
- return m.slice(32, 64);
89
+
90
+ return currentNode.slice(32, 64) as Entropy32;
70
91
  };
71
92
 
72
93
  /**
73
- * Creates a 21-character Base64URL ID (also known as nanoid) from a SLIP-21
74
- * derived key.
75
- *
76
- * Reduces the 256-bit SLIP-21 output to 126 bits (21 chars × 6 bits) for a
77
- * compact, human-readable, and shareable identifier suitable for UI display or
78
- * URL use. While this lowers entropy, 126 bits remains cryptographically secure
79
- * for uniqueness and unpredictability in most applications (comparable to
80
- * UUIDv4's 122 bits).
94
+ * Derives a single node in the SLIP-21 hierarchical key derivation.
81
95
  *
82
- * See https://github.com/satoshilabs/slips/blob/master/slip-0021.md
96
+ * @see {@link createSlip21}
83
97
  */
84
- export const createSlip21Id = (
85
- seed: MnemonicSeed,
86
- path: ReadonlyArray<string>,
87
- ): Id => {
88
- const slip21 = createSlip21(seed, path);
89
- let id = "" as Id;
90
-
91
- // Convert the key to the Id/NanoId/Base64Url format.
92
- for (let i = 0; i < 21; i++) {
93
- id = (id + urlAlphabet[slip21[i] & 63]) as Id;
94
- }
95
-
96
- return id;
98
+ export const deriveSlip21Node = (
99
+ label: string,
100
+ parentNode: Entropy64,
101
+ ): Entropy64 => {
102
+ const labelBytes = utf8ToBytes(label);
103
+ const message = new globalThis.Uint8Array(labelBytes.byteLength + 1);
104
+ message[0] = 0;
105
+ message.set(labelBytes, 1);
106
+ return hmac(sha512, parentNode.slice(0, 32), message) as Entropy64;
97
107
  };
98
108
 
99
109
  /** The encryption key for {@link SymmetricCrypto}. */
100
- export const EncryptionKey = brand("EncryptionKey", length(32)(Uint8Array));
110
+ export const EncryptionKey = brand("EncryptionKey", Entropy32);
101
111
  export type EncryptionKey = typeof EncryptionKey.Type;
102
112
 
103
- export const createEncryptionKey = (seed: MnemonicSeed): EncryptionKey =>
104
- createSlip21(seed, ["Evolu", "Encryption Key"]) as EncryptionKey;
105
-
106
113
  /** Symmetric cryptography. */
107
114
  export interface SymmetricCrypto {
108
115
  readonly nonceLength: NonNegativeInt;
@@ -137,15 +144,15 @@ export interface SymmetricCryptoDecryptError {
137
144
  * https://github.com/paulmillr/noble-ciphers?tab=readme-ov-file#which-cipher-should-i-pick
138
145
  */
139
146
  export const createSymmetricCrypto = (
140
- deps: CreateRandomBytesDep,
147
+ deps: RandomBytesDep,
141
148
  ): SymmetricCrypto => {
142
- const nonceLength = getOrThrow(NonNegativeInt.from(24));
149
+ const nonceLength = NonNegativeInt.orThrow(24);
143
150
 
144
151
  const symmetricCrypto: SymmetricCrypto = {
145
152
  nonceLength,
146
153
 
147
154
  encrypt: (plaintext, encryptionKey) => {
148
- const nonce = deps.createRandomBytes(nonceLength);
155
+ const nonce = deps.randomBytes.create(nonceLength);
149
156
  const ciphertext = xchacha20poly1305(encryptionKey, nonce).encrypt(
150
157
  plaintext,
151
158
  );
@@ -169,25 +176,26 @@ export const createSymmetricCrypto = (
169
176
  * Returns the PADMÉ padded length for a given input length.
170
177
  *
171
178
  * PADMÉ limits information leakage about the length of the plain-text for a
172
- * wide range of encrypted data sizes. See the PURBs paper for details:
173
- * https://bford.info/pub/sec/purb.pdf
179
+ * wide range of encrypted data sizes.
180
+ *
181
+ * See the PURBs paper for details: https://bford.info/pub/sec/purb.pdf
174
182
  */
175
- export const padmePaddedLength = (length: NonNegativeInt): NonNegativeInt => {
176
- if (length <= 0) return 0 as NonNegativeInt;
183
+ export const createPadmePaddedLength = (
184
+ length: NonNegativeInt,
185
+ ): NonNegativeInt => {
186
+ if (length <= 0) return NonNegativeInt.orThrow(0);
177
187
  const e = 31 - Math.clz32(length >>> 0);
178
188
  const s = 32 - Math.clz32(e >>> 0);
179
189
  const z = Math.max(0, e - s);
180
190
  const mask = (1 << z) - 1;
181
- return ((length + mask) & ~mask) as NonNegativeInt;
191
+ return NonNegativeInt.orThrow((length + mask) & ~mask);
182
192
  };
183
193
 
184
- /**
185
- * Returns the PADMÉ padding length for a given input length. Uses
186
- * {@link padmePaddedLength}.
187
- */
188
-
189
- export const padmePaddingLength = (length: NonNegativeInt): NonNegativeInt => {
190
- return (padmePaddedLength(length) - length) as NonNegativeInt;
194
+ /** Creates a PADMÉ padding array of zeros for the given input length. */
195
+ export const createPadmePadding = (length: NonNegativeInt): Uint8Array => {
196
+ const paddedLength = createPadmePaddedLength(length);
197
+ const paddingLength = NonNegativeInt.orThrow(paddedLength - length);
198
+ return new globalThis.Uint8Array(paddingLength);
191
199
  };
192
200
 
193
201
  /**