@mrbelloc/encrypted-store 0.2.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/LICENSE.md ADDED
@@ -0,0 +1,11 @@
1
+ # Released under MIT License
2
+
3
+ Copyright (c) 2013 Mark Otto.
4
+
5
+ Copyright (c) 2017 Andrew Fong.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Encrypted Store
2
+
3
+ Client-side encrypted storage with change detection for PWAs. Built on [Fireproof](https://use-fireproof.com).
4
+
5
+ **For small data that can live in memory** - Designed for PWAs that manage datasets that fit comfortably in browser memory.
6
+
7
+ ## Features
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
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @mrbelloc/encrypted-store
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```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
+ });
33
+ },
34
+ docsChanged: (events) => {
35
+ events.forEach(({ table, docs }) => {
36
+ console.log(`Updated ${table}:`, docs);
37
+ });
38
+ },
39
+ docsDeleted: (events) => {
40
+ events.forEach(({ table, docs }) => {
41
+ console.log(`Deleted ${table}:`, docs);
42
+ });
43
+ },
44
+ });
45
+
46
+ // Load existing data
47
+ await store.loadAll();
48
+
49
+ // Create/update documents
50
+ await store.put("users", { _id: "alice", name: "Alice", age: 30 });
51
+
52
+ // Get documents
53
+ const user = await store.get("users", "alice");
54
+
55
+ // Delete documents
56
+ await store.delete("users", "alice");
57
+ ```
58
+
59
+ ## API Reference
60
+
61
+ ### `new EncryptedStore(db, password, listener)`
62
+
63
+ Creates an encrypted store.
64
+
65
+ - `db`: Fireproof database instance
66
+ - `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
71
+
72
+ Each `TableEvent` has:
73
+
74
+ - `table`: Document type (e.g., "users", "transactions")
75
+ - `docs`: Array of documents with that type
76
+
77
+ ### `await store.loadAll()`
78
+
79
+ Loads all existing documents and sets up change detection. Call this once after creating the store.
80
+
81
+ ### `await store.put(type, doc)`
82
+
83
+ Creates or updates a document.
84
+
85
+ - `type`: Document type / table name (string)
86
+ - `doc`: Document object with `_id` field (will be generated if missing)
87
+
88
+ Returns the document.
89
+
90
+ ### `await store.get(type, id)`
91
+
92
+ Retrieves a document by type and ID. Returns `null` if not found.
93
+
94
+ ### `await store.delete(type, id)`
95
+
96
+ Deletes a document by type and ID.
97
+
98
+ ## Remote Sync
99
+
100
+ Sync encrypted data across devices with any Fireproof connector:
101
+
102
+ ```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
+ });
117
+
118
+ // Disconnect
119
+ store.disconnectRemote();
120
+
121
+ // Works with any connector that follows the Fireproof connector interface
122
+ ```
123
+
124
+ **Note:** Remote servers only see encrypted blobs - they cannot read your data.
125
+
126
+ ## How It Works
127
+
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)
133
+
134
+ ## Example: React Integration
135
+
136
+ ```typescript
137
+ import { useState, useEffect } from "react";
138
+ import { EncryptedStore, fireproof } from "@mrbelloc/encrypted-store";
139
+
140
+ function useEncryptedStore(dbName: string, password: string) {
141
+ const [users, setUsers] = useState<Map<string, any>>(new Map());
142
+ const [store, setStore] = useState<EncryptedStore | null>(null);
143
+
144
+ useEffect(() => {
145
+ const db = fireproof(dbName);
146
+ 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
+ }
156
+ });
157
+ },
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
+ }
167
+ });
168
+ },
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
+ }
178
+ });
179
+ },
180
+ });
181
+
182
+ encryptedStore.loadAll();
183
+ setStore(encryptedStore);
184
+ }, [dbName, password]);
185
+
186
+ return { users: Array.from(users.values()), store };
187
+ }
188
+ ```
189
+
190
+ ## TypeScript Types
191
+
192
+ ```typescript
193
+ interface TableEvent {
194
+ table: string;
195
+ docs: Doc[];
196
+ }
197
+
198
+ interface StoreListener {
199
+ docsAdded: (events: TableEvent[]) => void;
200
+ docsChanged: (events: TableEvent[]) => void;
201
+ docsDeleted: (events: TableEvent[]) => void;
202
+ }
203
+
204
+ interface Doc {
205
+ _id: string;
206
+ [key: string]: any;
207
+ }
208
+ ```
209
+
210
+ ## License
211
+
212
+ MIT
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Encrypted storage with change detection for small datasets
3
+ * Wraps Fireproof with AES-256-GCM encryption + real-time event system
4
+ */
5
+ interface Doc {
6
+ _id: string;
7
+ [key: string]: any;
8
+ }
9
+ export interface TableEvent {
10
+ table: string;
11
+ docs: Doc[];
12
+ }
13
+ export interface StoreListener {
14
+ docsAdded: (events: TableEvent[]) => void;
15
+ docsChanged: (events: TableEvent[]) => void;
16
+ docsDeleted: (events: TableEvent[]) => void;
17
+ }
18
+ export interface RemoteConnectOptions {
19
+ namespace: string;
20
+ host: string;
21
+ }
22
+ export interface SyncConnection {
23
+ ready?: Promise<void>;
24
+ disconnect?: () => void;
25
+ }
26
+ export type ConnectorFunction = (db: FireproofDb, namespace: string, host: string) => SyncConnection;
27
+ export interface FireproofDb {
28
+ put(doc: any): Promise<{
29
+ id: string;
30
+ rev?: string;
31
+ }>;
32
+ get(id: string): Promise<any>;
33
+ query(field: string, options?: {
34
+ limit?: number;
35
+ descending?: boolean;
36
+ }): Promise<{
37
+ docs?: any[];
38
+ rows: Array<{
39
+ key: string;
40
+ doc?: any;
41
+ value?: any;
42
+ }>;
43
+ }>;
44
+ subscribe(callback: (changes: any[]) => void, remote?: boolean): void;
45
+ del(id: string, rev?: string): Promise<any>;
46
+ }
47
+ /**
48
+ * EncryptedStore class
49
+ *
50
+ * Main entry point for encrypted storage with change detection
51
+ */
52
+ export declare class EncryptedStore {
53
+ private db;
54
+ private encryptionHelper;
55
+ private listener;
56
+ private knownIds;
57
+ private fullIdMap;
58
+ private isSubscribed;
59
+ private connection;
60
+ constructor(db: FireproofDb, password: string, listener: StoreListener);
61
+ /** Load all documents and set up change detection (call once after creating store) */
62
+ loadAll(): Promise<void>;
63
+ /** Create or update a document */
64
+ put(type: string, doc: any): Promise<Doc>;
65
+ /** Get a document (returns null if not found) */
66
+ get(type: string, id: string): Promise<Doc | null>;
67
+ /** Delete a document */
68
+ delete(type: string, id: string): Promise<void>;
69
+ /** Connect to remote sync with any Fireproof connector */
70
+ connectRemote(connector: ConnectorFunction, options: RemoteConnectOptions): Promise<void>;
71
+ /** Disconnect from remote sync */
72
+ disconnectRemote(): void;
73
+ /** Read all encrypted documents (without decrypting) */
74
+ private readAllEncrypted;
75
+ /**
76
+ * Process Fireproof changes: { _id, d? }
77
+ * With d = create/update, without d = deletion
78
+ */
79
+ private handleChange;
80
+ private decryptFromEncryptedData;
81
+ private encryptDoc;
82
+ private decryptDoc;
83
+ private parseFullId;
84
+ private generateId;
85
+ private groupByTable;
86
+ private groupDeletedByTable;
87
+ }
88
+ export {};
89
+ //# sourceMappingURL=encryptedStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptedStore.d.ts","sourceRoot":"","sources":["../src/encryptedStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Encrypted storage with change detection for small datasets
3
+ * Wraps Fireproof with AES-256-GCM encryption + real-time event system
4
+ */
5
+ import { EncryptionHelper } from "./encryption";
6
+ /**
7
+ * EncryptedStore class
8
+ *
9
+ * Main entry point for encrypted storage with change detection
10
+ */
11
+ export class EncryptedStore {
12
+ constructor(db, password, listener) {
13
+ this.knownIds = new Set(); // Set of known document IDs (stripped, e.g., "alice")
14
+ this.fullIdMap = new Map(); // stripped id -> full id with table
15
+ this.isSubscribed = false;
16
+ this.connection = null;
17
+ this.db = db;
18
+ this.encryptionHelper = new EncryptionHelper(password);
19
+ this.listener = listener;
20
+ }
21
+ /** Load all documents and set up change detection (call once after creating store) */
22
+ async loadAll() {
23
+ const { encryptedMap, fullIdMap } = await this.readAllEncrypted();
24
+ this.fullIdMap = fullIdMap;
25
+ // Build initial set of known IDs
26
+ this.knownIds = new Set(encryptedMap.keys());
27
+ // Decrypt all documents for initial docsAdded event
28
+ const docs = [];
29
+ for (const [id, encryptedData] of encryptedMap) {
30
+ try {
31
+ const decrypted = await this.decryptFromEncryptedData(encryptedData, id);
32
+ docs.push(decrypted);
33
+ }
34
+ catch (error) {
35
+ // Skip documents we can't decrypt
36
+ }
37
+ }
38
+ // Fire initial docsAdded for everything, grouped by table
39
+ if (docs.length > 0) {
40
+ const events = this.groupByTable(docs, fullIdMap);
41
+ this.listener.docsAdded(events);
42
+ }
43
+ // Set up subscribe (only once)
44
+ if (!this.isSubscribed) {
45
+ this.db.subscribe((changes) => {
46
+ this.handleChange(changes).catch((err) => {
47
+ console.error("EncryptedStore: Error handling change:", err);
48
+ });
49
+ }, true); // Include remote changes
50
+ this.isSubscribed = true;
51
+ }
52
+ }
53
+ /** Create or update a document */
54
+ async put(type, doc) {
55
+ // Generate ID if not provided
56
+ if (!doc._id) {
57
+ doc._id = this.generateId();
58
+ }
59
+ // Build full ID with type prefix
60
+ const fullId = `${type}_${doc._id}`;
61
+ // Encrypt the document
62
+ const encryptedDoc = await this.encryptDoc(doc, fullId);
63
+ // Store in Fireproof
64
+ await this.db.put(encryptedDoc);
65
+ // Fireproof's subscribe will trigger handleChange()
66
+ // which will reload, compute diff, and fire events
67
+ return doc;
68
+ }
69
+ /** Get a document (returns null if not found) */
70
+ async get(type, id) {
71
+ const fullId = `${type}_${id}`;
72
+ try {
73
+ const encryptedDoc = await this.db.get(fullId);
74
+ return await this.decryptDoc(encryptedDoc, id);
75
+ }
76
+ catch (error) {
77
+ // Document not found
78
+ return null;
79
+ }
80
+ }
81
+ /** Delete a document */
82
+ async delete(type, id) {
83
+ // Build full ID with type prefix
84
+ const fullId = `${type}_${id}`;
85
+ // Delete from Fireproof
86
+ await this.db.del(fullId);
87
+ // Fireproof's subscribe will trigger handleChange()
88
+ // which will detect the deletion and fire docsDeleted event
89
+ }
90
+ /** Connect to remote sync with any Fireproof connector */
91
+ async connectRemote(connector, options) {
92
+ this.disconnectRemote();
93
+ try {
94
+ console.log(`[EncryptedStore] Connecting to ${options.host} with namespace: ${options.namespace}`);
95
+ // Call the connector function
96
+ const connection = connector(this.db, options.namespace, options.host);
97
+ // Store connection
98
+ this.connection = {
99
+ ready: connection.ready || Promise.resolve(),
100
+ disconnect: connection.disconnect,
101
+ };
102
+ // Wait for connection to be ready
103
+ await this.connection.ready;
104
+ console.log(`[EncryptedStore] βœ“ Connected to remote`);
105
+ }
106
+ catch (error) {
107
+ console.error(`[EncryptedStore] βœ— Failed to connect:`, error);
108
+ this.connection = null;
109
+ throw error;
110
+ }
111
+ }
112
+ /** Disconnect from remote sync */
113
+ disconnectRemote() {
114
+ if (this.connection && this.connection.disconnect) {
115
+ this.connection.disconnect();
116
+ console.log("[EncryptedStore] Disconnected from remote");
117
+ }
118
+ this.connection = null;
119
+ }
120
+ /** Read all encrypted documents (without decrypting) */
121
+ async readAllEncrypted() {
122
+ const result = await this.db.query("_id", { descending: false });
123
+ const encryptedMap = new Map();
124
+ const fullIdMap = new Map();
125
+ // Get docs from either result.docs or result.rows
126
+ const allDocs = result.docs || result.rows.map((row) => row.doc).filter(Boolean);
127
+ for (const doc of allDocs) {
128
+ try {
129
+ const { type, id } = this.parseFullId(doc._id);
130
+ encryptedMap.set(id, doc.d); // Store encrypted data
131
+ fullIdMap.set(id, doc._id); // Map "alice" -> "users_alice"
132
+ }
133
+ catch (error) {
134
+ // Skip documents with invalid ID format
135
+ }
136
+ }
137
+ return { encryptedMap, fullIdMap };
138
+ }
139
+ /**
140
+ * Process Fireproof changes: { _id, d? }
141
+ * With d = create/update, without d = deletion
142
+ */
143
+ async handleChange(changes) {
144
+ const newDocs = [];
145
+ const changedDocs = [];
146
+ const deletedDocs = [];
147
+ for (const change of changes) {
148
+ if (change.d) {
149
+ // Has encrypted data - it's a create or update
150
+ try {
151
+ const { type, id } = this.parseFullId(change._id);
152
+ // Decrypt the document
153
+ const decrypted = await this.decryptFromEncryptedData(change.d, id);
154
+ // Check if it's new or changed
155
+ if (this.knownIds.has(id)) {
156
+ changedDocs.push(decrypted);
157
+ }
158
+ else {
159
+ newDocs.push(decrypted);
160
+ this.knownIds.add(id);
161
+ this.fullIdMap.set(id, change._id); // Update fullIdMap for new docs
162
+ }
163
+ }
164
+ catch (error) {
165
+ // Skip documents we can't decrypt or parse
166
+ }
167
+ }
168
+ else {
169
+ // No encrypted data - it's a deletion
170
+ try {
171
+ const { type, id } = this.parseFullId(change._id);
172
+ if (this.knownIds.has(id)) {
173
+ deletedDocs.push({ _id: id });
174
+ this.knownIds.delete(id);
175
+ // Keep fullIdMap entry for the deletion event, remove after
176
+ }
177
+ }
178
+ catch (error) {
179
+ // Skip invalid IDs
180
+ }
181
+ }
182
+ }
183
+ // Fire events grouped by table
184
+ if (newDocs.length > 0) {
185
+ const events = this.groupByTable(newDocs, this.fullIdMap);
186
+ this.listener.docsAdded(events);
187
+ }
188
+ if (changedDocs.length > 0) {
189
+ const events = this.groupByTable(changedDocs, this.fullIdMap);
190
+ this.listener.docsChanged(events);
191
+ }
192
+ if (deletedDocs.length > 0) {
193
+ const events = this.groupDeletedByTable(deletedDocs);
194
+ this.listener.docsDeleted(events);
195
+ // Clean up fullIdMap entries for deleted docs
196
+ for (const doc of deletedDocs) {
197
+ this.fullIdMap.delete(doc._id);
198
+ }
199
+ }
200
+ }
201
+ async decryptFromEncryptedData(encryptedData, id) {
202
+ // Decrypt the data
203
+ const decryptedJson = await this.encryptionHelper.decrypt(encryptedData);
204
+ const decryptedData = JSON.parse(decryptedJson);
205
+ // Build final document
206
+ const doc = {
207
+ _id: id,
208
+ ...decryptedData,
209
+ };
210
+ return doc;
211
+ }
212
+ async encryptDoc(doc, fullId) {
213
+ // Extract fields to encrypt (everything except _id)
214
+ const dataToEncrypt = {};
215
+ for (const [key, value] of Object.entries(doc)) {
216
+ if (!key.startsWith("_")) {
217
+ dataToEncrypt[key] = value;
218
+ }
219
+ }
220
+ // Encrypt as JSON
221
+ const encrypted = await this.encryptionHelper.encrypt(JSON.stringify(dataToEncrypt));
222
+ // Build encrypted doc
223
+ const encryptedDoc = {
224
+ _id: fullId,
225
+ d: encrypted,
226
+ };
227
+ return encryptedDoc;
228
+ }
229
+ async decryptDoc(encryptedDoc, id) {
230
+ // Decrypt the 'd' field
231
+ const decryptedJson = await this.encryptionHelper.decrypt(encryptedDoc.d);
232
+ const decryptedData = JSON.parse(decryptedJson);
233
+ // Build final document
234
+ const doc = {
235
+ _id: id,
236
+ ...decryptedData,
237
+ };
238
+ return doc;
239
+ }
240
+ parseFullId(fullId) {
241
+ const firstUnderscore = fullId.indexOf("_");
242
+ if (firstUnderscore === -1) {
243
+ throw new Error(`Invalid document ID format: ${fullId}`);
244
+ }
245
+ return {
246
+ type: fullId.substring(0, firstUnderscore),
247
+ id: fullId.substring(firstUnderscore + 1),
248
+ };
249
+ }
250
+ generateId() {
251
+ // Use crypto.randomUUID if available, otherwise fallback
252
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
253
+ return crypto.randomUUID();
254
+ }
255
+ // Fallback for environments without crypto.randomUUID
256
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
257
+ }
258
+ groupByTable(docs, fullIdMap) {
259
+ const grouped = new Map();
260
+ for (const doc of docs) {
261
+ const fullId = fullIdMap.get(doc._id);
262
+ if (fullId) {
263
+ const { type } = this.parseFullId(fullId);
264
+ if (!grouped.has(type)) {
265
+ grouped.set(type, []);
266
+ }
267
+ grouped.get(type).push(doc);
268
+ }
269
+ }
270
+ return Array.from(grouped.entries()).map(([table, docs]) => ({
271
+ table,
272
+ docs,
273
+ }));
274
+ }
275
+ groupDeletedByTable(deletedDocs) {
276
+ const grouped = new Map();
277
+ for (const doc of deletedDocs) {
278
+ const fullId = this.fullIdMap.get(doc._id);
279
+ if (fullId) {
280
+ const { type } = this.parseFullId(fullId);
281
+ if (!grouped.has(type)) {
282
+ grouped.set(type, []);
283
+ }
284
+ grouped.get(type).push(doc);
285
+ }
286
+ }
287
+ return Array.from(grouped.entries()).map(([table, docs]) => ({
288
+ table,
289
+ docs: docs, // Cast since deletedDocs is simpler structure
290
+ }));
291
+ }
292
+ }
293
+ //# sourceMappingURL=encryptedStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryptedStore.js","sourceRoot":"","sources":["../src/encryptedStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Interface defining the subset of the Web Crypto API that we use
3
+ */
4
+ interface CryptoInterface {
5
+ subtle: {
6
+ digest(algorithm: string, data: BufferSource): Promise<ArrayBuffer>;
7
+ importKey(format: string, keyData: BufferSource, algorithm: string | object, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>;
8
+ encrypt(algorithm: string | object, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
9
+ decrypt(algorithm: string | object, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>;
10
+ };
11
+ getRandomValues<T extends ArrayBufferView>(array: T): T;
12
+ }
13
+ declare class DecryptionError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ declare class EncryptionHelper {
17
+ private keyPromise;
18
+ private readonly passphrase;
19
+ private readonly crypto;
20
+ /**
21
+ * @param passphrase - The passphrase used for encryption/decryption
22
+ * @param crypto - Optional crypto implementation. If not provided, uses the global crypto object.
23
+ * This parameter is primarily for testing purposes.
24
+ */
25
+ constructor(passphrase: string, crypto?: CryptoInterface);
26
+ private getKey;
27
+ private static fromHexString;
28
+ private static toHexString;
29
+ encrypt(data: string): Promise<string>;
30
+ decrypt(data: string): Promise<string>;
31
+ }
32
+ export { EncryptionHelper, DecryptionError };
33
+ export type { CryptoInterface };
34
+ //# sourceMappingURL=encryption.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,UAAU,eAAe;IACvB,MAAM,EAAE;QACN,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;QACpE,SAAS,CACP,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,EACrB,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,WAAW,EAAE,OAAO,EACpB,SAAS,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,SAAS,CAAC,CAAC;QACtB,OAAO,CACL,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,WAAW,CAAC,CAAC;QACxB,OAAO,CACL,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,GAAG,EAAE,SAAS,EACd,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,WAAW,CAAC,CAAC;KACzB,CAAC;IACF,eAAe,CAAC,CAAC,SAAS,eAAe,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;CACzD;AAED,cAAM,eAAgB,SAAQ,KAAK;gBACrB,OAAO,EAAE,MAAM;CAK5B;AAED,cAAM,gBAAgB;IACpB,OAAO,CAAC,UAAU,CAAmC;IACrD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IAEzC;;;;OAIG;gBACS,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,eAAe;YAO1C,MAAM;IAkBpB,OAAO,CAAC,MAAM,CAAC,aAAa;IAM5B,OAAO,CAAC,MAAM,CAAC,WAAW;IAOpB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBtC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAqB7C;AAED,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,CAAC;AAC7C,YAAY,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,66 @@
1
+ class DecryptionError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "DecryptionError";
5
+ Object.setPrototypeOf(this, DecryptionError.prototype);
6
+ }
7
+ }
8
+ class EncryptionHelper {
9
+ /**
10
+ * @param passphrase - The passphrase used for encryption/decryption
11
+ * @param crypto - Optional crypto implementation. If not provided, uses the global crypto object.
12
+ * This parameter is primarily for testing purposes.
13
+ */
14
+ constructor(passphrase, crypto) {
15
+ this.keyPromise = null;
16
+ this.passphrase = passphrase;
17
+ this.crypto =
18
+ crypto ||
19
+ (typeof window !== "undefined" ? window.crypto : global.crypto);
20
+ }
21
+ async getKey() {
22
+ if (this.keyPromise) {
23
+ return this.keyPromise;
24
+ }
25
+ const enc = new TextEncoder();
26
+ const pwUtf8 = enc.encode(this.passphrase);
27
+ const pwHash = await this.crypto.subtle.digest("SHA-256", pwUtf8);
28
+ this.keyPromise = this.crypto.subtle.importKey("raw", pwHash, "AES-GCM", true, ["encrypt", "decrypt"]);
29
+ return this.keyPromise;
30
+ }
31
+ static fromHexString(hexString) {
32
+ return new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
33
+ }
34
+ static toHexString(bytes) {
35
+ return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");
36
+ }
37
+ async encrypt(data) {
38
+ const enc = new TextEncoder();
39
+ const key = await this.getKey();
40
+ const encoded = enc.encode(data);
41
+ const iv = this.crypto.getRandomValues(new Uint8Array(12));
42
+ const ciphertext = await this.crypto.subtle.encrypt({
43
+ name: "AES-GCM",
44
+ iv: iv,
45
+ }, key, encoded);
46
+ return `${EncryptionHelper.toHexString(iv)}|${EncryptionHelper.toHexString(new Uint8Array(ciphertext))}`;
47
+ }
48
+ async decrypt(data) {
49
+ const key = await this.getKey();
50
+ const [iv, ciphertext] = data
51
+ .split("|")
52
+ .map((s) => EncryptionHelper.fromHexString(s));
53
+ try {
54
+ const decrypted = await this.crypto.subtle.decrypt({
55
+ name: "AES-GCM",
56
+ iv: iv,
57
+ }, key, ciphertext);
58
+ return new TextDecoder().decode(decrypted);
59
+ }
60
+ catch (e) {
61
+ throw new DecryptionError(`Could not decrypt: ${e instanceof Error ? e.message : String(e)}`);
62
+ }
63
+ }
64
+ }
65
+ export { EncryptionHelper, DecryptionError };
66
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Encrypted storage with change detection for small datasets in PWAs
3
+ * Built on Fireproof with AES-256-GCM encryption
4
+ * @packageDocumentation
5
+ */
6
+ export { EncryptedStore } from "./encryptedStore";
7
+ export type { StoreListener, FireproofDb, TableEvent, ConnectorFunction, RemoteConnectOptions, SyncConnection, } from "./encryptedStore";
8
+ export { EncryptionHelper, DecryptionError } from "./encryption";
9
+ export type { CryptoInterface } from "./encryption";
10
+ export declare const VERSION = "0.2.0";
11
+ export { fireproof } from "use-fireproof";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EACV,aAAa,EACb,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACjE,YAAY,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,OAAO,UAAU,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Encrypted storage with change detection for small datasets in PWAs
3
+ * Built on Fireproof with AES-256-GCM encryption
4
+ * @packageDocumentation
5
+ */
6
+ export { EncryptedStore } from "./encryptedStore";
7
+ export { EncryptionHelper, DecryptionError } from "./encryption";
8
+ export const VERSION = "0.2.0";
9
+ export { fireproof } from "use-fireproof";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAUlD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGjE,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@mrbelloc/encrypted-store",
3
+ "version": "0.2.0",
4
+ "description": "Client-side encrypted storage with change detection for small datasets in PWAs",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
10
+ "test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
11
+ "build": "tsc --project tsconfig.build.json",
12
+ "typecheck": "tsc --noEmit",
13
+ "clean": "rm -rf dist",
14
+ "format": "prettier --write \"src/**/*.ts\"",
15
+ "format:check": "prettier --check \"src/**/*.ts\"",
16
+ "prepare": "husky"
17
+ },
18
+ "keywords": [
19
+ "encryption",
20
+ "storage",
21
+ "fireproof",
22
+ "pwa"
23
+ ],
24
+ "author": "Pablo de LeΓ³n Belloc <pablolb@gmail.com>",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/pablolb/encrypted-store.git"
29
+ },
30
+ "homepage": "https://github.com/pablolb/encrypted-store#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/pablolb/encrypted-store/issues"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "!dist/**/__tests__",
37
+ "README.md"
38
+ ],
39
+ "devDependencies": {
40
+ "@fireproof/partykit": "^0.19.118",
41
+ "@types/jest": "^29.5.11",
42
+ "@types/node": "^20.10.6",
43
+ "husky": "^9.1.7",
44
+ "jest": "^29.7.0",
45
+ "jest-environment-jsdom": "^30.2.0",
46
+ "lint-staged": "^16.2.7",
47
+ "partykit": "^0.0.115",
48
+ "partysocket": "^1.1.10",
49
+ "prettier": "^3.7.4",
50
+ "ts-jest": "^29.1.1",
51
+ "typescript": "^5.3.3"
52
+ },
53
+ "dependencies": {
54
+ "multiformats": "^13.4.2",
55
+ "use-fireproof": "^0.19.0"
56
+ },
57
+ "lint-staged": {
58
+ "src/**/*.ts": [
59
+ "prettier --write"
60
+ ]
61
+ }
62
+ }