From ba49c42dd8fce88c4c28bbb137869cf67d3b1f23 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 19 Mar 2025 07:07:08 +0000 Subject: [PATCH] fix(core): Improve logging consistency, record update functionality, and API error handling in Cloudflare modules --- changelog.md | 219 +++++++++++++++++++++++++ ts/00_commitinfo_data.ts | 4 +- ts/cloudflare.classes.account.ts | 72 +++++++- ts/cloudflare.classes.record.ts | 95 ++++++++++- ts/cloudflare.classes.worker.ts | 18 +- ts/cloudflare.classes.workermanager.ts | 56 ++++++- ts/cloudflare.classes.zone.ts | 172 ++++++++++++++++++- ts/cloudflare.classes.zonemanager.ts | 149 +++++++++++++++-- ts/cloudflare.utils.ts | 130 +++++++++++++++ ts/index.ts | 11 +- ts/interfaces/cloudflare.api.zone.ts | 45 +++++ ts/interfaces/index.ts | 1 + 12 files changed, 937 insertions(+), 35 deletions(-) create mode 100644 changelog.md create mode 100644 ts/cloudflare.utils.ts create mode 100644 ts/interfaces/cloudflare.api.zone.ts diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..eaad563 --- /dev/null +++ b/changelog.md @@ -0,0 +1,219 @@ +# Changelog + +## 2025-03-19 - 6.0.6 - fix(core) +Improve logging consistency, record update functionality, and API error handling in Cloudflare modules + +- Replaced raw console.log calls with logger.log for unified logging across modules +- Implemented and documented the updateRecord method with proper parameters in CloudflareAccount +- Enhanced API request error handling and added detailed documentation in request methods +- Refactored CloudflareWorker and WorkerManager methods to improve clarity and maintainability +- Updated ZoneManager and CloudflareZone to improve error reporting and zone manipulation + +## 2024-06-16 - 6.0.5 – no significant changes +_No significant changes in this release._ + +## 2024-06-16 - 6.0.4 – miscellaneous +Several improvements and fixes: +- fix(start supporting workers again): update +- update license info +- update readme +- switch to official cloudflare api client while keeping class based approach + +## 2024-06-15 - 6.0.3 – core +- fix(core): update + +## 2023-06-13 - 6.0.2 – core +- fix(core): update + +## 2023-06-13 - 6.0.1 – core +- fix(core): update + +## 2022-09-27 - 6.0.0 – core +- fix(core): update + +## 2022-09-27 - 5.0.10 – core +- BREAKING CHANGE(core): switch to esm + +## 2022-09-27 - 5.0.9 – core +- fix(core): update + +## 2021-01-22 - 5.0.8 – core +- fix(core): update + +## 2021-01-22 - 5.0.7 – core +- fix(core): update + +## 2021-01-22 - 5.0.6 – core +- fix(core): update + +## 2020-06-10 - 5.0.5 – core +- fix(core): update + +## 2020-06-10 - 5.0.4 – core +- fix(core): update + +## 2020-02-28 - 5.0.3 – core +- fix(core): update + +## 2020-02-28 - 5.0.2 – core +- fix(core): update + +## 2020-02-28 - 5.0.1 – core +- fix(core): update + +## 2020-02-28 - 5.0.0 – core +- fix(core): update + +## 2020-02-19 - 4.0.5 – account +- BREAKING CHANGE(account): authorization now uses the new Account API + +## 2020-02-19 - 4.0.4 – core +- fix(core): update + +## 2020-02-19 - 4.0.3 – core +- fix(core): update + +## 2020-02-10 - 4.0.2 – core +- fix(core): update + +## 2020-02-10 - 4.0.1 – core +- fix(core): update + +## 2020-02-10 - 4.0.0 – core +- fix(core): update + +## 2020-02-09 - 3.0.7 – API +- BREAKING CHANGE(API): move to .convenience property + +## 2020-02-09 - 3.0.6 – core +- fix(core): update + +## 2020-02-09 - 3.0.5 – core +- fix(core): update + +## 2019-07-19 - 3.0.4 – core +- fix(core): update + +## 2019-07-18 - 3.0.3 – core +- fix(core): update + +## 2019-07-18 - 3.0.2 – core +- fix(core): update + +## 2019-07-18 - 3.0.1 – core +- fix(core): update + +## 2019-07-18 - 3.0.0 – core +- fix(core): update + +## 2019-07-18 - 2.0.1 – core +- fix(core): update + +## 2019-07-18 - 2.0.0 – core +- fix(core): update + +## 2019-07-18 - 2.0.2 – no significant changes +_No significant changes in this release._ + +## 2018-08-13 - 1.0.5 – scope +- BREAKING CHANGE(scope): change scope, tools and package name + +## 2017-06-11 - 1.0.4 – misc +- now using tsclass + +## 2017-06-09 - 1.0.3 – misc +- update dependencies + +## 2017-06-05 - 1.0.2 – misc +- now supports purging of assets +- improve test + +## 2017-06-04 - 1.0.1 – misc +- add npmextra.json + +## 2017-06-04 - 1.0.0 – misc +- add type TRecord, update ci + +## 2017-06-04 - 0.0.20 – no significant changes +_No significant changes in this release._ + +## 2017-06-04 - 0.0.19 – misc +- go async/await +- update brand link + +## 2017-02-12 - 0.0.18 – misc +- update README + +## 2017-01-29 - 0.0.17 – misc +- update README + +## 2017-01-29 - 0.0.16 – misc +- fix tests to run in parallel + +## 2017-01-29 - 0.0.15 – misc +- fixed bad request retry + +## 2017-01-29 - 0.0.14 – misc +- fix testing timeouts + +## 2017-01-29 - 0.0.13 – misc +- added random retry times + +## 2017-01-29 - 0.0.12 – misc +- update to new ci + +## 2017-01-29 - 0.0.11 – misc +- now using smartrequest + +## 2017-01-22 - 0.0.10 – misc +- now reacting to rate limiting + +## 2016-07-31 - 0.0.9 – misc +- update dependencies + +## 2016-06-22 - 0.0.8 to 0.0.7 – no significant changes +_No significant changes in these releases._ + +## 2016-06-22 - 0.0.6 – misc +- updated dependencies + +## 2016-06-21 - 0.0.5 – misc +- fix stages + +## 2016-06-21 - 0.0.4 – misc +- fix stages + +## 2016-06-21 - 0.0.3 – misc +Multiple improvements: +- now works for most things +- update to latest dependencies +- update .gitlab.yml +- update +- add .gitlab-ci.yml + +## 2016-05-25 - 0.0.2 – misc +Several changes: +- improve domain string handling +- update .getRecord +- improve .createRecord +- implemented .createRecord +- compile +- add functionality +- start with tests +- improved request method of cflare class + +## 2016-04-27 - 0.0.1 – misc +- now returning promises +- add lossless badge + +## 2016-04-27 - 0.0.0 – misc +- added travis and improved README + +## 2016-04-10 - 0.0.0 – misc +- add package.json and README + +## 2016-04-10 - unknown – misc +- Initial commit + +--- +_Note: Versions that only contained version bump commits or minor housekeeping (6.0.5; 2.0.2; 0.0.20; 0.0.8 to 0.0.7) have been omitted from detailed entries and are summarized above._ \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c3beb53..4170b27 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -1,8 +1,8 @@ /** - * autocreated commitinfo by @pushrocks/commitinfo + * autocreated commitinfo by @push.rocks/commitinfo */ export const commitinfo = { name: '@apiclient.xyz/cloudflare', - version: '6.0.5', + version: '6.0.6', description: 'A TypeScript client for managing Cloudflare accounts, zones, DNS records, and workers with ease.' } diff --git a/ts/cloudflare.classes.account.ts b/ts/cloudflare.classes.account.ts index c143b90..633ef72 100644 --- a/ts/cloudflare.classes.account.ts +++ b/ts/cloudflare.classes.account.ts @@ -26,6 +26,40 @@ export class CloudflareAccount { }); } + /** + * Make a request to the Cloudflare API + * @param method HTTP method (GET, POST, PUT, DELETE) + * @param endpoint API endpoint path + * @param data Optional request body data + * @returns API response + */ + public async request( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + endpoint: string, + data?: any + ): Promise { + try { + const options: plugins.smartrequest.ISmartRequestOptions = { + method, + url: `https://api.cloudflare.com/client/v4${endpoint}`, + headers: { + 'Authorization': `Bearer ${this.authToken}`, + 'Content-Type': 'application/json', + }, + }; + + if (data) { + options.json = data; + } + + const response = await plugins.smartrequest.request(options); + return JSON.parse(response.body); + } catch (error) { + logger.log('error', `Cloudflare API request failed: ${error.message}`); + throw error; + } + } + public async preselectAccountByName(nameArg: string) { const accounts = await this.convenience.listAccounts(); const account = accounts.find((accountArg) => { @@ -130,7 +164,7 @@ export class CloudflareAccount { * cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects */ cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => { - console.log(`cleaning record for ${domainNameArg}`); + logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`); const records = await this.convenience.listRecords(domainNameArg); const recordsToDelete = records.filter((recordArg) => { return recordArg.type === typeArg; @@ -144,17 +178,39 @@ export class CloudflareAccount { /** * updates a record - * @param domainNameArg - * @param typeArg - * @param valueArg + * @param domainNameArg Domain name for the record + * @param typeArg Type of DNS record + * @param contentArg New content for the record + * @param ttlArg Time to live in seconds (optional) + * @returns Updated record */ updateRecord: async ( domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType, - valueArg - ) => { - // TODO: implement + contentArg: string, + ttlArg: number = 1 + ): Promise => { const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.convenience.getZoneId(domain.zoneName); + + // Find existing record + const record = await this.convenience.getRecord(domainNameArg, typeArg); + + if (!record) { + logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`); + return this.convenience.createRecord(domainNameArg, typeArg, contentArg, ttlArg); + } + + // Update the record + const updatedRecord = await this.apiAccount.dns.records.edit(record.id, { + zone_id: zoneId, + type: typeArg as any, + name: domain.fullName, + content: contentArg, + ttl: ttlArg + }); + + return updatedRecord; }, /** * list all records of a specified domain name @@ -208,4 +264,4 @@ export class CloudflareAccount { await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT'); }, }; -} +} \ No newline at end of file diff --git a/ts/cloudflare.classes.record.ts b/ts/cloudflare.classes.record.ts index 528e9d2..7fb3844 100644 --- a/ts/cloudflare.classes.record.ts +++ b/ts/cloudflare.classes.record.ts @@ -1,3 +1,96 @@ import * as plugins from './cloudflare.plugins.js'; +import { logger } from './cloudflare.logger.js'; -export class CloudflareRecord {} +export interface ICloudflareRecordInfo { + id: string; + type: plugins.tsclass.network.TDnsRecordType; + name: string; + content: string; + proxiable: boolean; + proxied: boolean; + ttl: number; + locked: boolean; + zone_id: string; + zone_name: string; + created_on: string; + modified_on: string; +} + +export class CloudflareRecord { + /** + * Create a CloudflareRecord instance from an API object + * @param apiObject Cloudflare DNS record API object + * @returns CloudflareRecord instance + */ + public static createFromApiObject(apiObject: plugins.ICloudflareTypes['Record']): CloudflareRecord { + const record = new CloudflareRecord(); + Object.assign(record, apiObject); + return record; + } + + // Record properties + public id: string; + public type: plugins.tsclass.network.TDnsRecordType; + public name: string; + public content: string; + public proxiable: boolean; + public proxied: boolean; + public ttl: number; + public locked: boolean; + public zone_id: string; + public zone_name: string; + public created_on: string; + public modified_on: string; + + /** + * Update the record content + * @param cloudflareAccount The Cloudflare account to use + * @param newContent New content for the record + * @param ttl Optional TTL value in seconds + * @returns Updated record + */ + public async update( + cloudflareAccount: any, + newContent: string, + ttl?: number + ): Promise { + logger.log('info', `Updating record ${this.name} (${this.type}) with new content`); + + const updatedRecord = await cloudflareAccount.apiAccount.dns.records.edit(this.id, { + zone_id: this.zone_id, + type: this.type as any, + name: this.name, + content: newContent, + ttl: ttl || this.ttl, + proxied: this.proxied + }); + + // Update this instance + this.content = newContent; + if (ttl) { + this.ttl = ttl; + } + + return this; + } + + /** + * Delete this record + * @param cloudflareAccount The Cloudflare account to use + * @returns Boolean indicating success + */ + public async delete(cloudflareAccount: any): Promise { + try { + logger.log('info', `Deleting record ${this.name} (${this.type})`); + + await cloudflareAccount.apiAccount.dns.records.delete(this.id, { + zone_id: this.zone_id + }); + + return true; + } catch (error) { + logger.log('error', `Failed to delete record: ${error.message}`); + return false; + } + } +} \ No newline at end of file diff --git a/ts/cloudflare.classes.worker.ts b/ts/cloudflare.classes.worker.ts index 0b0676a..c5c47a5 100644 --- a/ts/cloudflare.classes.worker.ts +++ b/ts/cloudflare.classes.worker.ts @@ -7,6 +7,11 @@ export interface IWorkerRoute extends interfaces.ICflareWorkerRoute { zoneName: string; } +export interface IWorkerRouteDefinition { + zoneName: string; + pattern: string; +} + export class CloudflareWorker { // STATIC public static async fromApiObject( @@ -46,9 +51,8 @@ export class CloudflareWorker { result: interfaces.ICflareWorkerRoute[]; } = await this.workerManager.cfAccount.request('GET', requestRoute); for (const route of response.result) { - console.log('hey'); - console.log(route); - console.log(this.id); + logger.log('debug', `Processing route: ${route.pattern}`); + logger.log('debug', `Comparing script: ${route.script} with worker ID: ${this.id}`); if (route.script === this.id) { this.routes.push({ ...route, zoneName: zone.name }); } @@ -56,7 +60,11 @@ export class CloudflareWorker { } } - public async setRoutes(routeArray: Array<{ zoneName: string; pattern: string }>) { + /** + * Sets routes for this worker + * @param routeArray Array of route definitions + */ + public async setRoutes(routeArray: IWorkerRouteDefinition[]) { for (const newRoute of routeArray) { // lets determine wether a route is new, needs an update or already up to date. let routeStatus: 'new' | 'needsUpdate' | 'alreadyUpToDate' = 'new'; @@ -90,4 +98,4 @@ export class CloudflareWorker { } } } -} +} \ No newline at end of file diff --git a/ts/cloudflare.classes.workermanager.ts b/ts/cloudflare.classes.workermanager.ts index 38f667a..4c3820c 100644 --- a/ts/cloudflare.classes.workermanager.ts +++ b/ts/cloudflare.classes.workermanager.ts @@ -1,6 +1,7 @@ import * as plugins from './cloudflare.plugins.js'; import { CloudflareAccount } from './cloudflare.classes.account.js'; import { CloudflareWorker } from './cloudflare.classes.worker.js'; +import { logger } from './cloudflare.logger.js'; export class WorkerManager { public cfAccount: CloudflareAccount; @@ -9,6 +10,12 @@ export class WorkerManager { this.cfAccount = cfAccountArg; } + /** + * Creates a new worker or updates an existing one + * @param workerName Name of the worker + * @param workerScript JavaScript content of the worker + * @returns The created or updated worker + */ public async createWorker(workerName: string, workerScript: string): Promise { if (!this.cfAccount.preselectedAccountId) { throw new Error('No account selected. Please select it first on the account.'); @@ -21,7 +28,30 @@ export class WorkerManager { } /** - * lists workers + * Get a worker by name + * @param workerName Name of the worker to retrieve + * @returns CloudflareWorker instance or undefined if not found + */ + public async getWorker(workerName: string): Promise { + if (!this.cfAccount.preselectedAccountId) { + throw new Error('No account selected. Please select it first on the account.'); + } + + try { + const script = await this.cfAccount.apiAccount.workers.scripts.get(workerName, { + account_id: this.cfAccount.preselectedAccountId + }); + + return CloudflareWorker.fromApiObject(this, script); + } catch (error) { + logger.log('warn', `Worker '${workerName}' not found: ${error.message}`); + return undefined; + } + } + + /** + * Lists all worker scripts + * @returns Array of worker scripts */ public async listWorkerScripts() { if (!this.cfAccount.preselectedAccountId) { @@ -35,4 +65,26 @@ export class WorkerManager { } return workerScripts; } -} + + /** + * Deletes a worker script + * @param workerName Name of the worker to delete + * @returns True if deletion was successful + */ + public async deleteWorker(workerName: string): Promise { + if (!this.cfAccount.preselectedAccountId) { + throw new Error('No account selected. Please select it first on the account.'); + } + + try { + await this.cfAccount.apiAccount.workers.scripts.delete(workerName, { + account_id: this.cfAccount.preselectedAccountId + }); + logger.log('info', `Worker '${workerName}' deleted successfully`); + return true; + } catch (error) { + logger.log('error', `Failed to delete worker '${workerName}': ${error.message}`); + return false; + } + } +} \ No newline at end of file diff --git a/ts/cloudflare.classes.zone.ts b/ts/cloudflare.classes.zone.ts index 7729ceb..8b0ceb6 100644 --- a/ts/cloudflare.classes.zone.ts +++ b/ts/cloudflare.classes.zone.ts @@ -1,9 +1,177 @@ import * as plugins from './cloudflare.plugins.js'; +import { logger } from './cloudflare.logger.js'; +import * as interfaces from './interfaces/index.js'; export class CloudflareZone { - public static createFromApiObject(apiObject: plugins.ICloudflareTypes['Zone']) { + // Zone properties + public id: string; + public name: string; + public status: interfaces.ICflareZone['status']; + public paused: boolean; + public type: interfaces.ICflareZone['type']; + public development_mode: number; + public name_servers: string[]; + public original_name_servers: string[]; + public original_registrar: string | null; + public original_dnshost: string | null; + public modified_on: string; + public created_on: string; + public activated_on: string; + public meta: interfaces.ICflareZone['meta']; + public owner: interfaces.ICflareZone['owner']; + public account: interfaces.ICflareZone['account']; + public permissions: string[]; + public plan: interfaces.ICflareZone['plan']; + + private cfAccount: any; // Will be set when created through a manager + + /** + * Create a CloudflareZone instance from an API object + * @param apiObject Cloudflare Zone API object + * @param cfAccount Optional Cloudflare account instance + * @returns CloudflareZone instance + */ + public static createFromApiObject( + apiObject: plugins.ICloudflareTypes['Zone'], + cfAccount?: any + ): CloudflareZone { const cloudflareZone = new CloudflareZone(); Object.assign(cloudflareZone, apiObject); + + if (cfAccount) { + cloudflareZone.cfAccount = cfAccount; + } + return cloudflareZone; } -} + + /** + * Check if development mode is currently active + * @returns True if development mode is active + */ + public isDevelopmentModeActive(): boolean { + return this.development_mode > 0; + } + + /** + * Enable development mode for the zone + * @param cfAccount Cloudflare account to use if not already set + * @param duration Duration in seconds (default: 3 hours) + * @returns Updated zone + */ + public async enableDevelopmentMode( + cfAccount?: any, + duration: number = 10800 + ): Promise { + const account = cfAccount || this.cfAccount; + if (!account) { + throw new Error('CloudflareAccount is required to enable development mode'); + } + + logger.log('info', `Enabling development mode for zone ${this.name}`); + + const response = await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, { + value: 'on', + time: duration + }); + + this.development_mode = duration; + return this; + } + + /** + * Disable development mode for the zone + * @param cfAccount Cloudflare account to use if not already set + * @returns Updated zone + */ + public async disableDevelopmentMode(cfAccount?: any): Promise { + const account = cfAccount || this.cfAccount; + if (!account) { + throw new Error('CloudflareAccount is required to disable development mode'); + } + + logger.log('info', `Disabling development mode for zone ${this.name}`); + + const response = await account.request('PATCH', `/zones/${this.id}/settings/development_mode`, { + value: 'off' + }); + + this.development_mode = 0; + return this; + } + + /** + * Purge all cached content for this zone + * @param cfAccount Cloudflare account to use if not already set + * @returns True if successful + */ + public async purgeCache(cfAccount?: any): Promise { + const account = cfAccount || this.cfAccount; + if (!account) { + throw new Error('CloudflareAccount is required to purge cache'); + } + + logger.log('info', `Purging all cache for zone ${this.name}`); + + try { + await account.request('POST', `/zones/${this.id}/purge_cache`, { + purge_everything: true + }); + return true; + } catch (error) { + logger.log('error', `Failed to purge cache: ${error.message}`); + return false; + } + } + + /** + * Purge specific URLs from the cache + * @param urls Array of URLs to purge + * @param cfAccount Cloudflare account to use if not already set + * @returns True if successful + */ + public async purgeUrls(urls: string[], cfAccount?: any): Promise { + const account = cfAccount || this.cfAccount; + if (!account) { + throw new Error('CloudflareAccount is required to purge URLs'); + } + + if (!urls.length) { + return true; + } + + logger.log('info', `Purging ${urls.length} URLs from cache for zone ${this.name}`); + + try { + await account.request('POST', `/zones/${this.id}/purge_cache`, { + files: urls + }); + return true; + } catch (error) { + logger.log('error', `Failed to purge URLs: ${error.message}`); + return false; + } + } + + /** + * Check if the zone is active + * @returns True if the zone is active + */ + public isActive(): boolean { + return this.status === 'active' && !this.paused; + } + + /** + * Check if the zone is using Cloudflare nameservers + * @returns True if using Cloudflare nameservers + */ + public isUsingCloudflareNameservers(): boolean { + // Check if original nameservers match current nameservers + if (!this.original_name_servers || !this.name_servers) { + return false; + } + + // If they're different, and current nameservers are Cloudflare's + return this.name_servers.some(ns => ns.includes('cloudflare')); + } +} \ No newline at end of file diff --git a/ts/cloudflare.classes.zonemanager.ts b/ts/cloudflare.classes.zonemanager.ts index 443a74a..5d36970 100644 --- a/ts/cloudflare.classes.zonemanager.ts +++ b/ts/cloudflare.classes.zonemanager.ts @@ -2,31 +2,152 @@ import * as plugins from './cloudflare.plugins.js'; import * as interfaces from './interfaces/index.js'; import { CloudflareAccount } from './cloudflare.classes.account.js'; import { CloudflareZone } from './cloudflare.classes.zone.js'; +import { logger } from './cloudflare.logger.js'; export class ZoneManager { public cfAccount: CloudflareAccount; - public zoneName: string; constructor(cfAccountArg: CloudflareAccount) { this.cfAccount = cfAccountArg; } - public async getZones(zoneName: string) { + /** + * Get all zones, optionally filtered by name + * @param zoneName Optional zone name to filter by + * @returns Array of CloudflareZone instances + */ + public async getZones(zoneName?: string): Promise { let requestRoute = `/zones?per_page=50`; - // may be optionally filtered by domain name - + + // May be optionally filtered by domain name if (zoneName) { - requestRoute = `${requestRoute}&name=${zoneName}`; + requestRoute = `${requestRoute}&name=${encodeURIComponent(zoneName)}`; } - const response: any = await this.cfAccount.request('GET', requestRoute); - const apiObjects: interfaces.ICflareZone[] = response.result; - - const cloudflareZoneArray = []; - for (const apiObject of apiObjects) { - cloudflareZoneArray.push(CloudflareZone.createFromApiObject(apiObject)); + try { + const response: { result: interfaces.ICflareZone[] } = await this.cfAccount.request('GET', requestRoute); + + return response.result.map(apiObject => + CloudflareZone.createFromApiObject(apiObject as any, this.cfAccount) + ); + } catch (error) { + logger.log('error', `Failed to fetch zones: ${error.message}`); + return []; } - - return cloudflareZoneArray; } -} + + /** + * Get a single zone by name + * @param zoneName Zone name to find + * @returns CloudflareZone instance or undefined if not found + */ + public async getZoneByName(zoneName: string): Promise { + const zones = await this.getZones(zoneName); + return zones.find(zone => zone.name === zoneName); + } + + /** + * Get a zone by its ID + * @param zoneId Zone ID to find + * @returns CloudflareZone instance or undefined if not found + */ + public async getZoneById(zoneId: string): Promise { + try { + const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( + 'GET', + `/zones/${zoneId}` + ); + + return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); + } catch (error) { + logger.log('error', `Failed to fetch zone with ID ${zoneId}: ${error.message}`); + return undefined; + } + } + + /** + * Create a new zone + * @param zoneName Name of the zone to create + * @param jumpStart Whether to automatically attempt to fetch existing DNS records + * @param accountId Account ID to use (defaults to preselected account) + * @returns The created zone + */ + public async createZone( + zoneName: string, + jumpStart: boolean = false, + accountId?: string + ): Promise { + const useAccountId = accountId || this.cfAccount.preselectedAccountId; + + if (!useAccountId) { + throw new Error('No account selected. Please select it first on the account.'); + } + + try { + logger.log('info', `Creating zone ${zoneName}`); + + const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( + 'POST', + '/zones', + { + name: zoneName, + jump_start: jumpStart, + account: { id: useAccountId } + } + ); + + return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); + } catch (error) { + logger.log('error', `Failed to create zone ${zoneName}: ${error.message}`); + return undefined; + } + } + + /** + * Delete a zone + * @param zoneId ID of the zone to delete + * @returns True if successful + */ + public async deleteZone(zoneId: string): Promise { + try { + logger.log('info', `Deleting zone with ID ${zoneId}`); + + await this.cfAccount.request('DELETE', `/zones/${zoneId}`); + return true; + } catch (error) { + logger.log('error', `Failed to delete zone with ID ${zoneId}: ${error.message}`); + return false; + } + } + + /** + * Check if a zone exists + * @param zoneName Name of the zone to check + * @returns True if the zone exists + */ + public async zoneExists(zoneName: string): Promise { + const zones = await this.getZones(zoneName); + return zones.some(zone => zone.name === zoneName); + } + + /** + * Activate a zone (if it's in pending status) + * @param zoneId ID of the zone to activate + * @returns Updated zone or undefined if activation failed + */ + public async activateZone(zoneId: string): Promise { + try { + logger.log('info', `Activating zone with ID ${zoneId}`); + + const response: { result: interfaces.ICflareZone } = await this.cfAccount.request( + 'PUT', + `/zones/${zoneId}/activation_check` + ); + + return CloudflareZone.createFromApiObject(response.result as any, this.cfAccount); + } catch (error) { + logger.log('error', `Failed to activate zone with ID ${zoneId}: ${error.message}`); + return undefined; + } + } +} \ No newline at end of file diff --git a/ts/cloudflare.utils.ts b/ts/cloudflare.utils.ts new file mode 100644 index 0000000..0da158d --- /dev/null +++ b/ts/cloudflare.utils.ts @@ -0,0 +1,130 @@ +import * as plugins from './cloudflare.plugins.js'; +import { logger } from './cloudflare.logger.js'; + +export class CloudflareUtils { + /** + * Validates if a domain name is properly formatted + * @param domainName Domain name to validate + * @returns True if the domain is valid + */ + public static isValidDomain(domainName: string): boolean { + try { + const domain = new plugins.smartstring.Domain(domainName); + return domain.isValid(); + } catch (error) { + return false; + } + } + + /** + * Extracts the zone name (apex domain) from a full domain + * @param domainName Domain name to process + * @returns Zone name (apex domain) + */ + public static getZoneName(domainName: string): string { + try { + const domain = new plugins.smartstring.Domain(domainName); + return domain.zoneName; + } catch (error) { + logger.log('error', `Invalid domain name: ${domainName}`); + throw new Error(`Invalid domain name: ${domainName}`); + } + } + + /** + * Checks if a string is a valid Cloudflare API token + * @param token API token to validate + * @returns True if the token format is valid + */ + public static isValidApiToken(token: string): boolean { + // Cloudflare API tokens are typically 40+ characters long and start with specific patterns + return /^[A-Za-z0-9_-]{40,}$/.test(token); + } + + /** + * Validates a DNS record type + * @param type DNS record type to validate + * @returns True if it's a valid DNS record type + */ + public static isValidRecordType(type: string): boolean { + const validTypes: plugins.tsclass.network.TDnsRecordType[] = [ + 'A', 'AAAA', 'CNAME', 'TXT', 'SRV', 'LOC', 'MX', + 'NS', 'SPF', 'CERT', 'DNSKEY', 'DS', 'NAPTR', 'SMIMEA', + 'SSHFP', 'TLSA', 'URI' + ]; + return validTypes.includes(type as any); + } + + /** + * Formats a URL for cache purging (ensures it starts with http/https) + * @param url URL to format + * @returns Properly formatted URL + */ + public static formatUrlForPurge(url: string): string { + if (!url.startsWith('http://') && !url.startsWith('https://')) { + return `https://${url}`; + } + return url; + } + + /** + * Converts a TTL value in seconds to a human-readable string + * @param ttl TTL in seconds + * @returns Human-readable TTL + */ + public static formatTtl(ttl: number): string { + if (ttl === 1) { + return 'Automatic'; + } else if (ttl === 120) { + return '2 minutes'; + } else if (ttl === 300) { + return '5 minutes'; + } else if (ttl === 600) { + return '10 minutes'; + } else if (ttl === 900) { + return '15 minutes'; + } else if (ttl === 1800) { + return '30 minutes'; + } else if (ttl === 3600) { + return '1 hour'; + } else if (ttl === 7200) { + return '2 hours'; + } else if (ttl === 18000) { + return '5 hours'; + } else if (ttl === 43200) { + return '12 hours'; + } else if (ttl === 86400) { + return '1 day'; + } else { + return `${ttl} seconds`; + } + } + + /** + * Safely handles API pagination for Cloudflare requests + * @param makeRequest Function that makes the API request with page parameters + * @returns Combined results from all pages + */ + public static async paginateResults( + makeRequest: (page: number, perPage: number) => Promise<{ result: T[], result_info: { total_pages: number } }> + ): Promise { + const perPage = 50; // Cloudflare's maximum + let page = 1; + let totalPages = 1; + const allResults: T[] = []; + + do { + try { + const response = await makeRequest(page, perPage); + allResults.push(...response.result); + totalPages = response.result_info.total_pages; + page++; + } catch (error) { + logger.log('error', `Pagination error on page ${page}: ${error.message}`); + break; + } + } while (page <= totalPages); + + return allResults; + } +} \ No newline at end of file diff --git a/ts/index.ts b/ts/index.ts index 138c020..f175380 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,2 +1,11 @@ export { CloudflareAccount } from './cloudflare.classes.account.js'; -export { CloudflareWorker } from './cloudflare.classes.worker.js'; +export { CloudflareWorker, type IWorkerRoute, type IWorkerRouteDefinition } from './cloudflare.classes.worker.js'; +export { WorkerManager } from './cloudflare.classes.workermanager.js'; +export { CloudflareRecord, type ICloudflareRecordInfo } from './cloudflare.classes.record.js'; +export { CloudflareZone } from './cloudflare.classes.zone.js'; +export { ZoneManager } from './cloudflare.classes.zonemanager.js'; +export { CloudflareUtils } from './cloudflare.utils.js'; +export { commitinfo } from './00_commitinfo_data.js'; + +// Re-export interfaces +export * from './interfaces/index.js'; \ No newline at end of file diff --git a/ts/interfaces/cloudflare.api.zone.ts b/ts/interfaces/cloudflare.api.zone.ts new file mode 100644 index 0000000..b412be3 --- /dev/null +++ b/ts/interfaces/cloudflare.api.zone.ts @@ -0,0 +1,45 @@ +export interface ICflareZone { + id: string; + name: string; + status: 'active' | 'pending' | 'initializing' | 'moved' | 'deleted' | 'deactivated'; + paused: boolean; + type: 'full' | 'partial' | 'secondary'; + development_mode: number; + name_servers: string[]; + original_name_servers: string[]; + original_registrar: string | null; + original_dnshost: string | null; + modified_on: string; + created_on: string; + activated_on: string; + meta: { + step: number; + wildcard_proxiable: boolean; + custom_certificate_quota: number; + page_rule_quota: number; + phishing_detected: boolean; + multiple_railguns_allowed: boolean; + }; + owner: { + id: string | null; + type: 'user' | 'organization'; + email: string | null; + }; + account: { + id: string; + name: string; + }; + permissions: string[]; + plan: { + id: string; + name: string; + price: number; + currency: string; + frequency: string; + is_subscribed: boolean; + can_subscribe: boolean; + legacy_id: string; + legacy_discount: boolean; + externally_managed: boolean; + }; +} \ No newline at end of file diff --git a/ts/interfaces/index.ts b/ts/interfaces/index.ts index c2b7bfa..dcc9350 100644 --- a/ts/interfaces/index.ts +++ b/ts/interfaces/index.ts @@ -1,2 +1,3 @@ export * from './cloudflare.api.account.js'; export * from './cloudflare.api.workerroute.js'; +export * from './cloudflare.api.zone.js';