@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/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
@@ -1 +0,0 @@
1
- {"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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"}