@tamyla/clodo-framework 3.0.10 → 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,19 @@
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
+
10
+ ## [3.0.11](https://github.com/tamylaa/clodo-framework/compare/v3.0.10...v3.0.11) (2025-10-14)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Update all bin/ imports from src/ to dist/ for published package compatibility ([c476528](https://github.com/tamylaa/clodo-framework/commit/c476528b575cf9d6338a967e740252ed4d41f66f))
16
+
1
17
  ## [3.0.10](https://github.com/tamylaa/clodo-framework/compare/v3.0.9...v3.0.10) (2025-10-14)
2
18
 
3
19
 
@@ -232,7 +232,7 @@ export async function listDatabases(options = {}) {
232
232
 
233
233
  // Use API-based operation if credentials provided
234
234
  if (apiToken && accountId) {
235
- const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
235
+ const { CloudflareAPI } = await import('../../../dist/utils/cloudflare/api.js');
236
236
  const cf = new CloudflareAPI(apiToken);
237
237
  return await cf.listD1Databases(accountId);
238
238
  }
@@ -251,7 +251,7 @@ export async function databaseExists(databaseName, options = {}) {
251
251
 
252
252
  // Use API-based operation if credentials provided
253
253
  if (apiToken && accountId) {
254
- const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
254
+ const { CloudflareAPI } = await import('../../../dist/utils/cloudflare/api.js');
255
255
  const cf = new CloudflareAPI(apiToken);
256
256
  return await cf.d1DatabaseExists(accountId, databaseName);
257
257
  }
@@ -270,7 +270,7 @@ export async function createDatabase(name, options = {}) {
270
270
 
271
271
  // Use API-based operation if credentials provided
272
272
  if (apiToken && accountId) {
273
- const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
273
+ const { CloudflareAPI } = await import('../../../dist/utils/cloudflare/api.js');
274
274
  const cf = new CloudflareAPI(apiToken);
275
275
  const result = await cf.createD1Database(accountId, name);
276
276
  return result.uuid; // Return UUID to match CLI behavior
@@ -355,7 +355,7 @@ export async function getDatabaseId(databaseName, options = {}) {
355
355
 
356
356
  // Use API-based operation if credentials provided
357
357
  if (apiToken && accountId) {
358
- const { CloudflareAPI } = await import('../../../src/utils/cloudflare/api.js');
358
+ const { CloudflareAPI } = await import('../../../dist/utils/cloudflare/api.js');
359
359
  const cf = new CloudflareAPI(apiToken);
360
360
  const db = await cf.getD1Database(accountId, databaseName);
361
361
  return db?.uuid || null;
@@ -20,7 +20,7 @@ export class DatabaseConnectionManager {
20
20
  */
21
21
  async initialize() {
22
22
  // Import framework config for consistent database connection settings
23
- const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
23
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
24
24
  const timing = frameworkConfig.getTiming();
25
25
  const database = frameworkConfig.getDatabaseConfig();
26
26
 
@@ -115,7 +115,7 @@ export class DatabaseOrchestrator {
115
115
  */
116
116
  async initialize() {
117
117
  // Import framework config for consistent timing and database settings
118
- const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
118
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
119
119
  const timing = frameworkConfig.getTiming();
120
120
  const database = frameworkConfig.getDatabaseConfig();
121
121
 
@@ -556,7 +556,7 @@ export class DeploymentValidator {
556
556
 
557
557
  try {
558
558
  // Import WranglerDeployer for D1 validation capabilities
559
- const { WranglerDeployer } = await import('../../../src/deployment/wrangler-deployer.js');
559
+ const { WranglerDeployer } = await import('../../../dist/deployment/wrangler-deployer.js');
560
560
 
561
561
  // Check if this is a framework-level validation (no specific service)
562
562
  if (!this.options?.servicePath) {
@@ -13,7 +13,7 @@ import http from 'http';
13
13
  const execAsync = promisify(exec);
14
14
 
15
15
  // Load framework configuration
16
- const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
16
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
17
17
  const timing = frameworkConfig.getTiming();
18
18
 
19
19
  function makeHttpRequest(url, method = 'GET', timeout = 5000) {
@@ -65,7 +65,7 @@ export class EnhancedSecretManager {
65
65
  */
66
66
  async initialize() {
67
67
  // Import framework config for consistent timing and retry settings
68
- const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
68
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
69
69
  const timing = frameworkConfig.getTiming();
70
70
  const security = frameworkConfig.getSecurity();
71
71
  const configPaths = frameworkConfig.getPaths();
@@ -34,7 +34,7 @@ export class SecureTokenManager {
34
34
  async initialize() {
35
35
  try {
36
36
  // Load framework configuration
37
- const { frameworkConfig } = await import('../../../src/utils/framework-config.js');
37
+ const { frameworkConfig } = await import('../../../dist/utils/framework-config.js');
38
38
  this.frameworkConfig = frameworkConfig;
39
39
 
40
40
  // Update paths with framework config
@@ -470,7 +470,7 @@ export class DeploymentValidator {
470
470
  // Import WranglerDeployer for D1 validation capabilities
471
471
  const {
472
472
  WranglerDeployer
473
- } = await import('../../../src/deployment/wrangler-deployer.js');
473
+ } = await import('../../../dist/deployment/wrangler-deployer.js');
474
474
 
475
475
  // Check if this is a framework-level validation (no specific service)
476
476
  if (!this.options?.servicePath) {
@@ -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
  }
@@ -240,7 +240,7 @@ export async function listDatabases(options = {}) {
240
240
  if (apiToken && accountId) {
241
241
  const {
242
242
  CloudflareAPI
243
- } = await import('../../../src/utils/cloudflare/api.js');
243
+ } = await import('../../../dist/utils/cloudflare/api.js');
244
244
  const cf = new CloudflareAPI(apiToken);
245
245
  return await cf.listD1Databases(accountId);
246
246
  }
@@ -265,7 +265,7 @@ export async function databaseExists(databaseName, options = {}) {
265
265
  if (apiToken && accountId) {
266
266
  const {
267
267
  CloudflareAPI
268
- } = await import('../../../src/utils/cloudflare/api.js');
268
+ } = await import('../../../dist/utils/cloudflare/api.js');
269
269
  const cf = new CloudflareAPI(apiToken);
270
270
  return await cf.d1DatabaseExists(accountId, databaseName);
271
271
  }
@@ -288,7 +288,7 @@ export async function createDatabase(name, options = {}) {
288
288
  if (apiToken && accountId) {
289
289
  const {
290
290
  CloudflareAPI
291
- } = await import('../../../src/utils/cloudflare/api.js');
291
+ } = await import('../../../dist/utils/cloudflare/api.js');
292
292
  const cf = new CloudflareAPI(apiToken);
293
293
  const result = await cf.createD1Database(accountId, name);
294
294
  return result.uuid; // Return UUID to match CLI behavior
@@ -403,7 +403,7 @@ export async function getDatabaseId(databaseName, options = {}) {
403
403
  if (apiToken && accountId) {
404
404
  const {
405
405
  CloudflareAPI
406
- } = await import('../../../src/utils/cloudflare/api.js');
406
+ } = await import('../../../dist/utils/cloudflare/api.js');
407
407
  const cf = new CloudflareAPI(apiToken);
408
408
  const db = await cf.getD1Database(accountId, databaseName);
409
409
  return db?.uuid || null;
@@ -21,7 +21,7 @@ export class DatabaseConnectionManager {
21
21
  // Import framework config for consistent database connection settings
22
22
  const {
23
23
  frameworkConfig
24
- } = await import('../../../src/utils/framework-config.js');
24
+ } = await import('../../../dist/utils/framework-config.js');
25
25
  const timing = frameworkConfig.getTiming();
26
26
  const database = frameworkConfig.getDatabaseConfig();
27
27
  this.config = {
@@ -110,7 +110,7 @@ export class DatabaseOrchestrator {
110
110
  // Import framework config for consistent timing and database settings
111
111
  const {
112
112
  frameworkConfig
113
- } = await import('../../../src/utils/framework-config.js');
113
+ } = await import('../../../dist/utils/framework-config.js');
114
114
  const timing = frameworkConfig.getTiming();
115
115
  const database = frameworkConfig.getDatabaseConfig();
116
116
  this.config = {
@@ -470,7 +470,7 @@ export class DeploymentValidator {
470
470
  // Import WranglerDeployer for D1 validation capabilities
471
471
  const {
472
472
  WranglerDeployer
473
- } = await import('../../../src/deployment/wrangler-deployer.js');
473
+ } = await import('../../../dist/deployment/wrangler-deployer.js');
474
474
 
475
475
  // Check if this is a framework-level validation (no specific service)
476
476
  if (!this.options?.servicePath) {
@@ -14,7 +14,7 @@ const execAsync = promisify(exec);
14
14
  // Load framework configuration
15
15
  const {
16
16
  frameworkConfig
17
- } = await import('../../../src/utils/framework-config.js');
17
+ } = await import('../../../dist/utils/framework-config.js');
18
18
  const timing = frameworkConfig.getTiming();
19
19
  function makeHttpRequest(url, method = 'GET', timeout = 5000) {
20
20
  return new Promise((resolve, reject) => {
@@ -105,7 +105,7 @@ export class EnhancedSecretManager {
105
105
  // Import framework config for consistent timing and retry settings
106
106
  const {
107
107
  frameworkConfig
108
- } = await import('../../../src/utils/framework-config.js');
108
+ } = await import('../../../dist/utils/framework-config.js');
109
109
  const timing = frameworkConfig.getTiming();
110
110
  const security = frameworkConfig.getSecurity();
111
111
  const configPaths = frameworkConfig.getPaths();
@@ -36,7 +36,7 @@ export class SecureTokenManager {
36
36
  // Load framework configuration
37
37
  const {
38
38
  frameworkConfig
39
- } = await import('../../../src/utils/framework-config.js');
39
+ } = await import('../../../dist/utils/framework-config.js');
40
40
  this.frameworkConfig = frameworkConfig;
41
41
 
42
42
  // Update paths with framework config
@@ -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.10",
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": [