@evolu/web 3.1.2 → 3.3.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/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +4 -1
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +70 -12
- package/dist/src/Task.d.ts +6 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +2 -2
- package/dist/src/Worker.d.ts +38 -1
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +61 -2
- package/dist/src/local-first/Evolu.d.ts +47 -3
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +336 -50
- package/dist/src/local-first/Shared.worker.js +14 -4
- package/package.json +4 -5
- package/src/Platform.test.ts +29 -1
- package/src/Platform.ts +2 -1
- package/src/Sqlite.test.ts +181 -4
- package/src/Sqlite.ts +85 -12
- package/src/Task.test.ts +3 -14
- package/src/Task.ts +8 -4
- package/src/Worker.test.ts +59 -0
- package/src/Worker.ts +66 -1
- package/src/local-first/Evolu.test.ts +601 -2
- package/src/local-first/Evolu.ts +321 -40
- package/src/local-first/Shared.worker.ts +32 -5
- package/dist/src/local-first/LocalAuth.d.ts +0 -3
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -185
- package/src/local-first/LocalAuth.ts +0 -320
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AuthResult,
|
|
3
|
-
Entropy32,
|
|
4
|
-
RandomBytesDep,
|
|
5
|
-
SecureStorage,
|
|
6
|
-
SensitiveInfoItem,
|
|
7
|
-
} from "@evolu/common";
|
|
8
|
-
import {
|
|
9
|
-
Base64Url,
|
|
10
|
-
base64UrlToUint8Array,
|
|
11
|
-
bytesToUtf8,
|
|
12
|
-
createSlip21,
|
|
13
|
-
decryptWithXChaCha20Poly1305,
|
|
14
|
-
EncryptionKey,
|
|
15
|
-
encryptWithXChaCha20Poly1305,
|
|
16
|
-
Entropy24,
|
|
17
|
-
uint8ArrayToBase64Url,
|
|
18
|
-
utf8ToBytes,
|
|
19
|
-
XChaCha20Poly1305Ciphertext,
|
|
20
|
-
} from "@evolu/common";
|
|
21
|
-
import type { UseStore } from "idb-keyval";
|
|
22
|
-
import { clear, createStore, del, get, keys, set } from "idb-keyval";
|
|
23
|
-
|
|
24
|
-
export const createWebAuthnStore = (deps: RandomBytesDep): SecureStorage => ({
|
|
25
|
-
setItem: async (key, value, options) => {
|
|
26
|
-
if (options?.accessControl === "none") {
|
|
27
|
-
const metadata = createMetadata(false);
|
|
28
|
-
await set(key, { value, metadata }, getStore(options.service));
|
|
29
|
-
return { metadata };
|
|
30
|
-
}
|
|
31
|
-
const seed = generateSeed(deps)();
|
|
32
|
-
const authResult = JSON.parse(value) as AuthResult;
|
|
33
|
-
const credential = await createCredential(deps)(
|
|
34
|
-
options?.webAuthnUsername ?? "Evolu User",
|
|
35
|
-
seed,
|
|
36
|
-
options?.relyingPartyID,
|
|
37
|
-
options?.relyingPartyName,
|
|
38
|
-
options?.webAuthnUserVerification,
|
|
39
|
-
options?.webAuthnAuthenticatorAttachment,
|
|
40
|
-
);
|
|
41
|
-
const encryptionKey = deriveEncryptionKey(seed);
|
|
42
|
-
const encryptedData = encryptAuthResult(deps)(authResult, encryptionKey);
|
|
43
|
-
const credentialId = uint8ArrayToBase64Url(
|
|
44
|
-
new Uint8Array(credential.rawId),
|
|
45
|
-
);
|
|
46
|
-
const metadata = createMetadata();
|
|
47
|
-
await set(
|
|
48
|
-
key,
|
|
49
|
-
{ credentialId, ...encryptedData, metadata },
|
|
50
|
-
getStore(options?.service),
|
|
51
|
-
);
|
|
52
|
-
return { metadata };
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
getItem: async (key, options) => {
|
|
56
|
-
if (options?.accessControl === "none") {
|
|
57
|
-
const data = await get<{
|
|
58
|
-
readonly value: string;
|
|
59
|
-
readonly metadata: SensitiveInfoItem["metadata"];
|
|
60
|
-
}>(key, getStore(options.service));
|
|
61
|
-
return data
|
|
62
|
-
? {
|
|
63
|
-
key,
|
|
64
|
-
value: data.value,
|
|
65
|
-
service: options.service ?? "default",
|
|
66
|
-
metadata: data.metadata,
|
|
67
|
-
}
|
|
68
|
-
: null;
|
|
69
|
-
}
|
|
70
|
-
const data = await get<{
|
|
71
|
-
readonly nonce: Base64Url;
|
|
72
|
-
readonly ciphertext: Base64Url;
|
|
73
|
-
readonly credentialId: string;
|
|
74
|
-
readonly metadata: SensitiveInfoItem["metadata"];
|
|
75
|
-
}>(key, getStore(options?.service));
|
|
76
|
-
if (!data) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
const credential = await getCredential(deps)(
|
|
81
|
-
data.credentialId,
|
|
82
|
-
options?.relyingPartyID,
|
|
83
|
-
options?.webAuthnUserVerification,
|
|
84
|
-
);
|
|
85
|
-
const credentialSeed = extractSeedFromCredential(credential);
|
|
86
|
-
const encryptionKey = deriveEncryptionKey(credentialSeed);
|
|
87
|
-
const authResultVal = decryptAuthResult(data, encryptionKey);
|
|
88
|
-
if (!authResultVal) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
key,
|
|
93
|
-
service: options?.service ?? "default",
|
|
94
|
-
value: authResultVal,
|
|
95
|
-
metadata: data.metadata,
|
|
96
|
-
};
|
|
97
|
-
} catch (_error) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
deleteItem: async (key, options) => {
|
|
103
|
-
await del(key, getStore(options?.service));
|
|
104
|
-
return true;
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
getAllItems: async (options) => {
|
|
108
|
-
const service = options?.service ?? "default";
|
|
109
|
-
const itemKeys = await keys<string>(getStore(service));
|
|
110
|
-
const items = await Promise.all(
|
|
111
|
-
itemKeys.map(async (key) => {
|
|
112
|
-
const data = await get<{
|
|
113
|
-
readonly metadata?: SensitiveInfoItem["metadata"];
|
|
114
|
-
readonly value?: string;
|
|
115
|
-
}>(key, getStore(service));
|
|
116
|
-
return {
|
|
117
|
-
key,
|
|
118
|
-
service,
|
|
119
|
-
metadata: data?.metadata ?? createMetadata(),
|
|
120
|
-
...(options?.includeValues && data?.value
|
|
121
|
-
? { value: data.value }
|
|
122
|
-
: {}),
|
|
123
|
-
};
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
return items;
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
clearService: async (options) => {
|
|
130
|
-
await clear(getStore(options?.service));
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Create default metadata for backwards compatibility with items that don't
|
|
136
|
-
* have stored metadata.
|
|
137
|
-
*/
|
|
138
|
-
const createMetadata = (isSecure = true): SensitiveInfoItem["metadata"] => ({
|
|
139
|
-
backend: "keychain",
|
|
140
|
-
accessControl: isSecure ? "biometryCurrentSet" : "none",
|
|
141
|
-
securityLevel: isSecure ? "biometry" : "software",
|
|
142
|
-
timestamp: Date.now(),
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
/** Get storage key for owner ID. (supports namespaces via prefix) */
|
|
146
|
-
const getStore = (prefix = "default"): UseStore =>
|
|
147
|
-
createStore(prefix, "evolu-auth");
|
|
148
|
-
|
|
149
|
-
const createCredential =
|
|
150
|
-
(deps: RandomBytesDep) =>
|
|
151
|
-
async (
|
|
152
|
-
username: string,
|
|
153
|
-
seed: Uint8Array,
|
|
154
|
-
relyingPartyID?: string,
|
|
155
|
-
relyingPartyName?: string,
|
|
156
|
-
userVerification?: UserVerificationRequirement,
|
|
157
|
-
authenticatorAttachment?: AuthenticatorAttachment,
|
|
158
|
-
): Promise<PublicKeyCredential> => {
|
|
159
|
-
const options = createCredentialCreationOptions(deps)(
|
|
160
|
-
username,
|
|
161
|
-
seed,
|
|
162
|
-
relyingPartyID,
|
|
163
|
-
relyingPartyName,
|
|
164
|
-
userVerification,
|
|
165
|
-
authenticatorAttachment,
|
|
166
|
-
);
|
|
167
|
-
const credential = (await navigator.credentials.create(
|
|
168
|
-
options,
|
|
169
|
-
)) as PublicKeyCredential | null;
|
|
170
|
-
if (!credential) {
|
|
171
|
-
throw new Error("Failed to create WebAuthn credential");
|
|
172
|
-
}
|
|
173
|
-
return credential;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const getCredential =
|
|
177
|
-
(deps: RandomBytesDep) =>
|
|
178
|
-
async (
|
|
179
|
-
credentialId: string,
|
|
180
|
-
relyingPartyID?: string,
|
|
181
|
-
userVerification?: UserVerificationRequirement,
|
|
182
|
-
): Promise<PublicKeyCredential> => {
|
|
183
|
-
const options = createCredentialRequestOptions(deps)(
|
|
184
|
-
credentialId,
|
|
185
|
-
relyingPartyID,
|
|
186
|
-
userVerification,
|
|
187
|
-
);
|
|
188
|
-
const credential = (await navigator.credentials.get(
|
|
189
|
-
options,
|
|
190
|
-
)) as PublicKeyCredential | null;
|
|
191
|
-
if (!credential?.response) {
|
|
192
|
-
throw new Error("Failed to get WebAuthn credential");
|
|
193
|
-
}
|
|
194
|
-
return credential;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const extractSeedFromCredential = (
|
|
198
|
-
credential: PublicKeyCredential,
|
|
199
|
-
): Uint8Array => {
|
|
200
|
-
const response = credential.response as AuthenticatorAssertionResponse;
|
|
201
|
-
if (!response.userHandle) {
|
|
202
|
-
throw new Error("No userHandle in credential response");
|
|
203
|
-
}
|
|
204
|
-
return new Uint8Array(response.userHandle);
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const createCredentialCreationOptions =
|
|
208
|
-
(deps: RandomBytesDep) =>
|
|
209
|
-
(
|
|
210
|
-
username: string,
|
|
211
|
-
seed: Uint8Array,
|
|
212
|
-
relyingPartyID?: string,
|
|
213
|
-
relyingPartyName?: string,
|
|
214
|
-
userVerification?: UserVerificationRequirement,
|
|
215
|
-
authenticatorAttachment?: AuthenticatorAttachment,
|
|
216
|
-
): CredentialCreationOptions => ({
|
|
217
|
-
publicKey: {
|
|
218
|
-
challenge: generateSeed(deps)() as BufferSource,
|
|
219
|
-
rp: {
|
|
220
|
-
id: relyingPartyID ?? document.location.hostname,
|
|
221
|
-
name: relyingPartyName ?? "Evolu",
|
|
222
|
-
},
|
|
223
|
-
user: {
|
|
224
|
-
id: seed as BufferSource,
|
|
225
|
-
name: username,
|
|
226
|
-
displayName: username,
|
|
227
|
-
},
|
|
228
|
-
pubKeyCredParams: [
|
|
229
|
-
// Ed25519
|
|
230
|
-
{ type: "public-key", alg: -8 },
|
|
231
|
-
// ES256
|
|
232
|
-
{ type: "public-key", alg: -7 },
|
|
233
|
-
// RS256
|
|
234
|
-
{ type: "public-key", alg: -257 },
|
|
235
|
-
],
|
|
236
|
-
attestation: "none",
|
|
237
|
-
authenticatorSelection: {
|
|
238
|
-
// - "platform": Uses the platform's built-in authenticator.
|
|
239
|
-
// - "cross-platform": Uses a device specific authenticator (yubikey, fido2, etc.)
|
|
240
|
-
authenticatorAttachment: authenticatorAttachment ?? "platform",
|
|
241
|
-
// - "discouraged": Only User Presence is needed.
|
|
242
|
-
// - "preferred": User Verification is preferred but not required. Falls back to User Presence.
|
|
243
|
-
// - "required": User Verification MUST occur (biometrics/PIN). Clients may silently downgrade to User Presence only.
|
|
244
|
-
userVerification: userVerification ?? "required",
|
|
245
|
-
// - "discouraged": Server-side credential is preferable, but will accept client-side discoverable credential.
|
|
246
|
-
// - "preferred": Relying Party strongly prefers client-side discoverable credential but will accept server-side credential.
|
|
247
|
-
// - "required": Client-side discoverable credential MUST be created, error if it can't be created.
|
|
248
|
-
residentKey: "required",
|
|
249
|
-
// Included for backwards compatibility. Deprecated in favor of residentKey (true = "required")
|
|
250
|
-
requireResidentKey: true,
|
|
251
|
-
},
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const createCredentialRequestOptions =
|
|
256
|
-
(deps: RandomBytesDep) =>
|
|
257
|
-
(
|
|
258
|
-
credentialId: string,
|
|
259
|
-
relyingPartyID?: string,
|
|
260
|
-
userVerification?: UserVerificationRequirement,
|
|
261
|
-
): CredentialRequestOptions => ({
|
|
262
|
-
publicKey: {
|
|
263
|
-
challenge: generateSeed(deps)() as BufferSource,
|
|
264
|
-
rpId: relyingPartyID ?? document.location.hostname,
|
|
265
|
-
userVerification: userVerification ?? "preferred",
|
|
266
|
-
allowCredentials: [
|
|
267
|
-
{
|
|
268
|
-
type: "public-key",
|
|
269
|
-
id: base64UrlToUint8Array(
|
|
270
|
-
Base64Url.orThrow(credentialId),
|
|
271
|
-
) as BufferSource,
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const deriveEncryptionKey = (seed: Uint8Array): EncryptionKey => {
|
|
278
|
-
const seed32 = seed.length === 32 ? seed : seed.slice(0, 32);
|
|
279
|
-
return EncryptionKey.orThrow(
|
|
280
|
-
createSlip21(seed32 as Entropy32, ["evolu", "auth"]),
|
|
281
|
-
);
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const encryptAuthResult =
|
|
285
|
-
(deps: RandomBytesDep) =>
|
|
286
|
-
(
|
|
287
|
-
authResult: AuthResult,
|
|
288
|
-
encryptionKey: EncryptionKey,
|
|
289
|
-
): {
|
|
290
|
-
nonce: Base64Url;
|
|
291
|
-
ciphertext: Base64Url;
|
|
292
|
-
} => {
|
|
293
|
-
const plaintext = utf8ToBytes(JSON.stringify(authResult));
|
|
294
|
-
const [ciphertext, nonce] = encryptWithXChaCha20Poly1305(deps)(
|
|
295
|
-
plaintext,
|
|
296
|
-
encryptionKey,
|
|
297
|
-
);
|
|
298
|
-
return {
|
|
299
|
-
nonce: uint8ArrayToBase64Url(nonce),
|
|
300
|
-
ciphertext: uint8ArrayToBase64Url(ciphertext),
|
|
301
|
-
};
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
const decryptAuthResult = (
|
|
305
|
-
encryptedData: { nonce: Base64Url; ciphertext: Base64Url },
|
|
306
|
-
encryptionKey: EncryptionKey,
|
|
307
|
-
): string | null => {
|
|
308
|
-
const nonce = base64UrlToUint8Array(encryptedData.nonce);
|
|
309
|
-
const ciphertext = base64UrlToUint8Array(encryptedData.ciphertext);
|
|
310
|
-
const result = decryptWithXChaCha20Poly1305(
|
|
311
|
-
XChaCha20Poly1305Ciphertext.orThrow(ciphertext),
|
|
312
|
-
Entropy24.orThrow(nonce),
|
|
313
|
-
encryptionKey,
|
|
314
|
-
);
|
|
315
|
-
if (!result.ok) return null;
|
|
316
|
-
return bytesToUtf8(result.value);
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const generateSeed = (deps: RandomBytesDep) => () =>
|
|
320
|
-
deps.randomBytes.create(32);
|