@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
- exists = await databaseExists(databaseName, {
287
- apiToken: this.cloudflareToken,
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
- console.log(` ✅ Database created: ${databaseName}`);
304
- console.log(` 📊 Database ID: ${databaseId}`);
305
- created = true;
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 isValid = await cfApi.verifyToken();
390
- if (isValid) {
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
- throw new Error(`Cloudflare API error: ${errorMsg} (${response.status})`);
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
- * Check if wrangler.toml exists
86
- * @returns {Promise<boolean>}
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 exists() {
89
- try {
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "3.0.11",
3
+ "version": "3.0.12",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [