@ursalock/zustand 0.2.8 → 0.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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * React hooks for vault status
3
+ */
4
+ import type { SyncStatus } from './sync.js';
5
+ /**
6
+ * Hook to get sync status from a vault store
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * const status = useSyncStatus(useStore)
11
+ * // 'idle' | 'syncing' | 'synced' | 'error' | 'offline'
12
+ * ```
13
+ */
14
+ export declare function useSyncStatus<T extends {
15
+ vault?: {
16
+ getSyncStatus?: () => SyncStatus;
17
+ };
18
+ }>(useStore: () => T): SyncStatus;
19
+ /**
20
+ * Hook to check if store has been hydrated
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const hydrated = useHydrated(useStore)
25
+ * if (!hydrated) return <Loading />
26
+ * ```
27
+ */
28
+ export declare function useHydrated<T extends {
29
+ vault?: {
30
+ hasHydrated?: () => boolean;
31
+ };
32
+ }>(useStore: () => T): boolean;
33
+ /**
34
+ * Hook to check if there are pending offline changes
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * const hasPending = usePendingChanges(useStore)
39
+ * if (hasPending) return <PendingBadge />
40
+ * ```
41
+ */
42
+ export declare function usePendingChanges<T extends {
43
+ vault?: {
44
+ hasPendingChanges?: () => boolean;
45
+ };
46
+ }>(useStore: () => T): boolean;
47
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAE3C;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,UAAU,CAAA;KAAE,CAAA;CAAE,EACtF,QAAQ,EAAE,MAAM,CAAC,GAChB,UAAU,CAGZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,OAAO,CAAA;KAAE,CAAA;CAAE,EAC/E,QAAQ,EAAE,MAAM,CAAC,GAChB,OAAO,CAGT;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,KAAK,CAAC,EAAE;QAAE,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAA;KAAE,CAAA;CAAE,EAC3F,QAAQ,EAAE,MAAM,CAAC,GAChB,OAAO,CAGT"}
package/dist/hooks.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * React hooks for vault status
3
+ */
4
+ /**
5
+ * Hook to get sync status from a vault store
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const status = useSyncStatus(useStore)
10
+ * // 'idle' | 'syncing' | 'synced' | 'error' | 'offline'
11
+ * ```
12
+ */
13
+ export function useSyncStatus(useStore) {
14
+ const store = useStore();
15
+ return store.vault?.getSyncStatus?.() ?? 'idle';
16
+ }
17
+ /**
18
+ * Hook to check if store has been hydrated
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * const hydrated = useHydrated(useStore)
23
+ * if (!hydrated) return <Loading />
24
+ * ```
25
+ */
26
+ export function useHydrated(useStore) {
27
+ const store = useStore();
28
+ return store.vault?.hasHydrated?.() ?? false;
29
+ }
30
+ /**
31
+ * Hook to check if there are pending offline changes
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const hasPending = usePendingChanges(useStore)
36
+ * if (hasPending) return <PendingBadge />
37
+ * ```
38
+ */
39
+ export function usePendingChanges(useStore) {
40
+ const store = useStore();
41
+ return store.vault?.hasPendingChanges?.() ?? false;
42
+ }
package/dist/index.d.ts CHANGED
@@ -1,353 +1,18 @@
1
- import { StoreMutatorIdentifier, StateCreator } from 'zustand';
2
- import { CipherJWK } from '@ursalock/crypto';
3
-
4
1
  /**
5
- * Storage interfaces for vault middleware
6
- * Follows Dependency Inversion Principle - depend on abstractions not concretions
7
- */
8
- /**
9
- * Base storage provider interface
10
- * Abstracts away the underlying storage mechanism (localStorage, AsyncStorage, etc.)
11
- */
12
- interface IStorageProvider {
13
- /** Get item from storage */
14
- getItem(key: string): Promise<string | null>;
15
- /** Set item in storage */
16
- setItem(key: string, value: string): Promise<void>;
17
- /** Remove item from storage */
18
- removeItem(key: string): Promise<void>;
19
- }
20
- /**
21
- * Encrypted vault storage interface
22
- * Higher-level abstraction that handles encryption/decryption
23
- */
24
- interface IVaultStorage {
25
- /** Get decrypted state from storage */
26
- getItem(name: string): Promise<string | null>;
27
- /** Set encrypted state in storage */
28
- setItem(name: string, value: string): Promise<void>;
29
- /** Remove state from storage */
30
- removeItem(name: string): Promise<void>;
31
- }
32
-
33
- /**
34
- * Encrypted storage layer for vault middleware
35
- * Supports both legacy recovery key and new CipherJWK encryption
36
- *
37
- * Refactored to follow SOLID principles:
38
- * - Uses IStorageProvider interface (Dependency Inversion)
39
- * - Separates encryption concerns from storage access (Single Responsibility)
40
- * - Injectable storage provider for testing
41
- */
42
-
43
- interface VaultStorage extends IVaultStorage {
44
- }
45
-
46
- /** Legacy options using recovery key string */
47
- interface LegacyEncryptedStorageOptions {
48
- /** Recovery key for encryption (legacy mode) */
49
- recoveryKey: string;
50
- /** Underlying storage provider (default: LocalStorageProvider) */
51
- storageProvider?: IStorageProvider;
52
- /** Key prefix in storage */
53
- prefix?: string;
54
- /** @deprecated Use storageProvider instead. For backward compatibility with VaultStorage */
55
- storage?: VaultStorage;
56
- }
57
- /** New options using CipherJWK directly */
58
- interface JwkEncryptedStorageOptions {
59
- /** CipherJWK for encryption (from ZKCredentials) */
60
- cipherJwk: CipherJWK;
61
- /** Underlying storage provider (default: LocalStorageProvider) */
62
- storageProvider?: IStorageProvider;
63
- /** Key prefix in storage */
64
- prefix?: string;
65
- /** @deprecated Use storageProvider instead. For backward compatibility with VaultStorage */
66
- storage?: VaultStorage;
67
- }
68
- type EncryptedStorageOptions = LegacyEncryptedStorageOptions | JwkEncryptedStorageOptions;
69
- /**
70
- * Create an encrypted storage wrapper
71
- * Supports both legacy recovery key and new CipherJWK modes
72
- *
73
- * Uses dependency injection for storage provider (Dependency Inversion Principle)
74
- */
75
- declare function createVaultStorage(options: EncryptedStorageOptions): VaultStorage;
76
-
77
- /**
78
- * HTTP client interface for sync engine
79
- * Follows Dependency Inversion Principle - allows mocking and alternative implementations
80
- */
81
- /** HTTP request options */
82
- interface IHttpRequest {
83
- url: string;
84
- method: "GET" | "POST" | "PUT" | "DELETE";
85
- headers?: Record<string, string>;
86
- body?: string;
87
- }
88
- /** HTTP response */
89
- interface IHttpResponse {
90
- ok: boolean;
91
- status: number;
92
- json<T = unknown>(): Promise<T>;
93
- text(): Promise<string>;
94
- }
95
- /** HTTP client interface */
96
- interface IHttpClient {
97
- /**
98
- * Make an HTTP request
99
- * @param request Request options
100
- * @returns Response
101
- */
102
- request(request: IHttpRequest): Promise<IHttpResponse>;
103
- }
104
-
105
- /**
106
- * Sync engine for vault middleware
107
- * Handles bidirectional sync with server + offline queue
2
+ * @ursalock/zustand
3
+ * Encrypted persistence middleware for Zustand
108
4
  *
109
5
  * Refactored to follow SOLID principles:
110
- * - IHttpClient interface (Dependency Inversion)
111
- * - Separated offline queue logic (Single Responsibility)
112
- * - Injectable HTTP client for testing
113
- */
114
-
115
- type SyncStatus = "idle" | "syncing" | "synced" | "error" | "offline";
116
- interface SyncState {
117
- /** Last successful sync timestamp */
118
- lastSyncAt: number | null;
119
- /** Current sync status */
120
- status: SyncStatus;
121
- /** Pending changes waiting to be synced */
122
- pendingChanges: boolean;
123
- /** Last error message */
124
- error: string | null;
125
- }
126
- interface SyncOptions {
127
- /** Server base URL */
128
- serverUrl: string;
129
- /** Vault name */
130
- name: string;
131
- /** Auth token getter */
132
- getToken: () => string | null;
133
- /** Called when server has newer data */
134
- onServerData: (data: string, salt: string, updatedAt: number) => void;
135
- /** Get current local data */
136
- getLocalData: () => {
137
- data: string;
138
- salt: string;
139
- updatedAt: number;
140
- };
141
- /** Called on sync status change */
142
- onStatusChange?: (status: SyncStatus) => void;
143
- /** HTTP client for making requests (default: FetchHttpClient) */
144
- httpClient?: IHttpClient;
145
- }
146
- /**
147
- * Create a sync engine instance
148
- * Uses dependency injection for HTTP client (Dependency Inversion Principle)
149
- */
150
- declare function createSyncEngine(options: SyncOptions): {
151
- sync: () => Promise<void>;
152
- push: () => Promise<void>;
153
- pull: () => Promise<boolean>;
154
- getState: () => SyncState;
155
- clearQueue: () => void;
156
- };
157
- type SyncEngine = ReturnType<typeof createSyncEngine>;
158
-
159
- /**
160
- * vault() middleware - Drop-in replacement for persist()
161
- * Adds E2EE encryption and cloud sync to Zustand stores
162
- *
163
- * Supports two encryption modes:
164
- * - Legacy: recoveryKey string (Argon2id key derivation)
165
- * - New: cipherJwk from ZKCredentials (PRF-derived)
166
- *
167
- * Type pattern follows zustand's persist middleware:
168
- * - Simple internal implementation type (VaultImpl)
169
- * - Complex public API type (Vault)
170
- * - Cast at export: `vaultImpl as unknown as Vault`
171
- */
172
-
173
- /** Base vault middleware options */
174
- interface VaultOptionsBase<S, PersistedState = S> {
175
- /** Unique name for this vault (used as storage key) */
176
- name: string;
177
- /**
178
- * Server URL for sync (optional)
179
- * If not provided, only local encrypted storage is used
180
- */
181
- server?: string;
182
- /**
183
- * Auth token getter for server sync
184
- * Required if server is provided
185
- */
186
- getToken?: () => string | null;
187
- /** Custom storage implementation */
188
- storage?: VaultStorage;
189
- /** Storage key prefix (default: 'ursalock:') */
190
- prefix?: string;
191
- /**
192
- * Partial state to persist
193
- * @default (state) => state (persist everything)
194
- */
195
- partialize?: (state: S) => PersistedState;
196
- /**
197
- * Merge function for rehydration
198
- * @default Object.assign
199
- */
200
- merge?: (persistedState: unknown, currentState: S) => S;
201
- /**
202
- * Called when state is loaded from storage
203
- */
204
- onRehydrateStorage?: (state: S) => ((state?: S, error?: unknown) => void) | void;
205
- /**
206
- * Skip hydration on init (useful for SSR)
207
- * Call rehydrate() manually when ready
208
- */
209
- skipHydration?: boolean;
210
- /**
211
- * Sync interval in ms (default: 30000 = 30s)
212
- * Set to 0 to disable auto-sync
213
- */
214
- syncInterval?: number;
215
- }
216
- /** Legacy options using recovery key string */
217
- interface VaultOptionsLegacy<S, PersistedState = S> extends VaultOptionsBase<S, PersistedState> {
218
- /** Recovery key for E2EE encryption (legacy mode) */
219
- recoveryKey: string;
220
- }
221
- /** New options using CipherJWK from ZKCredentials */
222
- interface VaultOptionsJwk<S, PersistedState = S> extends VaultOptionsBase<S, PersistedState> {
223
- /** CipherJWK for E2EE encryption (from ZKCredentials) */
224
- cipherJwk: CipherJWK;
225
- }
226
- type VaultOptions<S, PersistedState = S> = VaultOptionsLegacy<S, PersistedState> | VaultOptionsJwk<S, PersistedState>;
227
-
228
- type VaultListener<S> = (state: S) => void;
229
-
230
- /** Store shape extended with vault API */
231
- type StoreVault<S, Ps> = S extends {
232
- getState: () => infer T;
233
- setState: {
234
- (...args: infer Sa1): infer Sr1;
235
- (...args: infer Sa2): infer Sr2;
236
- };
237
- } ? {
238
- setState(...args: Sa1): Sr1 | Promise<void>;
239
- setState(...args: Sa2): Sr2 | Promise<void>;
240
- vault: {
241
- /** Full bidirectional sync with server */
242
- sync: () => Promise<void>;
243
- /** Push local changes to server */
244
- push: () => Promise<void>;
245
- /** Pull latest from server */
246
- pull: () => Promise<boolean>;
247
- /** Rehydrate from local storage */
248
- rehydrate: () => Promise<void>;
249
- /** Check if store has been hydrated */
250
- hasHydrated: () => boolean;
251
- /** Get current sync status */
252
- getSyncStatus: () => SyncStatus;
253
- /** Check if there are pending offline changes */
254
- hasPendingChanges: () => boolean;
255
- /** Clear all stored data (local + server) */
256
- clearStorage: () => Promise<void>;
257
- /** Subscribe to hydration start */
258
- onHydrate: (fn: VaultListener<T>) => () => void;
259
- /** Subscribe to hydration complete */
260
- onFinishHydration: (fn: VaultListener<T>) => () => void;
261
- };
262
- } : never;
263
- type Write<T, U> = Omit<T, keyof U> & U;
264
- type WithVault<S, A> = Write<S, StoreVault<S, A>>;
265
- /** Public API type with complex mutator support */
266
- 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]>;
267
- declare module "zustand" {
268
- interface StoreMutators<S, A> {
269
- vault: WithVault<S, A>;
270
- }
271
- }
272
- /**
273
- * vault() middleware - Add E2EE encrypted persistence to Zustand
274
- *
275
- * @example Using CipherJWK from ZKCredentials (recommended)
276
- * ```ts
277
- * const useStore = create(
278
- * vault(
279
- * (set) => ({
280
- * count: 0,
281
- * increment: () => set((s) => ({ count: s.count + 1 })),
282
- * }),
283
- * {
284
- * name: 'my-store',
285
- * cipherJwk: credential.cipherJwk, // From ZKCredentials
286
- * server: 'https://vault.example.com', // optional
287
- * }
288
- * )
289
- * )
290
- * ```
291
- *
292
- * @example Using recovery key (legacy)
293
- * ```ts
294
- * const useStore = create(
295
- * vault(
296
- * (set) => ({ ... }),
297
- * {
298
- * name: 'my-store',
299
- * recoveryKey: 'ABCD-EFGH-...',
300
- * }
301
- * )
302
- * )
303
- * ```
304
- */
305
- declare const vault: Vault;
306
-
307
- /**
308
- * LocalStorage implementation of IStorageProvider
309
- * Concrete implementation following Dependency Inversion Principle
310
- */
311
-
312
- /**
313
- * LocalStorage provider with async interface
314
- * Wraps synchronous localStorage with async API for consistency
315
- */
316
- declare class LocalStorageProvider implements IStorageProvider {
317
- getItem(key: string): Promise<string | null>;
318
- setItem(key: string, value: string): Promise<void>;
319
- removeItem(key: string): Promise<void>;
320
- }
321
-
322
- /**
323
- * Fetch-based HTTP client implementation
324
- * Concrete implementation of IHttpClient using browser fetch API
325
- */
326
-
327
- /**
328
- * HTTP client using native fetch API
329
- */
330
- declare class FetchHttpClient implements IHttpClient {
331
- request(request: IHttpRequest): Promise<IHttpResponse>;
332
- }
333
-
334
- /**
335
- * React hooks for vault status
336
- */
337
-
338
- /**
339
- * Hook to get sync status from a vault store
340
- *
341
- * @example
342
- * ```tsx
343
- * const status = useSyncStatus(useStore)
344
- * // 'idle' | 'syncing' | 'synced' | 'error' | 'offline'
345
- * ```
346
- */
347
- declare function useSyncStatus<T extends {
348
- vault?: {
349
- getSyncStatus?: () => SyncStatus;
350
- };
351
- }>(useStore: () => T): SyncStatus;
352
-
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 };
6
+ * - Interface-based design (Dependency Inversion)
7
+ * - Injectable dependencies for testing
8
+ * - Separated concerns (Single Responsibility)
9
+ */
10
+ export { vault, type VaultOptions, type VaultOptionsLegacy, type VaultOptionsJwk, } from "./vault.js";
11
+ export { createVaultStorage, type VaultStorage, type IStorageProvider, type EncryptedStorageOptions, type LegacyEncryptedStorageOptions, type JwkEncryptedStorageOptions, } from "./storage.js";
12
+ export type { IVaultStorage } from "./interfaces/storage.js";
13
+ export { LocalStorageProvider } from "./providers/local-storage.js";
14
+ export type { IHttpClient, IHttpRequest, IHttpResponse } from "./interfaces/http.js";
15
+ export { FetchHttpClient } from "./providers/fetch-http.js";
16
+ export { type SyncStatus, type SyncState, createSyncEngine, type SyncEngine } from "./sync.js";
17
+ export { useSyncStatus, useHydrated, usePendingChanges } from "./hooks.js";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EACL,KAAK,EACL,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,kBAAkB,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,GAChC,MAAM,cAAc,CAAC;AAGtB,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,SAAS,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAG/F,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC"}