@tamyla/clodo-framework 3.0.11 → 3.0.12
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [3.0.12](https://github.com/tamylaa/clodo-framework/compare/v3.0.11...v3.0.12) (2025-10-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Add graceful API token permission handling and validation ([6c973b0](https://github.com/tamylaa/clodo-framework/commit/6c973b077b6e2a80b7a6d93f0b39070925bb89af))
|
|
7
|
+
* Add missing exists() method to WranglerConfigManager class ([44ee17c](https://github.com/tamylaa/clodo-framework/commit/44ee17c8931db085ccef502e7e7ac15209b222a5))
|
|
8
|
+
* Ensure wrangler uses correct account for API token operations ([f671b10](https://github.com/tamylaa/clodo-framework/commit/f671b1004057b94dd8ba55c5c1f3c2d5bca54706))
|
|
9
|
+
|
|
1
10
|
## [3.0.11](https://github.com/tamylaa/clodo-framework/compare/v3.0.10...v3.0.11) (2025-10-14)
|
|
2
11
|
|
|
3
12
|
|
|
@@ -39,6 +39,13 @@ export class MultiDomainOrchestrator {
|
|
|
39
39
|
this.cloudflareToken = options.cloudflareToken;
|
|
40
40
|
this.cloudflareAccountId = options.cloudflareAccountId;
|
|
41
41
|
|
|
42
|
+
// Configure wrangler to use API token when available
|
|
43
|
+
// This ensures all wrangler operations use the same account as API operations
|
|
44
|
+
if (this.cloudflareToken) {
|
|
45
|
+
process.env.CLOUDFLARE_API_TOKEN = this.cloudflareToken;
|
|
46
|
+
console.log(`🔑 Configured wrangler to use API token authentication`);
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
// Initialize modular components
|
|
43
50
|
this.domainResolver = new DomainResolver({
|
|
44
51
|
environment: this.environment,
|
|
@@ -74,7 +81,8 @@ export class MultiDomainOrchestrator {
|
|
|
74
81
|
this.wranglerConfigManager = new WranglerConfigManager({
|
|
75
82
|
projectRoot: this.servicePath,
|
|
76
83
|
dryRun: this.dryRun,
|
|
77
|
-
verbose: options.verbose || false
|
|
84
|
+
verbose: options.verbose || false,
|
|
85
|
+
accountId: this.cloudflareAccountId
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
// ConfigurationValidator is a static class - don't instantiate
|
|
@@ -283,26 +291,60 @@ export class MultiDomainOrchestrator {
|
|
|
283
291
|
// Use API-based operations if credentials are available
|
|
284
292
|
if (this.cloudflareToken && this.cloudflareAccountId) {
|
|
285
293
|
console.log(` 🔑 Using API token authentication for account: ${this.cloudflareAccountId}`);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
accountId: this.cloudflareAccountId
|
|
289
|
-
});
|
|
290
|
-
if (exists) {
|
|
291
|
-
console.log(` ✅ Database already exists: ${databaseName}`);
|
|
292
|
-
databaseId = await getDatabaseId(databaseName, {
|
|
293
|
-
apiToken: this.cloudflareToken,
|
|
294
|
-
accountId: this.cloudflareAccountId
|
|
295
|
-
});
|
|
296
|
-
console.log(` 📊 Existing Database ID: ${databaseId}`);
|
|
297
|
-
} else {
|
|
298
|
-
console.log(` 📦 Creating database: ${databaseName}`);
|
|
299
|
-
databaseId = await createDatabase(databaseName, {
|
|
294
|
+
try {
|
|
295
|
+
exists = await databaseExists(databaseName, {
|
|
300
296
|
apiToken: this.cloudflareToken,
|
|
301
297
|
accountId: this.cloudflareAccountId
|
|
302
298
|
});
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
299
|
+
if (exists) {
|
|
300
|
+
console.log(` ✅ Database already exists: ${databaseName}`);
|
|
301
|
+
databaseId = await getDatabaseId(databaseName, {
|
|
302
|
+
apiToken: this.cloudflareToken,
|
|
303
|
+
accountId: this.cloudflareAccountId
|
|
304
|
+
});
|
|
305
|
+
console.log(` 📊 Existing Database ID: ${databaseId}`);
|
|
306
|
+
} else {
|
|
307
|
+
console.log(` 📦 Creating database: ${databaseName}`);
|
|
308
|
+
databaseId = await createDatabase(databaseName, {
|
|
309
|
+
apiToken: this.cloudflareToken,
|
|
310
|
+
accountId: this.cloudflareAccountId
|
|
311
|
+
});
|
|
312
|
+
console.log(` ✅ Database created: ${databaseName}`);
|
|
313
|
+
console.log(` 📊 Database ID: ${databaseId}`);
|
|
314
|
+
created = true;
|
|
315
|
+
}
|
|
316
|
+
} catch (apiError) {
|
|
317
|
+
// Check if this is an authentication or permission error
|
|
318
|
+
if (apiError.message.includes('permission denied') || apiError.message.includes('403') || apiError.message.includes('authentication failed') || apiError.message.includes('401')) {
|
|
319
|
+
if (apiError.message.includes('401')) {
|
|
320
|
+
console.log(` ❌ API token authentication failed (invalid/expired token)`);
|
|
321
|
+
console.log(` 🔗 Check/create token at: https://dash.cloudflare.com/profile/api-tokens`);
|
|
322
|
+
} else {
|
|
323
|
+
console.log(` ⚠️ API token lacks D1 database permissions`);
|
|
324
|
+
console.log(` 💡 Required permission: 'Cloudflare D1:Edit'`);
|
|
325
|
+
console.log(` 🔗 Update token at: https://dash.cloudflare.com/profile/api-tokens`);
|
|
326
|
+
}
|
|
327
|
+
console.log(` 🔄 Falling back to OAuth authentication...`);
|
|
328
|
+
console.log(` ⚠️ WARNING: OAuth uses your personal account, not the API token account!`);
|
|
329
|
+
|
|
330
|
+
// Fall back to OAuth-based operations with warning
|
|
331
|
+
console.log(` 🔐 Using OAuth authentication (wrangler CLI)`);
|
|
332
|
+
exists = await databaseExists(databaseName);
|
|
333
|
+
if (exists) {
|
|
334
|
+
console.log(` ✅ Database already exists: ${databaseName}`);
|
|
335
|
+
databaseId = await getDatabaseId(databaseName);
|
|
336
|
+
console.log(` 📊 Existing Database ID: ${databaseId}`);
|
|
337
|
+
} else {
|
|
338
|
+
console.log(` 📦 Creating database: ${databaseName}`);
|
|
339
|
+
databaseId = await createDatabase(databaseName);
|
|
340
|
+
console.log(` ✅ Database created: ${databaseName}`);
|
|
341
|
+
console.log(` 📊 Database ID: ${databaseId}`);
|
|
342
|
+
created = true;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Re-throw non-auth/permission errors
|
|
346
|
+
throw apiError;
|
|
347
|
+
}
|
|
306
348
|
}
|
|
307
349
|
} else {
|
|
308
350
|
// Fallback to CLI-based operations (OAuth)
|
|
@@ -333,6 +375,11 @@ export class MultiDomainOrchestrator {
|
|
|
333
375
|
console.log(` 📁 Service path: ${this.servicePath}`);
|
|
334
376
|
console.log(` 📁 Current working directory: ${process.cwd()}`);
|
|
335
377
|
try {
|
|
378
|
+
// Set account_id if API credentials are available
|
|
379
|
+
if (this.cloudflareAccountId) {
|
|
380
|
+
await this.wranglerConfigManager.setAccountId(this.cloudflareAccountId);
|
|
381
|
+
}
|
|
382
|
+
|
|
336
383
|
// Ensure environment section exists
|
|
337
384
|
await this.wranglerConfigManager.ensureEnvironment(this.environment);
|
|
338
385
|
|
|
@@ -386,8 +386,25 @@ export class InputCollector {
|
|
|
386
386
|
CloudflareAPI
|
|
387
387
|
} = await import('../utils/cloudflare/api.js');
|
|
388
388
|
const cfApi = new CloudflareAPI(token);
|
|
389
|
-
const
|
|
390
|
-
if (
|
|
389
|
+
const tokenCheck = await cfApi.verifyToken();
|
|
390
|
+
if (tokenCheck.valid) {
|
|
391
|
+
// Check D1 permissions
|
|
392
|
+
const permissionCheck = await cfApi.checkD1Permissions();
|
|
393
|
+
if (!permissionCheck.hasPermission) {
|
|
394
|
+
console.log(chalk.yellow(`⚠️ ${permissionCheck.error}`));
|
|
395
|
+
console.log(chalk.white(' 💡 You can update permissions at: https://dash.cloudflare.com/profile/api-tokens'));
|
|
396
|
+
console.log(chalk.white(' 💡 Or continue and the framework will fall back to OAuth authentication'));
|
|
397
|
+
console.log('');
|
|
398
|
+
const {
|
|
399
|
+
askYesNo
|
|
400
|
+
} = await import('../utils/interactive-prompts.js');
|
|
401
|
+
const continueAnyway = await askYesNo('Continue with limited API token permissions?', false);
|
|
402
|
+
if (!continueAnyway) {
|
|
403
|
+
console.log(chalk.blue('Please update your API token permissions and try again.'));
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
console.log(chalk.yellow('⚠️ Proceeding with limited permissions - database operations will use OAuth'));
|
|
407
|
+
}
|
|
391
408
|
console.log(chalk.green('✓ API token verified successfully'));
|
|
392
409
|
return token;
|
|
393
410
|
}
|
|
@@ -34,7 +34,20 @@ export class CloudflareAPI {
|
|
|
34
34
|
const data = await response.json();
|
|
35
35
|
if (!response.ok) {
|
|
36
36
|
const errorMsg = data.errors?.[0]?.message || 'Unknown error';
|
|
37
|
-
|
|
37
|
+
const statusCode = response.status;
|
|
38
|
+
|
|
39
|
+
// Provide specific guidance for common authentication/permission errors
|
|
40
|
+
if (statusCode === 401) {
|
|
41
|
+
throw new Error(`Cloudflare API authentication failed (401). Your API token may be invalid or expired. Please check your token at https://dash.cloudflare.com/profile/api-tokens`);
|
|
42
|
+
}
|
|
43
|
+
if (statusCode === 403) {
|
|
44
|
+
// Check if this is a D1-related endpoint to provide specific guidance
|
|
45
|
+
if (endpoint.includes('/d1/')) {
|
|
46
|
+
throw new Error(`Cloudflare API permission denied (403). Your API token lacks D1 database permissions. Required permissions: 'Cloudflare D1:Edit'. Update your token at https://dash.cloudflare.com/profile/api-tokens`);
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Cloudflare API permission denied (403). Your API token lacks required permissions for this operation. Please check your token permissions at https://dash.cloudflare.com/profile/api-tokens`);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Cloudflare API error: ${errorMsg} (${statusCode})`);
|
|
38
51
|
}
|
|
39
52
|
if (!data.success) {
|
|
40
53
|
const errorMsg = data.errors?.[0]?.message || 'Request failed';
|
|
@@ -65,6 +78,33 @@ export class CloudflareAPI {
|
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Check if API token has D1 database permissions
|
|
83
|
+
* @returns {Promise<Object>} Permission check result
|
|
84
|
+
*/
|
|
85
|
+
async checkD1Permissions() {
|
|
86
|
+
try {
|
|
87
|
+
// Try to list D1 databases - this will fail if no D1 permissions
|
|
88
|
+
// We use a dummy account ID that should fail safely if permissions are missing
|
|
89
|
+
await this.request('/accounts/dummy/d1/database');
|
|
90
|
+
return {
|
|
91
|
+
hasPermission: true
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error.message.includes('403') || error.message.includes('permission denied')) {
|
|
95
|
+
return {
|
|
96
|
+
hasPermission: false,
|
|
97
|
+
error: 'API token lacks D1 database permissions. Required: Cloudflare D1:Edit'
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// If it's a different error (like invalid account), assume permissions are OK
|
|
101
|
+
// The actual permission check happens during real operations
|
|
102
|
+
return {
|
|
103
|
+
hasPermission: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
68
108
|
/**
|
|
69
109
|
* List all zones (domains) accessible with this API token
|
|
70
110
|
* @param {Object} options - Query options
|
|
@@ -19,11 +19,13 @@ export class WranglerConfigManager {
|
|
|
19
19
|
this.projectRoot = dirname(options);
|
|
20
20
|
this.dryRun = false;
|
|
21
21
|
this.verbose = false;
|
|
22
|
+
this.accountId = null;
|
|
22
23
|
} else {
|
|
23
24
|
this.projectRoot = options.projectRoot || process.cwd();
|
|
24
25
|
this.configPath = options.configPath || join(this.projectRoot, 'wrangler.toml');
|
|
25
26
|
this.dryRun = options.dryRun || false;
|
|
26
27
|
this.verbose = options.verbose || false;
|
|
28
|
+
this.accountId = options.accountId || null;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -52,6 +54,19 @@ export class WranglerConfigManager {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Check if wrangler.toml file exists
|
|
59
|
+
* @returns {Promise<boolean>} True if file exists, false otherwise
|
|
60
|
+
*/
|
|
61
|
+
async exists() {
|
|
62
|
+
try {
|
|
63
|
+
await access(this.configPath, constants.F_OK);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
/**
|
|
56
71
|
* Write configuration back to wrangler.toml
|
|
57
72
|
* @param {Object} config - Configuration object to write
|
|
@@ -82,16 +97,26 @@ export class WranglerConfigManager {
|
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
/**
|
|
85
|
-
*
|
|
86
|
-
* @
|
|
100
|
+
* Set account_id in wrangler.toml
|
|
101
|
+
* @param {string} accountId - Cloudflare account ID
|
|
102
|
+
* @returns {Promise<boolean>} True if account_id was set
|
|
87
103
|
*/
|
|
88
|
-
async
|
|
89
|
-
|
|
90
|
-
await access(this.configPath, constants.F_OK);
|
|
91
|
-
return true;
|
|
92
|
-
} catch {
|
|
104
|
+
async setAccountId(accountId) {
|
|
105
|
+
if (!accountId) {
|
|
93
106
|
return false;
|
|
94
107
|
}
|
|
108
|
+
const config = await this.readConfig();
|
|
109
|
+
if (config.account_id === accountId) {
|
|
110
|
+
if (this.verbose) {
|
|
111
|
+
console.log(` ✓ account_id already set to ${accountId}`);
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
console.log(` 📝 Setting account_id to ${accountId} in wrangler.toml`);
|
|
116
|
+
config.account_id = accountId;
|
|
117
|
+
await this.writeConfig(config);
|
|
118
|
+
console.log(` ✅ account_id updated in wrangler.toml`);
|
|
119
|
+
return true;
|
|
95
120
|
}
|
|
96
121
|
|
|
97
122
|
/**
|