commons-proxy 2.0.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +757 -0
  3. package/bin/cli.js +146 -0
  4. package/package.json +97 -0
  5. package/public/Complaint Details.pdf +0 -0
  6. package/public/Cyber Crime Portal.pdf +0 -0
  7. package/public/app.js +229 -0
  8. package/public/css/src/input.css +523 -0
  9. package/public/css/style.css +1 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +549 -0
  12. package/public/js/components/account-manager.js +356 -0
  13. package/public/js/components/add-account-modal.js +414 -0
  14. package/public/js/components/claude-config.js +420 -0
  15. package/public/js/components/dashboard/charts.js +605 -0
  16. package/public/js/components/dashboard/filters.js +362 -0
  17. package/public/js/components/dashboard/stats.js +110 -0
  18. package/public/js/components/dashboard.js +236 -0
  19. package/public/js/components/logs-viewer.js +100 -0
  20. package/public/js/components/models.js +36 -0
  21. package/public/js/components/server-config.js +349 -0
  22. package/public/js/config/constants.js +102 -0
  23. package/public/js/data-store.js +375 -0
  24. package/public/js/settings-store.js +58 -0
  25. package/public/js/store.js +99 -0
  26. package/public/js/translations/en.js +367 -0
  27. package/public/js/translations/id.js +412 -0
  28. package/public/js/translations/pt.js +308 -0
  29. package/public/js/translations/tr.js +358 -0
  30. package/public/js/translations/zh.js +373 -0
  31. package/public/js/utils/account-actions.js +189 -0
  32. package/public/js/utils/error-handler.js +96 -0
  33. package/public/js/utils/model-config.js +42 -0
  34. package/public/js/utils/ui-logger.js +143 -0
  35. package/public/js/utils/validators.js +77 -0
  36. package/public/js/utils.js +69 -0
  37. package/public/proxy-server-64.png +0 -0
  38. package/public/views/accounts.html +361 -0
  39. package/public/views/dashboard.html +484 -0
  40. package/public/views/logs.html +97 -0
  41. package/public/views/models.html +331 -0
  42. package/public/views/settings.html +1327 -0
  43. package/src/account-manager/credentials.js +378 -0
  44. package/src/account-manager/index.js +462 -0
  45. package/src/account-manager/onboarding.js +112 -0
  46. package/src/account-manager/rate-limits.js +369 -0
  47. package/src/account-manager/storage.js +160 -0
  48. package/src/account-manager/strategies/base-strategy.js +109 -0
  49. package/src/account-manager/strategies/hybrid-strategy.js +339 -0
  50. package/src/account-manager/strategies/index.js +79 -0
  51. package/src/account-manager/strategies/round-robin-strategy.js +76 -0
  52. package/src/account-manager/strategies/sticky-strategy.js +138 -0
  53. package/src/account-manager/strategies/trackers/health-tracker.js +162 -0
  54. package/src/account-manager/strategies/trackers/index.js +9 -0
  55. package/src/account-manager/strategies/trackers/quota-tracker.js +120 -0
  56. package/src/account-manager/strategies/trackers/token-bucket-tracker.js +155 -0
  57. package/src/auth/database.js +169 -0
  58. package/src/auth/oauth.js +548 -0
  59. package/src/auth/token-extractor.js +117 -0
  60. package/src/cli/accounts.js +648 -0
  61. package/src/cloudcode/index.js +29 -0
  62. package/src/cloudcode/message-handler.js +510 -0
  63. package/src/cloudcode/model-api.js +248 -0
  64. package/src/cloudcode/rate-limit-parser.js +235 -0
  65. package/src/cloudcode/request-builder.js +93 -0
  66. package/src/cloudcode/session-manager.js +47 -0
  67. package/src/cloudcode/sse-parser.js +121 -0
  68. package/src/cloudcode/sse-streamer.js +293 -0
  69. package/src/cloudcode/streaming-handler.js +615 -0
  70. package/src/config.js +125 -0
  71. package/src/constants.js +407 -0
  72. package/src/errors.js +242 -0
  73. package/src/fallback-config.js +29 -0
  74. package/src/format/content-converter.js +193 -0
  75. package/src/format/index.js +20 -0
  76. package/src/format/request-converter.js +255 -0
  77. package/src/format/response-converter.js +120 -0
  78. package/src/format/schema-sanitizer.js +673 -0
  79. package/src/format/signature-cache.js +88 -0
  80. package/src/format/thinking-utils.js +648 -0
  81. package/src/index.js +148 -0
  82. package/src/modules/usage-stats.js +205 -0
  83. package/src/providers/anthropic-provider.js +258 -0
  84. package/src/providers/base-provider.js +157 -0
  85. package/src/providers/cloudcode.js +94 -0
  86. package/src/providers/copilot.js +399 -0
  87. package/src/providers/github-provider.js +287 -0
  88. package/src/providers/google-provider.js +192 -0
  89. package/src/providers/index.js +211 -0
  90. package/src/providers/openai-compatible.js +265 -0
  91. package/src/providers/openai-provider.js +271 -0
  92. package/src/providers/openrouter-provider.js +325 -0
  93. package/src/providers/setup.js +83 -0
  94. package/src/server.js +870 -0
  95. package/src/utils/claude-config.js +245 -0
  96. package/src/utils/helpers.js +51 -0
  97. package/src/utils/logger.js +142 -0
  98. package/src/utils/native-module-helper.js +162 -0
  99. package/src/webui/index.js +1134 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Health Tracker
3
+ *
4
+ * Tracks per-account health scores to prioritize healthy accounts.
5
+ * Scores increase on success and decrease on failures/rate limits.
6
+ * Passive recovery over time helps accounts recover from temporary issues.
7
+ */
8
+
9
+ // Default configuration (matches opencode-cloudcode-auth)
10
+ const DEFAULT_CONFIG = {
11
+ initial: 70, // Starting score for new accounts
12
+ successReward: 1, // Points on successful request
13
+ rateLimitPenalty: -10, // Points on rate limit
14
+ failurePenalty: -20, // Points on other failures
15
+ recoveryPerHour: 10, // Passive recovery rate (increased from 2 for faster recovery)
16
+ minUsable: 50, // Minimum score to be selected
17
+ maxScore: 100 // Maximum score cap
18
+ };
19
+
20
+ export class HealthTracker {
21
+ #scores = new Map(); // email -> { score, lastUpdated, consecutiveFailures }
22
+ #config;
23
+
24
+ /**
25
+ * Create a new HealthTracker
26
+ * @param {Object} config - Health score configuration
27
+ */
28
+ constructor(config = {}) {
29
+ this.#config = { ...DEFAULT_CONFIG, ...config };
30
+ }
31
+
32
+ /**
33
+ * Get the health score for an account
34
+ * @param {string} email - Account email
35
+ * @returns {number} Current health score (with passive recovery applied)
36
+ */
37
+ getScore(email) {
38
+ const record = this.#scores.get(email);
39
+ if (!record) {
40
+ return this.#config.initial;
41
+ }
42
+
43
+ // Apply passive recovery based on time elapsed
44
+ const now = Date.now();
45
+ const hoursElapsed = (now - record.lastUpdated) / (1000 * 60 * 60);
46
+ const recovery = hoursElapsed * this.#config.recoveryPerHour;
47
+ const recoveredScore = Math.min(
48
+ this.#config.maxScore,
49
+ record.score + recovery
50
+ );
51
+
52
+ return recoveredScore;
53
+ }
54
+
55
+ /**
56
+ * Record a successful request for an account
57
+ * @param {string} email - Account email
58
+ */
59
+ recordSuccess(email) {
60
+ const currentScore = this.getScore(email);
61
+ const newScore = Math.min(
62
+ this.#config.maxScore,
63
+ currentScore + this.#config.successReward
64
+ );
65
+ this.#scores.set(email, {
66
+ score: newScore,
67
+ lastUpdated: Date.now(),
68
+ consecutiveFailures: 0 // Reset on success
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Record a rate limit for an account
74
+ * @param {string} email - Account email
75
+ */
76
+ recordRateLimit(email) {
77
+ const record = this.#scores.get(email);
78
+ const currentScore = this.getScore(email);
79
+ const newScore = Math.max(
80
+ 0,
81
+ currentScore + this.#config.rateLimitPenalty
82
+ );
83
+ this.#scores.set(email, {
84
+ score: newScore,
85
+ lastUpdated: Date.now(),
86
+ consecutiveFailures: (record?.consecutiveFailures ?? 0) + 1
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Record a failure for an account
92
+ * @param {string} email - Account email
93
+ */
94
+ recordFailure(email) {
95
+ const record = this.#scores.get(email);
96
+ const currentScore = this.getScore(email);
97
+ const newScore = Math.max(
98
+ 0,
99
+ currentScore + this.#config.failurePenalty
100
+ );
101
+ this.#scores.set(email, {
102
+ score: newScore,
103
+ lastUpdated: Date.now(),
104
+ consecutiveFailures: (record?.consecutiveFailures ?? 0) + 1
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Check if an account is usable based on health score
110
+ * @param {string} email - Account email
111
+ * @returns {boolean} True if account health score is above minimum threshold
112
+ */
113
+ isUsable(email) {
114
+ return this.getScore(email) >= this.#config.minUsable;
115
+ }
116
+
117
+ /**
118
+ * Get the minimum usable score threshold
119
+ * @returns {number} Minimum score for an account to be usable
120
+ */
121
+ getMinUsable() {
122
+ return this.#config.minUsable;
123
+ }
124
+
125
+ /**
126
+ * Get the maximum score cap
127
+ * @returns {number} Maximum health score
128
+ */
129
+ getMaxScore() {
130
+ return this.#config.maxScore;
131
+ }
132
+
133
+ /**
134
+ * Reset the score for an account (e.g., after re-authentication)
135
+ * @param {string} email - Account email
136
+ */
137
+ reset(email) {
138
+ this.#scores.set(email, {
139
+ score: this.#config.initial,
140
+ lastUpdated: Date.now(),
141
+ consecutiveFailures: 0
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Get the consecutive failure count for an account
147
+ * @param {string} email - Account email
148
+ * @returns {number} Number of consecutive failures
149
+ */
150
+ getConsecutiveFailures(email) {
151
+ return this.#scores.get(email)?.consecutiveFailures ?? 0;
152
+ }
153
+
154
+ /**
155
+ * Clear all tracked scores
156
+ */
157
+ clear() {
158
+ this.#scores.clear();
159
+ }
160
+ }
161
+
162
+ export default HealthTracker;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Trackers Index
3
+ *
4
+ * Exports all tracker classes for account selection strategies.
5
+ */
6
+
7
+ export { HealthTracker } from './health-tracker.js';
8
+ export { TokenBucketTracker } from './token-bucket-tracker.js';
9
+ export { QuotaTracker } from './quota-tracker.js';
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Quota Tracker
3
+ *
4
+ * Tracks per-account quota levels to prioritize accounts with available quota.
5
+ * Uses quota data from account.quota.models[modelId].remainingFraction.
6
+ * Accounts below critical threshold are excluded from selection.
7
+ */
8
+
9
+ // Default configuration
10
+ const DEFAULT_CONFIG = {
11
+ lowThreshold: 0.10, // 10% - reduce score
12
+ criticalThreshold: 0.05, // 5% - exclude from candidates
13
+ staleMs: 300000, // 5 min - max age of quota data to trust
14
+ unknownScore: 50 // Score for accounts with unknown quota
15
+ };
16
+
17
+ export class QuotaTracker {
18
+ #config;
19
+
20
+ /**
21
+ * Create a new QuotaTracker
22
+ * @param {Object} config - Quota tracker configuration
23
+ */
24
+ constructor(config = {}) {
25
+ this.#config = { ...DEFAULT_CONFIG, ...config };
26
+ }
27
+
28
+ /**
29
+ * Get the quota fraction for an account and model
30
+ * @param {Object} account - Account object
31
+ * @param {string} modelId - Model ID to check
32
+ * @returns {number|null} Remaining fraction (0-1) or null if unknown
33
+ */
34
+ getQuotaFraction(account, modelId) {
35
+ if (!account?.quota?.models?.[modelId]) return null;
36
+ const fraction = account.quota.models[modelId].remainingFraction;
37
+ return typeof fraction === 'number' ? fraction : null;
38
+ }
39
+
40
+ /**
41
+ * Check if quota data is fresh enough to be trusted
42
+ * @param {Object} account - Account object
43
+ * @returns {boolean} True if quota data is fresh
44
+ */
45
+ isQuotaFresh(account) {
46
+ if (!account?.quota?.lastChecked) return false;
47
+ return (Date.now() - account.quota.lastChecked) < this.#config.staleMs;
48
+ }
49
+
50
+ /**
51
+ * Check if an account has critically low quota for a model
52
+ * @param {Object} account - Account object
53
+ * @param {string} modelId - Model ID to check
54
+ * @returns {boolean} True if quota is at or below critical threshold
55
+ */
56
+ isQuotaCritical(account, modelId) {
57
+ const fraction = this.getQuotaFraction(account, modelId);
58
+ // Unknown quota = not critical (assume OK)
59
+ if (fraction === null) return false;
60
+ // Only apply critical check if data is fresh
61
+ if (!this.isQuotaFresh(account)) return false;
62
+ return fraction <= this.#config.criticalThreshold;
63
+ }
64
+
65
+ /**
66
+ * Check if an account has low (but not critical) quota for a model
67
+ * @param {Object} account - Account object
68
+ * @param {string} modelId - Model ID to check
69
+ * @returns {boolean} True if quota is below low threshold but above critical
70
+ */
71
+ isQuotaLow(account, modelId) {
72
+ const fraction = this.getQuotaFraction(account, modelId);
73
+ if (fraction === null) return false;
74
+ return fraction <= this.#config.lowThreshold && fraction > this.#config.criticalThreshold;
75
+ }
76
+
77
+ /**
78
+ * Get a score (0-100) for an account based on quota
79
+ * Higher score = more quota available
80
+ * @param {Object} account - Account object
81
+ * @param {string} modelId - Model ID to check
82
+ * @returns {number} Score from 0-100
83
+ */
84
+ getScore(account, modelId) {
85
+ const fraction = this.getQuotaFraction(account, modelId);
86
+
87
+ // Unknown quota = middle score
88
+ if (fraction === null) {
89
+ return this.#config.unknownScore;
90
+ }
91
+
92
+ // Convert fraction (0-1) to score (0-100)
93
+ let score = fraction * 100;
94
+
95
+ // Apply small penalty for stale data (reduce confidence)
96
+ if (!this.isQuotaFresh(account)) {
97
+ score *= 0.9; // 10% penalty for stale data
98
+ }
99
+
100
+ return score;
101
+ }
102
+
103
+ /**
104
+ * Get the critical threshold
105
+ * @returns {number} Critical threshold (0-1)
106
+ */
107
+ getCriticalThreshold() {
108
+ return this.#config.criticalThreshold;
109
+ }
110
+
111
+ /**
112
+ * Get the low threshold
113
+ * @returns {number} Low threshold (0-1)
114
+ */
115
+ getLowThreshold() {
116
+ return this.#config.lowThreshold;
117
+ }
118
+ }
119
+
120
+ export default QuotaTracker;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Token Bucket Tracker
3
+ *
4
+ * Client-side rate limiting using the token bucket algorithm.
5
+ * Each account has a bucket of tokens that regenerate over time.
6
+ * Requests consume tokens; accounts without tokens are deprioritized.
7
+ */
8
+
9
+ // Default configuration (matches opencode-cloudcode-auth)
10
+ const DEFAULT_CONFIG = {
11
+ maxTokens: 50, // Maximum token capacity
12
+ tokensPerMinute: 6, // Regeneration rate
13
+ initialTokens: 50 // Starting tokens
14
+ };
15
+
16
+ export class TokenBucketTracker {
17
+ #buckets = new Map(); // email -> { tokens, lastUpdated }
18
+ #config;
19
+
20
+ /**
21
+ * Create a new TokenBucketTracker
22
+ * @param {Object} config - Token bucket configuration
23
+ */
24
+ constructor(config = {}) {
25
+ this.#config = { ...DEFAULT_CONFIG, ...config };
26
+ }
27
+
28
+ /**
29
+ * Get the current token count for an account
30
+ * @param {string} email - Account email
31
+ * @returns {number} Current token count (with regeneration applied)
32
+ */
33
+ getTokens(email) {
34
+ const bucket = this.#buckets.get(email);
35
+ if (!bucket) {
36
+ return this.#config.initialTokens;
37
+ }
38
+
39
+ // Apply token regeneration based on time elapsed
40
+ const now = Date.now();
41
+ const minutesElapsed = (now - bucket.lastUpdated) / (1000 * 60);
42
+ const regenerated = minutesElapsed * this.#config.tokensPerMinute;
43
+ const currentTokens = Math.min(
44
+ this.#config.maxTokens,
45
+ bucket.tokens + regenerated
46
+ );
47
+
48
+ return currentTokens;
49
+ }
50
+
51
+ /**
52
+ * Check if an account has tokens available
53
+ * @param {string} email - Account email
54
+ * @returns {boolean} True if account has at least 1 token
55
+ */
56
+ hasTokens(email) {
57
+ return this.getTokens(email) >= 1;
58
+ }
59
+
60
+ /**
61
+ * Consume a token from an account's bucket
62
+ * @param {string} email - Account email
63
+ * @returns {boolean} True if token was consumed, false if no tokens available
64
+ */
65
+ consume(email) {
66
+ const currentTokens = this.getTokens(email);
67
+ if (currentTokens < 1) {
68
+ return false;
69
+ }
70
+
71
+ this.#buckets.set(email, {
72
+ tokens: currentTokens - 1,
73
+ lastUpdated: Date.now()
74
+ });
75
+ return true;
76
+ }
77
+
78
+ /**
79
+ * Refund a token to an account's bucket (e.g., on request failure before processing)
80
+ * @param {string} email - Account email
81
+ */
82
+ refund(email) {
83
+ const currentTokens = this.getTokens(email);
84
+ const newTokens = Math.min(
85
+ this.#config.maxTokens,
86
+ currentTokens + 1
87
+ );
88
+ this.#buckets.set(email, {
89
+ tokens: newTokens,
90
+ lastUpdated: Date.now()
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Get the maximum token capacity
96
+ * @returns {number} Maximum tokens per bucket
97
+ */
98
+ getMaxTokens() {
99
+ return this.#config.maxTokens;
100
+ }
101
+
102
+ /**
103
+ * Reset the bucket for an account
104
+ * @param {string} email - Account email
105
+ */
106
+ reset(email) {
107
+ this.#buckets.set(email, {
108
+ tokens: this.#config.initialTokens,
109
+ lastUpdated: Date.now()
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Clear all tracked buckets
115
+ */
116
+ clear() {
117
+ this.#buckets.clear();
118
+ }
119
+
120
+ /**
121
+ * Get time in milliseconds until next token is available for an account
122
+ * @param {string} email - Account email
123
+ * @returns {number} Milliseconds until next token, 0 if tokens available now
124
+ */
125
+ getTimeUntilNextToken(email) {
126
+ const currentTokens = this.getTokens(email);
127
+ if (currentTokens >= 1) {
128
+ return 0;
129
+ }
130
+
131
+ // Calculate time to regenerate 1 token
132
+ const tokensNeeded = 1 - currentTokens;
133
+ const minutesNeeded = tokensNeeded / this.#config.tokensPerMinute;
134
+ return Math.ceil(minutesNeeded * 60 * 1000);
135
+ }
136
+
137
+ /**
138
+ * Get the minimum time until any account in the list has a token
139
+ * @param {Array<string>} emails - List of account emails
140
+ * @returns {number} Minimum milliseconds until any account has a token
141
+ */
142
+ getMinTimeUntilToken(emails) {
143
+ if (emails.length === 0) return 0;
144
+
145
+ let minWait = Infinity;
146
+ for (const email of emails) {
147
+ const wait = this.getTimeUntilNextToken(email);
148
+ if (wait === 0) return 0;
149
+ minWait = Math.min(minWait, wait);
150
+ }
151
+ return minWait === Infinity ? 0 : minWait;
152
+ }
153
+ }
154
+
155
+ export default TokenBucketTracker;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * SQLite Database Access Module
3
+ * Provides cross-platform database operations for Cloud Code IDE state.
4
+ *
5
+ * Uses better-sqlite3 for:
6
+ * - Windows compatibility (no CLI dependency)
7
+ * - Native performance
8
+ * - Synchronous API (simple error handling)
9
+ *
10
+ * Includes auto-rebuild capability for handling Node.js version updates
11
+ * that cause native module incompatibility.
12
+ */
13
+
14
+ import { createRequire } from 'module';
15
+ import { CLOUDCODE_DB_PATH } from '../constants.js';
16
+ import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js';
17
+ import { logger } from '../utils/logger.js';
18
+ import { NativeModuleError } from '../errors.js';
19
+
20
+ const require = createRequire(import.meta.url);
21
+
22
+ // Lazy-loaded Database constructor
23
+ let Database = null;
24
+ let moduleLoadError = null;
25
+
26
+ /**
27
+ * Load the better-sqlite3 module with auto-rebuild on version mismatch
28
+ * Uses synchronous require to maintain API compatibility
29
+ * @returns {Function} The Database constructor
30
+ * @throws {Error} If module cannot be loaded even after rebuild
31
+ */
32
+ function loadDatabaseModule() {
33
+ // Return cached module if already loaded
34
+ if (Database) return Database;
35
+
36
+ // Re-throw cached error if previous load failed permanently
37
+ if (moduleLoadError) throw moduleLoadError;
38
+
39
+ try {
40
+ Database = require('better-sqlite3');
41
+ return Database;
42
+ } catch (error) {
43
+ if (isModuleVersionError(error)) {
44
+ logger.warn('[Database] Native module version mismatch detected');
45
+
46
+ if (attemptAutoRebuild(error)) {
47
+ // Clear require cache and retry
48
+ try {
49
+ const resolvedPath = require.resolve('better-sqlite3');
50
+ // Clear the module and all its dependencies from cache
51
+ clearRequireCache(resolvedPath, require.cache);
52
+
53
+ Database = require('better-sqlite3');
54
+ logger.success('[Database] Module reloaded successfully after rebuild');
55
+ return Database;
56
+ } catch (retryError) {
57
+ // Rebuild succeeded but reload failed - user needs to restart
58
+ moduleLoadError = new NativeModuleError(
59
+ 'Native module rebuild completed. Please restart the server to apply the fix.',
60
+ true, // rebuildSucceeded
61
+ true // restartRequired
62
+ );
63
+ logger.info('[Database] Rebuild succeeded - server restart required');
64
+ throw moduleLoadError;
65
+ }
66
+ } else {
67
+ moduleLoadError = new NativeModuleError(
68
+ 'Failed to auto-rebuild native module. Please run manually:\n' +
69
+ ' npm rebuild better-sqlite3\n' +
70
+ 'Or if using npx, find the package location in the error and run:\n' +
71
+ ' cd /path/to/better-sqlite3 && npm rebuild',
72
+ false, // rebuildSucceeded
73
+ false // restartRequired
74
+ );
75
+ throw moduleLoadError;
76
+ }
77
+ }
78
+
79
+ // Non-version-mismatch error, just throw it
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Query Cloud Code IDE database for authentication status
86
+ * @param {string} [dbPath] - Optional custom database path
87
+ * @returns {Object} Parsed auth data with apiKey, email, name, etc.
88
+ * @throws {Error} If database doesn't exist, query fails, or no auth status found
89
+ */
90
+ export function getAuthStatus(dbPath = CLOUDCODE_DB_PATH) {
91
+ const Db = loadDatabaseModule();
92
+ let db;
93
+ try {
94
+ // Open database in read-only mode
95
+ db = new Db(dbPath, {
96
+ readonly: true,
97
+ fileMustExist: true
98
+ });
99
+
100
+ // Prepare and execute query
101
+ const stmt = db.prepare(
102
+ "SELECT value FROM ItemTable WHERE key = 'cloudcodeAuthStatus'"
103
+ );
104
+ const row = stmt.get();
105
+
106
+ if (!row || !row.value) {
107
+ throw new Error('No auth status found in database');
108
+ }
109
+
110
+ // Parse JSON value
111
+ const authData = JSON.parse(row.value);
112
+
113
+ if (!authData.apiKey) {
114
+ throw new Error('Auth data missing apiKey field');
115
+ }
116
+
117
+ return authData;
118
+ } catch (error) {
119
+ // Enhance error messages for common issues
120
+ if (error.code === 'SQLITE_CANTOPEN') {
121
+ throw new Error(
122
+ `Database not found at ${dbPath}. ` +
123
+ 'Make sure the Cloud Code IDE is installed and you are logged in.'
124
+ );
125
+ }
126
+ // Re-throw with context if not already our error
127
+ if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
128
+ throw error;
129
+ }
130
+ // Re-throw native module errors from loadDatabaseModule without wrapping
131
+ if (error instanceof NativeModuleError) {
132
+ throw error;
133
+ }
134
+ throw new Error(`Failed to read Cloud Code IDE database: ${error.message}`);
135
+ } finally {
136
+ // Always close database connection
137
+ if (db) {
138
+ db.close();
139
+ }
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Check if database exists and is accessible
145
+ * @param {string} [dbPath] - Optional custom database path
146
+ * @returns {boolean} True if database exists and can be opened
147
+ */
148
+ export function isDatabaseAccessible(dbPath = CLOUDCODE_DB_PATH) {
149
+ let db;
150
+ try {
151
+ const Db = loadDatabaseModule();
152
+ db = new Db(dbPath, {
153
+ readonly: true,
154
+ fileMustExist: true
155
+ });
156
+ return true;
157
+ } catch {
158
+ return false;
159
+ } finally {
160
+ if (db) {
161
+ db.close();
162
+ }
163
+ }
164
+ }
165
+
166
+ export default {
167
+ getAuthStatus,
168
+ isDatabaseAccessible
169
+ };