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