@ursalock/zustand 0.2.8 → 0.3.1
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/index.d.ts +46 -1
- package/dist/index.js +135 -29
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -142,6 +142,21 @@ interface SyncOptions {
|
|
|
142
142
|
onStatusChange?: (status: SyncStatus) => void;
|
|
143
143
|
/** HTTP client for making requests (default: FetchHttpClient) */
|
|
144
144
|
httpClient?: IHttpClient;
|
|
145
|
+
/** Storage provider for offline queue (default: localStorage) */
|
|
146
|
+
storageProvider?: {
|
|
147
|
+
getItem(key: string): string | null;
|
|
148
|
+
setItem(key: string, value: string): void;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* HMAC key for sync integrity verification (Encrypt-then-MAC).
|
|
152
|
+
* When provided, every push includes an HMAC-SHA256 tag over the
|
|
153
|
+
* ciphertext; every pull verifies it before passing data to the
|
|
154
|
+
* decryption layer. This detects server-side tampering.
|
|
155
|
+
*
|
|
156
|
+
* Should be derived from the user's master key via a separate
|
|
157
|
+
* derivation path (key separation principle).
|
|
158
|
+
*/
|
|
159
|
+
hmacKey?: Uint8Array;
|
|
145
160
|
}
|
|
146
161
|
/**
|
|
147
162
|
* Create a sync engine instance
|
|
@@ -254,6 +269,8 @@ type StoreVault<S, Ps> = S extends {
|
|
|
254
269
|
hasPendingChanges: () => boolean;
|
|
255
270
|
/** Clear all stored data (local + server) */
|
|
256
271
|
clearStorage: () => Promise<void>;
|
|
272
|
+
/** Clean up sync interval and timers */
|
|
273
|
+
destroy: () => void;
|
|
257
274
|
/** Subscribe to hydration start */
|
|
258
275
|
onHydrate: (fn: VaultListener<T>) => () => void;
|
|
259
276
|
/** Subscribe to hydration complete */
|
|
@@ -349,5 +366,33 @@ declare function useSyncStatus<T extends {
|
|
|
349
366
|
getSyncStatus?: () => SyncStatus;
|
|
350
367
|
};
|
|
351
368
|
}>(useStore: () => T): SyncStatus;
|
|
369
|
+
/**
|
|
370
|
+
* Hook to check if store has been hydrated
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```tsx
|
|
374
|
+
* const hydrated = useHydrated(useStore)
|
|
375
|
+
* if (!hydrated) return <Loading />
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
declare function useHydrated<T extends {
|
|
379
|
+
vault?: {
|
|
380
|
+
hasHydrated?: () => boolean;
|
|
381
|
+
};
|
|
382
|
+
}>(useStore: () => T): boolean;
|
|
383
|
+
/**
|
|
384
|
+
* Hook to check if there are pending offline changes
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```tsx
|
|
388
|
+
* const hasPending = usePendingChanges(useStore)
|
|
389
|
+
* if (hasPending) return <PendingBadge />
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
declare function usePendingChanges<T extends {
|
|
393
|
+
vault?: {
|
|
394
|
+
hasPendingChanges?: () => boolean;
|
|
395
|
+
};
|
|
396
|
+
}>(useStore: () => T): boolean;
|
|
352
397
|
|
|
353
|
-
export { type EncryptedStorageOptions, FetchHttpClient, type IHttpClient, type IHttpRequest, type IHttpResponse, type IStorageProvider, type IVaultStorage, type JwkEncryptedStorageOptions, type LegacyEncryptedStorageOptions, LocalStorageProvider, type SyncEngine, type SyncState, type SyncStatus, type VaultOptions, type VaultOptionsJwk, type VaultOptionsLegacy, type VaultStorage, createSyncEngine, createVaultStorage, useSyncStatus, vault };
|
|
398
|
+
export { type EncryptedStorageOptions, FetchHttpClient, type IHttpClient, type IHttpRequest, type IHttpResponse, type IStorageProvider, type IVaultStorage, type JwkEncryptedStorageOptions, type LegacyEncryptedStorageOptions, LocalStorageProvider, type SyncEngine, type SyncState, type SyncStatus, type VaultOptions, type VaultOptionsJwk, type VaultOptionsLegacy, type VaultStorage, createSyncEngine, createVaultStorage, useHydrated, usePendingChanges, useSyncStatus, vault };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
decrypt,
|
|
5
5
|
deriveKey,
|
|
6
6
|
recoveryKeyToBytes,
|
|
7
|
+
validateRecoveryKey,
|
|
7
8
|
encryptWithJwk,
|
|
8
|
-
decryptWithJwk
|
|
9
|
+
decryptWithJwk,
|
|
10
|
+
constantTimeEqual
|
|
9
11
|
} from "@ursalock/crypto";
|
|
10
12
|
|
|
11
13
|
// src/providers/local-storage.ts
|
|
@@ -77,6 +79,9 @@ function createJwkStorage(cipherJwk, storage, prefix) {
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
function createLegacyStorage(recoveryKey, storage, prefix) {
|
|
82
|
+
if (!validateRecoveryKey(recoveryKey)) {
|
|
83
|
+
throw new Error("[ursalock] Invalid recovery key format");
|
|
84
|
+
}
|
|
80
85
|
let cachedKey = null;
|
|
81
86
|
let cachedSalt = null;
|
|
82
87
|
async function getOrDeriveKey(salt) {
|
|
@@ -151,13 +156,7 @@ function base64ToBytes(base64) {
|
|
|
151
156
|
}
|
|
152
157
|
return bytes;
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
if (a.length !== b.length) return false;
|
|
156
|
-
for (let i = 0; i < a.length; i++) {
|
|
157
|
-
if (a[i] !== b[i]) return false;
|
|
158
|
-
}
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
159
|
+
var arrayEqual = constantTimeEqual;
|
|
161
160
|
|
|
162
161
|
// src/providers/fetch-http.ts
|
|
163
162
|
var FetchHttpClient = class {
|
|
@@ -177,6 +176,7 @@ var FetchHttpClient = class {
|
|
|
177
176
|
};
|
|
178
177
|
|
|
179
178
|
// src/sync.ts
|
|
179
|
+
import { computeHmac, verifyHmac } from "@ursalock/crypto";
|
|
180
180
|
var QUEUE_KEY = "ursalock:offline-queue";
|
|
181
181
|
function createSyncEngine(options) {
|
|
182
182
|
const {
|
|
@@ -186,29 +186,34 @@ function createSyncEngine(options) {
|
|
|
186
186
|
onServerData,
|
|
187
187
|
getLocalData,
|
|
188
188
|
onStatusChange,
|
|
189
|
-
httpClient = new FetchHttpClient()
|
|
189
|
+
httpClient = new FetchHttpClient(),
|
|
190
|
+
storageProvider,
|
|
191
|
+
hmacKey
|
|
190
192
|
} = options;
|
|
193
|
+
const textEncoder = new TextEncoder();
|
|
194
|
+
const queueStorage = storageProvider ?? (typeof localStorage !== "undefined" ? localStorage : null);
|
|
191
195
|
let status = "idle";
|
|
192
196
|
let lastSyncAt = null;
|
|
193
197
|
let error = null;
|
|
198
|
+
let knownServerVersion = null;
|
|
194
199
|
const setStatus = (newStatus, newError) => {
|
|
195
200
|
status = newStatus;
|
|
196
201
|
error = newError ?? null;
|
|
197
202
|
onStatusChange?.(newStatus);
|
|
198
203
|
};
|
|
199
204
|
const loadQueue = () => {
|
|
200
|
-
if (
|
|
205
|
+
if (!queueStorage) return { pending: [] };
|
|
201
206
|
try {
|
|
202
|
-
const stored =
|
|
207
|
+
const stored = queueStorage.getItem(`${QUEUE_KEY}:${name}`);
|
|
203
208
|
return stored ? JSON.parse(stored) : { pending: [] };
|
|
204
209
|
} catch {
|
|
205
210
|
return { pending: [] };
|
|
206
211
|
}
|
|
207
212
|
};
|
|
208
213
|
const saveQueue = (queue) => {
|
|
209
|
-
if (
|
|
214
|
+
if (!queueStorage) return;
|
|
210
215
|
try {
|
|
211
|
-
|
|
216
|
+
queueStorage.setItem(`${QUEUE_KEY}:${name}`, JSON.stringify(queue));
|
|
212
217
|
} catch {
|
|
213
218
|
}
|
|
214
219
|
};
|
|
@@ -228,7 +233,10 @@ function createSyncEngine(options) {
|
|
|
228
233
|
};
|
|
229
234
|
const fetchServer = async () => {
|
|
230
235
|
const token = getToken();
|
|
231
|
-
if (!token)
|
|
236
|
+
if (!token) {
|
|
237
|
+
console.warn("[ursalock] fetchServer: no auth token available, skipping");
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
232
240
|
const res = await httpClient.request({
|
|
233
241
|
url: `${serverUrl}/vault/by-name/${encodeURIComponent(name)}`,
|
|
234
242
|
method: "GET",
|
|
@@ -239,17 +247,47 @@ function createSyncEngine(options) {
|
|
|
239
247
|
});
|
|
240
248
|
if (res.status === 404) return null;
|
|
241
249
|
if (!res.ok) throw new Error(`Server error: ${res.status}`);
|
|
242
|
-
|
|
250
|
+
const vault2 = await res.json();
|
|
251
|
+
knownServerVersion = vault2.version;
|
|
252
|
+
return vault2;
|
|
253
|
+
};
|
|
254
|
+
const computeTag = async (data) => {
|
|
255
|
+
if (!hmacKey) return void 0;
|
|
256
|
+
return computeHmac(textEncoder.encode(data), hmacKey);
|
|
257
|
+
};
|
|
258
|
+
const verifyTag = async (vault2) => {
|
|
259
|
+
if (!hmacKey) return;
|
|
260
|
+
if (!vault2.hmac) {
|
|
261
|
+
console.warn(
|
|
262
|
+
"[ursalock] Server vault has no HMAC tag. This is expected for vaults created before integrity verification was enabled. The vault will be re-signed on next push."
|
|
263
|
+
);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const valid = await verifyHmac(
|
|
267
|
+
textEncoder.encode(vault2.data),
|
|
268
|
+
hmacKey,
|
|
269
|
+
vault2.hmac
|
|
270
|
+
);
|
|
271
|
+
if (!valid) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
"[ursalock] HMAC verification failed: server data has been tampered with or the integrity key is wrong"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
243
276
|
};
|
|
244
277
|
const pushServer = async (data, salt) => {
|
|
245
278
|
const token = getToken();
|
|
246
279
|
if (!token) throw new Error("Not authenticated");
|
|
280
|
+
const hmac = await computeTag(data);
|
|
247
281
|
let existing = null;
|
|
248
282
|
try {
|
|
249
283
|
existing = await fetchServer();
|
|
250
284
|
} catch {
|
|
251
285
|
}
|
|
252
286
|
if (existing) {
|
|
287
|
+
const body = { data, salt, ...hmac != null && { hmac } };
|
|
288
|
+
if (knownServerVersion != null) {
|
|
289
|
+
body.version = knownServerVersion;
|
|
290
|
+
}
|
|
253
291
|
const res = await httpClient.request({
|
|
254
292
|
url: `${serverUrl}/vault/${existing.uid}`,
|
|
255
293
|
method: "PUT",
|
|
@@ -257,13 +295,48 @@ function createSyncEngine(options) {
|
|
|
257
295
|
"Authorization": `Bearer ${token}`,
|
|
258
296
|
"Content-Type": "application/json"
|
|
259
297
|
},
|
|
260
|
-
body: JSON.stringify(
|
|
298
|
+
body: JSON.stringify(body)
|
|
261
299
|
});
|
|
300
|
+
if (res.status === 409) {
|
|
301
|
+
const latest = await fetchServer();
|
|
302
|
+
if (latest) {
|
|
303
|
+
await verifyTag(latest);
|
|
304
|
+
onServerData(latest.data, latest.salt, latest.updatedAt);
|
|
305
|
+
const retryLocal = getLocalData();
|
|
306
|
+
const retryHmac = await computeTag(retryLocal.data);
|
|
307
|
+
const retryBody = {
|
|
308
|
+
data: retryLocal.data,
|
|
309
|
+
salt: retryLocal.salt,
|
|
310
|
+
...retryHmac != null && { hmac: retryHmac }
|
|
311
|
+
};
|
|
312
|
+
if (knownServerVersion != null) {
|
|
313
|
+
retryBody.version = knownServerVersion;
|
|
314
|
+
}
|
|
315
|
+
const retryRes = await httpClient.request({
|
|
316
|
+
url: `${serverUrl}/vault/${existing.uid}`,
|
|
317
|
+
method: "PUT",
|
|
318
|
+
headers: {
|
|
319
|
+
"Authorization": `Bearer ${token}`,
|
|
320
|
+
"Content-Type": "application/json"
|
|
321
|
+
},
|
|
322
|
+
body: JSON.stringify(retryBody)
|
|
323
|
+
});
|
|
324
|
+
if (!retryRes.ok) {
|
|
325
|
+
const errorText = await retryRes.text().catch(() => "");
|
|
326
|
+
throw new Error(`Server error: ${retryRes.status} ${errorText}`);
|
|
327
|
+
}
|
|
328
|
+
const result3 = await retryRes.json();
|
|
329
|
+
knownServerVersion = result3.version;
|
|
330
|
+
return result3;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
262
333
|
if (!res.ok) {
|
|
263
334
|
const errorText = await res.text().catch(() => "");
|
|
264
335
|
throw new Error(`Server error: ${res.status} ${errorText}`);
|
|
265
336
|
}
|
|
266
|
-
|
|
337
|
+
const result2 = await res.json();
|
|
338
|
+
knownServerVersion = result2.version;
|
|
339
|
+
return result2;
|
|
267
340
|
}
|
|
268
341
|
const createRes = await httpClient.request({
|
|
269
342
|
url: `${serverUrl}/vault`,
|
|
@@ -272,13 +345,17 @@ function createSyncEngine(options) {
|
|
|
272
345
|
"Authorization": `Bearer ${token}`,
|
|
273
346
|
"Content-Type": "application/json"
|
|
274
347
|
},
|
|
275
|
-
body: JSON.stringify({ name, data, salt })
|
|
348
|
+
body: JSON.stringify({ name, data, salt, ...hmac != null && { hmac } })
|
|
276
349
|
});
|
|
277
350
|
if (createRes.status === 409) {
|
|
278
351
|
const nowExisting = await fetchServer();
|
|
279
352
|
if (!nowExisting) {
|
|
280
353
|
throw new Error("Vault conflict but not found on retry");
|
|
281
354
|
}
|
|
355
|
+
const retryBody = { data, salt, ...hmac != null && { hmac } };
|
|
356
|
+
if (knownServerVersion != null) {
|
|
357
|
+
retryBody.version = knownServerVersion;
|
|
358
|
+
}
|
|
282
359
|
const retryRes = await httpClient.request({
|
|
283
360
|
url: `${serverUrl}/vault/${nowExisting.uid}`,
|
|
284
361
|
method: "PUT",
|
|
@@ -286,19 +363,23 @@ function createSyncEngine(options) {
|
|
|
286
363
|
"Authorization": `Bearer ${token}`,
|
|
287
364
|
"Content-Type": "application/json"
|
|
288
365
|
},
|
|
289
|
-
body: JSON.stringify(
|
|
366
|
+
body: JSON.stringify(retryBody)
|
|
290
367
|
});
|
|
291
368
|
if (!retryRes.ok) {
|
|
292
369
|
const errorText = await retryRes.text().catch(() => "");
|
|
293
370
|
throw new Error(`Server error: ${retryRes.status} ${errorText}`);
|
|
294
371
|
}
|
|
295
|
-
|
|
372
|
+
const result2 = await retryRes.json();
|
|
373
|
+
knownServerVersion = result2.version;
|
|
374
|
+
return result2;
|
|
296
375
|
}
|
|
297
376
|
if (!createRes.ok) {
|
|
298
377
|
const errorText = await createRes.text().catch(() => "");
|
|
299
378
|
throw new Error(`Server error: ${createRes.status} ${errorText}`);
|
|
300
379
|
}
|
|
301
|
-
|
|
380
|
+
const result = await createRes.json();
|
|
381
|
+
knownServerVersion = result.version;
|
|
382
|
+
return result;
|
|
302
383
|
};
|
|
303
384
|
const sync = async () => {
|
|
304
385
|
if (!isOnline()) {
|
|
@@ -322,6 +403,7 @@ function createSyncEngine(options) {
|
|
|
322
403
|
await pushServer(local.data, local.salt);
|
|
323
404
|
}
|
|
324
405
|
} else if (server.updatedAt > local.updatedAt) {
|
|
406
|
+
await verifyTag(server);
|
|
325
407
|
onServerData(server.data, server.salt, server.updatedAt);
|
|
326
408
|
} else if (local.updatedAt > server.updatedAt) {
|
|
327
409
|
await pushServer(local.data, local.salt);
|
|
@@ -370,6 +452,7 @@ function createSyncEngine(options) {
|
|
|
370
452
|
if (server) {
|
|
371
453
|
const local = getLocalData();
|
|
372
454
|
if (server.updatedAt > local.updatedAt) {
|
|
455
|
+
await verifyTag(server);
|
|
373
456
|
onServerData(server.data, server.salt, server.updatedAt);
|
|
374
457
|
lastSyncAt = Date.now();
|
|
375
458
|
setStatus("synced");
|
|
@@ -401,6 +484,7 @@ function createSyncEngine(options) {
|
|
|
401
484
|
}
|
|
402
485
|
|
|
403
486
|
// src/vault.ts
|
|
487
|
+
var logCatch = (context) => (err) => console.error(`[ursalock] ${context}:`, err);
|
|
404
488
|
function isJwkMode2(options) {
|
|
405
489
|
return "cipherJwk" in options;
|
|
406
490
|
}
|
|
@@ -441,7 +525,7 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
441
525
|
getToken,
|
|
442
526
|
onServerData: (data, _salt, updatedAt) => {
|
|
443
527
|
if (localUpdatedAt > updatedAt) {
|
|
444
|
-
void syncEngine?.push();
|
|
528
|
+
void syncEngine?.push().catch(logCatch("Push after local-newer conflict"));
|
|
445
529
|
return;
|
|
446
530
|
}
|
|
447
531
|
try {
|
|
@@ -449,7 +533,7 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
449
533
|
const merged = merge(parsed, get());
|
|
450
534
|
set(merged, true);
|
|
451
535
|
localUpdatedAt = updatedAt;
|
|
452
|
-
void storage.setItem(name, JSON.stringify(partialize({ ...get() })));
|
|
536
|
+
void storage.setItem(name, JSON.stringify(partialize({ ...get() }))).catch(logCatch("Persist server data to local storage"));
|
|
453
537
|
} catch (err) {
|
|
454
538
|
console.error("[ursalock] Failed to parse server data:", err);
|
|
455
539
|
}
|
|
@@ -475,7 +559,7 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
475
559
|
}
|
|
476
560
|
syncDebounceTimer = setTimeout(() => {
|
|
477
561
|
syncDebounceTimer = null;
|
|
478
|
-
void syncEngine?.sync();
|
|
562
|
+
void syncEngine?.sync().catch(logCatch("Debounced sync"));
|
|
479
563
|
}, 3e3);
|
|
480
564
|
}
|
|
481
565
|
};
|
|
@@ -529,7 +613,7 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
529
613
|
} else {
|
|
530
614
|
savedSetState(state);
|
|
531
615
|
}
|
|
532
|
-
void persistState();
|
|
616
|
+
void persistState().catch(logCatch("Persist state"));
|
|
533
617
|
});
|
|
534
618
|
const configResult = config(
|
|
535
619
|
((partial, replace) => {
|
|
@@ -538,7 +622,7 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
538
622
|
} else {
|
|
539
623
|
set(partial);
|
|
540
624
|
}
|
|
541
|
-
void persistState();
|
|
625
|
+
void persistState().catch(logCatch("Persist state"));
|
|
542
626
|
}),
|
|
543
627
|
get,
|
|
544
628
|
api
|
|
@@ -565,15 +649,27 @@ var vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
|
565
649
|
if (!skipHydration) {
|
|
566
650
|
void rehydrate().then(() => {
|
|
567
651
|
if (syncEngine) {
|
|
568
|
-
void syncEngine.sync();
|
|
652
|
+
void syncEngine.sync().catch(logCatch("Initial sync after hydration"));
|
|
569
653
|
}
|
|
570
|
-
});
|
|
654
|
+
}).catch(logCatch("Auto-rehydration"));
|
|
571
655
|
} else {
|
|
572
656
|
hasHydrated = true;
|
|
573
657
|
}
|
|
658
|
+
let syncIntervalId = null;
|
|
574
659
|
if (server && syncInterval > 0) {
|
|
575
|
-
setInterval(() => void sync(), syncInterval);
|
|
660
|
+
syncIntervalId = setInterval(() => void sync().catch(logCatch("Periodic sync")), syncInterval);
|
|
576
661
|
}
|
|
662
|
+
const vaultApi = storeWithVault.vault;
|
|
663
|
+
vaultApi.destroy = () => {
|
|
664
|
+
if (syncIntervalId) {
|
|
665
|
+
clearInterval(syncIntervalId);
|
|
666
|
+
syncIntervalId = null;
|
|
667
|
+
}
|
|
668
|
+
if (syncDebounceTimer) {
|
|
669
|
+
clearTimeout(syncDebounceTimer);
|
|
670
|
+
syncDebounceTimer = null;
|
|
671
|
+
}
|
|
672
|
+
};
|
|
577
673
|
return configResult;
|
|
578
674
|
};
|
|
579
675
|
var vault = vaultImpl;
|
|
@@ -583,11 +679,21 @@ function useSyncStatus(useStore) {
|
|
|
583
679
|
const store = useStore();
|
|
584
680
|
return store.vault?.getSyncStatus?.() ?? "idle";
|
|
585
681
|
}
|
|
682
|
+
function useHydrated(useStore) {
|
|
683
|
+
const store = useStore();
|
|
684
|
+
return store.vault?.hasHydrated?.() ?? false;
|
|
685
|
+
}
|
|
686
|
+
function usePendingChanges(useStore) {
|
|
687
|
+
const store = useStore();
|
|
688
|
+
return store.vault?.hasPendingChanges?.() ?? false;
|
|
689
|
+
}
|
|
586
690
|
export {
|
|
587
691
|
FetchHttpClient,
|
|
588
692
|
LocalStorageProvider,
|
|
589
693
|
createSyncEngine,
|
|
590
694
|
createVaultStorage,
|
|
695
|
+
useHydrated,
|
|
696
|
+
usePendingChanges,
|
|
591
697
|
useSyncStatus,
|
|
592
698
|
vault
|
|
593
699
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ursalock/zustand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Encrypted persistence middleware for Zustand with passkey E2EE",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ursalock/crypto": "^0.
|
|
25
|
+
"@ursalock/crypto": "^0.3.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"zustand": ">=4.0.0"
|