fix(core): Improve logging consistency, record update functionality, and API error handling in Cloudflare modules

This commit is contained in:
Philipp Kunz 2025-03-19 07:07:08 +00:00
parent 4600749442
commit ba49c42dd8
12 changed files with 937 additions and 35 deletions

219
changelog.md Normal file
View File

@ -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._

View File

@ -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.'
}

View File

@ -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<T = any>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
data?: any
): Promise<T> {
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<plugins.ICloudflareTypes['Record']> => {
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');
},
};
}
}

View File

@ -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<CloudflareRecord> {
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<boolean> {
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;
}
}
}

View File

@ -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 {
}
}
}
}
}

View File

@ -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<plugins.ICloudflareTypes['Script']> {
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<CloudflareWorker | undefined> {
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<boolean> {
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;
}
}
}

View File

@ -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<CloudflareZone> {
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<CloudflareZone> {
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<boolean> {
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<boolean> {
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'));
}
}

View File

@ -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<CloudflareZone[]> {
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<CloudflareZone | undefined> {
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<CloudflareZone | undefined> {
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<CloudflareZone | undefined> {
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<boolean> {
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<boolean> {
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<CloudflareZone | undefined> {
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;
}
}
}

130
ts/cloudflare.utils.ts Normal file
View File

@ -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<T>(
makeRequest: (page: number, perPage: number) => Promise<{ result: T[], result_info: { total_pages: number } }>
): Promise<T[]> {
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;
}
}

View File

@ -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';

View File

@ -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;
};
}

View File

@ -1,2 +1,3 @@
export * from './cloudflare.api.account.js';
export * from './cloudflare.api.workerroute.js';
export * from './cloudflare.api.zone.js';