pomegranate-db 0.1.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 +21 -0
- package/NOTICE.md +38 -0
- package/PomegranateDB.podspec +67 -0
- package/README.md +122 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.d.ts +34 -0
- package/dist/adapters/expo-sqlite/ExpoSQLiteDriver.js +155 -0
- package/dist/adapters/expo-sqlite/index.d.ts +2 -0
- package/dist/adapters/expo-sqlite/index.js +6 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +13 -0
- package/dist/adapters/loki/LokiAdapter.d.ts +100 -0
- package/dist/adapters/loki/LokiAdapter.js +144 -0
- package/dist/adapters/loki/index.d.ts +6 -0
- package/dist/adapters/loki/index.js +12 -0
- package/dist/adapters/loki/worker/LokiDispatcher.d.ts +21 -0
- package/dist/adapters/loki/worker/LokiDispatcher.js +63 -0
- package/dist/adapters/loki/worker/LokiExecutor.d.ts +96 -0
- package/dist/adapters/loki/worker/LokiExecutor.js +462 -0
- package/dist/adapters/loki/worker/SynchronousWorker.d.ts +22 -0
- package/dist/adapters/loki/worker/SynchronousWorker.js +76 -0
- package/dist/adapters/loki/worker/loki.worker.d.ts +14 -0
- package/dist/adapters/loki/worker/loki.worker.js +112 -0
- package/dist/adapters/loki/worker/types.d.ts +44 -0
- package/dist/adapters/loki/worker/types.js +11 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.d.ts +55 -0
- package/dist/adapters/native-sqlite/NativeSQLiteDriver.js +145 -0
- package/dist/adapters/native-sqlite/index.d.ts +2 -0
- package/dist/adapters/native-sqlite/index.js +6 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.d.ts +49 -0
- package/dist/adapters/op-sqlite/OpSQLiteDriver.js +140 -0
- package/dist/adapters/op-sqlite/index.d.ts +2 -0
- package/dist/adapters/op-sqlite/index.js +6 -0
- package/dist/adapters/sqlite/SQLiteAdapter.d.ts +70 -0
- package/dist/adapters/sqlite/SQLiteAdapter.js +264 -0
- package/dist/adapters/sqlite/index.d.ts +2 -0
- package/dist/adapters/sqlite/index.js +6 -0
- package/dist/adapters/sqlite/sql.d.ts +35 -0
- package/dist/adapters/sqlite/sql.js +258 -0
- package/dist/adapters/types.d.ts +93 -0
- package/dist/adapters/types.js +9 -0
- package/dist/collection/Collection.d.ts +103 -0
- package/dist/collection/Collection.js +245 -0
- package/dist/collection/index.d.ts +2 -0
- package/dist/collection/index.js +6 -0
- package/dist/database/Database.d.ts +128 -0
- package/dist/database/Database.js +245 -0
- package/dist/database/index.d.ts +2 -0
- package/dist/database/index.js +6 -0
- package/dist/encryption/index.d.ts +62 -0
- package/dist/encryption/index.js +276 -0
- package/dist/encryption/nodeCrypto.d.ts +18 -0
- package/dist/encryption/nodeCrypto.js +25 -0
- package/dist/encryption/nodeCrypto.native.d.ts +13 -0
- package/dist/encryption/nodeCrypto.native.js +26 -0
- package/dist/expo.d.ts +12 -0
- package/dist/expo.js +32 -0
- package/dist/hooks/index.d.ts +115 -0
- package/dist/hooks/index.js +285 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +57 -0
- package/dist/model/Model.d.ts +92 -0
- package/dist/model/Model.js +251 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +7 -0
- package/dist/observable/Subject.d.ts +60 -0
- package/dist/observable/Subject.js +132 -0
- package/dist/observable/index.d.ts +2 -0
- package/dist/observable/index.js +10 -0
- package/dist/query/QueryBuilder.d.ts +51 -0
- package/dist/query/QueryBuilder.js +165 -0
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +7 -0
- package/dist/query/types.d.ts +60 -0
- package/dist/query/types.js +9 -0
- package/dist/schema/builder.d.ts +68 -0
- package/dist/schema/builder.js +168 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/types.d.ts +108 -0
- package/dist/schema/types.js +9 -0
- package/dist/sync/index.d.ts +2 -0
- package/dist/sync/index.js +6 -0
- package/dist/sync/sync.d.ts +15 -0
- package/dist/sync/sync.js +182 -0
- package/dist/sync/types.d.ts +41 -0
- package/dist/sync/types.js +6 -0
- package/dist/utils/index.d.ts +45 -0
- package/dist/utils/index.js +99 -0
- package/expo-plugin/index.d.ts +68 -0
- package/expo-plugin/index.js +83 -0
- package/native/android-jsi/build.gradle +45 -0
- package/native/android-jsi/src/main/AndroidManifest.xml +2 -0
- package/native/android-jsi/src/main/cpp/CMakeLists.txt +73 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.cpp +107 -0
- package/native/android-jsi/src/main/cpp/DatabasePlatformAndroid.h +16 -0
- package/native/android-jsi/src/main/cpp/JSIInstaller.cpp +27 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/JSIInstaller.kt +43 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIModule.kt +39 -0
- package/native/android-jsi/src/main/java/com/pomegranate/jsi/PomegranateJSIPackage.kt +17 -0
- package/native/ios/DatabasePlatformIOS.mm +83 -0
- package/native/ios/PomegranateJSI.h +15 -0
- package/native/ios/PomegranateJSI.mm +59 -0
- package/native/shared/Database.cpp +283 -0
- package/native/shared/Database.h +84 -0
- package/native/shared/Sqlite.cpp +61 -0
- package/native/shared/Sqlite.h +67 -0
- package/native/shared/sqlite3/sqlite3.c +260493 -0
- package/native/shared/sqlite3/sqlite3.h +13583 -0
- package/package.json +127 -0
- package/react-native.config.js +28 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database — the top-level entry point.
|
|
4
|
+
*
|
|
5
|
+
* The Database owns the adapter, manages collections, and provides
|
|
6
|
+
* the `write()` transactional boundary.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const db = new Database({
|
|
10
|
+
* adapter: new SQLiteAdapter({ databaseName: 'app.db' }),
|
|
11
|
+
* models: [Post, User, Comment],
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* await db.write(async () => {
|
|
15
|
+
* await db.get(Post).create({ title: 'Hello' });
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
21
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
22
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
23
|
+
}
|
|
24
|
+
Object.defineProperty(o, k2, desc);
|
|
25
|
+
}) : (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
o[k2] = m[k];
|
|
28
|
+
}));
|
|
29
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
30
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
31
|
+
}) : function(o, v) {
|
|
32
|
+
o["default"] = v;
|
|
33
|
+
});
|
|
34
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
35
|
+
var ownKeys = function(o) {
|
|
36
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
37
|
+
var ar = [];
|
|
38
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
39
|
+
return ar;
|
|
40
|
+
};
|
|
41
|
+
return ownKeys(o);
|
|
42
|
+
};
|
|
43
|
+
return function (mod) {
|
|
44
|
+
if (mod && mod.__esModule) return mod;
|
|
45
|
+
var result = {};
|
|
46
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
47
|
+
__setModuleDefault(result, mod);
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
})();
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
exports.Database = void 0;
|
|
53
|
+
const Collection_1 = require("../collection/Collection");
|
|
54
|
+
const Subject_1 = require("../observable/Subject");
|
|
55
|
+
// ─── Database class ────────────────────────────────────────────────────────
|
|
56
|
+
class Database {
|
|
57
|
+
config;
|
|
58
|
+
_adapter;
|
|
59
|
+
_collections = new Map();
|
|
60
|
+
_modelMap = new Map();
|
|
61
|
+
_initialized = false;
|
|
62
|
+
_isInWriter = false;
|
|
63
|
+
_writeQueue = [];
|
|
64
|
+
_isProcessingQueue = false;
|
|
65
|
+
_events$ = new Subject_1.Subject();
|
|
66
|
+
_schemaVersion;
|
|
67
|
+
constructor(config) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
this._adapter = config.adapter;
|
|
70
|
+
this._schemaVersion = config.schemaVersion ?? 1;
|
|
71
|
+
// Register all model classes
|
|
72
|
+
for (const modelClass of config.models) {
|
|
73
|
+
const schema = modelClass.schema;
|
|
74
|
+
if (!schema) {
|
|
75
|
+
throw new Error('Model class is missing static schema property');
|
|
76
|
+
}
|
|
77
|
+
this._modelMap.set(schema.table, modelClass);
|
|
78
|
+
this._collections.set(schema.table, new Collection_1.Collection(this, modelClass));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ─── Initialization ──────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Initialize the database. Must be called before any operations.
|
|
84
|
+
* Creates tables if they don't exist.
|
|
85
|
+
*/
|
|
86
|
+
async initialize() {
|
|
87
|
+
if (this._initialized)
|
|
88
|
+
return;
|
|
89
|
+
const dbSchema = this._buildDatabaseSchema();
|
|
90
|
+
await this._adapter.initialize(dbSchema);
|
|
91
|
+
this._initialized = true;
|
|
92
|
+
this._events$.next({ type: 'initialized' });
|
|
93
|
+
}
|
|
94
|
+
_buildDatabaseSchema() {
|
|
95
|
+
const tables = Array.from(this._collections.values()).map((collection) => {
|
|
96
|
+
const schema = collection.schema;
|
|
97
|
+
const columns = schema.columns.map((col) => ({
|
|
98
|
+
name: col.columnName,
|
|
99
|
+
type: col.type,
|
|
100
|
+
isOptional: col.isOptional,
|
|
101
|
+
isIndexed: col.isIndexed,
|
|
102
|
+
}));
|
|
103
|
+
return {
|
|
104
|
+
name: schema.table,
|
|
105
|
+
columns,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
version: this._schemaVersion,
|
|
110
|
+
tables,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ─── Collection Access ──────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Get the collection for a model class.
|
|
116
|
+
*/
|
|
117
|
+
get(modelClass) {
|
|
118
|
+
const table = modelClass.schema.table;
|
|
119
|
+
const collection = this._collections.get(table);
|
|
120
|
+
if (!collection) {
|
|
121
|
+
throw new Error(`No collection registered for table "${table}"`);
|
|
122
|
+
}
|
|
123
|
+
return collection;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get a collection by table name.
|
|
127
|
+
*/
|
|
128
|
+
collection(table) {
|
|
129
|
+
const collection = this._collections.get(table);
|
|
130
|
+
if (!collection) {
|
|
131
|
+
throw new Error(`No collection registered for table "${table}"`);
|
|
132
|
+
}
|
|
133
|
+
return collection;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* All registered collections.
|
|
137
|
+
*/
|
|
138
|
+
get collections() {
|
|
139
|
+
return Array.from(this._collections.values());
|
|
140
|
+
}
|
|
141
|
+
// ─── Write Transaction ──────────────────────────────────────────────
|
|
142
|
+
/**
|
|
143
|
+
* Execute a write transaction.
|
|
144
|
+
*
|
|
145
|
+
* All mutations (create, update, delete) must happen inside a write() call.
|
|
146
|
+
* Write calls are serialized — only one runs at a time.
|
|
147
|
+
*/
|
|
148
|
+
async write(fn) {
|
|
149
|
+
this._ensureInitialized();
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
this._writeQueue.push(async () => {
|
|
152
|
+
this._isInWriter = true;
|
|
153
|
+
this._events$.next({ type: 'write_started' });
|
|
154
|
+
try {
|
|
155
|
+
const result = await fn();
|
|
156
|
+
resolve(result);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
reject(error);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
this._isInWriter = false;
|
|
163
|
+
this._events$.next({ type: 'write_completed' });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
this._processWriteQueue();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async _processWriteQueue() {
|
|
170
|
+
if (this._isProcessingQueue)
|
|
171
|
+
return;
|
|
172
|
+
this._isProcessingQueue = true;
|
|
173
|
+
while (this._writeQueue.length > 0) {
|
|
174
|
+
const fn = this._writeQueue.shift();
|
|
175
|
+
await fn();
|
|
176
|
+
}
|
|
177
|
+
this._isProcessingQueue = false;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* @internal Throw if not inside a writer.
|
|
181
|
+
*/
|
|
182
|
+
_ensureInWriter(action) {
|
|
183
|
+
if (!this._isInWriter) {
|
|
184
|
+
throw new Error(`${action} must be called inside db.write(). ` +
|
|
185
|
+
'Wrap your mutation in: await db.write(async () => { ... })');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ─── Batch ──────────────────────────────────────────────────────────
|
|
189
|
+
/**
|
|
190
|
+
* Execute a batch of operations atomically.
|
|
191
|
+
* Must be called inside `db.write()`.
|
|
192
|
+
*/
|
|
193
|
+
async batch(operations) {
|
|
194
|
+
this._ensureInWriter('Database.batch()');
|
|
195
|
+
await this._adapter.batch(operations);
|
|
196
|
+
}
|
|
197
|
+
/** @internal used by Model */
|
|
198
|
+
async _batch(operations) {
|
|
199
|
+
await this._adapter.batch(operations);
|
|
200
|
+
}
|
|
201
|
+
// ─── Sync ──────────────────────────────────────────────────────────
|
|
202
|
+
/**
|
|
203
|
+
* Run a sync cycle.
|
|
204
|
+
* See sync/index.ts for the full implementation.
|
|
205
|
+
*/
|
|
206
|
+
async sync(opts) {
|
|
207
|
+
this._ensureInitialized();
|
|
208
|
+
// Import sync dynamically to keep the module boundary clean
|
|
209
|
+
const { performSync } = await Promise.resolve().then(() => __importStar(require('../sync')));
|
|
210
|
+
await performSync(this, opts);
|
|
211
|
+
}
|
|
212
|
+
// ─── Reset ──────────────────────────────────────────────────────────
|
|
213
|
+
/**
|
|
214
|
+
* Completely reset the database — drops all data.
|
|
215
|
+
*/
|
|
216
|
+
async reset() {
|
|
217
|
+
await this._adapter.reset();
|
|
218
|
+
for (const collection of this._collections.values()) {
|
|
219
|
+
collection._clearCache();
|
|
220
|
+
}
|
|
221
|
+
this._events$.next({ type: 'reset' });
|
|
222
|
+
}
|
|
223
|
+
// ─── Events ──────────────────────────────────────────────────────────
|
|
224
|
+
get events$() {
|
|
225
|
+
return this._events$;
|
|
226
|
+
}
|
|
227
|
+
// ─── Close ──────────────────────────────────────────────────────────
|
|
228
|
+
async close() {
|
|
229
|
+
await this._adapter.close();
|
|
230
|
+
}
|
|
231
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
232
|
+
_ensureInitialized() {
|
|
233
|
+
if (!this._initialized) {
|
|
234
|
+
throw new Error('Database is not initialized. Call `await db.initialize()` first.');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The tables this database manages.
|
|
239
|
+
*/
|
|
240
|
+
get tables() {
|
|
241
|
+
return Array.from(this._collections.keys());
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.Database = Database;
|
|
245
|
+
//# sourceMappingURL=Database.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Database = void 0;
|
|
4
|
+
var Database_1 = require("./Database");
|
|
5
|
+
Object.defineProperty(exports, "Database", { enumerable: true, get: function () { return Database_1.Database; } });
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encryption layer — transparent encrypt/decrypt for storage adapters.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a StorageAdapter, encrypting record values before writes
|
|
5
|
+
* and decrypting after reads. Uses AES-GCM (Web Crypto API or Node crypto).
|
|
6
|
+
*
|
|
7
|
+
* The encryption is transparent to the model/collection layer.
|
|
8
|
+
* Only user-data columns are encrypted; id, _status, _changed are stored in plaintext
|
|
9
|
+
* so the adapter can still query by them.
|
|
10
|
+
*/
|
|
11
|
+
import type { StorageAdapter, Migration } from '../adapters/types';
|
|
12
|
+
import type { QueryDescriptor, SearchDescriptor, BatchOperation } from '../query/types';
|
|
13
|
+
import type { DatabaseSchema, RawRecord } from '../schema/types';
|
|
14
|
+
export declare class EncryptionManager {
|
|
15
|
+
private _key;
|
|
16
|
+
private _keyProvider;
|
|
17
|
+
constructor(keyProvider: () => Promise<Uint8Array>);
|
|
18
|
+
getKey(): Promise<Uint8Array>;
|
|
19
|
+
/** Encrypt a string value */
|
|
20
|
+
encrypt(plaintext: string): Promise<string>;
|
|
21
|
+
/** Decrypt a string value */
|
|
22
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Wraps a StorageAdapter to provide transparent encryption.
|
|
26
|
+
* Only user-data columns are encrypted. Sync metadata and IDs remain in plaintext.
|
|
27
|
+
*/
|
|
28
|
+
export declare class EncryptingAdapter implements StorageAdapter {
|
|
29
|
+
private _inner;
|
|
30
|
+
private _encryption;
|
|
31
|
+
constructor(inner: StorageAdapter, keyProvider: () => Promise<Uint8Array>);
|
|
32
|
+
initialize(schema: DatabaseSchema): Promise<void>;
|
|
33
|
+
find(query: QueryDescriptor): Promise<RawRecord[]>;
|
|
34
|
+
count(query: QueryDescriptor): Promise<number>;
|
|
35
|
+
findById(table: string, id: string): Promise<RawRecord | null>;
|
|
36
|
+
insert(table: string, raw: RawRecord): Promise<void>;
|
|
37
|
+
update(table: string, raw: RawRecord): Promise<void>;
|
|
38
|
+
markAsDeleted(table: string, id: string): Promise<void>;
|
|
39
|
+
destroyPermanently(table: string, id: string): Promise<void>;
|
|
40
|
+
batch(operations: BatchOperation[]): Promise<void>;
|
|
41
|
+
search(descriptor: SearchDescriptor): Promise<{
|
|
42
|
+
records: RawRecord[];
|
|
43
|
+
total: number;
|
|
44
|
+
}>;
|
|
45
|
+
getLocalChanges(tables: string[]): Promise<Record<string, {
|
|
46
|
+
created: RawRecord[];
|
|
47
|
+
updated: RawRecord[];
|
|
48
|
+
deleted: string[];
|
|
49
|
+
}>>;
|
|
50
|
+
applyRemoteChanges(changes: Record<string, {
|
|
51
|
+
created: RawRecord[];
|
|
52
|
+
updated: RawRecord[];
|
|
53
|
+
deleted: string[];
|
|
54
|
+
}>): Promise<void>;
|
|
55
|
+
markAsSynced(table: string, ids: string[]): Promise<void>;
|
|
56
|
+
getSchemaVersion(): Promise<number>;
|
|
57
|
+
migrate(migrations: Migration[]): Promise<void>;
|
|
58
|
+
reset(): Promise<void>;
|
|
59
|
+
close(): Promise<void>;
|
|
60
|
+
private _encryptRecord;
|
|
61
|
+
private _decryptRecord;
|
|
62
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Encryption layer — transparent encrypt/decrypt for storage adapters.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a StorageAdapter, encrypting record values before writes
|
|
6
|
+
* and decrypting after reads. Uses AES-GCM (Web Crypto API or Node crypto).
|
|
7
|
+
*
|
|
8
|
+
* The encryption is transparent to the model/collection layer.
|
|
9
|
+
* Only user-data columns are encrypted; id, _status, _changed are stored in plaintext
|
|
10
|
+
* so the adapter can still query by them.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.EncryptingAdapter = exports.EncryptionManager = void 0;
|
|
14
|
+
const nodeCrypto_1 = require("./nodeCrypto");
|
|
15
|
+
// ─── Columns that are never encrypted ──────────────────────────────────
|
|
16
|
+
const PLAINTEXT_COLUMNS = new Set(['id', '_status', '_changed']);
|
|
17
|
+
// ─── Encryption Manager ───────────────────────────────────────────────
|
|
18
|
+
class EncryptionManager {
|
|
19
|
+
_key = null;
|
|
20
|
+
_keyProvider;
|
|
21
|
+
constructor(keyProvider) {
|
|
22
|
+
this._keyProvider = keyProvider;
|
|
23
|
+
}
|
|
24
|
+
async getKey() {
|
|
25
|
+
if (!this._key) {
|
|
26
|
+
this._key = await this._keyProvider();
|
|
27
|
+
}
|
|
28
|
+
return this._key;
|
|
29
|
+
}
|
|
30
|
+
/** Encrypt a string value */
|
|
31
|
+
async encrypt(plaintext) {
|
|
32
|
+
const key = await this.getKey();
|
|
33
|
+
const iv = await randomBytes(12);
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
const data = encoder.encode(plaintext);
|
|
36
|
+
if (globalThis.crypto !== undefined && globalThis.crypto.subtle) {
|
|
37
|
+
// Web Crypto API
|
|
38
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey('raw', key.buffer, { name: 'AES-GCM' }, false, ['encrypt']);
|
|
39
|
+
const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, cryptoKey, data.buffer);
|
|
40
|
+
return encodeBase64(iv) + ':' + encodeBase64(new Uint8Array(encrypted));
|
|
41
|
+
}
|
|
42
|
+
// Fallback: Node.js crypto
|
|
43
|
+
if (nodeCrypto_1.isNodeCryptoAvailable) {
|
|
44
|
+
try {
|
|
45
|
+
const cipher = (0, nodeCrypto_1.createCipheriv)('aes-256-gcm', key, iv);
|
|
46
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
47
|
+
const tag = cipher.getAuthTag();
|
|
48
|
+
return encodeBase64(iv) + ':' + encodeBase64(encrypted) + ':' + encodeBase64(tag);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new Error('No crypto implementation available for encryption');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error('No crypto implementation available for encryption');
|
|
55
|
+
}
|
|
56
|
+
/** Decrypt a string value */
|
|
57
|
+
async decrypt(ciphertext) {
|
|
58
|
+
const key = await this.getKey();
|
|
59
|
+
const parts = ciphertext.split(':');
|
|
60
|
+
if (globalThis.crypto !== undefined && globalThis.crypto.subtle) {
|
|
61
|
+
// Web Crypto API
|
|
62
|
+
const iv = decodeBase64(parts[0]);
|
|
63
|
+
const data = decodeBase64(parts[1]);
|
|
64
|
+
const cryptoKey = await globalThis.crypto.subtle.importKey('raw', key.buffer, { name: 'AES-GCM' }, false, ['decrypt']);
|
|
65
|
+
const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, cryptoKey, data.buffer);
|
|
66
|
+
return new TextDecoder().decode(decrypted);
|
|
67
|
+
}
|
|
68
|
+
// Fallback: Node.js crypto
|
|
69
|
+
if (nodeCrypto_1.isNodeCryptoAvailable) {
|
|
70
|
+
try {
|
|
71
|
+
const iv = decodeBase64(parts[0]);
|
|
72
|
+
const data = decodeBase64(parts[1]);
|
|
73
|
+
const tag = decodeBase64(parts[2]);
|
|
74
|
+
const decipher = (0, nodeCrypto_1.createDecipheriv)('aes-256-gcm', key, iv);
|
|
75
|
+
decipher.setAuthTag(tag);
|
|
76
|
+
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
|
|
77
|
+
return new TextDecoder().decode(decrypted);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new Error(`Decryption failed: ${error}`, { cause: error });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new Error('No crypto implementation available for decryption');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.EncryptionManager = EncryptionManager;
|
|
87
|
+
// ─── Encrypting Adapter Wrapper ──────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Wraps a StorageAdapter to provide transparent encryption.
|
|
90
|
+
* Only user-data columns are encrypted. Sync metadata and IDs remain in plaintext.
|
|
91
|
+
*/
|
|
92
|
+
class EncryptingAdapter {
|
|
93
|
+
_inner;
|
|
94
|
+
_encryption;
|
|
95
|
+
constructor(inner, keyProvider) {
|
|
96
|
+
this._inner = inner;
|
|
97
|
+
this._encryption = new EncryptionManager(keyProvider);
|
|
98
|
+
}
|
|
99
|
+
async initialize(schema) {
|
|
100
|
+
await this._inner.initialize(schema);
|
|
101
|
+
}
|
|
102
|
+
async find(query) {
|
|
103
|
+
// NOTE: Encrypted columns cannot be queried by value.
|
|
104
|
+
// Queries should only filter on plaintext columns (id, _status, etc.)
|
|
105
|
+
// or indexed columns stored unencrypted.
|
|
106
|
+
const rows = await this._inner.find(query);
|
|
107
|
+
return Promise.all(rows.map((r) => this._decryptRecord(r)));
|
|
108
|
+
}
|
|
109
|
+
async count(query) {
|
|
110
|
+
return this._inner.count(query);
|
|
111
|
+
}
|
|
112
|
+
async findById(table, id) {
|
|
113
|
+
const raw = await this._inner.findById(table, id);
|
|
114
|
+
if (!raw)
|
|
115
|
+
return null;
|
|
116
|
+
return this._decryptRecord(raw);
|
|
117
|
+
}
|
|
118
|
+
async insert(table, raw) {
|
|
119
|
+
const encrypted = await this._encryptRecord(raw);
|
|
120
|
+
await this._inner.insert(table, encrypted);
|
|
121
|
+
}
|
|
122
|
+
async update(table, raw) {
|
|
123
|
+
const encrypted = await this._encryptRecord(raw);
|
|
124
|
+
await this._inner.update(table, encrypted);
|
|
125
|
+
}
|
|
126
|
+
async markAsDeleted(table, id) {
|
|
127
|
+
await this._inner.markAsDeleted(table, id);
|
|
128
|
+
}
|
|
129
|
+
async destroyPermanently(table, id) {
|
|
130
|
+
await this._inner.destroyPermanently(table, id);
|
|
131
|
+
}
|
|
132
|
+
async batch(operations) {
|
|
133
|
+
const encrypted = [];
|
|
134
|
+
for (const op of operations) {
|
|
135
|
+
if (op.rawRecord && (op.type === 'create' || op.type === 'update')) {
|
|
136
|
+
const encRecord = await this._encryptRecord(op.rawRecord);
|
|
137
|
+
encrypted.push({ ...op, rawRecord: encRecord });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
encrypted.push(op);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
await this._inner.batch(encrypted);
|
|
144
|
+
}
|
|
145
|
+
async search(descriptor) {
|
|
146
|
+
// Full-text search on encrypted data is not possible.
|
|
147
|
+
// This will only work if search fields are stored unencrypted.
|
|
148
|
+
const result = await this._inner.search(descriptor);
|
|
149
|
+
const records = await Promise.all(result.records.map((r) => this._decryptRecord(r)));
|
|
150
|
+
return { records, total: result.total };
|
|
151
|
+
}
|
|
152
|
+
async getLocalChanges(tables) {
|
|
153
|
+
const changes = await this._inner.getLocalChanges(tables);
|
|
154
|
+
const result = {};
|
|
155
|
+
for (const [table, tc] of Object.entries(changes)) {
|
|
156
|
+
result[table] = {
|
|
157
|
+
created: await Promise.all(tc.created.map((r) => this._decryptRecord(r))),
|
|
158
|
+
updated: await Promise.all(tc.updated.map((r) => this._decryptRecord(r))),
|
|
159
|
+
deleted: tc.deleted,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
async applyRemoteChanges(changes) {
|
|
165
|
+
const encrypted = {};
|
|
166
|
+
for (const [table, tc] of Object.entries(changes)) {
|
|
167
|
+
encrypted[table] = {
|
|
168
|
+
created: await Promise.all(tc.created.map((r) => this._encryptRecord(r))),
|
|
169
|
+
updated: await Promise.all(tc.updated.map((r) => this._encryptRecord(r))),
|
|
170
|
+
deleted: tc.deleted,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
await this._inner.applyRemoteChanges(encrypted);
|
|
174
|
+
}
|
|
175
|
+
async markAsSynced(table, ids) {
|
|
176
|
+
await this._inner.markAsSynced(table, ids);
|
|
177
|
+
}
|
|
178
|
+
async getSchemaVersion() {
|
|
179
|
+
return this._inner.getSchemaVersion();
|
|
180
|
+
}
|
|
181
|
+
async migrate(migrations) {
|
|
182
|
+
await this._inner.migrate(migrations);
|
|
183
|
+
}
|
|
184
|
+
async reset() {
|
|
185
|
+
await this._inner.reset();
|
|
186
|
+
}
|
|
187
|
+
async close() {
|
|
188
|
+
await this._inner.close();
|
|
189
|
+
}
|
|
190
|
+
// ─── Internal helpers ──────────────────────────────────────────────
|
|
191
|
+
async _encryptRecord(raw) {
|
|
192
|
+
const encrypted = {};
|
|
193
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
194
|
+
if (PLAINTEXT_COLUMNS.has(key)) {
|
|
195
|
+
encrypted[key] = value;
|
|
196
|
+
}
|
|
197
|
+
else if (value === null || value === undefined) {
|
|
198
|
+
encrypted[key] = value;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
encrypted[key] = await this._encryption.encrypt(String(value));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return encrypted;
|
|
205
|
+
}
|
|
206
|
+
async _decryptRecord(raw) {
|
|
207
|
+
const decrypted = {};
|
|
208
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
209
|
+
if (PLAINTEXT_COLUMNS.has(key)) {
|
|
210
|
+
decrypted[key] = value;
|
|
211
|
+
}
|
|
212
|
+
else if (value === null || value === undefined) {
|
|
213
|
+
decrypted[key] = value;
|
|
214
|
+
}
|
|
215
|
+
else if (typeof value === 'string' && value.includes(':')) {
|
|
216
|
+
// Looks like encrypted data
|
|
217
|
+
try {
|
|
218
|
+
decrypted[key] = await this._encryption.decrypt(value);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Not encrypted or corrupted — pass through
|
|
222
|
+
decrypted[key] = value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
decrypted[key] = value;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return decrypted;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.EncryptingAdapter = EncryptingAdapter;
|
|
233
|
+
// ─── Utility functions ──────────────────────────────────────────────────
|
|
234
|
+
async function randomBytes(length) {
|
|
235
|
+
if (globalThis.crypto !== undefined && globalThis.crypto.getRandomValues) {
|
|
236
|
+
const buf = new Uint8Array(length);
|
|
237
|
+
globalThis.crypto.getRandomValues(buf);
|
|
238
|
+
return buf;
|
|
239
|
+
}
|
|
240
|
+
if (nodeCrypto_1.isNodeCryptoAvailable) {
|
|
241
|
+
try {
|
|
242
|
+
return (0, nodeCrypto_1.randomBytesNode)(length);
|
|
243
|
+
}
|
|
244
|
+
catch { /* fall through to Math.random */ }
|
|
245
|
+
}
|
|
246
|
+
// Last resort: Math.random (NOT cryptographically secure)
|
|
247
|
+
const buf = new Uint8Array(length);
|
|
248
|
+
for (let i = 0; i < length; i++) {
|
|
249
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
250
|
+
}
|
|
251
|
+
return buf;
|
|
252
|
+
}
|
|
253
|
+
function encodeBase64(data) {
|
|
254
|
+
if (typeof Buffer !== 'undefined') {
|
|
255
|
+
return Buffer.from(data).toString('base64');
|
|
256
|
+
}
|
|
257
|
+
// Browser
|
|
258
|
+
let binary = '';
|
|
259
|
+
for (const datum of data) {
|
|
260
|
+
binary += String.fromCodePoint(datum);
|
|
261
|
+
}
|
|
262
|
+
return btoa(binary);
|
|
263
|
+
}
|
|
264
|
+
function decodeBase64(str) {
|
|
265
|
+
if (typeof Buffer !== 'undefined') {
|
|
266
|
+
return new Uint8Array(Buffer.from(str, 'base64'));
|
|
267
|
+
}
|
|
268
|
+
// Browser
|
|
269
|
+
const binary = atob(str);
|
|
270
|
+
const bytes = new Uint8Array(binary.length);
|
|
271
|
+
for (let i = 0; i < binary.length; i++) {
|
|
272
|
+
bytes[i] = binary.codePointAt(i);
|
|
273
|
+
}
|
|
274
|
+
return bytes;
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js crypto provider — used on Node.js (tests, server-side).
|
|
3
|
+
*
|
|
4
|
+
* Metro bundler (React Native) will pick nodeCrypto.native.ts instead,
|
|
5
|
+
* which doesn't import the Node `crypto` module.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createCipheriv(algorithm: string, key: Uint8Array, iv: Uint8Array): {
|
|
8
|
+
update(data: Uint8Array): Buffer;
|
|
9
|
+
final(): Buffer;
|
|
10
|
+
getAuthTag(): Buffer;
|
|
11
|
+
};
|
|
12
|
+
export declare function createDecipheriv(algorithm: string, key: Uint8Array, iv: Uint8Array): {
|
|
13
|
+
setAuthTag(tag: Uint8Array): void;
|
|
14
|
+
update(data: Uint8Array): Buffer;
|
|
15
|
+
final(): Buffer;
|
|
16
|
+
};
|
|
17
|
+
export declare function randomBytesNode(length: number): Uint8Array;
|
|
18
|
+
export declare const isNodeCryptoAvailable = true;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node.js crypto provider — used on Node.js (tests, server-side).
|
|
4
|
+
*
|
|
5
|
+
* Metro bundler (React Native) will pick nodeCrypto.native.ts instead,
|
|
6
|
+
* which doesn't import the Node `crypto` module.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.isNodeCryptoAvailable = void 0;
|
|
10
|
+
exports.createCipheriv = createCipheriv;
|
|
11
|
+
exports.createDecipheriv = createDecipheriv;
|
|
12
|
+
exports.randomBytesNode = randomBytesNode;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
function createCipheriv(algorithm, key, iv) {
|
|
16
|
+
return crypto.createCipheriv(algorithm, key, iv);
|
|
17
|
+
}
|
|
18
|
+
function createDecipheriv(algorithm, key, iv) {
|
|
19
|
+
return crypto.createDecipheriv(algorithm, key, iv);
|
|
20
|
+
}
|
|
21
|
+
function randomBytesNode(length) {
|
|
22
|
+
return new Uint8Array(crypto.randomBytes(length));
|
|
23
|
+
}
|
|
24
|
+
exports.isNodeCryptoAvailable = true;
|
|
25
|
+
//# sourceMappingURL=nodeCrypto.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js crypto provider — React Native stub.
|
|
3
|
+
*
|
|
4
|
+
* Metro picks .native.ts files over .ts for React Native bundles.
|
|
5
|
+
* This avoids importing the Node `crypto` module which doesn't exist
|
|
6
|
+
* in React Native and would cause Metro bundling to fail.
|
|
7
|
+
*
|
|
8
|
+
* React Native should use globalThis.crypto (Web Crypto API) instead.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createCipheriv(_algorithm: string, _key: Uint8Array, _iv: Uint8Array): never;
|
|
11
|
+
export declare function createDecipheriv(_algorithm: string, _key: Uint8Array, _iv: Uint8Array): never;
|
|
12
|
+
export declare function randomBytesNode(_length: number): never;
|
|
13
|
+
export declare const isNodeCryptoAvailable = false;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Node.js crypto provider — React Native stub.
|
|
4
|
+
*
|
|
5
|
+
* Metro picks .native.ts files over .ts for React Native bundles.
|
|
6
|
+
* This avoids importing the Node `crypto` module which doesn't exist
|
|
7
|
+
* in React Native and would cause Metro bundling to fail.
|
|
8
|
+
*
|
|
9
|
+
* React Native should use globalThis.crypto (Web Crypto API) instead.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.isNodeCryptoAvailable = void 0;
|
|
13
|
+
exports.createCipheriv = createCipheriv;
|
|
14
|
+
exports.createDecipheriv = createDecipheriv;
|
|
15
|
+
exports.randomBytesNode = randomBytesNode;
|
|
16
|
+
function createCipheriv(_algorithm, _key, _iv) {
|
|
17
|
+
throw new Error('Node.js crypto is not available in React Native. Use Web Crypto API via globalThis.crypto.subtle.');
|
|
18
|
+
}
|
|
19
|
+
function createDecipheriv(_algorithm, _key, _iv) {
|
|
20
|
+
throw new Error('Node.js crypto is not available in React Native. Use Web Crypto API via globalThis.crypto.subtle.');
|
|
21
|
+
}
|
|
22
|
+
function randomBytesNode(_length) {
|
|
23
|
+
throw new Error('Node.js crypto is not available in React Native. Use globalThis.crypto.getRandomValues.');
|
|
24
|
+
}
|
|
25
|
+
exports.isNodeCryptoAvailable = false;
|
|
26
|
+
//# sourceMappingURL=nodeCrypto.native.js.map
|