@mrbelloc/encrypted-store 0.3.0 → 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.
- package/README.md +367 -179
- package/dist/encryptedStore.d.ts +55 -72
- package/dist/encryptedStore.d.ts.map +1 -1
- package/dist/encryptedStore.js +299 -270
- package/dist/encryptedStore.js.map +1 -1
- package/dist/encryption.d.ts +1 -6
- package/dist/encryption.d.ts.map +1 -1
- package/dist/encryption.js +3 -5
- package/dist/encryption.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_1_1768430534431_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_db_2_1768430536612_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430808558_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430839116_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430931049_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768430955075_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_data_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_idx_wal_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_meta_.json +4 -0
- package/dist/kb-dir-partykit/_test_sync_db_1768522643733_wal_.json +4 -0
- package/package.json +14 -9
package/README.md
CHANGED
|
@@ -1,292 +1,480 @@
|
|
|
1
1
|
# Encrypted Store
|
|
2
2
|
|
|
3
|
-
Client-side encrypted storage with change detection
|
|
3
|
+
Client-side encrypted document storage with change detection using PouchDB and AES-256-GCM encryption.
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
console.log(`Updated ${table}:`, docs);
|
|
37
|
-
});
|
|
53
|
+
onDelete: (docs) => {
|
|
54
|
+
console.log('Documents deleted:', docs);
|
|
38
55
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(`Deleted ${table}:`, docs);
|
|
42
|
-
});
|
|
56
|
+
onConflict: (conflicts) => {
|
|
57
|
+
console.log('Conflicts detected:', conflicts);
|
|
43
58
|
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.error(`Failed to decrypt ${docId}:`, error.message);
|
|
47
|
-
console.log("Raw encrypted document:", doc);
|
|
48
|
-
});
|
|
59
|
+
onSync: (info) => {
|
|
60
|
+
console.log('Sync progress:', info);
|
|
49
61
|
},
|
|
62
|
+
onError: (errors) => {
|
|
63
|
+
console.error('Decryption errors:', errors);
|
|
64
|
+
}
|
|
50
65
|
});
|
|
51
66
|
|
|
52
|
-
// Load existing data
|
|
67
|
+
// Load existing data and start change detection
|
|
53
68
|
await store.loadAll();
|
|
54
69
|
|
|
55
70
|
// Create/update documents
|
|
56
|
-
await store.put(
|
|
71
|
+
await store.put('expenses', {
|
|
72
|
+
_id: 'lunch',
|
|
73
|
+
amount: 15.50,
|
|
74
|
+
date: '2024-01-15'
|
|
75
|
+
});
|
|
76
|
+
|
|
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' }
|
|
80
|
+
|
|
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
|
|
57
97
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
});
|
|
60
108
|
|
|
61
|
-
|
|
62
|
-
await store.delete("users", "alice");
|
|
109
|
+
await store.loadAll();
|
|
63
110
|
```
|
|
64
111
|
|
|
65
112
|
## API Reference
|
|
66
113
|
|
|
67
|
-
### `new EncryptedStore(db, password, listener)`
|
|
114
|
+
### `new EncryptedStore(db, password, listener?)`
|
|
68
115
|
|
|
69
116
|
Creates an encrypted store.
|
|
70
117
|
|
|
71
|
-
- `db`:
|
|
118
|
+
- `db`: PouchDB database instance
|
|
72
119
|
- `password`: Encryption password (string)
|
|
73
|
-
- `listener`:
|
|
74
|
-
- `docsAdded(events: TableEvent[])`: Fired when new documents are added
|
|
75
|
-
- `docsChanged(events: TableEvent[])`: Fired when documents are updated
|
|
76
|
-
- `docsDeleted(events: TableEvent[])`: Fired when documents are deleted
|
|
77
|
-
- `decryptionError?(events: DecryptionErrorEvent[])`: Optional. Fired when documents fail to decrypt
|
|
78
|
-
|
|
79
|
-
Each `TableEvent` has:
|
|
120
|
+
- `listener`: Optional object with callbacks
|
|
80
121
|
|
|
81
|
-
|
|
82
|
-
- `docs`: Array of documents with that type
|
|
122
|
+
### Listener Callbacks
|
|
83
123
|
|
|
84
|
-
|
|
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
|
+
```
|
|
85
133
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
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
|
|
89
139
|
|
|
90
140
|
### `await store.loadAll()`
|
|
91
141
|
|
|
92
|
-
Loads all existing documents and
|
|
142
|
+
Loads all existing documents and starts change detection. Call this once after creating the store.
|
|
93
143
|
|
|
94
|
-
### `await store.put(
|
|
144
|
+
### `await store.put(table, doc)`
|
|
95
145
|
|
|
96
146
|
Creates or updates a document.
|
|
97
147
|
|
|
98
|
-
- `
|
|
99
|
-
- `doc`: Document object with `_id` field (
|
|
148
|
+
- `table`: Document type (e.g., "expenses", "tasks")
|
|
149
|
+
- `doc`: Document object with optional `_id` field (generated if missing)
|
|
100
150
|
|
|
101
|
-
Returns the document.
|
|
151
|
+
Returns the document with `_table` field added.
|
|
102
152
|
|
|
103
|
-
### `await store.get(
|
|
153
|
+
### `await store.get(table, id)`
|
|
104
154
|
|
|
105
|
-
|
|
155
|
+
Gets a document by table and ID. Returns `null` if not found.
|
|
106
156
|
|
|
107
|
-
### `await store.delete(
|
|
157
|
+
### `await store.delete(table, id)`
|
|
108
158
|
|
|
109
|
-
Deletes a document by
|
|
159
|
+
Deletes a document by table and ID.
|
|
110
160
|
|
|
111
|
-
|
|
161
|
+
### `await store.getAll(table?)`
|
|
112
162
|
|
|
113
|
-
|
|
163
|
+
Gets all documents, optionally filtered by table.
|
|
114
164
|
|
|
115
165
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// npm install @fireproof/netlify
|
|
120
|
-
|
|
121
|
-
import { connect } from "@fireproof/partykit";
|
|
122
|
-
// or
|
|
123
|
-
// import { connect } from "@fireproof/netlify";
|
|
124
|
-
|
|
125
|
-
// Connect using the connector function
|
|
126
|
-
await store.connectRemote(connect, {
|
|
127
|
-
namespace: "my-app",
|
|
128
|
-
host: "http://localhost:1999", // or your server URL
|
|
129
|
-
});
|
|
166
|
+
const allExpenses = await store.getAll('expenses');
|
|
167
|
+
const allDocs = await store.getAll();
|
|
168
|
+
```
|
|
130
169
|
|
|
131
|
-
|
|
132
|
-
store.disconnectRemote();
|
|
170
|
+
### `await store.connectRemote(options)`
|
|
133
171
|
|
|
134
|
-
|
|
172
|
+
Connects to a remote CouchDB server for sync.
|
|
173
|
+
|
|
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
|
+
}
|
|
135
180
|
```
|
|
136
181
|
|
|
137
|
-
|
|
182
|
+
### `store.disconnectRemote()`
|
|
138
183
|
|
|
139
|
-
|
|
184
|
+
Disconnects from remote sync.
|
|
140
185
|
|
|
141
|
-
|
|
186
|
+
### `await store.getConflictInfo(table, id)`
|
|
142
187
|
|
|
143
|
-
|
|
144
|
-
2. **Monitor decryption errors** - If data arrives from another client encrypted with a different password or corrupted, you'll receive `decryptionError` events
|
|
145
|
-
3. **Use the decryptionError callback** to verify connectivity and detect synchronization issues
|
|
188
|
+
Check if a document has conflicts without triggering the callback. Returns `ConflictInfo` if conflicts exist, or `null` if none.
|
|
146
189
|
|
|
147
190
|
```typescript
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
167
224
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
}
|
|
171
274
|
});
|
|
172
275
|
```
|
|
173
276
|
|
|
174
|
-
|
|
277
|
+
## Deployment Options
|
|
175
278
|
|
|
176
|
-
|
|
279
|
+
### Free Tier Options
|
|
177
280
|
|
|
178
|
-
|
|
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
|
+
```
|
|
179
287
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
```
|
|
184
293
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
-
|
|
189
|
-
|
|
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
|
+
```
|
|
190
300
|
|
|
191
|
-
|
|
301
|
+
### Backup Strategy
|
|
192
302
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
```
|
|
199
312
|
|
|
200
313
|
## Example: React Integration
|
|
201
314
|
|
|
202
315
|
```typescript
|
|
203
|
-
import { useState, useEffect } from
|
|
204
|
-
import
|
|
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';
|
|
205
320
|
|
|
206
321
|
function useEncryptedStore(dbName: string, password: string) {
|
|
207
|
-
const [
|
|
322
|
+
const [expenses, setExpenses] = useState<Map<string, any>>(new Map());
|
|
208
323
|
const [store, setStore] = useState<EncryptedStore | null>(null);
|
|
209
324
|
|
|
210
325
|
useEffect(() => {
|
|
211
|
-
const db =
|
|
326
|
+
const db = new PouchDB(dbName);
|
|
212
327
|
const encryptedStore = new EncryptedStore(db, password, {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
});
|
|
223
|
-
},
|
|
224
|
-
docsChanged: (events) => {
|
|
225
|
-
events.forEach(({ table, docs }) => {
|
|
226
|
-
if (table === "users") {
|
|
227
|
-
setUsers((prev) => {
|
|
228
|
-
const next = new Map(prev);
|
|
229
|
-
docs.forEach((doc) => next.set(doc._id, doc));
|
|
230
|
-
return next;
|
|
231
|
-
});
|
|
232
|
-
}
|
|
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;
|
|
233
337
|
});
|
|
234
338
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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;
|
|
244
348
|
});
|
|
245
349
|
},
|
|
246
|
-
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
251
356
|
});
|
|
252
|
-
}
|
|
357
|
+
}
|
|
253
358
|
});
|
|
254
359
|
|
|
255
360
|
encryptedStore.loadAll();
|
|
256
361
|
setStore(encryptedStore);
|
|
362
|
+
|
|
363
|
+
return () => {
|
|
364
|
+
encryptedStore.disconnectRemote();
|
|
365
|
+
};
|
|
257
366
|
}, [dbName, password]);
|
|
258
367
|
|
|
259
|
-
return {
|
|
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
|
+
);
|
|
260
393
|
}
|
|
261
394
|
```
|
|
262
395
|
|
|
263
396
|
## TypeScript Types
|
|
264
397
|
|
|
265
398
|
```typescript
|
|
266
|
-
interface
|
|
399
|
+
interface Doc {
|
|
400
|
+
_id: string;
|
|
401
|
+
_table: string;
|
|
402
|
+
[key: string]: any;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
interface ConflictInfo {
|
|
406
|
+
docId: string;
|
|
267
407
|
table: string;
|
|
268
|
-
|
|
408
|
+
id: string;
|
|
409
|
+
currentRev: string;
|
|
410
|
+
conflictRevs: string[];
|
|
411
|
+
winner: Doc;
|
|
412
|
+
losers: Doc[];
|
|
413
|
+
}
|
|
414
|
+
|
|
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
|
+
};
|
|
269
423
|
}
|
|
270
424
|
|
|
271
425
|
interface DecryptionErrorEvent {
|
|
272
426
|
docId: string;
|
|
273
427
|
error: Error;
|
|
274
|
-
|
|
428
|
+
rawDoc: any;
|
|
275
429
|
}
|
|
276
430
|
|
|
277
|
-
interface
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
decryptionError?: (events: DecryptionErrorEvent[]) => void;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
interface Doc {
|
|
285
|
-
_id: string;
|
|
286
|
-
[key: string]: any;
|
|
431
|
+
interface RemoteOptions {
|
|
432
|
+
url: string;
|
|
433
|
+
live?: boolean;
|
|
434
|
+
retry?: boolean;
|
|
287
435
|
}
|
|
288
436
|
```
|
|
289
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
|
+
|
|
290
469
|
## License
|
|
291
470
|
|
|
292
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
|