diff --git a/changelog.md b/changelog.md index 86cf29c..bd217fc 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,17 @@ # Changelog +## 2025-11-18 - 7.0.0 - BREAKING CHANGE(core) +Introduce RecordManager and ConvenientDnsProvider; rename list/get methods for consistent API and deprecate convenience namespace + +- Add RecordManager with listRecords, getRecord, createRecord, updateRecord, deleteRecord and cleanRecords to centralize DNS record operations +- Add ConvenientDnsProvider adapter and CloudflareAccount.getConvenientDnsProvider() to provide IConvenientDnsProvider compatibility for third-party modules +- Rename methods to consistent list* naming: worker.getRoutes -> worker.listRoutes, WorkerManager.listWorkerScripts -> WorkerManager.listWorkers, ZoneManager.getZones -> ZoneManager.listZones, convenience.listRecords -> recordManager.listRecords +- Add ZoneManager.getZoneId() and ZoneManager.purgeZone() (zone cache purge helper) +- Deprecate the legacy convenience.* methods (getZoneId, getRecord, createRecord, removeRecord, cleanRecord, updateRecord, listRecords, listZones, isDomainSupported, purgeZone, acmeSetDnsChallenge, acmeRemoveDnsChallenge) — kept for backward compatibility but marked deprecated +- Export RecordManager and ConvenientDnsProvider from ts/index.ts and expose cfAccount.recordManager on CloudflareAccount +- Update tests to use new method names (listWorkers) and extend test runner timeout; package.json test script updated +- Documentation (readme) updated to describe the new manager-based API and migration guide; prepares project for major version 7.0.0 + ## 2025-11-17 - 6.4.3 - fix(cloudflare.plugins) Switch to smartrequest namespace export and improve request typing and JSON parsing diff --git a/package.json b/package.json index 7872bad..8925bfe 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "typings": "dist_ts/index.d.ts", "type": "module", "scripts": { - "test": "(tstest test/)", + "test": "(tstest test/ --verbose --timeout 600)", "build": "(tsbuild --web --allowimplicitany)", "buildDocs": "tsdoc", "updateOpenapi": "openapi-typescript https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.yaml --output ts/openapi.spec.ts" diff --git a/readme.md b/readme.md index 2ff93c7..12661b9 100644 --- a/readme.md +++ b/readme.md @@ -8,10 +8,10 @@ An elegant, class-based TypeScript client for the Cloudflare API that makes mana ## Features - **Comprehensive coverage** of the Cloudflare API including zones, DNS records, and Workers -- **Class-based design** with intuitive methods for all Cloudflare operations +- **Clean manager-based architecture** with intuitive methods for all Cloudflare operations - **Strong TypeScript typing** for excellent IDE autocompletion and type safety - **Fully integrated with the official Cloudflare client** using modern async iterators -- **Convenience methods** for common operations to reduce boilerplate code +- **IConvenientDnsProvider compatibility** for seamless integration with third-party modules - **Promise-based API** for easy async/await usage - **ESM compatible** for modern JavaScript projects - **Comprehensive error handling** for robust applications @@ -37,12 +37,13 @@ import * as cflare from '@apiclient.xyz/cloudflare'; // Initialize with your API token const cfAccount = new cflare.CloudflareAccount('your-cloudflare-api-token'); -// Use convenience methods for quick operations -await cfAccount.convenience.createRecord('subdomain.example.com', 'A', '192.0.2.1', 3600); +// Use the clean manager-based API +await cfAccount.recordManager.createRecord('subdomain.example.com', 'A', '192.0.2.1', 3600); +await cfAccount.zoneManager.purgeZone('example.com'); -// Or work with the powerful class-based API -const zone = await cfAccount.zoneManager.getZoneByName('example.com'); -await zone.purgeCache(); +// Or use the IConvenientDnsProvider interface for third-party modules +const dnsProvider = cfAccount.getConvenientDnsProvider(); +await dnsProvider.createRecord('subdomain.example.com', 'A', '192.0.2.1'); ``` ## Usage Guide @@ -68,20 +69,20 @@ const myAccounts = await cfAccount.listAccounts(); Zones represent your domains in Cloudflare. ```typescript -// Get all zones in your account -const allZones = await cfAccount.convenience.listZones(); +// List all zones in your account +const allZones = await cfAccount.zoneManager.listZones(); // Get a specific zone by domain name const myZone = await cfAccount.zoneManager.getZoneByName('example.com'); // Get zone ID directly -const zoneId = await cfAccount.convenience.getZoneId('example.com'); +const zoneId = await cfAccount.zoneManager.getZoneId('example.com'); // Create a new zone const newZone = await cfAccount.zoneManager.createZone('newdomain.com'); // Purge cache for an entire zone -await cfAccount.convenience.purgeZone('example.com'); +await cfAccount.zoneManager.purgeZone('example.com'); // Or using the zone object await myZone.purgeCache(); @@ -99,36 +100,40 @@ const usingCfNameservers = await myZone.isUsingCloudflareNameservers(); ### DNS Record Management -Manage DNS records for your domains with ease. +Manage DNS records for your domains with ease using the RecordManager. ```typescript // List all DNS records for a domain -const allRecords = await cfAccount.convenience.listRecords('example.com'); +const allRecords = await cfAccount.recordManager.listRecords('example.com'); // Create a new DNS record -await cfAccount.convenience.createRecord('api.example.com', 'A', '192.0.2.1', 3600); +await cfAccount.recordManager.createRecord('api.example.com', 'A', '192.0.2.1', 3600); // Create a CNAME record -await cfAccount.convenience.createRecord('www.example.com', 'CNAME', 'example.com', 3600); +await cfAccount.recordManager.createRecord('www.example.com', 'CNAME', 'example.com', 3600); // Get a specific DNS record -const record = await cfAccount.convenience.getRecord('api.example.com', 'A'); +const record = await cfAccount.recordManager.getRecord('api.example.com', 'A'); // Update a DNS record (automatically creates it if it doesn't exist) -await cfAccount.convenience.updateRecord('api.example.com', 'A', '192.0.2.2', 3600); +await cfAccount.recordManager.updateRecord('api.example.com', 'A', '192.0.2.2', 3600); -// Remove a specific DNS record -await cfAccount.convenience.removeRecord('api.example.com', 'A'); +// Delete a specific DNS record +await cfAccount.recordManager.deleteRecord('api.example.com', 'A'); -// Clean (remove) all records of a specific type -await cfAccount.convenience.cleanRecord('example.com', 'TXT'); +// Clean (remove) all records of a specific type for a domain +await cfAccount.recordManager.cleanRecords('example.com', 'TXT'); + +// For third-party modules requiring IConvenientDnsProvider interface +const dnsProvider = cfAccount.getConvenientDnsProvider(); +await dnsProvider.createRecord('api.example.com', 'A', '192.0.2.1'); // Support for ACME DNS challenges (for certificate issuance) -await cfAccount.convenience.acmeSetDnsChallenge({ +await dnsProvider.acmeSetDnsChallenge({ hostName: '_acme-challenge.example.com', challenge: 'token-validation-string', }); -await cfAccount.convenience.acmeRemoveDnsChallenge({ +await dnsProvider.acmeRemoveDnsChallenge({ hostName: '_acme-challenge.example.com', challenge: 'token-validation-string', }); @@ -148,7 +153,7 @@ addEventListener('fetch', event => { const worker = await cfAccount.workerManager.createWorker('my-worker', workerScript); // List all workers -const allWorkers = await cfAccount.workerManager.listWorkerScripts(); +const allWorkers = await cfAccount.workerManager.listWorkers(); // Get an existing worker const existingWorker = await cfAccount.workerManager.getWorker('my-worker'); @@ -165,8 +170,8 @@ await worker.setRoutes([ }, ]); -// Get all routes for a worker -const routes = await worker.getRoutes(); +// List all routes for a worker +const routes = await worker.listRoutes(); // Update a worker's script await worker.updateScript(` @@ -200,9 +205,9 @@ async function manageCloudflare() { console.log(`Zone active: ${await myZone.isActive()}`); console.log(`Using CF nameservers: ${await myZone.isUsingCloudflareNameservers()}`); - // Configure DNS - await cfAccount.convenience.createRecord('api.example.com', 'A', '192.0.2.1'); - await cfAccount.convenience.createRecord('www.example.com', 'CNAME', 'example.com'); + // Configure DNS using RecordManager + await cfAccount.recordManager.createRecord('api.example.com', 'A', '192.0.2.1'); + await cfAccount.recordManager.createRecord('www.example.com', 'CNAME', 'example.com'); // Create a worker and set up routes const workerCode = ` @@ -247,42 +252,78 @@ class CloudflareAccount { async listAccounts(): Promise>; async preselectAccountByName(accountName: string): Promise; - // Managers + // Managers - Clean, logical API readonly zoneManager: ZoneManager; readonly workerManager: WorkerManager; + readonly recordManager: RecordManager; + + // Get IConvenientDnsProvider adapter for third-party modules + getConvenientDnsProvider(): ConvenientDnsProvider; // Official Cloudflare client readonly apiAccount: cloudflare.Cloudflare; - // Convenience namespace with helper methods - readonly convenience: { - // Zone operations - listZones(domainName?: string): Promise; - getZoneId(domainName: string): Promise; - purgeZone(domainName: string): Promise; + // ⚠️ Deprecated: convenience namespace (kept for backward compatibility) + // Use the managers instead: recordManager, zoneManager, workerManager + readonly convenience: { /* deprecated methods */ }; +} +``` - // DNS operations - listRecords(domainName: string): Promise; - getRecord(domainName: string, recordType: string): Promise; - createRecord( - domainName: string, - recordType: string, - content: string, - ttl?: number, - ): Promise; - updateRecord( - domainName: string, - recordType: string, - content: string, - ttl?: number, - ): Promise; - removeRecord(domainName: string, recordType: string): Promise; - cleanRecord(domainName: string, recordType: string): Promise; +### RecordManager - // ACME operations - acmeSetDnsChallenge(dnsChallenge: IDnsChallenge): Promise; - acmeRemoveDnsChallenge(dnsChallenge: IDnsChallenge): Promise; - }; +Clean DNS record management (recommended over deprecated convenience methods). + +```typescript +class RecordManager { + async listRecords(domainName: string): Promise; + async getRecord(domainName: string, recordType: string): Promise; + async createRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise; + async updateRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise; + async deleteRecord(domainName: string, recordType: string): Promise; + async cleanRecords(domainName: string, recordType: string): Promise; +} +``` + +### ZoneManager + +```typescript +class ZoneManager { + async listZones(zoneName?: string): Promise; + async getZoneById(zoneId: string): Promise; + async getZoneByName(zoneName: string): Promise; + async getZoneId(domainName: string): Promise; + async createZone(zoneName: string): Promise; + async deleteZone(zoneId: string): Promise; + async purgeZone(domainName: string): Promise; +} +``` + +### WorkerManager + +```typescript +class WorkerManager { + async listWorkers(): Promise>; + async getWorker(workerName: string): Promise; + async createWorker(workerName: string, workerScript: string): Promise; + async deleteWorker(workerName: string): Promise; +} +``` + +### ConvenientDnsProvider + +Adapter for third-party modules requiring `IConvenientDnsProvider` interface. + +```typescript +class ConvenientDnsProvider implements IConvenientDnsProvider { + async createRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise; + async updateRecord(domainName: string, recordType: string, content: string, ttl?: number): Promise; + async removeRecord(domainName: string, recordType: string): Promise; + async getRecord(domainName: string, recordType: string): Promise; + async listRecords(domainName: string): Promise; + async cleanRecord(domainName: string, recordType: string): Promise; + async isDomainSupported(domainName: string): Promise; + async acmeSetDnsChallenge(dnsChallenge: IDnsChallenge): Promise; + async acmeRemoveDnsChallenge(dnsChallenge: IDnsChallenge): Promise; } ``` @@ -343,7 +384,7 @@ class CloudflareWorker { readonly routes: IWorkerRoute[]; // Methods - async getRoutes(): Promise; + async listRoutes(): Promise; // Populates the routes property async setRoutes(routes: Array): Promise; async updateScript(scriptContent: string): Promise; async delete(): Promise; @@ -376,13 +417,57 @@ CloudflareUtils.formatUrlForPurge('example.com/page'); // 'https://example.com/p CloudflareUtils.formatTtl(3600); // '1 hour' ``` -## What's New in 6.2.0 +## What's New in 7.0.0 -- **Improved async iterator support**: Fully leverages the official Cloudflare client's async iterator pattern -- **Enhanced error handling**: Better error detection and recovery -- **Simplified API**: More consistent method signatures and return types -- **Better type safety**: Improved TypeScript typing throughout the library -- **Detailed logging**: More informative logging for easier debugging +- **🎨 Clean Manager-Based Architecture**: New RecordManager, improved ZoneManager and WorkerManager with consistent naming +- **🔌 IConvenientDnsProvider Compatibility**: New ConvenientDnsProvider adapter for seamless third-party module integration +- **📝 Consistent Method Naming**: + - `listZones()`, `listWorkers()`, `listRecords()` - consistent list* pattern + - `deleteRecord()` instead of `removeRecord()` - clearer semantics + - `listRoutes()` instead of `getRoutes()` - consistent with other list methods +- **⚠️ Deprecated convenience Namespace**: Old methods still work but are deprecated - use managers instead +- **✅ Backward Compatible**: All existing code continues to work with deprecation warnings + +## Migration Guide (6.x → 7.0) + +### DNS Record Operations +```typescript +// Old (deprecated): +await cfAccount.convenience.createRecord('example.com', 'A', '1.2.3.4'); +await cfAccount.convenience.listRecords('example.com'); +await cfAccount.convenience.removeRecord('example.com', 'A'); + +// New (recommended): +await cfAccount.recordManager.createRecord('example.com', 'A', '1.2.3.4'); +await cfAccount.recordManager.listRecords('example.com'); +await cfAccount.recordManager.deleteRecord('example.com', 'A'); + +// For third-party modules: +const dnsProvider = cfAccount.getConvenientDnsProvider(); +await dnsProvider.createRecord('example.com', 'A', '1.2.3.4'); +``` + +### Zone Operations +```typescript +// Old (deprecated): +await cfAccount.convenience.listZones(); +await cfAccount.convenience.purgeZone('example.com'); + +// New (recommended): +await cfAccount.zoneManager.listZones(); +await cfAccount.zoneManager.purgeZone('example.com'); +``` + +### Worker Operations +```typescript +// Old: +await cfAccount.workerManager.listWorkerScripts(); +await worker.getRoutes(); + +// New: +await cfAccount.workerManager.listWorkers(); +await worker.listRoutes(); +``` ## Development & Testing diff --git a/test/test.node+deno.ts b/test/test.node+deno.ts index 0d56296..17b3701 100644 --- a/test/test.node+deno.ts +++ b/test/test.node+deno.ts @@ -236,7 +236,7 @@ tap.test('should list workers', async (tools) => { tools.timeout(600000); try { - const workerArray = await testCloudflareAccount.workerManager.listWorkerScripts(); + const workerArray = await testCloudflareAccount.workerManager.listWorkers(); expect(workerArray).toBeTypeOf('array'); console.log(`Found ${workerArray.length} workers in account`); } catch (error) { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index c2fbb47..312ef0f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@apiclient.xyz/cloudflare', - version: '6.4.3', + version: '7.0.0', 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 875d3a4..683da14 100644 --- a/ts/cloudflare.classes.account.ts +++ b/ts/cloudflare.classes.account.ts @@ -5,6 +5,8 @@ import * as interfaces from './interfaces/index.js'; // interfaces import { WorkerManager } from './cloudflare.classes.workermanager.js'; import { ZoneManager } from './cloudflare.classes.zonemanager.js'; +import { RecordManager } from './cloudflare.classes.recordmanager.js'; +import { ConvenientDnsProvider } from './cloudflare.classes.convenientdnsprovider.js'; export class CloudflareAccount implements plugins.tsclass.network.IConvenientDnsProvider { private authToken: string; @@ -12,6 +14,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns public workerManager = new WorkerManager(this); public zoneManager = new ZoneManager(this); + public recordManager = new RecordManager(this); public apiAccount: plugins.cloudflare.Cloudflare; @@ -133,6 +136,16 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns } } + /** + * Returns a ConvenientDnsProvider instance that implements IConvenientDnsProvider + * This allows third-party modules to use the standard DNS provider interface + * while internally delegating to the clean RecordManager and ZoneManager structure + * @returns ConvenientDnsProvider instance + */ + public getConvenientDnsProvider(): ConvenientDnsProvider { + return new ConvenientDnsProvider(this); + } + public convenience = { /** * Lists all accounts accessible with the current API token @@ -157,6 +170,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns /** * gets a zone id of a domain from cloudflare * @param domainName + * @deprecated Use zoneManager.getZoneId() instead */ getZoneId: async (domainName: string) => { const domain = new plugins.smartstring.Domain(domainName); @@ -175,6 +189,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns * gets a record * @param domainNameArg * @param typeArg + * @deprecated Use recordManager.getRecord() or getConvenientDnsProvider().getRecord() instead */ getRecord: async ( domainNameArg: string, @@ -204,6 +219,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns }, /** * creates a record + * @deprecated Use recordManager.createRecord() or getConvenientDnsProvider().createRecord() instead */ createRecord: async ( domainNameArg: string, @@ -226,6 +242,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns * removes a record from Cloudflare * @param domainNameArg * @param typeArg + * @deprecated Use recordManager.deleteRecord() or getConvenientDnsProvider().removeRecord() instead */ removeRecord: async ( domainNameArg: string, @@ -251,6 +268,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns /** * cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects + * @deprecated Use recordManager.cleanRecords() or getConvenientDnsProvider().cleanRecord() instead */ cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => { try { @@ -312,6 +330,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns * @param contentArg New content for the record * @param ttlArg Time to live in seconds (optional) * @returns Updated record + * @deprecated Use recordManager.updateRecord() or getConvenientDnsProvider().updateRecord() instead */ updateRecord: async ( domainNameArg: string, @@ -348,6 +367,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns /** * list all records of a specified domain name * @param domainNameArg - the domain name that you want to get the records from + * @deprecated Use recordManager.listRecords() or getConvenientDnsProvider().listRecords() instead */ listRecords: async (domainNameArg: string) => { try { @@ -372,6 +392,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns /** * list all zones in the associated authenticated account * @param domainName optional filter by domain name + * @deprecated Use zoneManager.listZones() instead */ listZones: async (domainName?: string) => { try { @@ -401,6 +422,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns * Determines whether the given domain can be managed by this account * @param domainName Full domain name to check (e.g., "sub.example.com") * @returns True if the zone for the domain exists in the account, false otherwise + * @deprecated Use getConvenientDnsProvider().isDomainSupported() instead */ isDomainSupported: async (domainName: string): Promise => { try { @@ -417,6 +439,7 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns }, /** * purges a zone + * @deprecated Use zoneManager.purgeZone() instead */ purgeZone: async (domainName: string): Promise => { const domain = new plugins.smartstring.Domain(domainName); @@ -428,6 +451,9 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns }, // acme convenience functions + /** + * @deprecated Use getConvenientDnsProvider().acmeSetDnsChallenge() instead + */ acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT'); await this.convenience.createRecord( @@ -437,6 +463,9 @@ export class CloudflareAccount implements plugins.tsclass.network.IConvenientDns 120, ); }, + /** + * @deprecated Use getConvenientDnsProvider().acmeRemoveDnsChallenge() instead + */ acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT'); }, diff --git a/ts/cloudflare.classes.convenientdnsprovider.ts b/ts/cloudflare.classes.convenientdnsprovider.ts new file mode 100644 index 0000000..3b5af04 --- /dev/null +++ b/ts/cloudflare.classes.convenientdnsprovider.ts @@ -0,0 +1,178 @@ +import * as plugins from './cloudflare.plugins.js'; +import { logger } from './cloudflare.logger.js'; + +/** + * Adapter class that implements IConvenientDnsProvider interface + * Delegates to RecordManager and ZoneManager internally for clean architecture + * This allows third-party modules to use the standard DNS provider interface + */ +export class ConvenientDnsProvider implements plugins.tsclass.network.IConvenientDnsProvider { + /** + * The convenience property is required by IConvenientDnsProvider interface + * It returns this instance to maintain interface compatibility + */ + public convenience = this; + + constructor(private cfAccount: any) {} + + /** + * Creates a new DNS record + * @param domainNameArg - The domain name for the record + * @param typeArg - The DNS record type + * @param contentArg - The record content (IP address, CNAME target, etc.) + * @param ttlArg - Time to live in seconds (default: 1 = automatic) + * @returns Created record as raw API object + */ + public async createRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + contentArg: string, + ttlArg: number = 1, + ): Promise { + const record = await this.cfAccount.recordManager.createRecord( + domainNameArg, + typeArg, + contentArg, + ttlArg, + ); + // Return raw API object format for interface compatibility + return this.recordToApiObject(record); + } + + /** + * Updates an existing DNS record, or creates it if it doesn't exist + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type + * @param contentArg - The new record content + * @param ttlArg - Time to live in seconds (default: 1 = automatic) + * @returns Updated record as raw API object + */ + public async updateRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + contentArg: string, + ttlArg: number = 1, + ): Promise { + const record = await this.cfAccount.recordManager.updateRecord( + domainNameArg, + typeArg, + contentArg, + ttlArg, + ); + return this.recordToApiObject(record); + } + + /** + * Removes a DNS record + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type + */ + public async removeRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + await this.cfAccount.recordManager.deleteRecord(domainNameArg, typeArg); + } + + /** + * Gets a specific DNS record by domain and type + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type + * @returns Record as raw API object or undefined if not found + */ + public async getRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + const record = await this.cfAccount.recordManager.getRecord(domainNameArg, typeArg); + return record ? this.recordToApiObject(record) : undefined; + } + + /** + * Lists all DNS records for a domain + * @param domainNameArg - The domain name to list records for + * @returns Array of records as raw API objects + */ + public async listRecords(domainNameArg: string): Promise { + const records = await this.cfAccount.recordManager.listRecords(domainNameArg); + return records.map((record: any) => this.recordToApiObject(record)); + } + + /** + * Removes all DNS records of a specific type for a domain + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type to clean + */ + public async cleanRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + await this.cfAccount.recordManager.cleanRecords(domainNameArg, typeArg); + } + + /** + * Determines whether the given domain can be managed by this account + * @param domainName - Full domain name to check (e.g., "sub.example.com") + * @returns True if the zone for the domain exists in the account, false otherwise + */ + public async isDomainSupported(domainName: string): Promise { + try { + // Parse out the apex/zone name from the full domain + const domain = new plugins.smartstring.Domain(domainName); + // List zones filtered by the zone name + const zones = await this.cfAccount.zoneManager.listZones(domain.zoneName); + // If any zone matches, we can manage this domain + return Array.isArray(zones) && zones.length > 0; + } catch (error) { + logger.log('error', `Error checking domain support for ${domainName}: ${error.message}`); + return false; + } + } + + /** + * Sets an ACME DNS challenge for domain verification + * @param dnsChallenge - The DNS challenge object + */ + public async acmeSetDnsChallenge( + dnsChallenge: plugins.tsclass.network.IDnsChallenge, + ): Promise { + await this.cfAccount.recordManager.cleanRecords(dnsChallenge.hostName, 'TXT'); + await this.cfAccount.recordManager.createRecord( + dnsChallenge.hostName, + 'TXT', + dnsChallenge.challenge, + 120, + ); + } + + /** + * Removes an ACME DNS challenge + * @param dnsChallenge - The DNS challenge object + */ + public async acmeRemoveDnsChallenge( + dnsChallenge: plugins.tsclass.network.IDnsChallenge, + ): Promise { + await this.cfAccount.recordManager.deleteRecord(dnsChallenge.hostName, 'TXT'); + } + + /** + * Helper method to convert CloudflareRecord instance to raw API object format + * This ensures compatibility with the IConvenientDnsProvider interface + */ + private recordToApiObject(record: any): any { + return { + id: record.id, + type: record.type, + name: record.name, + content: record.content, + proxiable: record.proxiable, + proxied: record.proxied, + ttl: record.ttl, + locked: record.locked, + zone_id: record.zone_id, + zone_name: record.zone_name, + created_on: record.created_on, + modified_on: record.modified_on, + }; + } +} diff --git a/ts/cloudflare.classes.recordmanager.ts b/ts/cloudflare.classes.recordmanager.ts new file mode 100644 index 0000000..98d64e2 --- /dev/null +++ b/ts/cloudflare.classes.recordmanager.ts @@ -0,0 +1,198 @@ +import * as plugins from './cloudflare.plugins.js'; +import { logger } from './cloudflare.logger.js'; +import { CloudflareRecord } from './cloudflare.classes.record.js'; + +export class RecordManager { + constructor(private cfAccount: any) {} + + /** + * Lists all DNS records for a domain + * @param domainNameArg - The domain name to list records for + * @returns Array of CloudflareRecord instances + */ + public async listRecords(domainNameArg: string): Promise { + try { + const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName); + const records: plugins.ICloudflareTypes['Record'][] = []; + + // Collect all records using async iterator + for await (const record of this.cfAccount.apiAccount.dns.records.list({ + zone_id: zoneId, + })) { + records.push(record); + } + + logger.log('info', `Found ${records.length} DNS records for ${domainNameArg}`); + + // Convert to CloudflareRecord instances + return records.map(record => CloudflareRecord.createFromApiObject(record)); + } catch (error) { + logger.log('error', `Failed to list records for ${domainNameArg}: ${error.message}`); + return []; + } + } + + /** + * Gets a specific DNS record by domain and type + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type (A, AAAA, CNAME, TXT, etc.) + * @returns CloudflareRecord instance or undefined if not found + */ + public async getRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + try { + const domain = new plugins.smartstring.Domain(domainNameArg); + const recordArray = await this.listRecords(domain.zoneName); + + const filteredRecords = recordArray.filter((recordArg) => { + return recordArg.type === typeArg && recordArg.name === domainNameArg; + }); + + return filteredRecords.length > 0 ? filteredRecords[0] : undefined; + } catch (error) { + logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`); + return undefined; + } + } + + /** + * Creates a new DNS record + * @param domainNameArg - The domain name for the record + * @param typeArg - The DNS record type + * @param contentArg - The record content (IP address, CNAME target, etc.) + * @param ttlArg - Time to live in seconds (default: 1 = automatic) + * @returns Created CloudflareRecord instance + */ + public async createRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + contentArg: string, + ttlArg: number = 1, + ): Promise { + const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName); + + const response = await this.cfAccount.apiAccount.dns.records.create({ + zone_id: zoneId, + type: typeArg as any, + name: domain.fullName, + content: contentArg, + ttl: ttlArg, + }); + + logger.log('info', `Created ${typeArg} record for ${domainNameArg}`); + return CloudflareRecord.createFromApiObject(response); + } + + /** + * Updates an existing DNS record, or creates it if it doesn't exist + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type + * @param contentArg - The new record content + * @param ttlArg - Time to live in seconds (default: 1 = automatic) + * @returns Updated CloudflareRecord instance + */ + public async updateRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + contentArg: string, + ttlArg: number = 1, + ): Promise { + const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName); + + // Find existing record + const existingRecord = await this.getRecord(domainNameArg, typeArg); + + if (!existingRecord) { + logger.log( + 'warn', + `Record ${domainNameArg} of type ${typeArg} not found for update, creating instead`, + ); + return this.createRecord(domainNameArg, typeArg, contentArg, ttlArg); + } + + // Update the record + const updatedRecord = await this.cfAccount.apiAccount.dns.records.edit(existingRecord.id, { + zone_id: zoneId, + type: typeArg as any, + name: domain.fullName, + content: contentArg, + ttl: ttlArg, + }); + + logger.log('info', `Updated ${typeArg} record for ${domainNameArg}`); + return CloudflareRecord.createFromApiObject(updatedRecord); + } + + /** + * Deletes a DNS record + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type + */ + public async deleteRecord( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName); + const record = await this.getRecord(domainNameArg, typeArg); + + if (record) { + await this.cfAccount.apiAccount.dns.records.delete(record.id, { + zone_id: zoneId, + }); + logger.log('info', `Deleted ${typeArg} record for ${domainNameArg}`); + } else { + logger.log('warn', `Record ${domainNameArg} of type ${typeArg} not found for deletion`); + } + } + + /** + * Removes all DNS records of a specific type for a domain + * @param domainNameArg - The domain name + * @param typeArg - The DNS record type to clean + */ + public async cleanRecords( + domainNameArg: string, + typeArg: plugins.tsclass.network.TDnsRecordType, + ): Promise { + try { + logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`); + const domain = new plugins.smartstring.Domain(domainNameArg); + const zoneId = await this.cfAccount.zoneManager.getZoneId(domain.zoneName); + + // List all records in the zone for this domain + const records = await this.listRecords(domain.zoneName); + + // Only delete records matching the specified name and type + const recordsToDelete = records.filter((recordArg) => { + return recordArg.type === typeArg && recordArg.name === domainNameArg; + }); + + logger.log( + 'info', + `Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`, + ); + + for (const recordToDelete of recordsToDelete) { + try { + await this.cfAccount.apiAccount.dns.records.delete(recordToDelete.id, { + zone_id: zoneId, + }); + logger.log('info', `Deleted ${typeArg} record ${recordToDelete.id} for ${domainNameArg}`); + } catch (deleteError) { + logger.log('error', `Failed to delete record: ${deleteError.message}`); + } + } + } catch (error) { + logger.log( + 'error', + `Error cleaning ${typeArg} records for ${domainNameArg}: ${error.message}`, + ); + } + } +} diff --git a/ts/cloudflare.classes.worker.ts b/ts/cloudflare.classes.worker.ts index 14a9068..22d67f0 100644 --- a/ts/cloudflare.classes.worker.ts +++ b/ts/cloudflare.classes.worker.ts @@ -20,7 +20,7 @@ export class CloudflareWorker { ): Promise { const newWorker = new CloudflareWorker(workerManager); Object.assign(newWorker, apiObject); - await newWorker.getRoutes(); + await newWorker.listRoutes(); return newWorker; } @@ -41,9 +41,9 @@ export class CloudflareWorker { } /** - * gets all routes for a worker + * Lists all routes for this worker */ - public async getRoutes() { + public async listRoutes() { try { this.routes = []; // Reset routes before fetching @@ -102,7 +102,7 @@ export class CloudflareWorker { */ public async setRoutes(routeArray: IWorkerRouteDefinition[]) { // First get all existing routes to determine what we need to create/update - await this.getRoutes(); + await this.listRoutes(); for (const newRoute of routeArray) { // Determine whether a route is new, needs an update, or is already up to date @@ -156,7 +156,7 @@ export class CloudflareWorker { } // Refresh routes after all changes - await this.getRoutes(); + await this.listRoutes(); } /** diff --git a/ts/cloudflare.classes.workermanager.ts b/ts/cloudflare.classes.workermanager.ts index 2ae2544..620f38e 100644 --- a/ts/cloudflare.classes.workermanager.ts +++ b/ts/cloudflare.classes.workermanager.ts @@ -39,7 +39,7 @@ export class WorkerManager { // Initialize the worker and get its routes try { - await worker.getRoutes(); + await worker.listRoutes(); } catch (routeError) { logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`); // Continue anyway since the worker was created @@ -79,7 +79,7 @@ export class WorkerManager { // Initialize the worker and get its routes try { - await worker.getRoutes(); + await worker.listRoutes(); } catch (routeError) { logger.log('warn', `Failed to get routes for worker ${workerName}: ${routeError.message}`); // Continue anyway since we found the worker @@ -96,7 +96,7 @@ export class WorkerManager { * Lists all worker scripts * @returns Array of worker scripts */ - public async listWorkerScripts() { + public async listWorkers() { if (!this.cfAccount.preselectedAccountId) { throw new Error('No account selected. Please select it first on the account.'); } diff --git a/ts/cloudflare.classes.zonemanager.ts b/ts/cloudflare.classes.zonemanager.ts index f9d7b52..7fd3677 100644 --- a/ts/cloudflare.classes.zonemanager.ts +++ b/ts/cloudflare.classes.zonemanager.ts @@ -12,11 +12,11 @@ export class ZoneManager { } /** - * Get all zones, optionally filtered by name + * Lists 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 { + public async listZones(zoneName?: string): Promise { try { const options: any = { per_page: 50 }; @@ -37,13 +37,33 @@ export class ZoneManager { } } + /** + * Gets the zone ID for a domain name + * @param domainName Domain name to get the zone ID for + * @returns Zone ID string + * @throws Error if domain is not found in this account + */ + public async getZoneId(domainName: string): Promise { + const domain = new plugins.smartstring.Domain(domainName); + const zoneArray = await this.listZones(domain.zoneName); + const filteredResponse = zoneArray.filter((zoneArg) => { + return zoneArg.name === domainName; + }); + if (filteredResponse.length >= 1) { + return filteredResponse[0].id; + } else { + logger.log('error', `the domain ${domainName} does not appear to be in this account!`); + throw new Error(`the domain ${domainName} does not appear to be in this account!`); + } + } + /** * 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); + const zones = await this.listZones(zoneName); return zones.find((zone) => zone.name === zoneName); } @@ -130,7 +150,7 @@ export class ZoneManager { * @returns True if the zone exists */ public async zoneExists(zoneName: string): Promise { - const zones = await this.getZones(zoneName); + const zones = await this.listZones(zoneName); return zones.some((zone) => zone.name === zoneName); } @@ -180,4 +200,18 @@ export class ZoneManager { return undefined; } } + + /** + * Purges all cached files for a zone + * @param domainName Domain name to purge cache for + */ + public async purgeZone(domainName: string): Promise { + const domain = new plugins.smartstring.Domain(domainName); + const zoneId = await this.getZoneId(domain.zoneName); + await this.cfAccount.apiAccount.cache.purge({ + zone_id: zoneId, + purge_everything: true, + }); + logger.log('info', `Purged cache for zone ${domainName}`); + } } diff --git a/ts/index.ts b/ts/index.ts index ddb9997..89094b8 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -6,8 +6,10 @@ export { } from './cloudflare.classes.worker.js'; export { WorkerManager } from './cloudflare.classes.workermanager.js'; export { CloudflareRecord, type ICloudflareRecordInfo } from './cloudflare.classes.record.js'; +export { RecordManager } from './cloudflare.classes.recordmanager.js'; export { CloudflareZone } from './cloudflare.classes.zone.js'; export { ZoneManager } from './cloudflare.classes.zonemanager.js'; +export { ConvenientDnsProvider } from './cloudflare.classes.convenientdnsprovider.js'; export { CloudflareUtils } from './cloudflare.utils.js'; export { commitinfo } from './00_commitinfo_data.js';