@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.
- package/CHANGELOG.md +181 -118
- package/README.md +78 -0
- package/bin/clodo-service.js +2 -2
- package/bin/security/security-cli.js +1 -1
- package/bin/service-management/create-service.js +1 -1
- package/bin/service-management/init-service.js +1 -1
- package/bin/shared/config/customer-cli.js +39 -150
- package/dist/config/CustomerConfigCLI.js +18 -12
- package/dist/config/customers.js +183 -7
- package/dist/deployment/wrangler-deployer.js +21 -0
- package/dist/shared/config/customer-cli.js +42 -125
- package/dist/utils/cloudflare/api.js +295 -0
- package/dist/utils/cloudflare/index.js +79 -0
- package/package.json +4 -3
|
@@ -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.
|
|
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
|
-
"
|
|
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",
|