@ursalock/zustand 0.3.0 → 0.4.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/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/vault.js
DELETED
|
@@ -1,261 +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 { createVaultStorage } from "./storage.js";
|
|
15
|
-
import { createSyncEngine } from "./sync.js";
|
|
16
|
-
/** Log a caught error from a fire-and-forget promise */
|
|
17
|
-
const logCatch = (context) => (err) => console.error(`[ursalock] ${context}:`, err);
|
|
18
|
-
/** Helper to check if using JWK mode */
|
|
19
|
-
function isJwkMode(options) {
|
|
20
|
-
return "cipherJwk" in options;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Internal vault implementation
|
|
24
|
-
* Uses simplified types - complex types applied at export
|
|
25
|
-
*/
|
|
26
|
-
const vaultImpl = (config, baseOptions) => (set, get, api) => {
|
|
27
|
-
const options = {
|
|
28
|
-
partialize: (state) => state,
|
|
29
|
-
merge: (persistedState, currentState) => ({
|
|
30
|
-
...currentState,
|
|
31
|
-
...persistedState,
|
|
32
|
-
}),
|
|
33
|
-
syncInterval: 30000,
|
|
34
|
-
...baseOptions,
|
|
35
|
-
};
|
|
36
|
-
const { name, server, getToken, partialize, merge, onRehydrateStorage, skipHydration = false, syncInterval, } = options;
|
|
37
|
-
// Create encrypted storage based on mode
|
|
38
|
-
const storage = options.storage ?? createVaultStorage(isJwkMode(options)
|
|
39
|
-
? { cipherJwk: options.cipherJwk, prefix: options.prefix }
|
|
40
|
-
: { recoveryKey: options.recoveryKey, prefix: options.prefix });
|
|
41
|
-
// State
|
|
42
|
-
let hasHydrated = false;
|
|
43
|
-
let localUpdatedAt = 0; // Start at 0 - server data wins until we have local changes
|
|
44
|
-
let hasLocalData = false; // Track if we loaded data from local storage
|
|
45
|
-
let syncDebounceTimer = null;
|
|
46
|
-
const hydrationListeners = new Set();
|
|
47
|
-
const finishHydrationListeners = new Set();
|
|
48
|
-
// Create sync engine (if server configured)
|
|
49
|
-
let syncEngine = null;
|
|
50
|
-
if (server && getToken) {
|
|
51
|
-
syncEngine = createSyncEngine({
|
|
52
|
-
serverUrl: server,
|
|
53
|
-
name,
|
|
54
|
-
getToken,
|
|
55
|
-
onServerData: (data, _salt, updatedAt) => {
|
|
56
|
-
// Server has newer data, update local store
|
|
57
|
-
// Only pull if we haven't made local changes since last sync
|
|
58
|
-
if (localUpdatedAt > updatedAt) {
|
|
59
|
-
// Local is actually newer - don't overwrite, push instead
|
|
60
|
-
void syncEngine?.push().catch(logCatch("Push after local-newer conflict"));
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const parsed = JSON.parse(data);
|
|
65
|
-
const merged = merge(parsed, get());
|
|
66
|
-
set(merged, true);
|
|
67
|
-
localUpdatedAt = updatedAt;
|
|
68
|
-
// Also persist to local storage to keep in sync
|
|
69
|
-
void storage.setItem(name, JSON.stringify(partialize({ ...get() }))).catch(logCatch("Persist server data to local storage"));
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
console.error("[ursalock] Failed to parse server data:", err);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
getLocalData: () => {
|
|
76
|
-
// Get current encrypted local data
|
|
77
|
-
const state = partialize({ ...get() });
|
|
78
|
-
return {
|
|
79
|
-
data: JSON.stringify(state),
|
|
80
|
-
salt: "", // Salt is handled by storage layer
|
|
81
|
-
updatedAt: localUpdatedAt,
|
|
82
|
-
};
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
// Persist to storage and trigger debounced sync
|
|
87
|
-
const persistState = async () => {
|
|
88
|
-
const state = partialize({ ...get() });
|
|
89
|
-
await storage.setItem(name, JSON.stringify(state));
|
|
90
|
-
localUpdatedAt = Date.now();
|
|
91
|
-
// Debounced sync after changes (3 seconds)
|
|
92
|
-
if (syncEngine) {
|
|
93
|
-
if (syncDebounceTimer) {
|
|
94
|
-
clearTimeout(syncDebounceTimer);
|
|
95
|
-
}
|
|
96
|
-
syncDebounceTimer = setTimeout(() => {
|
|
97
|
-
syncDebounceTimer = null;
|
|
98
|
-
void syncEngine?.sync().catch(logCatch("Debounced sync"));
|
|
99
|
-
}, 3000);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
// Hydrate from storage
|
|
103
|
-
const rehydrate = async () => {
|
|
104
|
-
hasHydrated = false;
|
|
105
|
-
hydrationListeners.forEach((cb) => cb(get()));
|
|
106
|
-
const postRehydrationCallback = onRehydrateStorage?.(get()) || undefined;
|
|
107
|
-
try {
|
|
108
|
-
const stored = await storage.getItem(name);
|
|
109
|
-
if (stored) {
|
|
110
|
-
const parsed = JSON.parse(stored);
|
|
111
|
-
const merged = merge(parsed, get());
|
|
112
|
-
set(merged, true);
|
|
113
|
-
hasLocalData = true;
|
|
114
|
-
// Don't set localUpdatedAt here - keep at 0 so first sync pulls from server
|
|
115
|
-
// localUpdatedAt will be set properly after first successful sync or local change
|
|
116
|
-
}
|
|
117
|
-
hasHydrated = true;
|
|
118
|
-
postRehydrationCallback?.(get(), undefined);
|
|
119
|
-
finishHydrationListeners.forEach((cb) => cb(get()));
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
postRehydrationCallback?.(undefined, error);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
// Sync methods (delegate to sync engine)
|
|
126
|
-
const sync = async () => {
|
|
127
|
-
if (!syncEngine)
|
|
128
|
-
return;
|
|
129
|
-
await syncEngine.sync();
|
|
130
|
-
};
|
|
131
|
-
const push = async () => {
|
|
132
|
-
if (!syncEngine)
|
|
133
|
-
return;
|
|
134
|
-
await syncEngine.push();
|
|
135
|
-
};
|
|
136
|
-
const pull = async () => {
|
|
137
|
-
if (!syncEngine)
|
|
138
|
-
return false;
|
|
139
|
-
return syncEngine.pull();
|
|
140
|
-
};
|
|
141
|
-
const getSyncStatus = () => {
|
|
142
|
-
if (!syncEngine)
|
|
143
|
-
return "idle";
|
|
144
|
-
return syncEngine.getState().status;
|
|
145
|
-
};
|
|
146
|
-
const hasPendingChanges = () => {
|
|
147
|
-
if (!syncEngine)
|
|
148
|
-
return false;
|
|
149
|
-
return syncEngine.getState().pendingChanges;
|
|
150
|
-
};
|
|
151
|
-
// Clear storage
|
|
152
|
-
const clearStorage = async () => {
|
|
153
|
-
await storage.removeItem(name);
|
|
154
|
-
syncEngine?.clearQueue();
|
|
155
|
-
};
|
|
156
|
-
const savedSetState = api.setState;
|
|
157
|
-
api.setState = ((state, replace) => {
|
|
158
|
-
if (replace) {
|
|
159
|
-
savedSetState(state, true);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
savedSetState(state);
|
|
163
|
-
}
|
|
164
|
-
void persistState().catch(logCatch("Persist state"));
|
|
165
|
-
});
|
|
166
|
-
// Create store with wrapped set
|
|
167
|
-
const configResult = config(((partial, replace) => {
|
|
168
|
-
if (replace) {
|
|
169
|
-
set(partial, true);
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
set(partial);
|
|
173
|
-
}
|
|
174
|
-
void persistState().catch(logCatch("Persist state"));
|
|
175
|
-
}), get, api);
|
|
176
|
-
// Extend API with vault methods
|
|
177
|
-
const storeWithVault = api;
|
|
178
|
-
storeWithVault.vault = {
|
|
179
|
-
sync,
|
|
180
|
-
push,
|
|
181
|
-
pull,
|
|
182
|
-
rehydrate,
|
|
183
|
-
hasHydrated: () => hasHydrated,
|
|
184
|
-
getSyncStatus,
|
|
185
|
-
hasPendingChanges,
|
|
186
|
-
clearStorage,
|
|
187
|
-
onHydrate: (cb) => {
|
|
188
|
-
hydrationListeners.add(cb);
|
|
189
|
-
return () => hydrationListeners.delete(cb);
|
|
190
|
-
},
|
|
191
|
-
onFinishHydration: (cb) => {
|
|
192
|
-
finishHydrationListeners.add(cb);
|
|
193
|
-
return () => finishHydrationListeners.delete(cb);
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
// Auto-hydrate on init (unless skipHydration)
|
|
197
|
-
if (!skipHydration) {
|
|
198
|
-
void rehydrate().then(() => {
|
|
199
|
-
// Sync immediately after hydration to get latest server data
|
|
200
|
-
if (syncEngine) {
|
|
201
|
-
void syncEngine.sync().catch(logCatch("Initial sync after hydration"));
|
|
202
|
-
}
|
|
203
|
-
}).catch(logCatch("Auto-rehydration"));
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Even with skipHydration, mark as hydrated to allow persistence
|
|
207
|
-
hasHydrated = true;
|
|
208
|
-
}
|
|
209
|
-
// Setup sync interval (if server configured)
|
|
210
|
-
let syncIntervalId = null;
|
|
211
|
-
if (server && syncInterval > 0) {
|
|
212
|
-
syncIntervalId = setInterval(() => void sync().catch(logCatch("Periodic sync")), syncInterval);
|
|
213
|
-
}
|
|
214
|
-
// Expose destroy method to clean up interval
|
|
215
|
-
const vaultApi = storeWithVault.vault;
|
|
216
|
-
vaultApi.destroy = () => {
|
|
217
|
-
if (syncIntervalId) {
|
|
218
|
-
clearInterval(syncIntervalId);
|
|
219
|
-
syncIntervalId = null;
|
|
220
|
-
}
|
|
221
|
-
if (syncDebounceTimer) {
|
|
222
|
-
clearTimeout(syncDebounceTimer);
|
|
223
|
-
syncDebounceTimer = null;
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
return configResult;
|
|
227
|
-
};
|
|
228
|
-
/**
|
|
229
|
-
* vault() middleware - Add E2EE encrypted persistence to Zustand
|
|
230
|
-
*
|
|
231
|
-
* @example Using CipherJWK from ZKCredentials (recommended)
|
|
232
|
-
* ```ts
|
|
233
|
-
* const useStore = create(
|
|
234
|
-
* vault(
|
|
235
|
-
* (set) => ({
|
|
236
|
-
* count: 0,
|
|
237
|
-
* increment: () => set((s) => ({ count: s.count + 1 })),
|
|
238
|
-
* }),
|
|
239
|
-
* {
|
|
240
|
-
* name: 'my-store',
|
|
241
|
-
* cipherJwk: credential.cipherJwk, // From ZKCredentials
|
|
242
|
-
* server: 'https://vault.example.com', // optional
|
|
243
|
-
* }
|
|
244
|
-
* )
|
|
245
|
-
* )
|
|
246
|
-
* ```
|
|
247
|
-
*
|
|
248
|
-
* @example Using recovery key (legacy)
|
|
249
|
-
* ```ts
|
|
250
|
-
* const useStore = create(
|
|
251
|
-
* vault(
|
|
252
|
-
* (set) => ({ ... }),
|
|
253
|
-
* {
|
|
254
|
-
* name: 'my-store',
|
|
255
|
-
* recoveryKey: 'ABCD-EFGH-...',
|
|
256
|
-
* }
|
|
257
|
-
* )
|
|
258
|
-
* )
|
|
259
|
-
* ```
|
|
260
|
-
*/
|
|
261
|
-
export const vault = vaultImpl;
|