@mrbelloc/encrypted-store 0.2.3 → 1.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.
Files changed (56) hide show
  1. package/README.md +385 -117
  2. package/dist/encryptedStore.d.ts +55 -72
  3. package/dist/encryptedStore.d.ts.map +1 -1
  4. package/dist/encryptedStore.js +299 -270
  5. package/dist/encryptedStore.js.map +1 -1
  6. package/dist/encryption.d.ts +1 -6
  7. package/dist/encryption.d.ts.map +1 -1
  8. package/dist/encryption.js +3 -5
  9. package/dist/encryption.js.map +1 -1
  10. package/dist/index.d.ts +6 -5
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/kb-dir-partykit/_test_db_1_1768430534431_data_.json +4 -0
  15. package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_data_.json +4 -0
  16. package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_meta_.json +4 -0
  17. package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_wal_.json +4 -0
  18. package/dist/kb-dir-partykit/_test_db_1_1768430534431_meta_.json +4 -0
  19. package/dist/kb-dir-partykit/_test_db_1_1768430534431_wal_.json +4 -0
  20. package/dist/kb-dir-partykit/_test_db_2_1768430536612_data_.json +4 -0
  21. package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_data_.json +4 -0
  22. package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_meta_.json +4 -0
  23. package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_wal_.json +4 -0
  24. package/dist/kb-dir-partykit/_test_db_2_1768430536612_meta_.json +4 -0
  25. package/dist/kb-dir-partykit/_test_db_2_1768430536612_wal_.json +4 -0
  26. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_data_.json +4 -0
  27. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_data_.json +4 -0
  28. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_meta_.json +4 -0
  29. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_wal_.json +4 -0
  30. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_meta_.json +4 -0
  31. package/dist/kb-dir-partykit/_test_sync_db_1768430808558_wal_.json +4 -0
  32. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_data_.json +4 -0
  33. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_data_.json +4 -0
  34. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_meta_.json +4 -0
  35. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_wal_.json +4 -0
  36. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_meta_.json +4 -0
  37. package/dist/kb-dir-partykit/_test_sync_db_1768430839116_wal_.json +4 -0
  38. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_data_.json +4 -0
  39. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_data_.json +4 -0
  40. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_meta_.json +4 -0
  41. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_wal_.json +4 -0
  42. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_meta_.json +4 -0
  43. package/dist/kb-dir-partykit/_test_sync_db_1768430931049_wal_.json +4 -0
  44. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_data_.json +4 -0
  45. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_data_.json +4 -0
  46. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_meta_.json +4 -0
  47. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_wal_.json +4 -0
  48. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_meta_.json +4 -0
  49. package/dist/kb-dir-partykit/_test_sync_db_1768430955075_wal_.json +4 -0
  50. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_data_.json +4 -0
  51. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_data_.json +4 -0
  52. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_meta_.json +4 -0
  53. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_wal_.json +4 -0
  54. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_meta_.json +4 -0
  55. package/dist/kb-dir-partykit/_test_sync_db_1768522643733_wal_.json +4 -0
  56. package/package.json +14 -9
package/README.md CHANGED
@@ -1,212 +1,480 @@
1
1
  # Encrypted Store
2
2
 
3
- Client-side encrypted storage with change detection for PWAs. Built on [Fireproof](https://use-fireproof.com).
3
+ Client-side encrypted document storage with change detection using PouchDB and AES-256-GCM encryption.
4
4
 
5
- **For small data that can live in memory** - Designed for PWAs that manage datasets that fit comfortably in browser memory.
5
+ **Simple API for offline-first apps** - PUT, GET, DELETE documents with automatic sync to CouchDB.
6
6
 
7
7
  ## Features
8
8
 
9
- - 🔐 AES-256-GCM encryption before storage
10
- - 🔄 Real-time change detection (added/changed/deleted events)
11
- - 📱 PWA-ready with offline-first support
12
- - 🌐 Optional remote sync (PartyKit, Netlify)
13
- - 📦 TypeScript with full type safety
9
+ - 🔐 AES-256-GCM encryption with WebCrypto API
10
+ - 📦 Simple document API: `put`, `get`, `delete`, `getAll`
11
+ - 🔄 Real-time change detection (`onChange`, `onDelete`)
12
+ - ⚠️ Conflict detection and resolution
13
+ - 🌐 Sync to CouchDB (or any PouchDB-compatible server)
14
+ - 📊 Sync progress events
15
+ - 🔌 Offline-first with automatic retry
16
+ - 📱 Works in browser and Node.js
17
+ - 🎯 TypeScript with full type safety
14
18
 
15
19
  ## Installation
16
20
 
21
+ ### For Browser (Vite/Webpack)
22
+
23
+ ```bash
24
+ npm install @mrbelloc/encrypted-store pouchdb-browser@^8.0.1 events
25
+ ```
26
+
27
+ **Note:** Currently requires PouchDB v8. PouchDB v9 has compatibility issues with TypeScript types and some bundlers.
28
+
29
+ **Required for Vite:** Install the `events` package to fix "Class extends value [object Object]" errors.
30
+
31
+ ### For Node.js
32
+
17
33
  ```bash
18
- npm install @mrbelloc/encrypted-store
34
+ npm install @mrbelloc/encrypted-store pouchdb
19
35
  ```
20
36
 
21
37
  ## Quick Start
22
38
 
39
+ ### Browser (Vite/React/Vue/Svelte)
40
+
23
41
  ```typescript
24
- import { EncryptedStore, fireproof } from "@mrbelloc/encrypted-store";
25
-
26
- // Create database and encrypted store
27
- const db = fireproof("myapp");
28
- const store = new EncryptedStore(db, "my-password", {
29
- docsAdded: (events) => {
30
- events.forEach(({ table, docs }) => {
31
- console.log(`New ${table}:`, docs);
32
- });
42
+ import PouchDBModule from 'pouchdb-browser';
43
+ // Workaround for ESM/CommonJS compatibility in some bundlers
44
+ const PouchDB = PouchDBModule.default || PouchDBModule;
45
+ import { EncryptedStore } from '@mrbelloc/encrypted-store';
46
+
47
+ // Create database and encrypted store (uses IndexedDB in browser)
48
+ const db = new PouchDB('myapp');
49
+ const store = new EncryptedStore(db, 'my-password', {
50
+ onChange: (docs) => {
51
+ console.log('Documents changed:', docs);
33
52
  },
34
- docsChanged: (events) => {
35
- events.forEach(({ table, docs }) => {
36
- console.log(`Updated ${table}:`, docs);
37
- });
53
+ onDelete: (docs) => {
54
+ console.log('Documents deleted:', docs);
38
55
  },
39
- docsDeleted: (events) => {
40
- events.forEach(({ table, docs }) => {
41
- console.log(`Deleted ${table}:`, docs);
42
- });
56
+ onConflict: (conflicts) => {
57
+ console.log('Conflicts detected:', conflicts);
58
+ },
59
+ onSync: (info) => {
60
+ console.log('Sync progress:', info);
43
61
  },
62
+ onError: (errors) => {
63
+ console.error('Decryption errors:', errors);
64
+ }
44
65
  });
45
66
 
46
- // Load existing data
67
+ // Load existing data and start change detection
47
68
  await store.loadAll();
48
69
 
49
70
  // Create/update documents
50
- await store.put("users", { _id: "alice", name: "Alice", age: 30 });
71
+ await store.put('expenses', {
72
+ _id: 'lunch',
73
+ amount: 15.50,
74
+ date: '2024-01-15'
75
+ });
51
76
 
52
- // Get documents
53
- const user = await store.get("users", "alice");
77
+ // Get a document
78
+ const expense = await store.get('expenses', 'lunch');
79
+ console.log(expense); // { _id: 'lunch', _table: 'expenses', amount: 15.50, date: '2024-01-15' }
54
80
 
55
- // Delete documents
56
- await store.delete("users", "alice");
81
+ // Get all documents (optionally filtered by table)
82
+ const allExpenses = await store.getAll('expenses');
83
+ const allDocs = await store.getAll();
84
+
85
+ // Delete a document
86
+ await store.delete('expenses', 'lunch');
87
+
88
+ // Sync to CouchDB
89
+ await store.connectRemote({
90
+ url: 'http://localhost:5984/myapp',
91
+ live: true,
92
+ retry: true
93
+ });
94
+ ```
95
+
96
+ ### Node.js
97
+
98
+ ```typescript
99
+ import PouchDB from 'pouchdb';
100
+ import { EncryptedStore } from '@mrbelloc/encrypted-store';
101
+
102
+ // Create database and encrypted store (uses LevelDB in Node)
103
+ const db = new PouchDB('myapp');
104
+ const store = new EncryptedStore(db, 'my-password', {
105
+ onChange: (docs) => console.log('Changed:', docs),
106
+ onDelete: (docs) => console.log('Deleted:', docs),
107
+ });
108
+
109
+ await store.loadAll();
57
110
  ```
58
111
 
59
112
  ## API Reference
60
113
 
61
- ### `new EncryptedStore(db, password, listener)`
114
+ ### `new EncryptedStore(db, password, listener?)`
62
115
 
63
116
  Creates an encrypted store.
64
117
 
65
- - `db`: Fireproof database instance
118
+ - `db`: PouchDB database instance
66
119
  - `password`: Encryption password (string)
67
- - `listener`: Object with three callbacks:
68
- - `docsAdded(events: TableEvent[])`: Fired when new documents are added
69
- - `docsChanged(events: TableEvent[])`: Fired when documents are updated
70
- - `docsDeleted(events: TableEvent[])`: Fired when documents are deleted
120
+ - `listener`: Optional object with callbacks
71
121
 
72
- Each `TableEvent` has:
122
+ ### Listener Callbacks
73
123
 
74
- - `table`: Document type (e.g., "users", "transactions")
75
- - `docs`: Array of documents with that type
124
+ ```typescript
125
+ interface StoreListener {
126
+ onChange: (docs: Doc[]) => void;
127
+ onDelete: (docs: Doc[]) => void;
128
+ onConflict?: (conflicts: ConflictInfo[]) => void;
129
+ onSync?: (info: SyncInfo) => void;
130
+ onError?: (errors: DecryptionErrorEvent[]) => void;
131
+ }
132
+ ```
133
+
134
+ - **`onChange(docs)`**: Called when documents are added or updated
135
+ - **`onDelete(docs)`**: Called when documents are deleted
136
+ - **`onConflict(conflicts)`**: Called when conflicts are detected
137
+ - **`onSync(info)`**: Called during sync operations
138
+ - **`onError(errors)`**: Called when documents fail to decrypt
76
139
 
77
140
  ### `await store.loadAll()`
78
141
 
79
- Loads all existing documents and sets up change detection. Call this once after creating the store.
142
+ Loads all existing documents and starts change detection. Call this once after creating the store.
80
143
 
81
- ### `await store.put(type, doc)`
144
+ ### `await store.put(table, doc)`
82
145
 
83
146
  Creates or updates a document.
84
147
 
85
- - `type`: Document type / table name (string)
86
- - `doc`: Document object with `_id` field (will be generated if missing)
148
+ - `table`: Document type (e.g., "expenses", "tasks")
149
+ - `doc`: Document object with optional `_id` field (generated if missing)
87
150
 
88
- Returns the document.
151
+ Returns the document with `_table` field added.
89
152
 
90
- ### `await store.get(type, id)`
153
+ ### `await store.get(table, id)`
91
154
 
92
- Retrieves a document by type and ID. Returns `null` if not found.
155
+ Gets a document by table and ID. Returns `null` if not found.
93
156
 
94
- ### `await store.delete(type, id)`
157
+ ### `await store.delete(table, id)`
95
158
 
96
- Deletes a document by type and ID.
159
+ Deletes a document by table and ID.
97
160
 
98
- ## Remote Sync
161
+ ### `await store.getAll(table?)`
99
162
 
100
- Sync encrypted data across devices with any Fireproof connector:
163
+ Gets all documents, optionally filtered by table.
101
164
 
102
165
  ```typescript
103
- // Install the connector you want
104
- // npm install @fireproof/partykit
105
- // or
106
- // npm install @fireproof/netlify
107
-
108
- import { connect } from "@fireproof/partykit";
109
- // or
110
- // import { connect } from "@fireproof/netlify";
111
-
112
- // Connect using the connector function
113
- await store.connectRemote(connect, {
114
- namespace: "my-app",
115
- host: "http://localhost:1999", // or your server URL
116
- });
166
+ const allExpenses = await store.getAll('expenses');
167
+ const allDocs = await store.getAll();
168
+ ```
169
+
170
+ ### `await store.connectRemote(options)`
117
171
 
118
- // Disconnect
119
- store.disconnectRemote();
172
+ Connects to a remote CouchDB server for sync.
120
173
 
121
- // Works with any connector that follows the Fireproof connector interface
174
+ ```typescript
175
+ interface RemoteOptions {
176
+ url: string; // CouchDB URL
177
+ live?: boolean; // Continuous sync (default: true)
178
+ retry?: boolean; // Auto-retry on failure (default: true)
179
+ }
122
180
  ```
123
181
 
124
- **Note:** Remote servers only see encrypted blobs - they cannot read your data.
182
+ ### `store.disconnectRemote()`
125
183
 
126
- ## How It Works
184
+ Disconnects from remote sync.
127
185
 
128
- 1. **Encryption**: Documents are encrypted with AES-256-GCM before storage
129
- 2. **Storage**: Encrypted blobs stored in Fireproof (local-first IndexedDB)
130
- 3. **Change Detection**: Fireproof's subscribe notifies us of changes
131
- 4. **Diff Computation**: We track IDs and decrypt only changed documents
132
- 5. **Events**: Your app gets organized events by table (added/changed/deleted)
186
+ ### `await store.getConflictInfo(table, id)`
187
+
188
+ Check if a document has conflicts without triggering the callback. Returns `ConflictInfo` if conflicts exist, or `null` if none.
189
+
190
+ ```typescript
191
+ const conflictInfo = await store.getConflictInfo('expenses', 'lunch');
192
+ if (conflictInfo) {
193
+ console.log('Conflict detected!');
194
+ console.log('Winner:', conflictInfo.winner);
195
+ console.log('Losers:', conflictInfo.losers);
196
+ // Handle the conflict
197
+ }
198
+ ```
199
+
200
+ ### `await store.resolveConflict(table, id, winningDoc)`
201
+
202
+ Manually resolve a conflict by choosing the winning document.
203
+
204
+ ```typescript
205
+ // Option 1: Use in onConflict callback
206
+ store.listener.onConflict = async (conflicts) => {
207
+ for (const conflict of conflicts) {
208
+ // Pick the document with the latest timestamp
209
+ const latest = [conflict.winner, ...conflict.losers]
210
+ .sort((a, b) => b.timestamp - a.timestamp)[0];
211
+
212
+ await store.resolveConflict(conflict.table, conflict.id, latest);
213
+ }
214
+ };
215
+
216
+ // Option 2: Check manually and resolve
217
+ const conflict = await store.getConflictInfo('expenses', 'lunch');
218
+ if (conflict) {
219
+ await store.resolveConflict('expenses', 'lunch', conflict.winner);
220
+ }
221
+ ```
222
+
223
+ ## Conflict Detection
224
+
225
+ When the same document is edited offline on multiple devices, PouchDB detects conflicts automatically:
226
+
227
+ ```typescript
228
+ interface ConflictInfo {
229
+ docId: string; // Full document ID (e.g., "expenses_lunch")
230
+ table: string; // Document table (e.g., "expenses")
231
+ id: string; // Document ID (e.g., "lunch")
232
+ currentRev: string; // Current revision ID
233
+ conflictRevs: string[];// Conflicting revision IDs
234
+ winner: Doc; // The winning document (current version)
235
+ losers: Doc[]; // Conflicting versions
236
+ }
237
+ ```
238
+
239
+ The `onConflict` callback gives you both the winner and all conflicting versions, so you can:
240
+ - Show a UI for manual resolution
241
+ - Auto-resolve based on timestamps
242
+ - Merge changes programmatically
243
+ - Log conflicts for review
244
+
245
+ ## Sync Events
246
+
247
+ Monitor sync progress with the `onSync` callback:
248
+
249
+ ```typescript
250
+ interface SyncInfo {
251
+ direction: 'push' | 'pull' | 'both';
252
+ change: {
253
+ docs_read?: number;
254
+ docs_written?: number;
255
+ doc_write_failures?: number;
256
+ errors?: any[];
257
+ };
258
+ }
259
+ ```
260
+
261
+ Example:
262
+
263
+ ```typescript
264
+ const store = new EncryptedStore(db, password, {
265
+ onChange: (docs) => console.log('Changed:', docs.length),
266
+ onDelete: (docs) => console.log('Deleted:', docs.length),
267
+ onSync: (info) => {
268
+ if (info.direction === 'push') {
269
+ console.log(`Pushed ${info.change.docs_written} docs to server`);
270
+ } else {
271
+ console.log(`Pulled ${info.change.docs_read} docs from server`);
272
+ }
273
+ }
274
+ });
275
+ ```
276
+
277
+ ## Deployment Options
278
+
279
+ ### Free Tier Options
280
+
281
+ 1. **IBM Cloudant** - Free tier: 1GB storage, 20 req/sec
282
+ ```typescript
283
+ await store.connectRemote({
284
+ url: 'https://username:password@username.cloudant.com/mydb'
285
+ });
286
+ ```
287
+
288
+ 2. **Oracle Cloud Free Tier** - Run your own CouchDB
289
+ ```bash
290
+ # On Oracle VM
291
+ docker run -d -p 5984:5984 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password couchdb
292
+ ```
293
+
294
+ 3. **Self-hosted** - CouchDB on any VPS ($5/month)
295
+ ```typescript
296
+ await store.connectRemote({
297
+ url: 'http://admin:password@your-server.com:5984/mydb'
298
+ });
299
+ ```
300
+
301
+ ### Backup Strategy
302
+
303
+ Example using Oracle Free Tier + S3:
304
+
305
+ ```bash
306
+ # Daily backup script
307
+ #!/bin/bash
308
+ TODAY=$(date +%Y-%m-%d)
309
+ curl -X GET http://admin:password@localhost:5984/mydb/_all_docs?include_docs=true > backup-$TODAY.json
310
+ aws s3 cp backup-$TODAY.json s3://my-backups/couchdb/
311
+ ```
133
312
 
134
313
  ## Example: React Integration
135
314
 
136
315
  ```typescript
137
- import { useState, useEffect } from "react";
138
- import { EncryptedStore, fireproof } from "@mrbelloc/encrypted-store";
316
+ import { useState, useEffect } from 'react';
317
+ import PouchDBModule from 'pouchdb-browser';
318
+ const PouchDB = PouchDBModule.default || PouchDBModule;
319
+ import { EncryptedStore } from '@mrbelloc/encrypted-store';
139
320
 
140
321
  function useEncryptedStore(dbName: string, password: string) {
141
- const [users, setUsers] = useState<Map<string, any>>(new Map());
322
+ const [expenses, setExpenses] = useState<Map<string, any>>(new Map());
142
323
  const [store, setStore] = useState<EncryptedStore | null>(null);
143
324
 
144
325
  useEffect(() => {
145
- const db = fireproof(dbName);
326
+ const db = new PouchDB(dbName);
146
327
  const encryptedStore = new EncryptedStore(db, password, {
147
- docsAdded: (events) => {
148
- events.forEach(({ table, docs }) => {
149
- if (table === "users") {
150
- setUsers((prev) => {
151
- const next = new Map(prev);
152
- docs.forEach((doc) => next.set(doc._id, doc));
153
- return next;
154
- });
155
- }
328
+ onChange: (docs) => {
329
+ setExpenses((prev) => {
330
+ const next = new Map(prev);
331
+ docs.forEach((doc) => {
332
+ if (doc._table === 'expenses') {
333
+ next.set(doc._id, doc);
334
+ }
335
+ });
336
+ return next;
156
337
  });
157
338
  },
158
- docsChanged: (events) => {
159
- events.forEach(({ table, docs }) => {
160
- if (table === "users") {
161
- setUsers((prev) => {
162
- const next = new Map(prev);
163
- docs.forEach((doc) => next.set(doc._id, doc));
164
- return next;
165
- });
166
- }
339
+ onDelete: (docs) => {
340
+ setExpenses((prev) => {
341
+ const next = new Map(prev);
342
+ docs.forEach((doc) => {
343
+ if (doc._table === 'expenses') {
344
+ next.delete(doc._id);
345
+ }
346
+ });
347
+ return next;
167
348
  });
168
349
  },
169
- docsDeleted: (events) => {
170
- events.forEach(({ table, docs }) => {
171
- if (table === "users") {
172
- setUsers((prev) => {
173
- const next = new Map(prev);
174
- docs.forEach((doc) => next.delete(doc._id));
175
- return next;
176
- });
177
- }
350
+ onConflict: (conflicts) => {
351
+ // Auto-resolve: pick latest by timestamp
352
+ conflicts.forEach(async (conflict) => {
353
+ const latest = [conflict.winner, ...conflict.losers]
354
+ .sort((a, b) => b.timestamp - a.timestamp)[0];
355
+ await encryptedStore.resolveConflict(conflict.table, conflict.id, latest);
178
356
  });
179
- },
357
+ }
180
358
  });
181
359
 
182
360
  encryptedStore.loadAll();
183
361
  setStore(encryptedStore);
362
+
363
+ return () => {
364
+ encryptedStore.disconnectRemote();
365
+ };
184
366
  }, [dbName, password]);
185
367
 
186
- return { users: Array.from(users.values()), store };
368
+ return { expenses: Array.from(expenses.values()), store };
369
+ }
370
+
371
+ function App() {
372
+ const { expenses, store } = useEncryptedStore('myapp', 'my-password');
373
+
374
+ const addExpense = async () => {
375
+ await store?.put('expenses', {
376
+ _id: crypto.randomUUID(),
377
+ amount: 25,
378
+ description: 'Coffee',
379
+ timestamp: Date.now()
380
+ });
381
+ };
382
+
383
+ return (
384
+ <div>
385
+ <button onClick={addExpense}>Add Expense</button>
386
+ <ul>
387
+ {expenses.map((exp) => (
388
+ <li key={exp._id}>{exp.description}: ${exp.amount}</li>
389
+ ))}
390
+ </ul>
391
+ </div>
392
+ );
187
393
  }
188
394
  ```
189
395
 
190
396
  ## TypeScript Types
191
397
 
192
398
  ```typescript
193
- interface TableEvent {
399
+ interface Doc {
400
+ _id: string;
401
+ _table: string;
402
+ [key: string]: any;
403
+ }
404
+
405
+ interface ConflictInfo {
406
+ docId: string;
194
407
  table: string;
195
- docs: Doc[];
408
+ id: string;
409
+ currentRev: string;
410
+ conflictRevs: string[];
411
+ winner: Doc;
412
+ losers: Doc[];
196
413
  }
197
414
 
198
- interface StoreListener {
199
- docsAdded: (events: TableEvent[]) => void;
200
- docsChanged: (events: TableEvent[]) => void;
201
- docsDeleted: (events: TableEvent[]) => void;
415
+ interface SyncInfo {
416
+ direction: 'push' | 'pull' | 'both';
417
+ change: {
418
+ docs_read?: number;
419
+ docs_written?: number;
420
+ doc_write_failures?: number;
421
+ errors?: any[];
422
+ };
202
423
  }
203
424
 
204
- interface Doc {
205
- _id: string;
206
- [key: string]: any;
425
+ interface DecryptionErrorEvent {
426
+ docId: string;
427
+ error: Error;
428
+ rawDoc: any;
429
+ }
430
+
431
+ interface RemoteOptions {
432
+ url: string;
433
+ live?: boolean;
434
+ retry?: boolean;
207
435
  }
208
436
  ```
209
437
 
438
+ ## How It Works
439
+
440
+ 1. **Encryption**: Documents are encrypted with AES-256-GCM before storage
441
+ 2. **Storage**: Encrypted data stored in PouchDB (IndexedDB in browser, LevelDB in Node)
442
+ 3. **Change Detection**: PouchDB's changes feed notifies of all changes
443
+ 4. **Conflict Detection**: PouchDB's MVCC detects conflicts automatically
444
+ 5. **Sync**: Bi-directional sync with CouchDB using PouchDB replication
445
+ 6. **Events**: Callbacks notify your app of changes, conflicts, and sync progress
446
+
447
+ ## Browser vs Node.js
448
+
449
+ ### Browser (Vite/Webpack)
450
+ - Use `pouchdb-browser@^8.0.1` - includes IndexedDB adapter
451
+ - Smaller bundle size
452
+ - Works with Vite, Webpack, etc.
453
+ - **Vite users**:
454
+ - Add `define: { global: 'globalThis' }` to vite.config.ts (PouchDB v8 requirement)
455
+ - Install `events` package: `npm install events` (fixes "Class extends" errors)
456
+ - **Import**: Use `const PouchDB = PouchDBModule.default || PouchDBModule` for better compatibility
457
+
458
+ ### Node.js
459
+ - Use `pouchdb` - includes LevelDB adapter
460
+ - For CLI tools, servers, etc.
461
+
462
+ ## Security Notes
463
+
464
+ - Encryption happens client-side before any data leaves the device
465
+ - Remote servers only see encrypted blobs
466
+ - Password is never transmitted or stored
467
+ - Use a strong password (consider using a key derivation function like PBKDF2)
468
+
210
469
  ## License
211
470
 
212
471
  MIT
472
+
473
+ ## Why PouchDB?
474
+
475
+ - **Mature**: 10+ years of production use
476
+ - **Reliable**: Battle-tested conflict resolution
477
+ - **Compatible**: Works with any CouchDB server
478
+ - **Offline-first**: Built for unreliable networks
479
+ - **Simple**: Easy to understand replication model
480
+ - **Free**: No vendor lock-in, self-hostable