140 lines
4.4 KiB
TypeScript
140 lines
4.4 KiB
TypeScript
import * as plugins from '../../plugins.js';
|
|
import { logger } from '../../logger.js';
|
|
import type {
|
|
IDnsProviderClient,
|
|
IConnectionTestResult,
|
|
IProviderRecord,
|
|
IProviderRecordInput,
|
|
} from './interfaces.js';
|
|
import type { IProviderDomainListing } from '../../../ts_interfaces/data/dns-provider.js';
|
|
import type { TDnsRecordType } from '../../../ts_interfaces/data/dns-record.js';
|
|
|
|
/**
|
|
* Cloudflare implementation of IDnsProviderClient.
|
|
*
|
|
* Wraps `@apiclient.xyz/cloudflare`. Records at Cloudflare are addressed by
|
|
* an internal record id, which we surface as `providerRecordId` so the rest
|
|
* of the system can issue updates and deletes without ambiguity (Cloudflare
|
|
* can have multiple records of the same name+type).
|
|
*/
|
|
export class CloudflareDnsProvider implements IDnsProviderClient {
|
|
private cfAccount: plugins.cloudflare.CloudflareAccount;
|
|
|
|
constructor(apiToken: string) {
|
|
if (!apiToken) {
|
|
throw new Error('CloudflareDnsProvider: apiToken is required');
|
|
}
|
|
this.cfAccount = new plugins.cloudflare.CloudflareAccount(apiToken);
|
|
}
|
|
|
|
/**
|
|
* Returns the underlying CloudflareAccount — used by ACME DNS-01
|
|
* to wrap into a smartacme Dns01Handler.
|
|
*/
|
|
public getCloudflareAccount(): plugins.cloudflare.CloudflareAccount {
|
|
return this.cfAccount;
|
|
}
|
|
|
|
public async testConnection(): Promise<IConnectionTestResult> {
|
|
try {
|
|
// Listing zones is the lightest-weight call that proves the token works.
|
|
await this.cfAccount.zoneManager.listZones();
|
|
return { ok: true };
|
|
} catch (err: unknown) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
logger.log('warn', `CloudflareDnsProvider testConnection failed: ${message}`);
|
|
return { ok: false, error: message };
|
|
}
|
|
}
|
|
|
|
public async listDomains(): Promise<IProviderDomainListing[]> {
|
|
const zones = await this.cfAccount.zoneManager.listZones();
|
|
return zones.map((zone) => ({
|
|
name: zone.name,
|
|
externalId: zone.id,
|
|
nameservers: zone.name_servers ?? [],
|
|
}));
|
|
}
|
|
|
|
public async listRecords(domain: string): Promise<IProviderRecord[]> {
|
|
const records = await this.cfAccount.recordManager.listRecords(domain);
|
|
return records
|
|
.filter((r) => this.isSupportedType(r.type))
|
|
.map((r) => ({
|
|
providerRecordId: r.id,
|
|
name: r.name,
|
|
type: r.type as TDnsRecordType,
|
|
value: r.content,
|
|
ttl: r.ttl,
|
|
proxied: r.proxied,
|
|
}));
|
|
}
|
|
|
|
public async createRecord(
|
|
domain: string,
|
|
record: IProviderRecordInput,
|
|
): Promise<IProviderRecord> {
|
|
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain);
|
|
const apiRecord: any = {
|
|
zone_id: zoneId,
|
|
type: record.type,
|
|
name: record.name,
|
|
content: record.value,
|
|
ttl: record.ttl ?? 1, // 1 = automatic
|
|
};
|
|
if (record.proxied !== undefined) {
|
|
apiRecord.proxied = record.proxied;
|
|
}
|
|
const created = await (this.cfAccount as any).apiAccount.dns.records.create(apiRecord);
|
|
return {
|
|
providerRecordId: created.id,
|
|
name: created.name,
|
|
type: created.type as TDnsRecordType,
|
|
value: created.content,
|
|
ttl: created.ttl,
|
|
proxied: created.proxied,
|
|
};
|
|
}
|
|
|
|
public async updateRecord(
|
|
domain: string,
|
|
providerRecordId: string,
|
|
record: IProviderRecordInput,
|
|
): Promise<IProviderRecord> {
|
|
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain);
|
|
const apiRecord: any = {
|
|
zone_id: zoneId,
|
|
type: record.type,
|
|
name: record.name,
|
|
content: record.value,
|
|
ttl: record.ttl ?? 1,
|
|
};
|
|
if (record.proxied !== undefined) {
|
|
apiRecord.proxied = record.proxied;
|
|
}
|
|
const updated = await (this.cfAccount as any).apiAccount.dns.records.edit(
|
|
providerRecordId,
|
|
apiRecord,
|
|
);
|
|
return {
|
|
providerRecordId: updated.id,
|
|
name: updated.name,
|
|
type: updated.type as TDnsRecordType,
|
|
value: updated.content,
|
|
ttl: updated.ttl,
|
|
proxied: updated.proxied,
|
|
};
|
|
}
|
|
|
|
public async deleteRecord(domain: string, providerRecordId: string): Promise<void> {
|
|
const zoneId = await this.cfAccount.zoneManager.getZoneId(domain);
|
|
await (this.cfAccount as any).apiAccount.dns.records.delete(providerRecordId, {
|
|
zone_id: zoneId,
|
|
});
|
|
}
|
|
|
|
private isSupportedType(type: string): boolean {
|
|
return ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SOA', 'CAA'].includes(type);
|
|
}
|
|
}
|