@tamyla/clodo-framework 2.0.6 → 2.0.8

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.
@@ -0,0 +1,295 @@
1
+ /**
2
+ * Cloudflare API Client
3
+ * Simple client for fetching zone information and verifying API tokens
4
+ *
5
+ * Used by InputCollector for new deployment flow to:
6
+ * 1. Verify API token is valid
7
+ * 2. Fetch list of user's domains
8
+ * 3. Get zone details (zone_id, account_id, name servers, etc.)
9
+ */
10
+
11
+ export class CloudflareAPI {
12
+ constructor(apiToken) {
13
+ if (!apiToken) {
14
+ throw new Error('Cloudflare API token is required');
15
+ }
16
+ this.apiToken = apiToken;
17
+ this.baseUrl = 'https://api.cloudflare.com/client/v4';
18
+ }
19
+
20
+ /**
21
+ * Make authenticated request to Cloudflare API
22
+ * @private
23
+ */
24
+ async request(endpoint, options = {}) {
25
+ const url = `${this.baseUrl}${endpoint}`;
26
+ const response = await fetch(url, {
27
+ ...options,
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.apiToken}`,
30
+ 'Content-Type': 'application/json',
31
+ ...options.headers
32
+ }
33
+ });
34
+ const data = await response.json();
35
+ if (!response.ok) {
36
+ const errorMsg = data.errors?.[0]?.message || 'Unknown error';
37
+ throw new Error(`Cloudflare API error: ${errorMsg} (${response.status})`);
38
+ }
39
+ if (!data.success) {
40
+ const errorMsg = data.errors?.[0]?.message || 'Request failed';
41
+ throw new Error(`Cloudflare API error: ${errorMsg}`);
42
+ }
43
+ return data;
44
+ }
45
+
46
+ /**
47
+ * Verify that the API token is valid
48
+ * @returns {Promise<Object>} Token verification result
49
+ */
50
+ async verifyToken() {
51
+ try {
52
+ const data = await this.request('/user/tokens/verify');
53
+ return {
54
+ valid: true,
55
+ id: data.result.id,
56
+ status: data.result.status,
57
+ expiresOn: data.result.expires_on,
58
+ notBefore: data.result.not_before
59
+ };
60
+ } catch (error) {
61
+ return {
62
+ valid: false,
63
+ error: error.message
64
+ };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * List all zones (domains) accessible with this API token
70
+ * @param {Object} options - Query options
71
+ * @returns {Promise<Array>} Array of zone objects
72
+ */
73
+ async listZones(options = {}) {
74
+ const params = new URLSearchParams({
75
+ page: options.page || 1,
76
+ per_page: options.perPage || 50,
77
+ ...(options.name && {
78
+ name: options.name
79
+ }),
80
+ ...(options.status && {
81
+ status: options.status
82
+ }),
83
+ ...(options.accountId && {
84
+ 'account.id': options.accountId
85
+ })
86
+ });
87
+ const data = await this.request(`/zones?${params}`);
88
+ return data.result.map(zone => ({
89
+ id: zone.id,
90
+ name: zone.name,
91
+ status: zone.status,
92
+ accountId: zone.account.id,
93
+ accountName: zone.account.name,
94
+ nameServers: zone.name_servers,
95
+ originalNameServers: zone.original_name_servers,
96
+ type: zone.type,
97
+ planName: zone.plan.name,
98
+ createdOn: zone.created_on,
99
+ modifiedOn: zone.modified_on
100
+ }));
101
+ }
102
+
103
+ /**
104
+ * Get detailed information about a specific zone
105
+ * @param {string} zoneId - Zone ID
106
+ * @returns {Promise<Object>} Zone details
107
+ */
108
+ async getZoneDetails(zoneId) {
109
+ const data = await this.request(`/zones/${zoneId}`);
110
+ const zone = data.result;
111
+ return {
112
+ id: zone.id,
113
+ name: zone.name,
114
+ status: zone.status,
115
+ accountId: zone.account.id,
116
+ accountName: zone.account.name,
117
+ nameServers: zone.name_servers,
118
+ originalNameServers: zone.original_name_servers,
119
+ type: zone.type,
120
+ planName: zone.plan.name,
121
+ planId: zone.plan.id,
122
+ createdOn: zone.created_on,
123
+ modifiedOn: zone.modified_on,
124
+ activatedOn: zone.activated_on,
125
+ meta: {
126
+ step: zone.meta.step,
127
+ customCertificateQuota: zone.meta.custom_certificate_quota,
128
+ pageRuleQuota: zone.meta.page_rule_quota,
129
+ phishingDetected: zone.meta.phishing_detected,
130
+ multipleRailgunsAllowed: zone.meta.multiple_railguns_allowed
131
+ },
132
+ owner: {
133
+ id: zone.owner.id,
134
+ type: zone.owner.type,
135
+ email: zone.owner.email
136
+ }
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Get DNS records for a zone
142
+ * @param {string} zoneId - Zone ID
143
+ * @param {Object} options - Query options
144
+ * @returns {Promise<Array>} Array of DNS records
145
+ */
146
+ async listDNSRecords(zoneId, options = {}) {
147
+ const params = new URLSearchParams({
148
+ page: options.page || 1,
149
+ per_page: options.perPage || 100,
150
+ ...(options.type && {
151
+ type: options.type
152
+ }),
153
+ ...(options.name && {
154
+ name: options.name
155
+ })
156
+ });
157
+ const data = await this.request(`/zones/${zoneId}/dns_records?${params}`);
158
+ return data.result.map(record => ({
159
+ id: record.id,
160
+ type: record.type,
161
+ name: record.name,
162
+ content: record.content,
163
+ proxied: record.proxied,
164
+ ttl: record.ttl,
165
+ priority: record.priority,
166
+ createdOn: record.created_on,
167
+ modifiedOn: record.modified_on
168
+ }));
169
+ }
170
+
171
+ /**
172
+ * Get account information
173
+ * @param {string} accountId - Account ID
174
+ * @returns {Promise<Object>} Account details
175
+ */
176
+ async getAccountDetails(accountId) {
177
+ const data = await this.request(`/accounts/${accountId}`);
178
+ const account = data.result;
179
+ return {
180
+ id: account.id,
181
+ name: account.name,
182
+ type: account.type,
183
+ settings: account.settings,
184
+ createdOn: account.created_on
185
+ };
186
+ }
187
+
188
+ /**
189
+ * List all workers for an account
190
+ * @param {string} accountId - Account ID
191
+ * @returns {Promise<Array>} Array of worker scripts
192
+ */
193
+ async listWorkers(accountId) {
194
+ const data = await this.request(`/accounts/${accountId}/workers/scripts`);
195
+ return data.result.map(worker => ({
196
+ id: worker.id,
197
+ name: worker.id,
198
+ // Worker name is the ID
199
+ createdOn: worker.created_on,
200
+ modifiedOn: worker.modified_on,
201
+ etag: worker.etag
202
+ }));
203
+ }
204
+
205
+ /**
206
+ * List D1 databases for an account
207
+ * @param {string} accountId - Account ID
208
+ * @returns {Promise<Array>} Array of D1 databases
209
+ */
210
+ async listD1Databases(accountId) {
211
+ try {
212
+ const data = await this.request(`/accounts/${accountId}/d1/database`);
213
+ return data.result.map(db => ({
214
+ uuid: db.uuid,
215
+ name: db.name,
216
+ version: db.version,
217
+ createdAt: db.created_at,
218
+ numTables: db.num_tables,
219
+ fileSize: db.file_size
220
+ }));
221
+ } catch (error) {
222
+ // D1 might not be available on all plans
223
+ if (error.message.includes('not found') || error.message.includes('403')) {
224
+ return [];
225
+ }
226
+ throw error;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Helper: Get complete deployment info for a zone
232
+ * This combines zone details with useful metadata for deployment
233
+ * @param {string} zoneId - Zone ID
234
+ * @returns {Promise<Object>} Complete deployment info
235
+ */
236
+ async getDeploymentInfo(zoneId) {
237
+ const zone = await this.getZoneDetails(zoneId);
238
+ const dnsRecords = await this.listDNSRecords(zoneId);
239
+
240
+ // Try to get D1 databases (might fail if not available)
241
+ let databases = [];
242
+ try {
243
+ databases = await this.listD1Databases(zone.accountId);
244
+ } catch (error) {
245
+ // Ignore if D1 not available
246
+ }
247
+ return {
248
+ // Core deployment info (the 6 pieces)
249
+ accountId: zone.accountId,
250
+ zoneId: zone.id,
251
+ domain: zone.name,
252
+ // Additional useful info
253
+ accountName: zone.accountName,
254
+ status: zone.status,
255
+ nameServers: zone.nameServers,
256
+ planName: zone.planName,
257
+ // DNS records (for reference)
258
+ dnsRecords: dnsRecords.slice(0, 10),
259
+ // First 10 records
260
+
261
+ // Available databases
262
+ databases,
263
+ // Metadata
264
+ createdOn: zone.createdOn,
265
+ owner: zone.owner
266
+ };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Helper function to format zones for display in prompts
272
+ * @param {Array} zones - Array of zone objects from listZones()
273
+ * @returns {Array<string>} Array of formatted strings for askChoice()
274
+ */
275
+ export function formatZonesForDisplay(zones) {
276
+ return zones.map(zone => {
277
+ const status = zone.status === 'active' ? '✅' : '⚠️';
278
+ const plan = zone.planName === 'Free' ? '(Free)' : `(${zone.planName})`;
279
+ return `${status} ${zone.name} ${plan} - Account: ${zone.accountName}`;
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Helper function to validate zone selection and get index
285
+ * @param {string} selection - User's selection string
286
+ * @param {Array} zones - Array of zones
287
+ * @returns {number} Index of selected zone, or -1 if invalid
288
+ */
289
+ export function parseZoneSelection(selection, zones) {
290
+ const index = parseInt(selection) - 1;
291
+ if (isNaN(index) || index < 0 || index >= zones.length) {
292
+ return -1;
293
+ }
294
+ return index;
295
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Cloudflare Utilities - Unified Exports
3
+ *
4
+ * This module provides a unified interface for all Cloudflare operations.
5
+ *
6
+ * ## Architecture:
7
+ *
8
+ * ### API-Based Operations (api.js)
9
+ * Use for operations that interact directly with Cloudflare's REST API:
10
+ * - Token verification
11
+ * - Zone/domain listing and discovery
12
+ * - Zone details retrieval
13
+ * - D1 database listing
14
+ * - Account information
15
+ *
16
+ * Benefits: No CLI dependency, programmatic control, better for automation
17
+ *
18
+ * ### CLI-Based Operations (../../bin/shared/cloudflare/ops.js)
19
+ * Use for operations that require wrangler CLI:
20
+ * - Worker deployment
21
+ * - Secret management (put/list/delete)
22
+ * - Database migrations
23
+ * - SQL execution
24
+ *
25
+ * Benefits: Leverages wrangler's built-in features, rate limiting, monitoring
26
+ *
27
+ * ## Usage:
28
+ *
29
+ * ```javascript
30
+ * // For API operations
31
+ * import { CloudflareAPI } from '@tamyla/clodo-framework/utils/cloudflare';
32
+ *
33
+ * const cf = new CloudflareAPI(apiToken);
34
+ * const zones = await cf.listZones();
35
+ * const zoneDetails = await cf.getZoneDetails(zoneId);
36
+ *
37
+ * // For CLI operations
38
+ * import { deployWorker, deploySecret, listSecrets } from '@tamyla/clodo-framework/utils/cloudflare';
39
+ *
40
+ * await deployWorker('production');
41
+ * await deploySecret('API_KEY', 'secret-value', 'production');
42
+ * const secrets = await listSecrets('production');
43
+ * ```
44
+ */
45
+
46
+ // API-based operations
47
+ export { CloudflareAPI, formatZonesForDisplay, parseZoneSelection } from './api.js';
48
+
49
+ // CLI-based operations (re-export from ops.js for convenience)
50
+ // Note: These are also available directly from bin/shared/cloudflare/ops.js
51
+ export {
52
+ // Authentication
53
+ checkAuth, authenticate, storeCloudflareToken, getCloudflareToken,
54
+ // Worker operations
55
+ listWorkers, workerExists, deployWorker,
56
+ // Secret operations
57
+ deploySecret, deleteSecret, listSecrets,
58
+ // D1 Database operations
59
+ listDatabases, databaseExists, createDatabase, deleteDatabase, runMigrations, executeSql, getDatabaseId,
60
+ // Utilities
61
+ validatePrerequisites } from '../../../bin/shared/cloudflare/ops.js';
62
+
63
+ /**
64
+ * Helper: Choose the right tool for the job
65
+ *
66
+ * Use CloudflareAPI when:
67
+ * - You need to verify an API token
68
+ * - You want to list/discover zones/domains
69
+ * - You need zone details (zone_id, account_id, etc.)
70
+ * - You're building interactive domain selection UI
71
+ * - You want programmatic control without CLI dependency
72
+ *
73
+ * Use ops.js functions when:
74
+ * - You need to deploy workers
75
+ * - You need to manage secrets (set/list/delete)
76
+ * - You need to run database migrations
77
+ * - You need to execute SQL queries
78
+ * - You want built-in retry logic and monitoring
79
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -114,9 +114,10 @@
114
114
  "author": "Tamyla",
115
115
  "license": "GPL-3.0-or-later",
116
116
  "dependencies": {
117
- "wrangler": ">=3.0.0",
117
+ "@iarna/toml": "^2.2.5",
118
118
  "chalk": "^5.3.0",
119
- "commander": "^11.0.0"
119
+ "commander": "^11.0.0",
120
+ "wrangler": ">=3.0.0"
120
121
  },
121
122
  "devDependencies": {
122
123
  "@babel/cli": "^7.23.0",