@ursalock/zustand 0.3.0 → 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 +395 -15
- package/package.json +3 -3
- package/dist/hooks.d.ts +0 -47
- package/dist/hooks.d.ts.map +0 -1
- package/dist/hooks.js +0 -42
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces/http.d.ts +0 -28
- package/dist/interfaces/http.d.ts.map +0 -1
- package/dist/interfaces/http.js +0 -5
- package/dist/interfaces/storage.d.ts +0 -29
- package/dist/interfaces/storage.d.ts.map +0 -1
- package/dist/interfaces/storage.js +0 -5
- package/dist/providers/fetch-http.d.ts +0 -12
- package/dist/providers/fetch-http.d.ts.map +0 -1
- package/dist/providers/fetch-http.js +0 -22
- package/dist/providers/local-storage.d.ts +0 -15
- package/dist/providers/local-storage.d.ts.map +0 -1
- package/dist/providers/local-storage.js +0 -25
- package/dist/storage.d.ts +0 -45
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -180
- package/dist/sync.d.ts +0 -79
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -389
- package/dist/vault.d.ts +0 -152
- package/dist/vault.d.ts.map +0 -1
- package/dist/vault.js +0 -261
package/dist/sync.js
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sync engine for vault middleware
|
|
3
|
-
* Handles bidirectional sync with server + offline queue
|
|
4
|
-
*
|
|
5
|
-
* Refactored to follow SOLID principles:
|
|
6
|
-
* - IHttpClient interface (Dependency Inversion)
|
|
7
|
-
* - Separated offline queue logic (Single Responsibility)
|
|
8
|
-
* - Injectable HTTP client for testing
|
|
9
|
-
*/
|
|
10
|
-
import { FetchHttpClient } from "./providers/fetch-http.js";
|
|
11
|
-
import { computeHmac, verifyHmac } from "@ursalock/crypto";
|
|
12
|
-
const QUEUE_KEY = "ursalock:offline-queue";
|
|
13
|
-
/**
|
|
14
|
-
* Create a sync engine instance
|
|
15
|
-
* Uses dependency injection for HTTP client (Dependency Inversion Principle)
|
|
16
|
-
*/
|
|
17
|
-
export function createSyncEngine(options) {
|
|
18
|
-
const { serverUrl, name, getToken, onServerData, getLocalData, onStatusChange, httpClient = new FetchHttpClient(), storageProvider, hmacKey, } = options;
|
|
19
|
-
const textEncoder = new TextEncoder();
|
|
20
|
-
// Use provided storage or fall back to localStorage
|
|
21
|
-
const queueStorage = storageProvider ?? (typeof localStorage !== "undefined" ? localStorage : null);
|
|
22
|
-
let status = "idle";
|
|
23
|
-
let lastSyncAt = null;
|
|
24
|
-
let error = null;
|
|
25
|
-
/** Last known server version for optimistic locking */
|
|
26
|
-
let knownServerVersion = null;
|
|
27
|
-
const setStatus = (newStatus, newError) => {
|
|
28
|
-
status = newStatus;
|
|
29
|
-
error = newError ?? null;
|
|
30
|
-
onStatusChange?.(newStatus);
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Load offline queue from localStorage
|
|
34
|
-
*/
|
|
35
|
-
const loadQueue = () => {
|
|
36
|
-
if (!queueStorage)
|
|
37
|
-
return { pending: [] };
|
|
38
|
-
try {
|
|
39
|
-
const stored = queueStorage.getItem(`${QUEUE_KEY}:${name}`);
|
|
40
|
-
return stored ? JSON.parse(stored) : { pending: [] };
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return { pending: [] };
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Save offline queue to storage
|
|
48
|
-
*/
|
|
49
|
-
const saveQueue = (queue) => {
|
|
50
|
-
if (!queueStorage)
|
|
51
|
-
return;
|
|
52
|
-
try {
|
|
53
|
-
queueStorage.setItem(`${QUEUE_KEY}:${name}`, JSON.stringify(queue));
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Storage full or unavailable
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
/**
|
|
60
|
-
* Add to offline queue
|
|
61
|
-
*/
|
|
62
|
-
const enqueue = (data, salt) => {
|
|
63
|
-
const queue = loadQueue();
|
|
64
|
-
queue.pending.push({ data, salt, timestamp: Date.now() });
|
|
65
|
-
// Keep only last 10 pending changes
|
|
66
|
-
if (queue.pending.length > 10) {
|
|
67
|
-
queue.pending = queue.pending.slice(-10);
|
|
68
|
-
}
|
|
69
|
-
saveQueue(queue);
|
|
70
|
-
};
|
|
71
|
-
/**
|
|
72
|
-
* Clear offline queue
|
|
73
|
-
*/
|
|
74
|
-
const clearQueue = () => {
|
|
75
|
-
saveQueue({ pending: [] });
|
|
76
|
-
};
|
|
77
|
-
/**
|
|
78
|
-
* Check if online
|
|
79
|
-
*/
|
|
80
|
-
const isOnline = () => {
|
|
81
|
-
return typeof navigator === "undefined" || navigator.onLine;
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Fetch vault from server
|
|
85
|
-
* Uses injected HTTP client (Dependency Inversion)
|
|
86
|
-
*/
|
|
87
|
-
const fetchServer = async () => {
|
|
88
|
-
const token = getToken();
|
|
89
|
-
if (!token) {
|
|
90
|
-
console.warn("[ursalock] fetchServer: no auth token available, skipping");
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const res = await httpClient.request({
|
|
94
|
-
url: `${serverUrl}/vault/by-name/${encodeURIComponent(name)}`,
|
|
95
|
-
method: "GET",
|
|
96
|
-
headers: {
|
|
97
|
-
"Authorization": `Bearer ${token}`,
|
|
98
|
-
"Content-Type": "application/json",
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
if (res.status === 404)
|
|
102
|
-
return null;
|
|
103
|
-
if (!res.ok)
|
|
104
|
-
throw new Error(`Server error: ${res.status}`);
|
|
105
|
-
const vault = await res.json();
|
|
106
|
-
// Track server version for optimistic locking
|
|
107
|
-
knownServerVersion = vault.version;
|
|
108
|
-
return vault;
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* Compute HMAC tag for outgoing data (if hmacKey is configured).
|
|
112
|
-
*/
|
|
113
|
-
const computeTag = async (data) => {
|
|
114
|
-
if (!hmacKey)
|
|
115
|
-
return undefined;
|
|
116
|
-
return computeHmac(textEncoder.encode(data), hmacKey);
|
|
117
|
-
};
|
|
118
|
-
/**
|
|
119
|
-
* Verify HMAC integrity of incoming server data.
|
|
120
|
-
* - Missing HMAC on server data: warn and allow (backward compat with older vaults)
|
|
121
|
-
* - Invalid HMAC: reject with a clear error
|
|
122
|
-
*/
|
|
123
|
-
const verifyTag = async (vault) => {
|
|
124
|
-
if (!hmacKey)
|
|
125
|
-
return;
|
|
126
|
-
if (!vault.hmac) {
|
|
127
|
-
console.warn("[ursalock] Server vault has no HMAC tag. " +
|
|
128
|
-
"This is expected for vaults created before integrity verification was enabled. " +
|
|
129
|
-
"The vault will be re-signed on next push.");
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const valid = await verifyHmac(textEncoder.encode(vault.data), hmacKey, vault.hmac);
|
|
133
|
-
if (!valid) {
|
|
134
|
-
throw new Error("[ursalock] HMAC verification failed: server data has been tampered with or the integrity key is wrong");
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
/**
|
|
138
|
-
* Push vault to server (handles race conditions with retry on 409)
|
|
139
|
-
*
|
|
140
|
-
* Sends the last known server version for optimistic locking. If the server
|
|
141
|
-
* detects a version mismatch it returns 409 and we force a pull + re-merge
|
|
142
|
-
* before retrying the push once.
|
|
143
|
-
*/
|
|
144
|
-
const pushServer = async (data, salt) => {
|
|
145
|
-
const token = getToken();
|
|
146
|
-
if (!token)
|
|
147
|
-
throw new Error("Not authenticated");
|
|
148
|
-
// Compute HMAC tag over ciphertext before sending (Encrypt-then-MAC)
|
|
149
|
-
const hmac = await computeTag(data);
|
|
150
|
-
// Try to get existing vault first
|
|
151
|
-
let existing = null;
|
|
152
|
-
try {
|
|
153
|
-
existing = await fetchServer();
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
// Ignore fetch errors, try to create
|
|
157
|
-
}
|
|
158
|
-
if (existing) {
|
|
159
|
-
// Build body with version for optimistic locking
|
|
160
|
-
const body = { data, salt, ...(hmac != null && { hmac }) };
|
|
161
|
-
if (knownServerVersion != null) {
|
|
162
|
-
body.version = knownServerVersion;
|
|
163
|
-
}
|
|
164
|
-
// Update existing vault
|
|
165
|
-
const res = await httpClient.request({
|
|
166
|
-
url: `${serverUrl}/vault/${existing.uid}`,
|
|
167
|
-
method: "PUT",
|
|
168
|
-
headers: {
|
|
169
|
-
"Authorization": `Bearer ${token}`,
|
|
170
|
-
"Content-Type": "application/json",
|
|
171
|
-
},
|
|
172
|
-
body: JSON.stringify(body),
|
|
173
|
-
});
|
|
174
|
-
// Handle version conflict: pull latest, re-merge, retry once
|
|
175
|
-
if (res.status === 409) {
|
|
176
|
-
const latest = await fetchServer();
|
|
177
|
-
if (latest) {
|
|
178
|
-
await verifyTag(latest);
|
|
179
|
-
onServerData(latest.data, latest.salt, latest.updatedAt);
|
|
180
|
-
// Retry with fresh local data and updated version
|
|
181
|
-
const retryLocal = getLocalData();
|
|
182
|
-
const retryHmac = await computeTag(retryLocal.data);
|
|
183
|
-
const retryBody = {
|
|
184
|
-
data: retryLocal.data,
|
|
185
|
-
salt: retryLocal.salt,
|
|
186
|
-
...(retryHmac != null && { hmac: retryHmac }),
|
|
187
|
-
};
|
|
188
|
-
if (knownServerVersion != null) {
|
|
189
|
-
retryBody.version = knownServerVersion;
|
|
190
|
-
}
|
|
191
|
-
const retryRes = await httpClient.request({
|
|
192
|
-
url: `${serverUrl}/vault/${existing.uid}`,
|
|
193
|
-
method: "PUT",
|
|
194
|
-
headers: {
|
|
195
|
-
"Authorization": `Bearer ${token}`,
|
|
196
|
-
"Content-Type": "application/json",
|
|
197
|
-
},
|
|
198
|
-
body: JSON.stringify(retryBody),
|
|
199
|
-
});
|
|
200
|
-
if (!retryRes.ok) {
|
|
201
|
-
const errorText = await retryRes.text().catch(() => "");
|
|
202
|
-
throw new Error(`Server error: ${retryRes.status} ${errorText}`);
|
|
203
|
-
}
|
|
204
|
-
const result = await retryRes.json();
|
|
205
|
-
knownServerVersion = result.version;
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (!res.ok) {
|
|
210
|
-
const errorText = await res.text().catch(() => "");
|
|
211
|
-
throw new Error(`Server error: ${res.status} ${errorText}`);
|
|
212
|
-
}
|
|
213
|
-
const result = await res.json();
|
|
214
|
-
knownServerVersion = result.version;
|
|
215
|
-
return result;
|
|
216
|
-
}
|
|
217
|
-
// Try to create new vault
|
|
218
|
-
const createRes = await httpClient.request({
|
|
219
|
-
url: `${serverUrl}/vault`,
|
|
220
|
-
method: "POST",
|
|
221
|
-
headers: {
|
|
222
|
-
"Authorization": `Bearer ${token}`,
|
|
223
|
-
"Content-Type": "application/json",
|
|
224
|
-
},
|
|
225
|
-
body: JSON.stringify({ name, data, salt, ...(hmac != null && { hmac }) }),
|
|
226
|
-
});
|
|
227
|
-
// Handle race condition: vault was created between our check and POST
|
|
228
|
-
if (createRes.status === 409) {
|
|
229
|
-
// Fetch the existing vault and update it instead
|
|
230
|
-
const nowExisting = await fetchServer();
|
|
231
|
-
if (!nowExisting) {
|
|
232
|
-
throw new Error("Vault conflict but not found on retry");
|
|
233
|
-
}
|
|
234
|
-
const retryBody = { data, salt, ...(hmac != null && { hmac }) };
|
|
235
|
-
if (knownServerVersion != null) {
|
|
236
|
-
retryBody.version = knownServerVersion;
|
|
237
|
-
}
|
|
238
|
-
const retryRes = await httpClient.request({
|
|
239
|
-
url: `${serverUrl}/vault/${nowExisting.uid}`,
|
|
240
|
-
method: "PUT",
|
|
241
|
-
headers: {
|
|
242
|
-
"Authorization": `Bearer ${token}`,
|
|
243
|
-
"Content-Type": "application/json",
|
|
244
|
-
},
|
|
245
|
-
body: JSON.stringify(retryBody),
|
|
246
|
-
});
|
|
247
|
-
if (!retryRes.ok) {
|
|
248
|
-
const errorText = await retryRes.text().catch(() => "");
|
|
249
|
-
throw new Error(`Server error: ${retryRes.status} ${errorText}`);
|
|
250
|
-
}
|
|
251
|
-
const result = await retryRes.json();
|
|
252
|
-
knownServerVersion = result.version;
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
255
|
-
if (!createRes.ok) {
|
|
256
|
-
const errorText = await createRes.text().catch(() => "");
|
|
257
|
-
throw new Error(`Server error: ${createRes.status} ${errorText}`);
|
|
258
|
-
}
|
|
259
|
-
const result = await createRes.json();
|
|
260
|
-
knownServerVersion = result.version;
|
|
261
|
-
return result;
|
|
262
|
-
};
|
|
263
|
-
/**
|
|
264
|
-
* Sync with server (bidirectional)
|
|
265
|
-
*/
|
|
266
|
-
const sync = async () => {
|
|
267
|
-
if (!isOnline()) {
|
|
268
|
-
setStatus("offline");
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
setStatus("syncing");
|
|
272
|
-
try {
|
|
273
|
-
// Process offline queue first
|
|
274
|
-
const queue = loadQueue();
|
|
275
|
-
if (queue.pending.length > 0) {
|
|
276
|
-
// Push most recent pending change
|
|
277
|
-
const latest = queue.pending[queue.pending.length - 1];
|
|
278
|
-
if (latest) {
|
|
279
|
-
await pushServer(latest.data, latest.salt);
|
|
280
|
-
clearQueue();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
// Get local and server state
|
|
284
|
-
const local = getLocalData();
|
|
285
|
-
const server = await fetchServer();
|
|
286
|
-
if (!server) {
|
|
287
|
-
// No server data, push local
|
|
288
|
-
if (local.data) {
|
|
289
|
-
await pushServer(local.data, local.salt);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else if (server.updatedAt > local.updatedAt) {
|
|
293
|
-
// Server is newer — verify integrity before accepting
|
|
294
|
-
await verifyTag(server);
|
|
295
|
-
onServerData(server.data, server.salt, server.updatedAt);
|
|
296
|
-
}
|
|
297
|
-
else if (local.updatedAt > server.updatedAt) {
|
|
298
|
-
// Local is newer, push
|
|
299
|
-
await pushServer(local.data, local.salt);
|
|
300
|
-
}
|
|
301
|
-
// If equal, nothing to do
|
|
302
|
-
lastSyncAt = Date.now();
|
|
303
|
-
setStatus("synced");
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const message = err instanceof Error ? err.message : "Sync failed";
|
|
307
|
-
console.error("[ursalock] Sync error:", message);
|
|
308
|
-
setStatus("error", message);
|
|
309
|
-
// Queue for later if push failed
|
|
310
|
-
if (message.includes("Server error")) {
|
|
311
|
-
const local = getLocalData();
|
|
312
|
-
enqueue(local.data, local.salt);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
/**
|
|
317
|
-
* Push local changes to server (with offline support)
|
|
318
|
-
*/
|
|
319
|
-
const push = async () => {
|
|
320
|
-
if (!isOnline()) {
|
|
321
|
-
const local = getLocalData();
|
|
322
|
-
enqueue(local.data, local.salt);
|
|
323
|
-
setStatus("offline");
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
setStatus("syncing");
|
|
327
|
-
try {
|
|
328
|
-
const local = getLocalData();
|
|
329
|
-
await pushServer(local.data, local.salt);
|
|
330
|
-
lastSyncAt = Date.now();
|
|
331
|
-
setStatus("synced");
|
|
332
|
-
}
|
|
333
|
-
catch (err) {
|
|
334
|
-
const message = err instanceof Error ? err.message : "Push failed";
|
|
335
|
-
console.error("[ursalock] Push error:", message);
|
|
336
|
-
// Queue for retry
|
|
337
|
-
const local = getLocalData();
|
|
338
|
-
enqueue(local.data, local.salt);
|
|
339
|
-
setStatus("error", message);
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
/**
|
|
343
|
-
* Pull latest from server
|
|
344
|
-
*/
|
|
345
|
-
const pull = async () => {
|
|
346
|
-
if (!isOnline()) {
|
|
347
|
-
setStatus("offline");
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
setStatus("syncing");
|
|
351
|
-
try {
|
|
352
|
-
const server = await fetchServer();
|
|
353
|
-
if (server) {
|
|
354
|
-
const local = getLocalData();
|
|
355
|
-
if (server.updatedAt > local.updatedAt) {
|
|
356
|
-
await verifyTag(server);
|
|
357
|
-
onServerData(server.data, server.salt, server.updatedAt);
|
|
358
|
-
lastSyncAt = Date.now();
|
|
359
|
-
setStatus("synced");
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
setStatus("synced");
|
|
364
|
-
return false;
|
|
365
|
-
}
|
|
366
|
-
catch (err) {
|
|
367
|
-
const message = err instanceof Error ? err.message : "Pull failed";
|
|
368
|
-
console.error("[ursalock] Pull error:", message);
|
|
369
|
-
setStatus("error", message);
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
};
|
|
373
|
-
/**
|
|
374
|
-
* Get current sync state
|
|
375
|
-
*/
|
|
376
|
-
const getState = () => ({
|
|
377
|
-
lastSyncAt,
|
|
378
|
-
status,
|
|
379
|
-
pendingChanges: loadQueue().pending.length > 0,
|
|
380
|
-
error,
|
|
381
|
-
});
|
|
382
|
-
return {
|
|
383
|
-
sync,
|
|
384
|
-
push,
|
|
385
|
-
pull,
|
|
386
|
-
getState,
|
|
387
|
-
clearQueue,
|
|
388
|
-
};
|
|
389
|
-
}
|
package/dist/vault.d.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vault() middleware - Drop-in replacement for persist()
|
|
3
|
-
* Adds E2EE encryption and cloud sync to Zustand stores
|
|
4
|
-
*
|
|
5
|
-
* Supports two encryption modes:
|
|
6
|
-
* - Legacy: recoveryKey string (Argon2id key derivation)
|
|
7
|
-
* - New: cipherJwk from ZKCredentials (PRF-derived)
|
|
8
|
-
*
|
|
9
|
-
* Type pattern follows zustand's persist middleware:
|
|
10
|
-
* - Simple internal implementation type (VaultImpl)
|
|
11
|
-
* - Complex public API type (Vault)
|
|
12
|
-
* - Cast at export: `vaultImpl as unknown as Vault`
|
|
13
|
-
*/
|
|
14
|
-
import type { StateCreator, StoreMutatorIdentifier } from "zustand";
|
|
15
|
-
import { type VaultStorage } from "./storage.js";
|
|
16
|
-
import type { CipherJWK } from "@ursalock/crypto";
|
|
17
|
-
/** Base vault middleware options */
|
|
18
|
-
interface VaultOptionsBase<S, PersistedState = S> {
|
|
19
|
-
/** Unique name for this vault (used as storage key) */
|
|
20
|
-
name: string;
|
|
21
|
-
/**
|
|
22
|
-
* Server URL for sync (optional)
|
|
23
|
-
* If not provided, only local encrypted storage is used
|
|
24
|
-
*/
|
|
25
|
-
server?: string;
|
|
26
|
-
/**
|
|
27
|
-
* Auth token getter for server sync
|
|
28
|
-
* Required if server is provided
|
|
29
|
-
*/
|
|
30
|
-
getToken?: () => string | null;
|
|
31
|
-
/** Custom storage implementation */
|
|
32
|
-
storage?: VaultStorage;
|
|
33
|
-
/** Storage key prefix (default: 'ursalock:') */
|
|
34
|
-
prefix?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Partial state to persist
|
|
37
|
-
* @default (state) => state (persist everything)
|
|
38
|
-
*/
|
|
39
|
-
partialize?: (state: S) => PersistedState;
|
|
40
|
-
/**
|
|
41
|
-
* Merge function for rehydration
|
|
42
|
-
* @default Object.assign
|
|
43
|
-
*/
|
|
44
|
-
merge?: (persistedState: unknown, currentState: S) => S;
|
|
45
|
-
/**
|
|
46
|
-
* Called when state is loaded from storage
|
|
47
|
-
*/
|
|
48
|
-
onRehydrateStorage?: (state: S) => ((state?: S, error?: unknown) => void) | void;
|
|
49
|
-
/**
|
|
50
|
-
* Skip hydration on init (useful for SSR)
|
|
51
|
-
* Call rehydrate() manually when ready
|
|
52
|
-
*/
|
|
53
|
-
skipHydration?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Sync interval in ms (default: 30000 = 30s)
|
|
56
|
-
* Set to 0 to disable auto-sync
|
|
57
|
-
*/
|
|
58
|
-
syncInterval?: number;
|
|
59
|
-
}
|
|
60
|
-
/** Legacy options using recovery key string */
|
|
61
|
-
export interface VaultOptionsLegacy<S, PersistedState = S> extends VaultOptionsBase<S, PersistedState> {
|
|
62
|
-
/** Recovery key for E2EE encryption (legacy mode) */
|
|
63
|
-
recoveryKey: string;
|
|
64
|
-
}
|
|
65
|
-
/** New options using CipherJWK from ZKCredentials */
|
|
66
|
-
export interface VaultOptionsJwk<S, PersistedState = S> extends VaultOptionsBase<S, PersistedState> {
|
|
67
|
-
/** CipherJWK for E2EE encryption (from ZKCredentials) */
|
|
68
|
-
cipherJwk: CipherJWK;
|
|
69
|
-
}
|
|
70
|
-
export type VaultOptions<S, PersistedState = S> = VaultOptionsLegacy<S, PersistedState> | VaultOptionsJwk<S, PersistedState>;
|
|
71
|
-
export type { SyncStatus, SyncState } from "./sync.js";
|
|
72
|
-
type VaultListener<S> = (state: S) => void;
|
|
73
|
-
import type { SyncStatus } from "./sync.js";
|
|
74
|
-
/** Store shape extended with vault API */
|
|
75
|
-
type StoreVault<S, Ps> = S extends {
|
|
76
|
-
getState: () => infer T;
|
|
77
|
-
setState: {
|
|
78
|
-
(...args: infer Sa1): infer Sr1;
|
|
79
|
-
(...args: infer Sa2): infer Sr2;
|
|
80
|
-
};
|
|
81
|
-
} ? {
|
|
82
|
-
setState(...args: Sa1): Sr1 | Promise<void>;
|
|
83
|
-
setState(...args: Sa2): Sr2 | Promise<void>;
|
|
84
|
-
vault: {
|
|
85
|
-
/** Full bidirectional sync with server */
|
|
86
|
-
sync: () => Promise<void>;
|
|
87
|
-
/** Push local changes to server */
|
|
88
|
-
push: () => Promise<void>;
|
|
89
|
-
/** Pull latest from server */
|
|
90
|
-
pull: () => Promise<boolean>;
|
|
91
|
-
/** Rehydrate from local storage */
|
|
92
|
-
rehydrate: () => Promise<void>;
|
|
93
|
-
/** Check if store has been hydrated */
|
|
94
|
-
hasHydrated: () => boolean;
|
|
95
|
-
/** Get current sync status */
|
|
96
|
-
getSyncStatus: () => SyncStatus;
|
|
97
|
-
/** Check if there are pending offline changes */
|
|
98
|
-
hasPendingChanges: () => boolean;
|
|
99
|
-
/** Clear all stored data (local + server) */
|
|
100
|
-
clearStorage: () => Promise<void>;
|
|
101
|
-
/** Clean up sync interval and timers */
|
|
102
|
-
destroy: () => void;
|
|
103
|
-
/** Subscribe to hydration start */
|
|
104
|
-
onHydrate: (fn: VaultListener<T>) => () => void;
|
|
105
|
-
/** Subscribe to hydration complete */
|
|
106
|
-
onFinishHydration: (fn: VaultListener<T>) => () => void;
|
|
107
|
-
};
|
|
108
|
-
} : never;
|
|
109
|
-
type Write<T, U> = Omit<T, keyof U> & U;
|
|
110
|
-
type WithVault<S, A> = Write<S, StoreVault<S, A>>;
|
|
111
|
-
/** Public API type with complex mutator support */
|
|
112
|
-
type Vault = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = [], U = T>(initializer: StateCreator<T, [...Mps, ["vault", unknown]], Mcs>, options: VaultOptions<T, U>) => StateCreator<T, Mps, [["vault", U], ...Mcs]>;
|
|
113
|
-
declare module "zustand" {
|
|
114
|
-
interface StoreMutators<S, A> {
|
|
115
|
-
vault: WithVault<S, A>;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* vault() middleware - Add E2EE encrypted persistence to Zustand
|
|
120
|
-
*
|
|
121
|
-
* @example Using CipherJWK from ZKCredentials (recommended)
|
|
122
|
-
* ```ts
|
|
123
|
-
* const useStore = create(
|
|
124
|
-
* vault(
|
|
125
|
-
* (set) => ({
|
|
126
|
-
* count: 0,
|
|
127
|
-
* increment: () => set((s) => ({ count: s.count + 1 })),
|
|
128
|
-
* }),
|
|
129
|
-
* {
|
|
130
|
-
* name: 'my-store',
|
|
131
|
-
* cipherJwk: credential.cipherJwk, // From ZKCredentials
|
|
132
|
-
* server: 'https://vault.example.com', // optional
|
|
133
|
-
* }
|
|
134
|
-
* )
|
|
135
|
-
* )
|
|
136
|
-
* ```
|
|
137
|
-
*
|
|
138
|
-
* @example Using recovery key (legacy)
|
|
139
|
-
* ```ts
|
|
140
|
-
* const useStore = create(
|
|
141
|
-
* vault(
|
|
142
|
-
* (set) => ({ ... }),
|
|
143
|
-
* {
|
|
144
|
-
* name: 'my-store',
|
|
145
|
-
* recoveryKey: 'ABCD-EFGH-...',
|
|
146
|
-
* }
|
|
147
|
-
* )
|
|
148
|
-
* )
|
|
149
|
-
* ```
|
|
150
|
-
*/
|
|
151
|
-
export declare const vault: Vault;
|
|
152
|
-
//# sourceMappingURL=vault.d.ts.map
|
package/dist/vault.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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"}
|