import * as plugins from './cloudflare.plugins.js'; import { logger } from './cloudflare.logger.js'; import * as interfaces from './interfaces/index.js'; // interfaces import { WorkerManager } from './cloudflare.classes.workermanager.js'; import { ZoneManager } from './cloudflare.classes.zonemanager.js'; export class CloudflareAccount { private authToken: string; public preselectedAccountId: string; public workerManager = new WorkerManager(this); public zoneManager = new ZoneManager(this); public apiAccount: plugins.cloudflare.Cloudflare; /** * constructor sets auth information on the CloudflareAccountInstance * @param authTokenArg Cloudflare API token */ constructor(authTokenArg: string) { this.authToken = authTokenArg; this.apiAccount = new plugins.cloudflare.Cloudflare({ apiToken: this.authToken, }); } /** * Make a request to the Cloudflare API for endpoints not directly supported by the official client * Only use this for endpoints that don't have a direct method in the official client * @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' | 'PATCH', endpoint: string, data?: any ): Promise { try { const options: plugins.smartrequest.ISmartRequestOptions = { method, headers: { 'Authorization': `Bearer ${this.authToken}`, 'Content-Type': 'application/json', }, }; if (data) { options.requestBody = JSON.stringify(data); } const response = await plugins.smartrequest.request(`https://api.cloudflare.com/client/v4${endpoint}`, 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) => { return accountArg.name === nameArg; }); if (account) { this.preselectedAccountId = account.id; } else { throw new Error(`account with name ${nameArg} not found`); } } public convenience = { /** * listAccounts */ listAccounts: async () => { const accounts: plugins.ICloudflareTypes['Account'][] = []; for await (const account of this.apiAccount.accounts.list()) { accounts.push(account as interfaces.ICloudflareApiAccountObject); } return accounts; }, /** * gets a zone id of a domain from cloudflare * @param domainName */ getZoneId: async (domainName: string) => { const domain = new plugins.smartstring.Domain(domainName); const zoneArray = await this.convenience.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!`); } }, /** * gets a record * @param domainNameArg * @param typeArg */ getRecord: async ( domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType ): Promise => { try { const domain = new plugins.smartstring.Domain(domainNameArg); const recordArrayArg = await this.convenience.listRecords(domain.zoneName); if (!Array.isArray(recordArrayArg)) { logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof recordArrayArg}`); return undefined; } const filteredResponse = recordArrayArg.filter((recordArg) => { return recordArg.type === typeArg && recordArg.name === domainNameArg; }); return filteredResponse.length > 0 ? filteredResponse[0] : undefined; } catch (error) { logger.log('error', `Error getting record for ${domainNameArg}: ${error.message}`); return undefined; } }, /** * creates a record */ createRecord: async ( domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType, contentArg: string, ttlArg = 1 ): Promise => { const domain = new plugins.smartstring.Domain(domainNameArg); const zoneId = await this.convenience.getZoneId(domain.zoneName); const response = await this.apiAccount.dns.records.create({ zone_id: zoneId, type: typeArg as any, name: domain.fullName, content: contentArg, ttl: ttlArg, }) return response; }, /** * removes a record from Cloudflare * @param domainNameArg * @param typeArg */ removeRecord: async ( domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType ): Promise => { const domain = new plugins.smartstring.Domain(domainNameArg); const zoneId = await this.convenience.getZoneId(domain.zoneName); const records = await this.convenience.listRecords(domain.zoneName); const recordToDelete = records.find((recordArg) => { return recordArg.name === domainNameArg && recordArg.type === typeArg; }); if (recordToDelete) { // The official client might have the id in a different location // Casting to any to access the id property const recordId = (recordToDelete as any).id; await this.apiAccount.dns.records.delete(recordId, { zone_id: zoneId, }); } else { logger.log('warn', `record ${domainNameArg} of type ${typeArg} not found`); } }, /** * cleanrecord allows the cleaning of any previous records to avoid unwanted sideeffects */ cleanRecord: async (domainNameArg: string, typeArg: plugins.tsclass.network.TDnsRecordType) => { try { logger.log('info', `Cleaning ${typeArg} records for ${domainNameArg}`); const domain = new plugins.smartstring.Domain(domainNameArg); const zoneId = await this.convenience.getZoneId(domain.zoneName); const records = await this.convenience.listRecords(domainNameArg); if (!Array.isArray(records)) { logger.log('warn', `Expected records array for ${domainNameArg} but got ${typeof records}`); return; } const recordsToDelete = records.filter((recordArg) => { return recordArg.type === typeArg; }); logger.log('info', `Found ${recordsToDelete.length} ${typeArg} records to delete for ${domainNameArg}`); for (const recordToDelete of recordsToDelete) { try { // The official client might have different property locations // Casting to any to access properties safely const recordId = (recordToDelete as any).id; if (!recordId) { logger.log('warn', `Record ID not found for ${domainNameArg} record`); continue; } await this.apiAccount.dns.records.delete(recordId, { zone_id: zoneId, }); logger.log('info', `Deleted ${typeArg} record ${recordId} 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}`); } }, /** * updates a record * @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, 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 - cast to any to access the id property const recordId = (record as any).id; const updatedRecord = await this.apiAccount.dns.records.edit(recordId, { zone_id: zoneId, type: typeArg as any, name: domain.fullName, content: contentArg, ttl: ttlArg }); return updatedRecord; }, /** * list all records of a specified domain name * @param domainNameArg - the domain name that you want to get the records from */ listRecords: async (domainNameArg: string) => { const domain = new plugins.smartstring.Domain(domainNameArg); const zoneId = await this.convenience.getZoneId(domain.zoneName); const records: plugins.ICloudflareTypes['Record'][] = []; try { const result = await this.apiAccount.dns.records.list({ zone_id: zoneId, }); // Check if the result has a 'result' property (API response format) if (result && result.result && Array.isArray(result.result)) { return result.result; } // Otherwise iterate through async iterator (new client format) for await (const record of this.apiAccount.dns.records.list({ zone_id: zoneId, })) { records.push(record); } return records; } catch (error) { logger.log('error', `Failed to list records for ${domainNameArg}: ${error.message}`); return []; } }, /** * list all zones in the associated authenticated account * @param domainName */ listZones: async (domainName?: string) => { const options: any = {}; if (domainName) { options.name = domainName; } const zones: plugins.ICloudflareTypes['Zone'][] = []; try { const result = await this.apiAccount.zones.list(options); // Check if the result has a 'result' property (API response format) if (result && result.result && Array.isArray(result.result)) { return result.result; } // Otherwise iterate through async iterator (new client format) for await (const zone of this.apiAccount.zones.list(options)) { zones.push(zone); } return zones; } catch (error) { logger.log('error', `Failed to list zones: ${error.message}`); return []; } }, /** * purges a zone */ purgeZone: async (domainName: string): Promise => { const domain = new plugins.smartstring.Domain(domainName); const zoneId = await this.convenience.getZoneId(domain.zoneName); await this.apiAccount.cache.purge({ zone_id: zoneId, purge_everything: true, }); }, // acme convenience functions acmeSetDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { await this.convenience.cleanRecord(dnsChallenge.hostName, 'TXT'); await this.convenience.createRecord( dnsChallenge.hostName, 'TXT', dnsChallenge.challenge, 120 ); }, acmeRemoveDnsChallenge: async (dnsChallenge: plugins.tsclass.network.IDnsChallenge) => { await this.convenience.removeRecord(dnsChallenge.hostName, 'TXT'); }, }; }