sqlite-cloud-backup 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/dist/index.cjs ADDED
@@ -0,0 +1,832 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SqliteCloudBackup: () => SqliteCloudBackup,
34
+ default: () => index_default
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/core/db-manager.ts
39
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
40
+ var import_fs3 = __toESM(require("fs"), 1);
41
+ var import_path = __toESM(require("path"), 1);
42
+
43
+ // src/utils/checksum.ts
44
+ var import_crypto = __toESM(require("crypto"), 1);
45
+ var import_fs = __toESM(require("fs"), 1);
46
+ var ChecksumUtil = class {
47
+ /**
48
+ * Calculate SHA-256 checksum of a file
49
+ */
50
+ static async calculateFileChecksum(filePath) {
51
+ return new Promise((resolve, reject) => {
52
+ const hash = import_crypto.default.createHash("sha256");
53
+ const stream = import_fs.default.createReadStream(filePath);
54
+ stream.on("data", (data) => hash.update(data));
55
+ stream.on("end", () => resolve(hash.digest("hex")));
56
+ stream.on("error", reject);
57
+ });
58
+ }
59
+ /**
60
+ * Calculate SHA-256 checksum of a buffer
61
+ */
62
+ static calculateBufferChecksum(buffer) {
63
+ return import_crypto.default.createHash("sha256").update(buffer).digest("hex");
64
+ }
65
+ /**
66
+ * Verify file integrity
67
+ */
68
+ static async verifyChecksum(filePath, expectedChecksum) {
69
+ const actualChecksum = await this.calculateFileChecksum(filePath);
70
+ return actualChecksum === expectedChecksum;
71
+ }
72
+ };
73
+
74
+ // src/utils/file-operations.ts
75
+ var import_fs2 = __toESM(require("fs"), 1);
76
+ var FileOperations = class {
77
+ /**
78
+ * Ensure directory exists, create if not
79
+ */
80
+ static ensureDir(dirPath) {
81
+ if (!import_fs2.default.existsSync(dirPath)) {
82
+ import_fs2.default.mkdirSync(dirPath, { recursive: true });
83
+ }
84
+ }
85
+ /**
86
+ * Copy file atomically
87
+ */
88
+ static async copyFile(source, destination) {
89
+ const tempDest = `${destination}.tmp`;
90
+ await import_fs2.default.promises.copyFile(source, tempDest);
91
+ await import_fs2.default.promises.rename(tempDest, destination);
92
+ }
93
+ };
94
+
95
+ // src/core/db-manager.ts
96
+ var DatabaseManager = class {
97
+ db = null;
98
+ dbPath;
99
+ metadataPath;
100
+ logger;
101
+ constructor(dbPath, logger) {
102
+ this.dbPath = dbPath;
103
+ this.logger = logger;
104
+ const dbDir = import_path.default.dirname(dbPath);
105
+ const dbName = import_path.default.basename(dbPath, import_path.default.extname(dbPath));
106
+ this.metadataPath = import_path.default.join(dbDir, ".sqlite-cloud-backup", dbName, "metadata.json");
107
+ FileOperations.ensureDir(import_path.default.dirname(this.metadataPath));
108
+ }
109
+ /**
110
+ * Open database connection
111
+ */
112
+ open() {
113
+ if (!import_fs3.default.existsSync(this.dbPath)) {
114
+ throw new Error(`Database not found: ${this.dbPath}`);
115
+ }
116
+ this.db = new import_better_sqlite3.default(this.dbPath, { readonly: false });
117
+ }
118
+ /**
119
+ * Close database connection
120
+ */
121
+ close() {
122
+ if (this.db) {
123
+ this.db.close();
124
+ this.db = null;
125
+ }
126
+ }
127
+ /**
128
+ * Get database file as buffer
129
+ */
130
+ async getBuffer() {
131
+ this.close();
132
+ return import_fs3.default.promises.readFile(this.dbPath);
133
+ }
134
+ /**
135
+ * Calculate current database checksum
136
+ */
137
+ async getChecksum() {
138
+ this.close();
139
+ return ChecksumUtil.calculateFileChecksum(this.dbPath);
140
+ }
141
+ /**
142
+ * Replace database with buffer
143
+ */
144
+ async replaceWithBuffer(buffer) {
145
+ this.close();
146
+ const tempPath = `${this.dbPath}.tmp`;
147
+ await import_fs3.default.promises.writeFile(tempPath, buffer);
148
+ await import_fs3.default.promises.rename(tempPath, this.dbPath);
149
+ this.logger.info("Database replaced from cloud");
150
+ }
151
+ /**
152
+ * Get local metadata
153
+ */
154
+ async getLocalMetadata() {
155
+ if (!import_fs3.default.existsSync(this.metadataPath)) {
156
+ return {
157
+ lastSyncTimestamp: 0,
158
+ lastSyncChecksum: ""
159
+ };
160
+ }
161
+ const content = await import_fs3.default.promises.readFile(this.metadataPath, "utf-8");
162
+ return JSON.parse(content);
163
+ }
164
+ /**
165
+ * Update local metadata
166
+ */
167
+ async updateLocalMetadata(metadata) {
168
+ const current = await this.getLocalMetadata();
169
+ const updated = { ...current, ...metadata };
170
+ await import_fs3.default.promises.writeFile(
171
+ this.metadataPath,
172
+ JSON.stringify(updated, null, 2)
173
+ );
174
+ }
175
+ /**
176
+ * Get modification time of database file
177
+ */
178
+ async getModifiedTime() {
179
+ const stats = await import_fs3.default.promises.stat(this.dbPath);
180
+ return stats.mtimeMs;
181
+ }
182
+ };
183
+
184
+ // src/core/sync-engine.ts
185
+ var SyncEngine = class {
186
+ dbManager;
187
+ provider;
188
+ logger;
189
+ constructor(dbManager, provider, logger) {
190
+ this.dbManager = dbManager;
191
+ this.provider = provider;
192
+ this.logger = logger;
193
+ }
194
+ /**
195
+ * Push local database to cloud
196
+ */
197
+ async pushToCloud() {
198
+ const startTime = Date.now();
199
+ try {
200
+ const buffer = await this.dbManager.getBuffer();
201
+ const originalSize = buffer.length;
202
+ const checksum = ChecksumUtil.calculateBufferChecksum(buffer);
203
+ await this.provider.uploadFile("current.db", buffer);
204
+ const metadata = {
205
+ dbName: "current",
206
+ lastSyncTimestamp: Date.now(),
207
+ lastSyncType: "push",
208
+ checksum,
209
+ version: 1
210
+ };
211
+ await this.provider.updateMetadata(metadata);
212
+ await this.dbManager.updateLocalMetadata({
213
+ lastSyncTimestamp: metadata.lastSyncTimestamp,
214
+ lastSyncChecksum: checksum
215
+ });
216
+ const result = {
217
+ success: true,
218
+ type: "push",
219
+ timestamp: Date.now(),
220
+ localChecksum: checksum,
221
+ cloudChecksum: checksum,
222
+ bytesTransferred: buffer.length,
223
+ duration: Date.now() - startTime
224
+ };
225
+ this.logger.info(`Push successful: ${originalSize} bytes`);
226
+ return result;
227
+ } catch (error) {
228
+ this.logger.error("Push failed", error);
229
+ throw error;
230
+ }
231
+ }
232
+ /**
233
+ * Pull database from cloud to local
234
+ */
235
+ async pullFromCloud() {
236
+ const startTime = Date.now();
237
+ try {
238
+ const exists = await this.provider.fileExists("current.db");
239
+ if (!exists) {
240
+ throw new Error("No cloud version found");
241
+ }
242
+ const buffer = await this.provider.downloadFile("current.db");
243
+ const checksum = ChecksumUtil.calculateBufferChecksum(buffer);
244
+ const cloudMetadata = await this.provider.getMetadata("current.db");
245
+ if (cloudMetadata && cloudMetadata.checksum !== checksum) {
246
+ throw new Error("Checksum mismatch - data corruption detected");
247
+ }
248
+ await this.dbManager.replaceWithBuffer(buffer);
249
+ await this.dbManager.updateLocalMetadata({
250
+ lastSyncTimestamp: Date.now(),
251
+ lastSyncChecksum: checksum
252
+ });
253
+ const result = {
254
+ success: true,
255
+ type: "pull",
256
+ timestamp: Date.now(),
257
+ localChecksum: checksum,
258
+ cloudChecksum: checksum,
259
+ bytesTransferred: buffer.length,
260
+ duration: Date.now() - startTime
261
+ };
262
+ this.logger.info(`Pull successful: ${buffer.length} bytes`);
263
+ return result;
264
+ } catch (error) {
265
+ this.logger.error("Pull failed", error);
266
+ throw error;
267
+ }
268
+ }
269
+ /**
270
+ * Bidirectional sync - simple version for v0.1
271
+ */
272
+ async sync() {
273
+ const startTime = Date.now();
274
+ try {
275
+ const cloudExists = await this.provider.fileExists("current.db");
276
+ if (!cloudExists) {
277
+ this.logger.info("No cloud version found, pushing local database");
278
+ return await this.pushToCloud();
279
+ }
280
+ const localChecksum = await this.dbManager.getChecksum();
281
+ const cloudMetadata = await this.provider.getMetadata("current.db");
282
+ if (!cloudMetadata) {
283
+ this.logger.info("No cloud metadata, pushing local database");
284
+ return await this.pushToCloud();
285
+ }
286
+ if (localChecksum === cloudMetadata.checksum) {
287
+ const result = {
288
+ success: true,
289
+ type: "bidirectional",
290
+ timestamp: Date.now(),
291
+ localChecksum,
292
+ cloudChecksum: cloudMetadata.checksum,
293
+ bytesTransferred: 0,
294
+ duration: Date.now() - startTime
295
+ };
296
+ this.logger.info("Already in sync");
297
+ return result;
298
+ }
299
+ const localModified = await this.dbManager.getModifiedTime();
300
+ if (localModified > cloudMetadata.modifiedAt) {
301
+ this.logger.info("Local is newer, pushing");
302
+ return await this.pushToCloud();
303
+ } else {
304
+ this.logger.info("Cloud is newer, pulling");
305
+ return await this.pullFromCloud();
306
+ }
307
+ } catch (error) {
308
+ this.logger.error("Sync failed", error);
309
+ throw error;
310
+ }
311
+ }
312
+ };
313
+
314
+ // src/providers/google-drive/google-drive-provider.ts
315
+ var import_googleapis = require("googleapis");
316
+
317
+ // src/providers/base-provider.ts
318
+ var BaseProvider = class {
319
+ };
320
+
321
+ // src/providers/google-drive/google-drive-provider.ts
322
+ var import_stream = require("stream");
323
+ var GoogleDriveProvider = class extends BaseProvider {
324
+ drive;
325
+ oauth2Client;
326
+ rootFolderId = null;
327
+ logger;
328
+ dbName;
329
+ constructor(credentials, dbName, logger) {
330
+ super();
331
+ this.logger = logger;
332
+ this.dbName = dbName;
333
+ this.oauth2Client = new import_googleapis.google.auth.OAuth2(
334
+ credentials.clientId,
335
+ credentials.clientSecret,
336
+ credentials.redirectUri
337
+ );
338
+ this.oauth2Client.setCredentials({
339
+ refresh_token: credentials.refreshToken
340
+ });
341
+ this.drive = import_googleapis.google.drive({ version: "v3", auth: this.oauth2Client });
342
+ }
343
+ /**
344
+ * Initialize folder structure
345
+ */
346
+ async ensureRootFolder() {
347
+ if (this.rootFolderId) return this.rootFolderId;
348
+ const response = await this.drive.files.list({
349
+ q: "name='.sqlite-cloud-backup' and mimeType='application/vnd.google-apps.folder' and trashed=false",
350
+ fields: "files(id, name)",
351
+ spaces: "drive"
352
+ });
353
+ if (response.data.files && response.data.files.length > 0) {
354
+ this.rootFolderId = response.data.files[0].id;
355
+ } else {
356
+ const folder = await this.drive.files.create({
357
+ requestBody: {
358
+ name: ".sqlite-cloud-backup",
359
+ mimeType: "application/vnd.google-apps.folder"
360
+ },
361
+ fields: "id"
362
+ });
363
+ this.rootFolderId = folder.data.id;
364
+ }
365
+ const dbFolderId = await this.ensureDbFolder();
366
+ return dbFolderId;
367
+ }
368
+ async ensureDbFolder() {
369
+ const response = await this.drive.files.list({
370
+ q: `name='${this.dbName}' and mimeType='application/vnd.google-apps.folder' and '${this.rootFolderId}' in parents and trashed=false`,
371
+ fields: "files(id, name)"
372
+ });
373
+ if (response.data.files && response.data.files.length > 0) {
374
+ return response.data.files[0].id;
375
+ }
376
+ const folder = await this.drive.files.create({
377
+ requestBody: {
378
+ name: this.dbName,
379
+ mimeType: "application/vnd.google-apps.folder",
380
+ parents: [this.rootFolderId]
381
+ },
382
+ fields: "id"
383
+ });
384
+ return folder.data.id;
385
+ }
386
+ async uploadFile(fileName, buffer) {
387
+ const folderId = await this.ensureRootFolder();
388
+ const existing = await this.findFile(fileName);
389
+ const media = {
390
+ mimeType: "application/x-sqlite3",
391
+ body: import_stream.Readable.from(buffer)
392
+ };
393
+ if (existing) {
394
+ await this.drive.files.update({
395
+ fileId: existing.id,
396
+ media,
397
+ fields: "id"
398
+ });
399
+ this.logger.info(`Updated file in Google Drive: ${fileName}`);
400
+ } else {
401
+ await this.drive.files.create({
402
+ requestBody: {
403
+ name: fileName,
404
+ parents: [folderId]
405
+ },
406
+ media,
407
+ fields: "id"
408
+ });
409
+ this.logger.info(`Uploaded file to Google Drive: ${fileName}`);
410
+ }
411
+ }
412
+ async downloadFile(fileName) {
413
+ const file = await this.findFile(fileName);
414
+ if (!file) {
415
+ throw new Error(`File not found: ${fileName}`);
416
+ }
417
+ const response = await this.drive.files.get(
418
+ { fileId: file.id, alt: "media" },
419
+ { responseType: "arraybuffer" }
420
+ );
421
+ this.logger.info(`Downloaded file from Google Drive: ${fileName}`);
422
+ return Buffer.from(response.data);
423
+ }
424
+ async fileExists(fileName) {
425
+ const file = await this.findFile(fileName);
426
+ return file !== null;
427
+ }
428
+ async getMetadata(_fileName) {
429
+ const metadataFile = await this.findFile("metadata.json");
430
+ if (!metadataFile) return null;
431
+ const buffer = await this.downloadFile("metadata.json");
432
+ const metadata = JSON.parse(buffer.toString("utf-8"));
433
+ return {
434
+ checksum: metadata.checksum,
435
+ modifiedAt: metadata.lastSyncTimestamp,
436
+ size: 0
437
+ // Not tracked in metadata
438
+ };
439
+ }
440
+ async updateMetadata(metadata) {
441
+ const buffer = Buffer.from(JSON.stringify(metadata, null, 2));
442
+ await this.uploadFile("metadata.json", buffer);
443
+ }
444
+ async deleteFile(fileName) {
445
+ const file = await this.findFile(fileName);
446
+ if (file) {
447
+ await this.drive.files.delete({ fileId: file.id });
448
+ this.logger.info(`Deleted file from Google Drive: ${fileName}`);
449
+ }
450
+ }
451
+ async findFile(fileName) {
452
+ const folderId = await this.ensureRootFolder();
453
+ const response = await this.drive.files.list({
454
+ q: `name='${fileName}' and '${folderId}' in parents and trashed=false`,
455
+ fields: "files(id, name, modifiedTime)"
456
+ });
457
+ return response.data.files?.[0] || null;
458
+ }
459
+ };
460
+
461
+ // src/providers/google-drive/oauth-flow.ts
462
+ var import_http = __toESM(require("http"), 1);
463
+ var import_url = require("url");
464
+ var import_open = __toESM(require("open"), 1);
465
+ var OAuthFlow = class {
466
+ logger;
467
+ server = null;
468
+ redirectUri = "http://localhost:3000/oauth/callback";
469
+ scopes = ["https://www.googleapis.com/auth/drive.file"];
470
+ constructor(logger) {
471
+ this.logger = logger;
472
+ }
473
+ /**
474
+ * Start OAuth flow - opens browser and waits for callback
475
+ */
476
+ async authenticate(clientId, clientSecret) {
477
+ this.logger.info("Starting OAuth flow...");
478
+ const authCode = await this.startLocalServerAndWaitForCode(clientId);
479
+ const tokens = await this.exchangeCodeForTokens(authCode, clientId, clientSecret);
480
+ this.logger.info("OAuth authentication successful");
481
+ return tokens;
482
+ }
483
+ /**
484
+ * Start local HTTP server and wait for OAuth callback
485
+ */
486
+ async startLocalServerAndWaitForCode(clientId) {
487
+ return new Promise((resolve, reject) => {
488
+ this.server = import_http.default.createServer((req, res) => {
489
+ const url = new import_url.URL(req.url || "", `http://localhost:3000`);
490
+ if (url.pathname === "/oauth/callback") {
491
+ const code = url.searchParams.get("code");
492
+ const error = url.searchParams.get("error");
493
+ if (error) {
494
+ res.writeHead(400, { "Content-Type": "text/html" });
495
+ res.end("<h1>Authentication Failed</h1><p>You can close this window.</p>");
496
+ this.stopServer();
497
+ reject(new Error(`OAuth error: ${error}`));
498
+ return;
499
+ }
500
+ if (code) {
501
+ res.writeHead(200, { "Content-Type": "text/html" });
502
+ res.end("<h1>Authentication Successful!</h1><p>You can close this window and return to the app.</p>");
503
+ this.stopServer();
504
+ resolve(code);
505
+ return;
506
+ }
507
+ res.writeHead(400, { "Content-Type": "text/html" });
508
+ res.end("<h1>Invalid Request</h1><p>No authorization code received.</p>");
509
+ this.stopServer();
510
+ reject(new Error("No authorization code received"));
511
+ } else {
512
+ res.writeHead(404);
513
+ res.end();
514
+ }
515
+ });
516
+ this.server.listen(3e3, () => {
517
+ this.logger.info("Local OAuth server started on http://localhost:3000");
518
+ const authUrl = this.buildAuthUrl(clientId);
519
+ this.logger.info("Opening browser for authentication...");
520
+ (0, import_open.default)(authUrl).catch((err) => {
521
+ this.logger.error("Failed to open browser", err);
522
+ this.logger.info(`Please open this URL manually: ${authUrl}`);
523
+ });
524
+ });
525
+ setTimeout(() => {
526
+ this.stopServer();
527
+ reject(new Error("OAuth flow timed out after 5 minutes"));
528
+ }, 5 * 60 * 1e3);
529
+ });
530
+ }
531
+ /**
532
+ * Build Google OAuth authorization URL
533
+ */
534
+ buildAuthUrl(clientId) {
535
+ const params = new URLSearchParams({
536
+ client_id: clientId,
537
+ redirect_uri: this.redirectUri,
538
+ response_type: "code",
539
+ scope: this.scopes.join(" "),
540
+ access_type: "offline",
541
+ prompt: "consent"
542
+ // Force to get refresh token
543
+ });
544
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
545
+ }
546
+ /**
547
+ * Exchange authorization code for tokens
548
+ */
549
+ async exchangeCodeForTokens(code, clientId, clientSecret) {
550
+ this.logger.info("Exchanging authorization code for tokens...");
551
+ const response = await fetch("https://oauth2.googleapis.com/token", {
552
+ method: "POST",
553
+ headers: {
554
+ "Content-Type": "application/x-www-form-urlencoded"
555
+ },
556
+ body: new URLSearchParams({
557
+ code,
558
+ client_id: clientId,
559
+ client_secret: clientSecret,
560
+ redirect_uri: this.redirectUri,
561
+ grant_type: "authorization_code"
562
+ }).toString()
563
+ });
564
+ if (!response.ok) {
565
+ const error = await response.text();
566
+ throw new Error(`Failed to exchange code for tokens: ${error}`);
567
+ }
568
+ const data = await response.json();
569
+ return {
570
+ access_token: data.access_token,
571
+ refresh_token: data.refresh_token,
572
+ expiry_date: data.expires_in ? Date.now() + data.expires_in * 1e3 : void 0
573
+ };
574
+ }
575
+ /**
576
+ * Stop the local server
577
+ */
578
+ stopServer() {
579
+ if (this.server) {
580
+ this.server.close();
581
+ this.server = null;
582
+ this.logger.info("Local OAuth server stopped");
583
+ }
584
+ }
585
+ };
586
+
587
+ // src/providers/google-drive/token-storage.ts
588
+ var import_promises = __toESM(require("fs/promises"), 1);
589
+ var import_path2 = __toESM(require("path"), 1);
590
+
591
+ // src/utils/logger.ts
592
+ var Logger = class {
593
+ level;
594
+ constructor(level = "info") {
595
+ this.level = level;
596
+ }
597
+ shouldLog(level) {
598
+ const levels = ["debug", "info", "warn", "error"];
599
+ return levels.indexOf(level) >= levels.indexOf(this.level);
600
+ }
601
+ debug(message, meta) {
602
+ if (this.shouldLog("debug")) {
603
+ console.debug(`[DEBUG] ${message}`, meta || "");
604
+ }
605
+ }
606
+ info(message, meta) {
607
+ if (this.shouldLog("info")) {
608
+ console.info(`[INFO] ${message}`, meta || "");
609
+ }
610
+ }
611
+ warn(message, meta) {
612
+ if (this.shouldLog("warn")) {
613
+ console.warn(`[WARN] ${message}`, meta || "");
614
+ }
615
+ }
616
+ error(message, error) {
617
+ if (this.shouldLog("error")) {
618
+ console.error(`[ERROR] ${message}`, error || "");
619
+ }
620
+ }
621
+ };
622
+
623
+ // src/providers/google-drive/token-storage.ts
624
+ var TokenStorage = class {
625
+ logger;
626
+ tokenDir;
627
+ tokenFile;
628
+ constructor(dbPath, logLevel = "info") {
629
+ this.logger = new Logger(logLevel);
630
+ const dbDir = import_path2.default.dirname(dbPath);
631
+ this.tokenDir = import_path2.default.join(dbDir, ".sqlite-cloud-backup");
632
+ this.tokenFile = import_path2.default.join(this.tokenDir, "tokens.json");
633
+ }
634
+ async hasTokens() {
635
+ try {
636
+ await import_promises.default.access(this.tokenFile);
637
+ return true;
638
+ } catch {
639
+ return false;
640
+ }
641
+ }
642
+ async getTokens() {
643
+ try {
644
+ const data = await import_promises.default.readFile(this.tokenFile, "utf-8");
645
+ const tokens = JSON.parse(data);
646
+ this.logger.debug("Retrieved stored tokens");
647
+ return tokens;
648
+ } catch (error) {
649
+ if (error.code !== "ENOENT") {
650
+ this.logger.warn("Failed to read tokens", error);
651
+ }
652
+ return null;
653
+ }
654
+ }
655
+ async saveTokens(tokens) {
656
+ try {
657
+ await import_promises.default.mkdir(this.tokenDir, { recursive: true });
658
+ await import_promises.default.writeFile(
659
+ this.tokenFile,
660
+ JSON.stringify(tokens, null, 2),
661
+ "utf-8"
662
+ );
663
+ this.logger.debug("Saved tokens to storage");
664
+ } catch (error) {
665
+ this.logger.error("Failed to save tokens", error);
666
+ throw new Error("Failed to save authentication tokens");
667
+ }
668
+ }
669
+ async clearTokens() {
670
+ try {
671
+ await import_promises.default.unlink(this.tokenFile);
672
+ this.logger.debug("Cleared stored tokens");
673
+ } catch (error) {
674
+ if (error.code !== "ENOENT") {
675
+ this.logger.warn("Failed to clear tokens", error);
676
+ }
677
+ }
678
+ }
679
+ };
680
+
681
+ // src/index.ts
682
+ var import_path3 = __toESM(require("path"), 1);
683
+ var SqliteCloudBackup = class {
684
+ dbManager;
685
+ provider;
686
+ syncEngine;
687
+ logger;
688
+ oauthFlow;
689
+ tokenStorage;
690
+ credentials;
691
+ dbPath;
692
+ constructor(config) {
693
+ this.logger = new Logger(config.options?.logLevel ?? "info");
694
+ this.dbPath = config.dbPath;
695
+ this.credentials = config.credentials;
696
+ this.oauthFlow = new OAuthFlow(this.logger);
697
+ this.tokenStorage = new TokenStorage(config.dbPath, config.options?.logLevel ?? "info");
698
+ this.dbManager = new DatabaseManager(config.dbPath, this.logger);
699
+ this.provider = this.createProvider(config);
700
+ this.syncEngine = new SyncEngine(
701
+ this.dbManager,
702
+ this.provider,
703
+ this.logger
704
+ );
705
+ }
706
+ createProvider(config) {
707
+ const dbName = import_path3.default.basename(config.dbPath, import_path3.default.extname(config.dbPath));
708
+ switch (config.provider) {
709
+ case "google-drive":
710
+ return new GoogleDriveProvider(
711
+ config.credentials,
712
+ dbName,
713
+ this.logger
714
+ );
715
+ default:
716
+ throw new Error(`Unsupported provider: ${config.provider}`);
717
+ }
718
+ }
719
+ /**
720
+ * Check if user needs authentication
721
+ */
722
+ async needsAuthentication() {
723
+ if (this.credentials.refreshToken) {
724
+ return false;
725
+ }
726
+ return !await this.tokenStorage.hasTokens();
727
+ }
728
+ /**
729
+ * Ensure user is authenticated, trigger OAuth flow if needed
730
+ */
731
+ async ensureAuthenticated() {
732
+ if (this.credentials.refreshToken) {
733
+ this.logger.debug("Using provided refresh token");
734
+ return;
735
+ }
736
+ const storedTokens = await this.tokenStorage.getTokens();
737
+ if (storedTokens) {
738
+ this.logger.debug("Using stored refresh token");
739
+ this.credentials.refreshToken = storedTokens.refreshToken;
740
+ const dbName = import_path3.default.basename(this.dbPath, import_path3.default.extname(this.dbPath));
741
+ this.provider = new GoogleDriveProvider(
742
+ this.credentials,
743
+ dbName,
744
+ this.logger
745
+ );
746
+ this.syncEngine = new SyncEngine(
747
+ this.dbManager,
748
+ this.provider,
749
+ this.logger
750
+ );
751
+ return;
752
+ }
753
+ this.logger.info("No authentication found, starting OAuth flow...");
754
+ await this.authenticate();
755
+ }
756
+ /**
757
+ * Trigger OAuth authentication flow
758
+ */
759
+ async authenticate() {
760
+ this.logger.info("Starting OAuth authentication flow...");
761
+ const tokens = await this.oauthFlow.authenticate(
762
+ this.credentials.clientId,
763
+ this.credentials.clientSecret
764
+ );
765
+ await this.tokenStorage.saveTokens({
766
+ refreshToken: tokens.refresh_token,
767
+ accessToken: tokens.access_token,
768
+ expiryDate: tokens.expiry_date
769
+ });
770
+ this.credentials.refreshToken = tokens.refresh_token;
771
+ const dbName = import_path3.default.basename(this.dbPath, import_path3.default.extname(this.dbPath));
772
+ this.provider = new GoogleDriveProvider(
773
+ this.credentials,
774
+ dbName,
775
+ this.logger
776
+ );
777
+ this.syncEngine = new SyncEngine(
778
+ this.dbManager,
779
+ this.provider,
780
+ this.logger
781
+ );
782
+ this.logger.info("Authentication successful");
783
+ }
784
+ /**
785
+ * Check if user is authenticated
786
+ */
787
+ async isAuthenticated() {
788
+ return !await this.needsAuthentication();
789
+ }
790
+ /**
791
+ * Logout and clear stored tokens
792
+ */
793
+ async logout() {
794
+ await this.tokenStorage.clearTokens();
795
+ this.credentials.refreshToken = void 0;
796
+ this.logger.info("Logged out successfully");
797
+ }
798
+ /**
799
+ * Push local database to cloud
800
+ */
801
+ async pushToCloud() {
802
+ await this.ensureAuthenticated();
803
+ return this.syncEngine.pushToCloud();
804
+ }
805
+ /**
806
+ * Pull database from cloud to local
807
+ */
808
+ async pullFromCloud() {
809
+ await this.ensureAuthenticated();
810
+ return this.syncEngine.pullFromCloud();
811
+ }
812
+ /**
813
+ * Bidirectional sync
814
+ */
815
+ async sync() {
816
+ await this.ensureAuthenticated();
817
+ return this.syncEngine.sync();
818
+ }
819
+ /**
820
+ * Cleanup and shutdown
821
+ */
822
+ async shutdown() {
823
+ this.dbManager.close();
824
+ this.logger.info("SqliteCloudBackup shutdown complete");
825
+ }
826
+ };
827
+ var index_default = SqliteCloudBackup;
828
+ // Annotate the CommonJS export names for ESM import in node:
829
+ 0 && (module.exports = {
830
+ SqliteCloudBackup
831
+ });
832
+ //# sourceMappingURL=index.cjs.map