349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
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<T = any>(
|
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
|
endpoint: string,
|
|
data?: any
|
|
): Promise<T> {
|
|
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<plugins.ICloudflareTypes['Record'] | undefined> => {
|
|
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<any> => {
|
|
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<any> => {
|
|
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<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 - 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<void> => {
|
|
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');
|
|
},
|
|
};
|
|
} |