@evolu/common 6.0.1-preview.3 → 6.0.1-preview.30

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 (173) hide show
  1. package/dist/src/Array.d.ts +58 -5
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +53 -5
  4. package/dist/src/Assert.d.ts +6 -16
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +6 -18
  7. package/dist/src/Brand.d.ts +75 -0
  8. package/dist/src/Brand.d.ts.map +1 -0
  9. package/dist/src/Brand.js +1 -0
  10. package/dist/src/Buffer.d.ts +1 -1
  11. package/dist/src/Buffer.d.ts.map +1 -1
  12. package/dist/src/Buffer.js +8 -7
  13. package/dist/src/Cache.d.ts +44 -0
  14. package/dist/src/Cache.d.ts.map +1 -0
  15. package/dist/src/Cache.js +52 -0
  16. package/dist/src/Callbacks.d.ts +45 -12
  17. package/dist/src/Callbacks.d.ts.map +1 -1
  18. package/dist/src/Callbacks.js +14 -7
  19. package/dist/src/Console.d.ts +31 -6
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +72 -9
  22. package/dist/src/Crypto.d.ts +61 -34
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +32 -45
  25. package/dist/src/Evolu/Db.d.ts +158 -65
  26. package/dist/src/Evolu/Db.d.ts.map +1 -1
  27. package/dist/src/Evolu/Db.js +286 -694
  28. package/dist/src/Evolu/Diff.d.ts +3 -3
  29. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  30. package/dist/src/Evolu/Diff.js +7 -5
  31. package/dist/src/Evolu/Evolu.d.ts +208 -133
  32. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  33. package/dist/src/Evolu/Evolu.js +188 -183
  34. package/dist/src/Evolu/Internal.d.ts +0 -2
  35. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  36. package/dist/src/Evolu/Internal.js +0 -2
  37. package/dist/src/Evolu/LocalAuth.d.ts +150 -0
  38. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  39. package/dist/src/Evolu/LocalAuth.js +174 -0
  40. package/dist/src/Evolu/Owner.d.ts +264 -120
  41. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  42. package/dist/src/Evolu/Owner.js +130 -104
  43. package/dist/src/Evolu/Platform.d.ts +9 -7
  44. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  45. package/dist/src/Evolu/Protocol.d.ts +277 -232
  46. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  47. package/dist/src/Evolu/Protocol.js +603 -378
  48. package/dist/src/Evolu/Public.d.ts +6 -8
  49. package/dist/src/Evolu/Public.d.ts.map +1 -1
  50. package/dist/src/Evolu/Public.js +2 -3
  51. package/dist/src/Evolu/PublicKysely.js +3 -3
  52. package/dist/src/Evolu/Query.d.ts +2 -1
  53. package/dist/src/Evolu/Query.d.ts.map +1 -1
  54. package/dist/src/Evolu/Relay.d.ts +92 -7
  55. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  56. package/dist/src/Evolu/Relay.js +243 -76
  57. package/dist/src/Evolu/Schema.d.ts +129 -73
  58. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  59. package/dist/src/Evolu/Schema.js +169 -89
  60. package/dist/src/Evolu/Storage.d.ts +212 -26
  61. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  62. package/dist/src/Evolu/Storage.js +137 -79
  63. package/dist/src/Evolu/Sync.d.ts +68 -13
  64. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  65. package/dist/src/Evolu/Sync.js +422 -20
  66. package/dist/src/Evolu/Timestamp.d.ts +85 -27
  67. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  68. package/dist/src/Evolu/Timestamp.js +77 -18
  69. package/dist/src/Identicon.d.ts +35 -0
  70. package/dist/src/Identicon.d.ts.map +1 -0
  71. package/dist/src/Identicon.js +143 -0
  72. package/dist/src/Instances.d.ts +34 -0
  73. package/dist/src/Instances.d.ts.map +1 -0
  74. package/dist/src/Instances.js +44 -0
  75. package/dist/src/ManyToManyMap.d.ts +71 -10
  76. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  77. package/dist/src/ManyToManyMap.js +41 -6
  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/Platform.d.ts +20 -0
  82. package/dist/src/Platform.d.ts.map +1 -0
  83. package/dist/src/Platform.js +22 -0
  84. package/dist/src/Random.d.ts +3 -2
  85. package/dist/src/Random.d.ts.map +1 -1
  86. package/dist/src/Resources.d.ts +118 -0
  87. package/dist/src/Resources.d.ts.map +1 -0
  88. package/dist/src/Resources.js +197 -0
  89. package/dist/src/Result.d.ts +184 -52
  90. package/dist/src/Result.d.ts.map +1 -1
  91. package/dist/src/Result.js +30 -241
  92. package/dist/src/Skiplist.js +2 -1
  93. package/dist/src/Sqlite.d.ts +63 -5
  94. package/dist/src/Sqlite.d.ts.map +1 -1
  95. package/dist/src/Sqlite.js +110 -9
  96. package/dist/src/Task.d.ts +586 -0
  97. package/dist/src/Task.d.ts.map +1 -0
  98. package/dist/src/Task.js +469 -0
  99. package/dist/src/Time.d.ts +66 -1
  100. package/dist/src/Time.d.ts.map +1 -1
  101. package/dist/src/Time.js +99 -5
  102. package/dist/src/Type.d.ts +621 -340
  103. package/dist/src/Type.d.ts.map +1 -1
  104. package/dist/src/Type.js +665 -464
  105. package/dist/src/Types.d.ts +1 -75
  106. package/dist/src/Types.d.ts.map +1 -1
  107. package/dist/src/WebSocket.d.ts +5 -2
  108. package/dist/src/WebSocket.d.ts.map +1 -1
  109. package/dist/src/WebSocket.js +12 -18
  110. package/dist/src/Worker.d.ts +39 -11
  111. package/dist/src/Worker.d.ts.map +1 -1
  112. package/dist/src/Worker.js +22 -4
  113. package/dist/src/index.d.ts +7 -2
  114. package/dist/src/index.d.ts.map +1 -1
  115. package/dist/src/index.js +7 -2
  116. package/package.json +14 -13
  117. package/src/Array.ts +76 -11
  118. package/src/Assert.ts +6 -24
  119. package/src/Brand.ts +75 -0
  120. package/src/Buffer.ts +7 -7
  121. package/src/Cache.ts +85 -0
  122. package/src/Callbacks.ts +62 -22
  123. package/src/Console.ts +91 -11
  124. package/src/Crypto.ts +97 -82
  125. package/src/Evolu/Db.ts +514 -1020
  126. package/src/Evolu/Diff.ts +7 -5
  127. package/src/Evolu/Evolu.ts +464 -355
  128. package/src/Evolu/Internal.ts +0 -2
  129. package/src/Evolu/LocalAuth.ts +463 -0
  130. package/src/Evolu/Owner.ts +369 -228
  131. package/src/Evolu/Platform.ts +9 -9
  132. package/src/Evolu/Protocol.ts +859 -676
  133. package/src/Evolu/Public.ts +7 -14
  134. package/src/Evolu/PublicKysely.ts +3 -3
  135. package/src/Evolu/Query.ts +2 -1
  136. package/src/Evolu/Relay.ts +420 -92
  137. package/src/Evolu/Schema.ts +391 -191
  138. package/src/Evolu/Storage.ts +451 -118
  139. package/src/Evolu/Sync.ts +720 -37
  140. package/src/Evolu/Timestamp.ts +88 -35
  141. package/src/Identicon.ts +197 -0
  142. package/src/Instances.ts +90 -0
  143. package/src/ManyToManyMap.ts +124 -24
  144. package/src/Number.ts +6 -10
  145. package/src/Platform.ts +26 -0
  146. package/src/Random.ts +3 -2
  147. package/src/Resources.ts +367 -0
  148. package/src/Result.ts +191 -54
  149. package/src/Skiplist.ts +1 -1
  150. package/src/Sqlite.ts +122 -17
  151. package/src/Task.ts +901 -0
  152. package/src/Time.ts +180 -5
  153. package/src/Type.ts +1083 -727
  154. package/src/Types.ts +1 -77
  155. package/src/WebSocket.ts +27 -25
  156. package/src/Worker.ts +72 -23
  157. package/src/index.ts +7 -2
  158. package/dist/src/Evolu/Config.d.ts +0 -69
  159. package/dist/src/Evolu/Config.d.ts.map +0 -1
  160. package/dist/src/Evolu/Config.js +0 -9
  161. package/dist/src/Evolu/Kysely.d.ts +0 -6
  162. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  163. package/dist/src/Evolu/Kysely.js +0 -21
  164. package/dist/src/NanoId.d.ts +0 -27
  165. package/dist/src/NanoId.d.ts.map +0 -1
  166. package/dist/src/NanoId.js +0 -6
  167. package/dist/src/Promise.d.ts +0 -180
  168. package/dist/src/Promise.d.ts.map +0 -1
  169. package/dist/src/Promise.js +0 -176
  170. package/src/Evolu/Config.ts +0 -83
  171. package/src/Evolu/Kysely.ts +0 -38
  172. package/src/NanoId.ts +0 -39
  173. package/src/Promise.ts +0 -295
@@ -1,302 +1,443 @@
1
- /**
2
- * TODO:
3
- *
4
- * @module
5
- */
6
-
7
- import { assert } from "../Assert.js";
1
+ import * as bip39 from "@scure/bip39";
2
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
3
+ import { NonEmptyReadonlyArray } from "../Array.js";
8
4
  import {
9
- createEncryptionKey,
10
- CreateMnemonicDep,
11
- CreateRandomBytesDep,
12
5
  createSlip21,
13
- createSlip21Id,
14
6
  EncryptionKey,
15
- MnemonicSeed,
16
- mnemonicToMnemonicSeed,
7
+ Entropy16,
8
+ Entropy32,
9
+ RandomBytesDep,
17
10
  } from "../Crypto.js";
18
- import { NanoIdLibDep } from "../NanoId.js";
19
- import { TimeDep } from "../Time.js";
11
+ import { getOrNull } from "../Result.js";
20
12
  import {
21
- Base64Url,
22
13
  brand,
23
- DateIso,
24
- DateIsoString,
25
14
  Id,
26
- length,
15
+ IdBytes,
16
+ idBytesToId,
17
+ idToIdBytes,
27
18
  Mnemonic,
28
19
  NonNegativeInt,
29
- Uint8Array,
30
20
  } from "../Type.js";
31
- import {
32
- createInitialTimestamp,
33
- TimestampString,
34
- timestampToTimestampString,
35
- } from "./Timestamp.js";
36
-
37
- // TODO: Clean API
38
- // - createOwner should be createAppOwner
39
- // - Docs mention WriteKey is optional but it's required in Owner.
40
- // - For Protocol, we need only ownerId, encryptionKey, and writeKey.
41
- // - Not sure whether we need JSDoc for this module, and we don't
42
- // use modules for Evolu internal API yet.
43
- // It's not single responsibility API.
21
+ import type { Storage, EncryptedDbChange } from "./Storage.js";
44
22
 
45
23
  /**
46
- * `Owner` is an entity in Evolu that owns data, meaning it is locally stored on
47
- * a device under the user’s control. Data can be personal private, peer-to-peer
48
- * shared, or aggregated from multiple owners.
49
- *
50
- * An owner has a {@link Mnemonic} from which {@link OwnerId} and
51
- * {@link EncryptionKey} are deterministically derived using SLIP-21, and an
52
- * optional {@link WriteKey} that, when present, enables writing to the Evolu
53
- * Relay or peers. The {@link WriteKey} can be rotated.
54
- *
55
- * Variants include {@link AppOwner}, {@link ShardOwner}, {@link SharedOwner}, and
56
- * {@link SharedReadonlyOwner}, each with specific roles and properties detailed
57
- * in their respective definitions.
58
- *
59
- * Public-key cryptography isn’t included here as it belongs to app and varies
60
- * by use case. An Evolu app without collaboration doesn’t need it, while a
61
- * Nostr-like app can leverage Nostr NIPs, or a super-safe app can use
62
- * post-quantum cryptography.
24
+ * The Owner represents ownership of data in Evolu. Every database change is
25
+ * assigned to an owner, enabling sync functionality and access control.
26
+ *
27
+ * Owners enable **partial sync** - applications can choose which owners to
28
+ * sync, allowing selective data synchronization based on specific needs.
29
+ *
30
+ * Owners also provide **real data deletion** - while individual changes in
31
+ * local-first/distributed systems can only be marked as deleted, entire owners
32
+ * can be completely deleted from both relays and devices (except for
33
+ * {@link AppOwner}, which must be preserved for sync coordination).
34
+ *
35
+ * Evolu provides different owner types depending on their use case:
36
+ *
37
+ * - **Coordination**: {@link AppOwner} for sync coordination and long-term
38
+ * persistence
39
+ * - **Data partitioning**: {@link ShardOwner} for partitioning application data
40
+ * - **Collaboration**: {@link SharedOwner} for collaborative write access
41
+ * - **Data sharing**: {@link SharedReadonlyOwner} for read-only access to shared
42
+ * data
43
+ *
44
+ * Owners are cryptographically derived from an {@link OwnerSecret} using
45
+ * SLIP-21, ensuring secure and deterministic key generation:
46
+ *
47
+ * - {@link OwnerId}: Globally unique public identifier
48
+ * - {@link EncryptionKey}: Symmetric encryption key for data protection
49
+ * - {@link OwnerWriteKey}: Authentication token for write operations (rotatable)
50
+ *
51
+ * @see {@link createOwner}
63
52
  */
64
53
  export interface Owner {
65
- readonly mnemonic: Mnemonic;
66
- readonly createdAt: DateIsoString;
67
54
  readonly id: OwnerId;
68
- readonly encryptionKey: EncryptionKey;
69
- readonly writeKey: WriteKey;
70
- }
71
-
72
- export interface OwnerWithWriteAccess {
73
- readonly id: OwnerId;
74
- readonly encryptionKey: EncryptionKey;
75
- readonly writeKey: WriteKey;
55
+ readonly encryptionKey: OwnerEncryptionKey;
56
+ readonly writeKey: OwnerWriteKey;
76
57
  }
77
58
 
78
59
  /**
79
- * The unique identifier of {@link Owner} derived from the {@link Mnemonic}.
80
- *
81
- * This branded {@link Id} type, generated by {@link createSlip21Id}, is a
82
- * 21-character {@link Base64Url} string (126 bits of entropy), providing a
83
- * compact, shareable, and secure identifier for UI use, tied to the owner's
84
- * mnemonic and derivation path.
60
+ * OwnerId is a branded {@link Id} that uniquely identifies an {@link Owner}.
61
+ * Branded from {@link Id} to leverage existing helpers like {@link idToIdBytes}.
85
62
  */
86
63
  export const OwnerId = brand("OwnerId", Id);
87
64
  export type OwnerId = typeof OwnerId.Type;
88
65
 
89
- export const writeKeyLength = 16 as NonNegativeInt;
66
+ /** Bytes representation of {@link OwnerId}. */
67
+ export const OwnerIdBytes = brand("OwnerIdBytes", IdBytes);
68
+ export type OwnerIdBytes = typeof OwnerIdBytes.Type;
69
+
70
+ export const ownerIdToOwnerIdBytes = (ownerId: OwnerId): OwnerIdBytes =>
71
+ idToIdBytes(ownerId) as OwnerIdBytes;
72
+
73
+ export const ownerIdBytesToOwnerId = (ownerIdBytes: OwnerIdBytes): OwnerId =>
74
+ idBytesToId(ownerIdBytes as IdBytes) as OwnerId;
75
+
76
+ export const ownerWriteKeyLength = NonNegativeInt.orThrow(16);
77
+
78
+ export const OwnerEncryptionKey = brand("OwnerEncryptionKey", EncryptionKey);
79
+ export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
90
80
 
91
81
  /**
92
- * A secure token proving the initiator can write changes. Derived from a
93
- * mnemonic or randomly generated. It's rotatable.
82
+ * A secure token for write operations. It's derived from {@link OwnerSecret} by
83
+ * default and can be rotated via {@link createOwnerWriteKey}.
94
84
  */
95
- export const WriteKey = brand("WriteKey", length(writeKeyLength)(Uint8Array));
96
- export type WriteKey = typeof WriteKey.Type;
85
+ export const OwnerWriteKey = brand("OwnerWriteKey", Entropy16);
86
+ export type OwnerWriteKey = typeof OwnerWriteKey.Type;
97
87
 
98
88
  /**
99
- * The root owner of an Evolu app, created from a mnemonic safely generated on a
100
- * device when the app is initialized or restored on another device using an
101
- * existing mnemonic. It manages the app's core data, including the storage of
102
- * other owners' mnemonics in an encrypted app table. Its `writeKey` is
103
- * deterministic and rotatable. Never share the AppOwner mnemonic with anyone.
89
+ * 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
90
+ *
91
+ * Can be created using {@link createOwnerSecret} or converted from a
92
+ * {@link Mnemonic} using {@link mnemonicToOwnerSecret}.
104
93
  */
105
- export interface AppOwner extends Owner {
106
- readonly type: "AppOwner";
107
- }
94
+ export const OwnerSecret = brand("OwnerSecret", Entropy32);
95
+ export type OwnerSecret = typeof OwnerSecret.Type;
96
+
97
+ /** Creates a {@link OwnerSecret}. */
98
+ export const createOwnerSecret = (deps: RandomBytesDep): OwnerSecret =>
99
+ deps.randomBytes.create(32) as OwnerSecret;
100
+
101
+ /** Converts an {@link OwnerSecret} to a {@link Mnemonic}. */
102
+ export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
103
+ bip39.entropyToMnemonic(secret, wordlist) as Mnemonic;
104
+
105
+ /** Converts a {@link Mnemonic} to an {@link OwnerSecret}. */
106
+ export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
107
+ bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
108
+
109
+ /** Creates a randomly generated {@link OwnerWriteKey}. */
110
+ export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
111
+ deps.randomBytes.create(16) as OwnerWriteKey;
108
112
 
109
113
  /**
110
- * Used to shard data within an app for partial or deferred sync. Created in the
111
- * Evolu `initialData` function or dynamically as needed. Its mnemonic is stored
112
- * in an app table (encrypted by `AppOwner`) and synced between devices. Its
113
- * `writeKey` is deterministic and rotatable, enabling selective syncing of data
114
- * subsets. Not intended for sharing outside the app.
115
- *
116
- * This type omits `id`, `encryptionKey`, and `createdAt` as they are derived by
117
- * Evolu from the `mnemonic`, reducing storage overhead.
114
+ * Creates an {@link Owner} from a {@link OwnerSecret} using SLIP-21 key
115
+ * derivation.
116
+ *
117
+ * This is an internal helper function, use:
118
+ *
119
+ * - {@link createAppOwner}
120
+ * - {@link createShardOwner}
121
+ * - {@link createSharedOwner}
122
+ * - {@link createSharedReadonlyOwner}
118
123
  */
119
- export interface ShardOwner {
120
- readonly type: "ShardOwner";
121
- readonly mnemonic: Mnemonic;
122
- readonly writeKey: WriteKey;
123
- }
124
+ export const createOwner = (secret: OwnerSecret): Owner => ({
125
+ id: ownerIdBytesToOwnerId(
126
+ OwnerIdBytes.orThrow(
127
+ createSlip21(secret, ["Evolu", "OwnerIdBytes"]).slice(0, 16),
128
+ ),
129
+ ),
130
+
131
+ encryptionKey: OwnerEncryptionKey.orThrow(
132
+ createSlip21(secret, ["Evolu", "OwnerEncryptionKey"]),
133
+ ),
134
+
135
+ writeKey: OwnerWriteKey.orThrow(
136
+ createSlip21(secret, ["Evolu", "OwnerWriteKey"]).slice(0, 16),
137
+ ),
138
+ });
124
139
 
125
140
  /**
126
- * Used to share data among one or more users, enabling collaboration or
127
- * controlled access. Its `writeKey` is random (not derived from the mnemonic,
128
- * stored alongside it in the app table) and rotatable, ensuring it cannot be
129
- * regenerated by others if shared. Share the `mnemonic` alone for read-only
130
- * access (as `SharedReadonlyOwner`) or share SharedOwner itself for write
131
- * access.
132
- *
133
- * This type omits `id`, `encryptionKey`, and `createdAt` as they are derived by
134
- * Evolu from the `mnemonic`, reducing storage overhead.
141
+ * The AppOwner represents the application owner. It's created using a
142
+ * cryptographically secure random generator or derived from an external source,
143
+ * e.g., mnemonic stored securely in a hardware device.
144
+ *
145
+ * While it's possible to store all application data in AppOwner, the better
146
+ * approach is to use it only for sync coordination. Storing all app data in
147
+ * AppOwner means that data will be stored/synced forever. And that's a problem
148
+ * if we want to provide real data deletion or in-app data migration without
149
+ * data duplication. In local-first apps/distributed systems, we can't delete
150
+ * individual changes, we only mark them as deleted, otherwise sync could not
151
+ * work.
152
+ *
153
+ * If we really want to delete data or at least avoid syncing it, we must store
154
+ * it using a different owner than AppOwner, e.g. {@link ShardOwner} or
155
+ * {@link SharedOwner}, and delete that owner. The AppOwner itself must be
156
+ * preserved because it coordinates deletion information across devices. Other
157
+ * devices need to sync the information that an owner was deleted so they can
158
+ * delete their local data as well.
159
+ *
160
+ * ### Privacy Considerations
161
+ *
162
+ * AppOwner must never be shared with anyone, except for its {@link OwnerId},
163
+ * which can be used for authorization with
164
+ * {@link createOwnerWebSocketTransport}. It's safe because OwnerId is
165
+ * pseudonymous (it can't be assigned to a specific person).
166
+ *
167
+ * For data sharing scenarios, use {@link SharedOwner} and
168
+ * {@link SharedReadonlyOwner} instead, which are designed specifically for
169
+ * collaborative access.
135
170
  */
136
- export interface SharedOwner {
137
- readonly type: "SharedOwner";
138
- readonly mnemonic: Mnemonic;
139
- readonly writeKey: WriteKey;
171
+ export interface AppOwner extends Owner {
172
+ readonly type: "AppOwner";
173
+
174
+ /**
175
+ * The mnemonic that was used to derive the AppOwner keys. Optional when the
176
+ * AppOwner is created from external keys to avoid sharing the mnemonic with
177
+ * the Evolu app.
178
+ */
179
+ readonly mnemonic?: Mnemonic | null;
140
180
  }
141
181
 
182
+ /** Creates an {@link AppOwner} from an {@link OwnerSecret}. */
183
+ export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
184
+ type: "AppOwner",
185
+ mnemonic: ownerSecretToMnemonic(secret),
186
+ ...createOwner(secret),
187
+ });
188
+
142
189
  /**
143
- * Used for sharing data that can only be read, such as with followers or peers
144
- * in a read-only sync scenario. It lacks a `writeKey`, containing only the
145
- * `mnemonic` from which `id` and `encryptionKey` are derived by Evolu.
146
- * Typically derived from a `SharedOwner` by sharing its `mnemonic` without the
147
- * `writeKey`.
190
+ * An {@link Owner} for sharding data.
191
+ *
192
+ * ShardOwners are the recommended storage location for most application data
193
+ * because they can be completely deleted (both on relays and devices) and
194
+ * conditionally synced.
195
+ *
196
+ * Can be created from {@link OwnerSecret} via {@link createShardOwner} or
197
+ * deterministically derived from {@link AppOwner} using
198
+ * {@link deriveShardOwner}.
148
199
  */
149
- export interface SharedReadonlyOwner {
150
- readonly type: "SharedReadonlyOwner";
151
- readonly mnemonic: Mnemonic;
200
+ export interface ShardOwner extends Owner {
201
+ readonly type: "ShardOwner";
202
+ readonly transports?: ReadonlyArray<OwnerTransport>;
152
203
  }
153
204
 
154
- /**
155
- * Creates an {@link AppOwner}, optionally from an existing mnemonic to restore
156
- * it on another device; otherwise, generates a new mnemonic.
157
- */
158
- export const createAppOwner =
159
- (deps: TimeDep & CreateRandomBytesDep & CreateMnemonicDep) =>
160
- (mnemonic = deps.createMnemonic()): AppOwner => {
161
- const owner = createOwner(deps)(mnemonic);
162
- return { type: "AppOwner", ...owner };
205
+ /** Creates a {@link ShardOwner} from an {@link OwnerSecret}. */
206
+ export const createShardOwner = (
207
+ secret: OwnerSecret,
208
+ transports?: ReadonlyArray<OwnerTransport>,
209
+ ): ShardOwner => {
210
+ return {
211
+ type: "ShardOwner",
212
+ ...createOwner(secret),
213
+ ...(transports && { transports }),
163
214
  };
215
+ };
164
216
 
165
217
  /**
166
- * Creates a {@link ShardOwner} for sharding app data with a freshly generated
167
- * mnemonic. Unlike {@link createAppOwner}, it doesn’t accept an existing
168
- * mnemonic because ShardOwner mnemonics are always generated and restored
169
- * automatically via database sync.
218
+ * Derives a {@link ShardOwner} from an {@link AppOwner} using the specified path.
219
+ *
220
+ * **Advantages of derived owners:**
221
+ *
222
+ * - **Deterministic**: Same path always produces the same ShardOwner across all
223
+ * devices
224
+ * - **Immediate availability**: Can be hardcoded and used before sync occurs
225
+ * - **Consistent setup**: All devices start with identical data structure
226
+ * - **Lifecycle management**: Can implement epoch patterns for clean data
227
+ * deletion and recreation
228
+ *
229
+ * **Common patterns:**
230
+ *
231
+ * - Use paths like `["shard", 1]` for versioned data lifecycle
232
+ * - Use paths like `["project", "MyApp", 1]` for named partitions with versions
233
+ * - Each device can derive the same owners and set up initial structure
170
234
  */
171
- export const createShardOwner = (
172
- deps: TimeDep & CreateRandomBytesDep & CreateMnemonicDep,
235
+ export const deriveShardOwner = (
236
+ owner: AppOwner,
237
+ path: NonEmptyReadonlyArray<string | number>,
238
+ transports?: ReadonlyArray<OwnerTransport>,
173
239
  ): ShardOwner => {
174
- const owner = createOwner(deps)();
240
+ const secret = createSlip21(owner.encryptionKey, path) as OwnerSecret;
241
+
175
242
  return {
176
243
  type: "ShardOwner",
177
- mnemonic: owner.mnemonic,
178
- writeKey: owner.writeKey,
244
+ ...createOwner(secret),
245
+ ...(transports && { transports }),
179
246
  };
180
247
  };
181
248
 
249
+ /** An {@link Owner} for collaborative data with write access. */
250
+ export interface SharedOwner extends Owner {
251
+ readonly type: "SharedOwner";
252
+ readonly transports?: ReadonlyArray<OwnerTransport>;
253
+ }
254
+
182
255
  /**
183
- * Creates a fresh {@link SharedOwner} for sharing data with write access. Takes
184
- * no arguments as both `mnemonic` and rotatable `writeKey` are newly generated;
185
- * when shared, recipients use the provided `mnemonic` and `writeKey` directly
186
- * as a {@link SharedOwner} without needing to recreate it.
256
+ * Creates a {@link SharedOwner} from an {@link OwnerSecret} for collaborative
257
+ * write access.
258
+ *
259
+ * Use {@link createSharedReadonlyOwner} to create a read-only version for
260
+ * sharing.
187
261
  */
188
262
  export const createSharedOwner = (
189
- deps: CreateRandomBytesDep & CreateMnemonicDep,
263
+ secret: OwnerSecret,
264
+ transports?: ReadonlyArray<OwnerTransport>,
190
265
  ): SharedOwner => {
191
- const mnemonic = deps.createMnemonic();
192
- const writeKey = createWriteKey(deps)(); // Random, no seed
193
266
  return {
194
267
  type: "SharedOwner",
195
- mnemonic,
196
- writeKey,
268
+ ...createOwner(secret),
269
+ ...(transports && { transports }),
197
270
  };
198
271
  };
199
272
 
200
273
  /**
201
- * Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner} for read-only
202
- * data sharing. Extracts the `mnemonic` from the provided {@link SharedOwner},
203
- * omitting its `writeKey` to ensure read-only access.
274
+ * Read-only version of a {@link SharedOwner} for data sharing. Contains only the
275
+ * {@link OwnerId} and {@link EncryptionKey} needed for others to read the shared
276
+ * data without write access.
204
277
  */
278
+ export interface SharedReadonlyOwner {
279
+ readonly type: "SharedReadonlyOwner";
280
+ readonly id: OwnerId;
281
+ readonly encryptionKey: EncryptionKey;
282
+ readonly transports?: ReadonlyArray<OwnerTransport>;
283
+ }
284
+
285
+ /** Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner}. */
205
286
  export const createSharedReadonlyOwner = (
206
287
  sharedOwner: SharedOwner,
207
- ): SharedReadonlyOwner => {
208
- return {
209
- type: "SharedReadonlyOwner",
210
- mnemonic: sharedOwner.mnemonic,
211
- };
212
- };
213
-
214
- /** Creates an {@link Owner} with optional `mnemonic` and `writeKey`. */
215
- export const createOwner =
216
- (deps: TimeDep & CreateRandomBytesDep & CreateMnemonicDep) =>
217
- (mnemonic = deps.createMnemonic(), writeKey?: WriteKey): Owner => {
218
- const seed = mnemonicToMnemonicSeed(mnemonic);
219
-
220
- const id = createSlip21Id(seed, ["Evolu", "Owner Id"]) as OwnerId;
221
- const encryptionKey = createEncryptionKey(seed);
222
-
223
- const createdAt = DateIso.fromParent(new Date(deps.time.now()));
224
- assert(createdAt.ok, "Invalid DateIso: bad system clock");
225
-
226
- return {
227
- mnemonic,
228
- createdAt: createdAt.value,
229
- id,
230
- encryptionKey,
231
- writeKey: writeKey ?? createWriteKey(deps)(seed),
232
- };
233
- };
234
-
235
- export const createWriteKey =
236
- (deps: CreateRandomBytesDep) =>
237
- (seed?: MnemonicSeed): WriteKey => {
238
- const key = seed
239
- ? createSlip21(seed, ["Evolu", "Write Key"]).slice(0, 16)
240
- : deps.createRandomBytes(16);
288
+ ): SharedReadonlyOwner => ({
289
+ type: "SharedReadonlyOwner",
290
+ id: sharedOwner.id,
291
+ encryptionKey: sharedOwner.encryptionKey,
292
+ ...(sharedOwner.transports && { transports: sharedOwner.transports }),
293
+ });
241
294
 
242
- const writeKey = WriteKey.from(key);
243
- assert(writeKey.ok, "Ensure valid WriteKey");
295
+ /**
296
+ * Transport configuration for connecting to relays.
297
+ *
298
+ * Currently only WebSocket, in the future Bluetooth, LocalNetwork, etc.
299
+ */
300
+ export type OwnerTransport = OwnerWebSocketTransport;
244
301
 
245
- return writeKey.value;
246
- };
302
+ /**
303
+ * WebSocket transport configuration.
304
+ *
305
+ * ### Authentication via URL
306
+ *
307
+ * The {@link OwnerId} is passed as a URL query parameter. While this approach is
308
+ * generally discouraged for authentication tokens (they get logged), it's safe
309
+ * here because OwnerId is pseudonymous and used only for access verification -
310
+ * it provides no ability to read encrypted data or write changes.
311
+ *
312
+ * See: [HTTP headers in Websockets client
313
+ * API](https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api/74564827#74564827)
314
+ *
315
+ * ### Error Handling
316
+ *
317
+ * When a relay rejects a connection (invalid OwnerId, unauthorized owner, or
318
+ * server error), the browser WebSocket API does not expose the specific HTTP
319
+ * status code or reason - it only reports a generic connection failure. The
320
+ * client automatically retries with exponential backoff and jitter, eventually
321
+ * succeeding once the configuration or server issue is resolved.
322
+ *
323
+ * Legitimate clients will be properly configured with valid credentials, so
324
+ * automatic retry is OK.
325
+ *
326
+ * @see {@link createOwnerWebSocketTransport}
327
+ * @see {@link parseOwnerIdFromOwnerWebSocketTransportUrl}
328
+ */
329
+ export interface OwnerWebSocketTransport {
330
+ readonly type: "WebSocket";
331
+ readonly url: string;
332
+ }
247
333
 
248
334
  /**
249
- * An `OwnerRow` represents a row in the `evolu_owner` table, based on an
250
- * {@link Owner} with an added `timestamp` ({@link TimestampString}) for CRDT
251
- * sync. It supports all {@link Owner} variants with an optional `writeKey`; use
252
- * {@link createOwnerRow} to align it with a specific {@link Owner}.
335
+ * Creates an {@link OwnerWebSocketTransport} for the given relay URL and
336
+ * {@link OwnerId}.
337
+ *
338
+ * The URL must be a WebSocket base URL without query parameters or fragments
339
+ * (e.g., `wss://relay.evolu.dev`, not `wss://relay.evolu.dev?foo=bar`). The
340
+ * function appends the `ownerId` as a query parameter.
341
+ *
342
+ * ### Example
343
+ *
344
+ * ```ts
345
+ * // Create transport "wss://relay.evolu.dev?ownerId=..."
346
+ * const transport = createOwnerWebSocketTransport({
347
+ * url: "wss://relay.evolu.dev",
348
+ * ownerId: owner.id,
349
+ * });
350
+ *
351
+ * // Use with createEvolu
352
+ * const evolu = createEvolu(deps)(Schema, {
353
+ * transports: [transport],
354
+ * });
355
+ * ```
253
356
  */
254
- export type OwnerRow = Omit<Owner, "writeKey"> & {
255
- readonly writeKey: WriteKey | null;
256
- readonly timestamp: TimestampString;
257
- };
357
+ export const createOwnerWebSocketTransport = (config: {
358
+ readonly url: string;
359
+ readonly ownerId: OwnerId;
360
+ }): OwnerWebSocketTransport => ({
361
+ type: "WebSocket",
362
+ url: `${config.url}?ownerId=${config.ownerId}`,
363
+ });
258
364
 
259
365
  /**
260
- * Creates an {@link OwnerRow} from any {@link Owner} variant for the
261
- * `evolu_owner` table, adding a `timestamp` ({@link TimestampString}) for CRDT
262
- * sync.
366
+ * Extracts {@link OwnerId} from an {@link OwnerWebSocketTransport} URL query
367
+ * string.
368
+ *
369
+ * Parses the query string `?ownerId=...` and validates that the extracted value
370
+ * is a valid {@link OwnerId}.
371
+ *
372
+ * ### Example
373
+ *
374
+ * ```ts
375
+ * parseOwnerIdFromOwnerWebSocketTransportUrl(
376
+ * "/sync?ownerId=_12345678abcdefgh",
377
+ * );
378
+ * // Returns: OwnerId or null
379
+ * ```
263
380
  */
264
- export const createOwnerRow =
265
- (deps: TimeDep & CreateRandomBytesDep & CreateMnemonicDep & NanoIdLibDep) =>
266
- (
267
- owner: AppOwner | ShardOwner | SharedOwner | SharedReadonlyOwner,
268
- ): OwnerRow => {
269
- const timestamp = timestampToTimestampString(createInitialTimestamp(deps));
270
- switch (owner.type) {
271
- case "AppOwner": {
272
- const { type, ...rest } = owner;
273
- return { ...rest, timestamp };
274
- }
275
- case "ShardOwner":
276
- case "SharedOwner":
277
- return {
278
- ...createOwner(deps)(owner.mnemonic, owner.writeKey),
279
- timestamp,
280
- };
281
- case "SharedReadonlyOwner":
282
- return {
283
- ...createOwner(deps)(owner.mnemonic),
284
- writeKey: null,
285
- timestamp,
286
- };
287
- }
288
- };
381
+ export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
382
+ url: string,
383
+ ): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
384
+
385
+ /** Base interface for all owner errors. */
386
+ export interface BaseOwnerError {
387
+ readonly ownerId: OwnerId;
388
+ }
289
389
 
290
390
  /**
291
- * Rotates the {@link WriteKey} for an {@link AppOwner}, {@link ShardOwner}, or
292
- * {@link SharedOwner}, returning a new instance with the updated key.
391
+ * Usage data for an {@link OwnerId}.
392
+ *
393
+ * Tracks data consumption to monitor usage patterns and enforce quotas if
394
+ * needed. Used by both relays and clients.
395
+ *
396
+ * Relays and clients must handle rate limiting, connection limits, and request
397
+ * throttling separately with in-memory state.
293
398
  */
294
- export const rotateWriteKey = <T extends AppOwner | ShardOwner | SharedOwner>(
295
- owner: T,
296
- newWriteKey: WriteKey,
297
- ): T => {
298
- return {
299
- ...owner,
300
- writeKey: newWriteKey,
301
- } as T;
302
- };
399
+ export interface OwnerUsage {
400
+ /** The {@link Owner} this usage data belongs to. */
401
+ readonly ownerId: OwnerIdBytes;
402
+
403
+ /**
404
+ * Total logical data bytes stored.
405
+ *
406
+ * Measures the size of {@link EncryptedDbChange}s only, excluding
407
+ * {@link Storage} implementation overhead (with SqliteStorage: indexes,
408
+ * skiplist columns, etc.). This provides:
409
+ *
410
+ * - **Predictable measurement** - same data = same byte count across all
411
+ * instances
412
+ * - **Quota enforcement** - consistent billing/limits independent of storage
413
+ * implementation
414
+ * - **Overhead tracking** - actual Storage size can be compared against this to
415
+ * monitor efficiency
416
+ */
417
+ readonly storedBytes: NonNegativeInt;
418
+
419
+ // TODO: Decide how to use receivedBytes and sentBytes.
420
+ // /** Total bytes received. */
421
+ // readonly receivedBytes: NonNegativeInt;
422
+
423
+ // TODO: Decide how to use sentBytes.
424
+ // /** Total bytes sent. */
425
+ // readonly sentBytes: NonNegativeInt;
426
+
427
+ // TODO: Decide how to use firstTimestamp.
428
+ // /**
429
+ // * The minimum {@link Timestamp}.
430
+ // *
431
+ // * Helps {@link Storage} choose faster algorithms.
432
+ // */
433
+ // readonly firstTimestamp: TimestampBytes | null;
434
+
435
+ // TODO: Decide how to use lastTimestamp.
436
+ // /**
437
+ // * The maximum {@link Timestamp}.
438
+ // *
439
+ // * Helps {@link Storage} choose faster algorithms. Free relays can use it to
440
+ // * identify inactive accounts for cleanup or archival.
441
+ // */
442
+ // readonly lastTimestamp: TimestampBytes | null;
443
+ }