@hasna/domains
Version:
Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, shared account API and PostgreSQL
1,114 lines (1,108 loc) • 48 kB
JavaScript
// src/lib/cloudflare.ts
import { createHash } from "node:crypto";
// src/lib/dns-txt.ts
function decodeDnsTxt(value) {
const input = value.trim();
if (!input.startsWith('"'))
return value;
let result = "";
let i = 0;
while (i < input.length) {
if (input[i++] !== '"')
throw new Error("Malformed DNS TXT presentation");
let closed = false;
while (i < input.length) {
let char = input[i++];
if (char === '"') {
closed = true;
break;
}
if (char === "\\") {
if (i === input.length)
throw new Error("Malformed DNS TXT escape");
if (/\d/u.test(input[i])) {
const digits = input.slice(i, i + 3);
if (!/^\d{3}$/u.test(digits) || Number(digits) > 255)
throw new Error("Malformed DNS TXT octet");
char = String.fromCharCode(Number(digits));
i += 3;
} else
char = input[i++];
}
if (/[\x00-\x1f\x7f]/u.test(char))
throw new Error("DNS TXT control character is not supported");
result += char;
}
if (!closed)
throw new Error("Unterminated DNS TXT string");
if (i < input.length && !/\s/u.test(input[i]))
throw new Error("Malformed DNS TXT string separator");
while (i < input.length && /\s/u.test(input[i]))
i++;
}
return result;
}
function mailPolicyKind(value) {
const semantic = decodeDnsTxt(value).trim();
if (/^v=spf1(?:\s|$)/iu.test(semantic))
return "spf";
if (/^v=DMARC1(?:;|\s|$)/iu.test(semantic))
return "dmarc";
return null;
}
// src/lib/env-aliases.ts
function firstEnv(env, names) {
for (const key of names) {
const value = env[key];
if (value)
return { key, value };
}
return;
}
function hasEveryEnv(env, groups) {
return groups.every((names) => !!firstEnv(env, names));
}
function flattenEnvNames(groups) {
return Array.from(new Set(groups.flatMap((names) => [...names])));
}
var NAMECHEAP_ENV = {
apiKey: ["NAMECHEAP_API_KEY"],
username: ["NAMECHEAP_USERNAME"],
clientIp: ["NAMECHEAP_CLIENT_IP"]
};
var GODADDY_ENV = {
apiKey: ["GODADDY_API_KEY"],
apiSecret: ["GODADDY_API_SECRET"]
};
var BRANDSIGHT_ENV = {
apiKey: ["BRANDSIGHT_API_KEY"],
apiSecret: ["BRANDSIGHT_API_SECRET"],
customerId: ["BRANDSIGHT_CUSTOMER_ID"],
shopperId: ["BRANDSIGHT_SHOPPER_ID"],
accountId: ["BRANDSIGHT_ACCOUNT_ID"]
};
var SEDO_ENV = {
partnerId: ["SEDO_PARTNER_ID"],
signKey: ["SEDO_API_KEY", "SEDO_SIGN_KEY"],
username: ["SEDO_USERNAME", "SEDO_EMAIL"],
password: ["SEDO_PASSWORD"]
};
var CLOUDFLARE_ENV = {
apiToken: ["CLOUDFLARE_API_TOKEN"],
apiKey: ["CLOUDFLARE_API_KEY"],
email: ["CLOUDFLARE_EMAIL"],
accountId: ["CLOUDFLARE_ACCOUNT_ID"]
};
var ROUTE53_ENV = {
accessKeyId: ["ROUTE53_ACCESS_KEY_ID", "AWS_ACCESS_KEY_ID"],
secretAccessKey: ["ROUTE53_SECRET_ACCESS_KEY", "AWS_SECRET_ACCESS_KEY"],
sessionToken: ["ROUTE53_SESSION_TOKEN", "AWS_SESSION_TOKEN"],
profile: ["ROUTE53_AWS_PROFILE", "AWS_PROFILE"],
region: ["ROUTE53_REGION", "ROUTE53_AWS_REGION", "AWS_REGION"],
webIdentityTokenFile: ["AWS_WEB_IDENTITY_TOKEN_FILE"],
roleArn: ["AWS_ROLE_ARN"],
containerCredentials: [
"AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
"AWS_CONTAINER_CREDENTIALS_FULL_URI"
]
};
function providerEnvNames(provider) {
switch (provider.toLowerCase()) {
case "namecheap":
return flattenEnvNames([NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
case "godaddy":
return flattenEnvNames([GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
case "brandsight":
return flattenEnvNames([
BRANDSIGHT_ENV.apiKey,
BRANDSIGHT_ENV.apiSecret,
BRANDSIGHT_ENV.customerId,
BRANDSIGHT_ENV.shopperId,
BRANDSIGHT_ENV.accountId
]);
case "sedo":
return flattenEnvNames([SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
case "cloudflare":
return flattenEnvNames([CLOUDFLARE_ENV.apiToken, CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email, CLOUDFLARE_ENV.accountId]);
case "route53":
return flattenEnvNames([
ROUTE53_ENV.profile,
ROUTE53_ENV.accessKeyId,
ROUTE53_ENV.secretAccessKey,
ROUTE53_ENV.sessionToken,
ROUTE53_ENV.region,
ROUTE53_ENV.webIdentityTokenFile,
ROUTE53_ENV.roleArn,
ROUTE53_ENV.containerCredentials
]);
default:
return [];
}
}
function hasProviderCredentials(provider, env = process.env) {
switch (provider.toLowerCase()) {
case "namecheap":
return hasEveryEnv(env, [NAMECHEAP_ENV.apiKey, NAMECHEAP_ENV.username, NAMECHEAP_ENV.clientIp]);
case "godaddy":
return hasEveryEnv(env, [GODADDY_ENV.apiKey, GODADDY_ENV.apiSecret]);
case "brandsight":
return hasEveryEnv(env, [BRANDSIGHT_ENV.apiKey, BRANDSIGHT_ENV.apiSecret, BRANDSIGHT_ENV.customerId]);
case "sedo":
return hasEveryEnv(env, [SEDO_ENV.partnerId, SEDO_ENV.signKey, SEDO_ENV.username, SEDO_ENV.password]);
case "cloudflare": {
const tokenMode = !!firstEnv(env, CLOUDFLARE_ENV.apiToken);
const keyMode = hasEveryEnv(env, [CLOUDFLARE_ENV.apiKey, CLOUDFLARE_ENV.email]);
return tokenMode || keyMode;
}
case "route53": {
const profileMode = !!firstEnv(env, ROUTE53_ENV.profile);
const keyMode = hasEveryEnv(env, [ROUTE53_ENV.accessKeyId, ROUTE53_ENV.secretAccessKey]);
const webIdentityMode = hasEveryEnv(env, [ROUTE53_ENV.webIdentityTokenFile, ROUTE53_ENV.roleArn]);
const containerMode = !!firstEnv(env, ROUTE53_ENV.containerCredentials);
return profileMode || keyMode || webIdentityMode || containerMode;
}
default:
return false;
}
}
// src/lib/cloudflare-auth.ts
function resolveCloudflareConfig(env = process.env) {
const accountId = firstEnv(env, CLOUDFLARE_ENV.accountId)?.value;
const apiToken = firstEnv(env, CLOUDFLARE_ENV.apiToken)?.value;
if (apiToken)
return { apiToken, accountId };
const apiKey = firstEnv(env, CLOUDFLARE_ENV.apiKey)?.value;
const email = firstEnv(env, CLOUDFLARE_ENV.email)?.value;
if (apiKey && email)
return { apiKey, email, accountId };
return accountId ? { accountId } : {};
}
function cloudflareAuthHeaders(cfg) {
if (cfg.apiToken)
return { Authorization: `Bearer ${cfg.apiToken}` };
if (cfg.apiKey && cfg.email)
return { "X-Auth-Key": cfg.apiKey, "X-Auth-Email": cfg.email };
throw new Error("Cloudflare credentials not configured. Set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY + CLOUDFLARE_EMAIL.");
}
// src/lib/cloudflare.ts
class CloudflareApiError extends Error {
statusCode;
providerCode;
constructor(message, statusCode, providerCode) {
const code = providerCode ? `, code ${providerCode}` : "";
super(`Cloudflare API error (HTTP ${statusCode}${code}): ${message}`);
this.statusCode = statusCode;
this.providerCode = providerCode;
this.name = "CloudflareApiError";
}
}
function getConfig() {
return resolveCloudflareConfig();
}
function checkCredentials(cfg) {
cloudflareAuthHeaders(cfg);
}
var CF_BASE = "https://api.cloudflare.com/client/v4";
var DEFAULT_CF_TIMEOUT_MS = 30000;
var MAX_CF_TIMEOUT_MS = 120000;
var DEFAULT_CF_MAX_RESPONSE_BYTES = 1048576;
var MAX_CF_RESPONSE_BYTES = 4194304;
function positiveBoundedEnv(name, fallback, maximum) {
const raw = process.env[name]?.trim();
if (!raw)
return fallback;
const value = Number(raw);
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
throw new Error(`${name} must be an integer between 1 and ${maximum}`);
}
return value;
}
async function boundedResponseText(response, maxBytes, signal) {
const contentLength = Number(response.headers.get("content-length"));
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
throw new Error(`Cloudflare API response exceeds ${maxBytes} bytes`);
}
if (!response.body)
return "";
const reader = response.body.getReader();
const abort = () => void reader.cancel("Cloudflare API request aborted");
if (signal.aborted)
abort();
signal.addEventListener("abort", abort, { once: true });
const chunks = [];
let length = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done)
break;
length += value.byteLength;
if (length > maxBytes) {
await reader.cancel("Cloudflare API response exceeds bounded size");
throw new Error(`Cloudflare API response exceeds ${maxBytes} bytes`);
}
chunks.push(value);
}
} finally {
signal.removeEventListener("abort", abort);
}
const bytes = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder().decode(bytes);
}
async function cfFetch(path, opts = {}) {
const cfg = opts.config ?? getConfig();
checkCredentials(cfg);
const timeoutMs = Math.min(opts.timeoutMs ?? MAX_CF_TIMEOUT_MS, positiveBoundedEnv("DOMAINS_PROVIDER_HTTP_TIMEOUT_MS", DEFAULT_CF_TIMEOUT_MS, MAX_CF_TIMEOUT_MS));
const maxResponseBytes = positiveBoundedEnv("DOMAINS_PROVIDER_MAX_RESPONSE_BYTES", DEFAULT_CF_MAX_RESPONSE_BYTES, MAX_CF_RESPONSE_BYTES);
const controller = new AbortController;
let timer;
const timeout = new Promise((_resolve, reject) => {
timer = setTimeout(() => {
controller.abort();
reject(new Error(`Cloudflare API request exceeded ${timeoutMs}ms`));
}, timeoutMs);
});
let json;
let status = 0;
try {
({ json, status } = await Promise.race([
(async () => {
const response = await fetch(`${CF_BASE}${path}`, {
method: opts.method ?? "GET",
headers: {
...cloudflareAuthHeaders(cfg),
"Content-Type": "application/json"
},
body: opts.body ? JSON.stringify(opts.body) : undefined,
signal: controller.signal
});
const text = await boundedResponseText(response, maxResponseBytes, controller.signal);
try {
return {
status: response.status,
json: JSON.parse(text)
};
} catch {
throw new Error(`Cloudflare API returned invalid JSON (${response.status})`);
}
})(),
timeout
]));
} finally {
if (timer !== undefined)
clearTimeout(timer);
}
if (status < 200 || status >= 300 || json.success !== true) {
const first = json.errors?.[0];
const msg = first?.message ?? "request failed";
const providerCode = typeof first?.code === "string" || typeof first?.code === "number" ? String(first.code) : undefined;
throw new CloudflareApiError(msg, status, providerCode);
}
opts.onResultInfo?.(json.result_info);
return json.result;
}
function exactDnsName(value, label) {
if (typeof value !== "string")
throw new Error(`${label} must be a fully qualified DNS name`);
const name = value.toLowerCase().replace(/\.$/, "");
if (name.length > 253 || !name.includes(".") || !name.split(".").every((part) => part.length > 0 && part.length <= 63 && /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/.test(part))) {
throw new Error(`${label} must be a fully qualified DNS name without wildcards`);
}
return name;
}
async function createRecordIfAbsent(input, config) {
if (!/^[a-f0-9]{32}$/.test(input.zoneId))
throw new Error("zoneId must be an exact Cloudflare zone ID");
const zoneName = exactDnsName(input.zoneName, "zoneName");
const name = exactDnsName(input.name, "name");
const content = exactDnsName(input.content, "content");
if (!name.endsWith(`.${zoneName}`))
throw new Error("name must be a fully qualified subdomain of the exact zone");
if (name === content)
throw new Error("CNAME cannot point to itself");
if (input.type !== "CNAME" || input.proxied !== false)
throw new Error("create-only currently requires type CNAME and explicit proxied false");
if (!Number.isSafeInteger(input.ttl) || !(input.ttl === 1 || input.ttl >= 60 && input.ttl <= 86400)) {
throw new Error("ttl must be 1 (automatic) or an integer between 60 and 86400");
}
const base = `/zones/${input.zoneId}`;
const zone = await cfFetch(base, { config });
if (!zone || zone.id !== input.zoneId || exactDnsName(zone.name, "provider zone name") !== zoneName || zone.status !== "active") {
throw new Error("Provider zone identity or active status does not match the requested zone");
}
const readName = async () => {
const records = await cfFetch(`${base}/dns_records?name=${encodeURIComponent(name)}&per_page=100&page=1`, { config });
if (!Array.isArray(records) || records.some((record) => !record || exactDnsName(record.name, "provider record name") !== name)) {
throw new Error("Provider returned invalid exact-name DNS records");
}
return records;
};
if ((await readName()).length !== 0)
throw new Error("DNS name already exists; create-only refuses every existing record");
const body = { type: "CNAME", name, content, ttl: input.ttl, proxied: false };
const matches = (record, id) => !!record && typeof record.id === "string" && /^[a-f0-9]{32}$/.test(record.id) && (!id || record.id === id) && record.type === "CNAME" && exactDnsName(record.name, "provider record name") === name && exactDnsName(record.content, "provider record content") === content && record.ttl === input.ttl && record.proxied === false;
try {
const created = await cfFetch(`${base}/dns_records`, { method: "POST", body, config });
if (!matches(created))
throw new Error("Provider create result does not match the exact requested record");
const record = await cfFetch(`${base}/dns_records/${created.id}`, { config });
const atName = await readName();
if (!matches(record, created.id) || atName.length !== 1 || !matches(atName[0], created.id)) {
throw new Error("Provider readback does not match the exact created record ID and content");
}
return { created: true, provider: "cloudflare", zone: { id: zone.id, name: zoneName }, record };
} catch (error) {
throw new Error(`DNS create is unverified; no retry or rollback was attempted. Read the exact provider name before retrying. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
}
}
async function updateRecordInPlace(input, config) {
if (!/^[a-f0-9]{32}$/.test(input.zoneId))
throw new Error("zoneId must be an exact Cloudflare zone ID");
if (!/^[a-f0-9]{32}$/.test(input.recordId))
throw new Error("recordId must be an exact Cloudflare record ID");
const zoneName = exactDnsName(input.zoneName, "zoneName");
const { update } = input;
const body = {};
if (update.content !== undefined) {
if (typeof update.content !== "string" || !update.content.trim() || update.content.length > 2048 || /[\u0000-\u001f\u007f]/u.test(update.content)) {
throw new Error("content must be a non-empty DNS value without control characters");
}
body.content = update.content.trim();
}
if (update.ttl !== undefined) {
if (!Number.isSafeInteger(update.ttl) || !(update.ttl === 1 || update.ttl >= 60 && update.ttl <= 86400)) {
throw new Error("ttl must be 1 (automatic) or an integer between 60 and 86400");
}
body.ttl = update.ttl;
}
if (update.proxied !== undefined) {
if (typeof update.proxied !== "boolean")
throw new Error("proxied must be a boolean");
body.proxied = update.proxied;
}
if (Object.keys(body).length === 0)
throw new Error("At least one of content, ttl or proxied must be given");
if (update.expectContent !== undefined && (typeof update.expectContent !== "string" || !update.expectContent.trim())) {
throw new Error("expectContent must be a non-empty DNS value");
}
const base = `/zones/${input.zoneId}`;
const zone = await cfFetch(base, { config });
if (!zone || zone.id !== input.zoneId || exactDnsName(zone.name, "provider zone name") !== zoneName || zone.status !== "active") {
throw new Error("Provider zone identity or active status does not match the requested zone");
}
const readRecord = async () => {
const record = await cfFetch(`${base}/dns_records/${input.recordId}`, { config });
if (!record || record.id !== input.recordId) {
throw new Error("Provider record identity does not match the exact requested record ID");
}
return record;
};
const previous = await readRecord();
if (update.expectContent !== undefined && previous.content !== update.expectContent.trim()) {
throw new Error(`DNS record content is ${previous.content}, not the expected ${update.expectContent}; refusing the change`);
}
try {
await cfFetch(`${base}/dns_records/${input.recordId}`, { method: "PATCH", body, config });
const record = await readRecord();
if (record.type !== previous.type || record.name !== previous.name) {
throw new Error("Provider readback changed the record identity (type or name)");
}
if (body.content !== undefined && record.content !== body.content || body.ttl !== undefined && record.ttl !== body.ttl || body.proxied !== undefined && record.proxied !== body.proxied) {
throw new Error("Provider readback does not match the requested record fields");
}
return { changed: true, provider: "cloudflare", zone: { id: zone.id, name: zoneName }, previous, record };
} catch (error) {
throw new Error(`DNS change is unverified; no retry or rollback was attempted. Read the exact provider record before retrying. ${error instanceof Error ? error.message : String(error)}`, { cause: error });
}
}
async function listZones(config) {
const zones = [];
let page = 1;
while (true) {
const result = await cfFetch(`/zones?per_page=50&page=${page}`, { config });
if (!result || result.length === 0)
break;
for (const z of result) {
zones.push({ id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers });
}
if (result.length < 50)
break;
page++;
}
return zones;
}
async function getZone(domain, config) {
const result = await cfFetch(`/zones?name=${encodeURIComponent(domain)}`, { config });
if (!result || result.length === 0)
return null;
const z = result[0];
return { id: z.id, name: z.name, status: z.status, nameservers: z.name_servers, original_nameservers: z.original_name_servers };
}
async function createZone(domain, config) {
const cfg = config ?? getConfig();
if (!cfg.accountId) {
throw new Error("CLOUDFLARE_ACCOUNT_ID is required to create a zone.");
}
const result = await cfFetch("/zones", { method: "POST", body: { name: domain, account: { id: cfg.accountId }, jump_start: false }, config: cfg });
return { id: result.id, name: result.name, status: result.status, nameservers: result.name_servers };
}
async function ensureZone(domain, config, deps) {
const get = deps?.getZone ?? getZone;
const create = deps?.createZone ?? createZone;
const existing = await get(domain, config);
const zone = existing ?? await create(domain, config);
if (!zone.nameservers || zone.nameservers.length === 0) {
throw new Error(`Cloudflare zone for ${domain} has no nameservers yet`);
}
return zone;
}
async function deleteZone(zoneId, config) {
await cfFetch(`/zones/${zoneId}`, { method: "DELETE", config });
}
async function getZoneOriginTlsMode(zoneId, config) {
const setting = await cfFetch(`/zones/${encodeURIComponent(zoneId)}/settings/ssl`, { config });
if (setting.id !== "ssl" || typeof setting.value !== "string" || !setting.value.trim()) {
throw new Error("Cloudflare returned an invalid zone SSL setting");
}
const mode = setting.value.trim().toLowerCase();
if (!["off", "flexible", "full", "strict", "origin_pull"].includes(mode)) {
throw new Error(`Cloudflare returned unsupported zone SSL setting ${mode}`);
}
return mode;
}
async function ensureZoneOriginTlsMode(zoneId, requested, config) {
const current = await getZoneOriginTlsMode(zoneId, config);
if (current === "origin_pull" || current === "strict" && requested === "full") {
return { mode: current, changed: false, downgradeRefused: true };
}
let changed = false;
if (current !== requested) {
await cfFetch(`/zones/${encodeURIComponent(zoneId)}/settings/ssl`, { method: "PATCH", body: { value: requested }, config });
changed = true;
}
const readback = await getZoneOriginTlsMode(zoneId, config);
if (readback !== requested) {
throw new Error(`Cloudflare zone SSL setting readback is ${readback}, expected ${requested}`);
}
return { mode: requested, changed, downgradeRefused: false };
}
var CLOUDFLARE_ZONE_SETTINGS = {
email_obfuscation: ["on", "off"]
};
function requireZoneSettingName(setting) {
if (typeof setting !== "string" || !Object.hasOwn(CLOUDFLARE_ZONE_SETTINGS, setting)) {
throw new Error(`Unsupported Cloudflare zone setting '${setting}'; supported: ${Object.keys(CLOUDFLARE_ZONE_SETTINGS).join(", ")}`);
}
return setting;
}
function requireZoneSettingValue(setting, value) {
const allowed = CLOUDFLARE_ZONE_SETTINGS[setting];
if (typeof value !== "string" || !allowed.includes(value.trim().toLowerCase())) {
throw new Error(`Unsupported value '${value}' for zone setting ${setting}; supported: ${allowed.join(", ")}`);
}
return value.trim().toLowerCase();
}
function requireZoneId(zoneId) {
const id = typeof zoneId === "string" ? zoneId.trim() : "";
if (!/^[a-f0-9]{32}$/.test(id)) {
throw new Error("zoneId must be an exact Cloudflare zone ID");
}
return id;
}
async function getZoneSetting(zoneId, setting, config) {
const name = requireZoneSettingName(setting);
const id = requireZoneId(zoneId);
const result = await cfFetch(`/zones/${encodeURIComponent(id)}/settings/${name}`, { config });
if (!result || result.id !== name || typeof result.value !== "string" || !result.value.trim()) {
throw new Error(`Cloudflare returned an invalid zone setting for ${name}`);
}
const value = result.value.trim().toLowerCase();
const allowed = CLOUDFLARE_ZONE_SETTINGS[name];
if (!allowed.includes(value)) {
throw new Error(`Cloudflare returned unsupported ${name} value ${value}`);
}
return value;
}
async function ensureZoneSetting(zoneId, setting, value, config) {
const name = requireZoneSettingName(setting);
const requested = requireZoneSettingValue(name, value);
const id = requireZoneId(zoneId);
const current = await getZoneSetting(id, name, config);
if (current === requested)
return { setting: name, value: requested, changed: false };
await cfFetch(`/zones/${encodeURIComponent(id)}/settings/${name}`, { method: "PATCH", body: { value: requested }, config });
const readback = await getZoneSetting(id, name, config);
if (readback !== requested) {
throw new Error(`Cloudflare zone setting ${name} readback is ${readback}, expected ${requested}`);
}
return { setting: name, value: requested, changed: true };
}
class CloudflareDnsPaginationError extends Error {
constructor(reason, page, count, metadata) {
const numeric = metadata ? Object.fromEntries(["page", "per_page", "count", "total_count", "total_pages"].map((key) => [key, typeof metadata[key] === "number" && Number.isFinite(metadata[key]) ? metadata[key] : null])) : null;
super(`Cloudflare DNS pagination ${reason} (page=${page}, returned=${count}, metadata=${JSON.stringify(numeric)})`);
this.name = "CloudflareDnsPaginationError";
}
}
var DNS_MAIL_RECONCILIATION_TIMEOUT_MS = 25000;
async function listRecords(zoneId, config, options = {}) {
const deadline = Math.min(performance.now() + 1e4, options.deadline ?? Infinity);
if (!Number.isFinite(deadline))
throw new Error("Invalid DNS enumeration deadline");
const assertRead = async () => {
await options.assertLease?.();
if (performance.now() >= deadline)
throw new Error("Cloudflare DNS enumeration time budget exhausted");
};
for (let attempt = 1;; attempt++) {
await assertRead();
try {
const records = await listRecordsAttempt(zoneId, config, assertRead, deadline);
await assertRead();
return records;
} catch (error) {
if (!(error instanceof CloudflareDnsPaginationError) || attempt >= 6)
throw error;
await assertRead();
const delay = 250 * 2 ** (attempt - 1);
if (performance.now() + delay >= deadline)
throw error;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
var CF_DNS_READ_TYPES = new Set(["A", "AAAA", "CAA", "CERT", "CNAME", "DNSKEY", "DS", "HTTPS", "LOC", "MX", "NAPTR", "NS", "OPENPGPKEY", "PTR", "SMIMEA", "SRV", "SSHFP", "SVCB", "TLSA", "TXT", "URI"]);
function readDnsRecord(value) {
const malformed = () => new Error("Cloudflare returned a malformed DNS record");
if (!value || typeof value !== "object" || Array.isArray(value))
throw malformed();
const r = value;
if (typeof r.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/u.test(r.id) || typeof r.type !== "string" || !CF_DNS_READ_TYPES.has(r.type) || typeof r.name !== "string" || r.name.length > 253 || !/^(?:\*|[A-Za-z0-9_-]+)(?:\.[A-Za-z0-9_-]+)+\.?$/u.test(r.name) || typeof r.content !== "string" || r.content.length === 0 && r.type !== "TXT" || typeof r.ttl !== "number" || !Number.isSafeInteger(r.ttl) || !(r.ttl === 1 || r.ttl >= 30 && r.ttl <= 86400) || (r.type === "MX" || r.priority !== undefined) && (typeof r.priority !== "number" || !Number.isSafeInteger(r.priority) || r.priority < 0 || r.priority > 65535) || r.proxied !== undefined && typeof r.proxied !== "boolean")
throw malformed();
return {
id: r.id,
type: r.type,
name: r.name,
content: r.content,
ttl: r.ttl,
priority: r.priority,
proxied: r.proxied
};
}
async function listRecordsAttempt(zoneId, config, assertRead, deadline) {
const records = [];
let page = 1;
const seenIds = new Set;
let pagination;
while (true) {
await assertRead();
let resultInfo;
const result = await cfFetch(`/zones/${zoneId}/dns_records?per_page=100&page=${page}`, { config, onResultInfo: (value) => {
resultInfo = value;
}, timeoutMs: Math.max(1, Math.ceil(deadline - performance.now())) });
await assertRead();
if (!Array.isArray(result))
throw new Error("Cloudflare DNS list result is not an array");
const pageRecords = result.map(readDnsRecord);
for (const record of pageRecords) {
if (seenIds.has(record.id))
throw new Error("Cloudflare DNS pagination repeated a record identity");
seenIds.add(record.id);
}
if (resultInfo !== undefined) {
if (!resultInfo || typeof resultInfo !== "object")
throw new Error("Cloudflare DNS pagination is malformed");
const info = resultInfo;
const values = [info.page, info.per_page, info.count, info.total_count, info.total_pages];
if (!values.every((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0))
throw new CloudflareDnsPaginationError("is incomplete", page, result.length, info);
const current = { perPage: info.per_page, totalCount: info.total_count, totalPages: info.total_pages };
const expectedPages = Math.ceil(current.totalCount / current.perPage);
if (info.page !== page || current.perPage < 1 || current.perPage > 100)
throw new Error("Cloudflare DNS pagination has invalid page identity or size");
if (info.count !== result.length || current.totalPages !== expectedPages && !(current.totalCount === 0 && current.totalPages === 1) || result.length !== Math.min(current.perPage, Math.max(0, current.totalCount - (page - 1) * current.perPage)) || pagination && JSON.stringify(pagination) !== JSON.stringify(current))
throw new CloudflareDnsPaginationError("changed or contradicts the result", page, result.length, info);
pagination = current;
} else if (pagination)
throw new CloudflareDnsPaginationError("disappeared before completion", page, result.length);
records.push(...pageRecords);
if (pagination ? page >= pagination.totalPages : result.length < 100) {
if (pagination && records.length !== pagination.totalCount)
throw new CloudflareDnsPaginationError("did not yield the complete population", page, result.length);
break;
}
page++;
}
return records;
}
async function listRecordsByNameType(zoneId, type, name, config) {
const result = await cfFetch(`/zones/${zoneId}/dns_records?type=${encodeURIComponent(type)}&name=${encodeURIComponent(name)}`, { config });
return (result ?? []).map((r) => ({
id: r.id,
type: r.type,
name: r.name,
content: r.content,
ttl: r.ttl,
priority: r.priority,
proxied: r.proxied
}));
}
async function upsertRecord(zoneId, record, config) {
const existing = await listRecordsByNameType(zoneId, record.type, record.name, config);
const body = {
type: record.type,
name: record.name,
content: record.content,
ttl: record.ttl ?? 1,
priority: record.priority,
proxied: record.proxied ?? false
};
const sameRecord = existing.find((r) => r.content === record.content && (r.priority ?? undefined) === (record.priority ?? undefined) && (r.proxied ?? false) === (record.proxied ?? false));
if (sameRecord?.id) {
await cfFetch(`/zones/${zoneId}/dns_records/${sameRecord.id}`, { method: "PUT", body, config });
} else {
await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body, config });
}
}
function sameRecordIdentity(a, b) {
return a.type === b.type && a.name === b.name && a.content === b.content && (a.priority ?? undefined) === (b.priority ?? undefined);
}
function resolveDesiredProxied(records, existing) {
const omitted = records.filter((record) => record.proxied === undefined);
if (omitted.length === 0)
return records;
const { type, name } = records[0];
if (omitted.length === records.length) {
const proxyStates = new Set(existing.map((record) => record.proxied));
if (proxyStates.size > 1) {
throw new Error(`Cannot safely preserve Cloudflare proxied state for ${type} ${name}: ` + "existing records have mixed proxied values while desired records omit proxied; " + "set proxied explicitly on every desired record");
}
const inherited = existing[0]?.proxied;
return inherited === undefined ? records : records.map((record) => ({ ...record, proxied: inherited }));
}
const existingHasProxyState = existing.some((record) => record.proxied !== undefined);
return records.map((record) => {
if (record.proxied !== undefined)
return record;
const matches = existing.filter((candidate) => sameRecordIdentity(candidate, record));
if (matches.length === 1 && matches[0].proxied !== undefined) {
return { ...record, proxied: matches[0].proxied };
}
if (!existingHasProxyState)
return record;
throw new Error(`Cannot safely preserve Cloudflare proxied state for ${type} ${name} record ${record.content}: ` + "omitted proxied does not resolve to exactly one existing value; " + "set proxied explicitly on this desired record");
});
}
async function prepareRecordsByNameType(zoneId, records, config) {
if (records.length === 0)
return;
const { type, name } = records[0];
const existing = await listRecordsByNameType(zoneId, type, name, config);
if (existing.length === records.length && records.every((record) => existing.some((candidate) => sameRecordIdentity(candidate, record) && candidate.ttl === record.ttl && (record.proxied === undefined || candidate.proxied === record.proxied)))) {
return;
}
return { existing, desired: resolveDesiredProxied(records, existing) };
}
async function replaceRecordsByNameType(zoneId, replacement, config) {
for (const record of replacement.existing) {
if (record.id)
await deleteRecord(zoneId, record.id, config);
}
for (const record of replacement.desired) {
await cfFetch(`/zones/${zoneId}/dns_records`, {
method: "POST",
body: {
type: record.type,
name: record.name,
content: record.content,
ttl: record.ttl ?? 1,
priority: record.priority,
...record.proxied === undefined ? {} : { proxied: record.proxied }
},
config
});
}
}
async function reconcileRecords(zoneId, records, config) {
const grouped = new Map;
for (const record of records) {
const key = `${record.type}|${record.name}`;
const group = grouped.get(key) ?? [];
group.push(record);
grouped.set(key, group);
}
const replacements = [];
for (const group of grouped.values()) {
const replacement = await prepareRecordsByNameType(zoneId, group, config);
if (replacement)
replacements.push(replacement);
}
for (const replacement of replacements) {
await replaceRecordsByNameType(zoneId, replacement, config);
}
}
async function getDnsRecordGroupState(zoneId, type, name, config, options = {}) {
const records = (await listRecords(zoneId, config, options)).filter((record) => record.type === type && record.name.toLowerCase().replace(/\.$/u, "") === name).map((record) => {
if (!record.id)
throw new Error("DNS state record has no provider identity");
return {
id: record.id,
type: record.type,
name: record.name,
value: record.content,
ttl: record.ttl,
priority: record.priority ?? null,
proxied: record.proxied ?? null
};
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
if (records.length > 1000 || new Set(records.map((record) => record.id)).size !== records.length) {
throw new Error("DNS state group is oversized or has ambiguous provider identities");
}
const state = { hash_scheme: "domains-dns-group-v1", zone_id: zoneId, record_name: name, record_type: type, records };
return { ...state, records_sha256: createHash("sha256").update(JSON.stringify(state)).digest("hex") };
}
async function reconcileGuardedRecords(zoneId, desired, expectedHash, config, assertLease = async () => {}) {
const first = desired[0];
if (!first || desired.some((record) => record.type !== first.type || record.name !== first.name))
throw new Error("DNS guard requires exactly one record group");
await assertLease();
const before = await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease });
if (before.records_sha256 !== expectedHash)
throw new Error("DNS current record state does not match expected_current_records_sha256");
const existing = before.records.map((record) => ({
id: record.id,
type: record.type,
name: record.name,
content: record.value,
ttl: record.ttl,
...record.priority === null ? {} : { priority: record.priority },
...record.proxied === null ? {} : { proxied: record.proxied }
}));
const targets = resolveDesiredProxied(desired, existing);
const matches = (actual, target) => sameRecordIdentity(actual, target) && actual.ttl === target.ttl && (target.proxied === undefined || actual.proxied === target.proxied);
const remaining = [...existing];
const retained = [];
const additions = [];
for (const target of targets) {
const index = remaining.findIndex((record) => matches(record, target));
if (index < 0)
additions.push(target);
else
retained.push(remaining.splice(index, 1)[0]);
}
if (additions.some((record) => record.ttl === 1))
throw new Error("Guarded automatic TTL requires an exact retained provider record");
await assertLease();
if ((await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease })).records_sha256 !== expectedHash)
throw new Error("DNS record group changed before mutation");
for (const record of remaining) {
await assertLease();
await deleteRecord(zoneId, record.id, config);
}
for (const record of additions) {
await assertLease();
await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body: record, config });
}
await assertLease();
const after = await getDnsRecordGroupState(zoneId, first.type, first.name, config, { assertLease });
const actual = after.records.map((record) => ({
id: record.id,
type: record.type,
name: record.name,
content: record.value,
ttl: record.ttl,
...record.priority === null ? {} : { priority: record.priority },
...record.proxied === null ? {} : { proxied: record.proxied }
}));
if (actual.length !== targets.length || !targets.every((target) => actual.some((record) => matches(record, target))) || !retained.every((original) => actual.some((record) => record.id === original.id && matches(record, original)))) {
throw new Error("Guarded DNS complete record group readback mismatch");
}
return after;
}
async function reconcileMailRecords(zoneId, records, config, assertLease = async () => {}, operationDeadline = performance.now() + DNS_MAIL_RECONCILIATION_TIMEOUT_MS) {
const deadline = Math.min(operationDeadline, performance.now() + DNS_MAIL_RECONCILIATION_TIMEOUT_MS);
const remaining = () => {
const milliseconds = deadline - performance.now();
if (!Number.isFinite(milliseconds) || milliseconds < 1)
throw new Error("DNS reconciliation time budget exhausted");
return Math.floor(milliseconds);
};
const assertOperation = async () => {
remaining();
await assertLease();
remaining();
};
const applied = [];
for (const desired of records) {
const all = await listRecords(zoneId, config, { assertLease: assertOperation, deadline });
const named = all.filter((record) => record.name.toLowerCase().replace(/\.$/u, "") === desired.name);
const existing = named.filter((record) => record.type === desired.type);
const kind = desired.type === "TXT" ? mailPolicyKind(desired.content) : null;
const dmarc = kind === "dmarc";
const spf = kind === "spf";
const policies = existing.filter((record) => kind !== null && mailPolicyKind(record.content) === kind);
if ((dmarc || spf) && policies.length > 1)
throw new Error("Multiple existing mail policies require review");
if (dmarc && policies[0]) {
applied.push(policies[0]);
continue;
}
const exact = existing.find((record) => sameRecordIdentity(record, desired) && (desired.type !== "CNAME" || record.proxied === false));
if (exact) {
applied.push(exact);
continue;
}
if (spf && policies[0]) {
const current = policies[0];
const tokens = decodeDnsTxt(current.content).trim().split(/\s+/u);
const known = new Set(tokens.map((token) => token.toLowerCase()));
const inserts = decodeDnsTxt(desired.content).trim().split(/\s+/u).slice(1).filter((token) => !/^[+?~-]?all$/iu.test(token) && !known.has(token.toLowerCase()));
if (inserts.some((token) => !/^include:[a-z0-9.-]+$/iu.test(token))) {
throw new Error("Mail preservation accepts only additional SPF includes");
}
if (inserts.length === 0) {
applied.push(current);
continue;
}
const terminal = tokens.findIndex((token) => /^[+?~-]?all$/iu.test(token));
tokens.splice(terminal < 0 ? tokens.length : terminal, 0, ...inserts);
if (!current.id)
throw new Error("Existing SPF record has no provider identity");
const updated = { ...current, content: tokens.join(" ") };
await assertOperation();
const fresh = await listRecords(zoneId, config, { assertLease: assertOperation, deadline });
const freshPolicies = fresh.filter((record) => record.type === "TXT" && record.name === current.name && mailPolicyKind(record.content) === "spf");
const same = freshPolicies.length === 1 ? freshPolicies.find((record) => record.id === current.id) : undefined;
if (!same || JSON.stringify(same) !== JSON.stringify(current))
throw new Error("SPF record changed during reconciliation");
await assertOperation();
await cfFetch(`/zones/${zoneId}/dns_records/${current.id}`, {
method: "PATCH",
body: { content: updated.content },
config,
timeoutMs: remaining()
});
await assertOperation();
applied.push(updated);
continue;
}
if (desired.type !== "TXT" && existing.length > 0)
throw new Error("Existing mail record conflicts with requested authorization");
if (named.some((record) => record.type === "CNAME") && desired.type !== "CNAME" && named.some((record) => record.type === "CNAME" && record.proxied !== true)) {
throw new Error("Mail record conflicts with an existing CNAME");
}
if (desired.type === "CNAME" && named.length > 0)
throw new Error("Mail CNAME conflicts with existing records");
await assertOperation();
await cfFetch(`/zones/${zoneId}/dns_records`, { method: "POST", body: desired, config, timeoutMs: remaining() });
await assertOperation();
applied.push(desired);
}
return applied;
}
async function deleteRecord(zoneId, recordId, config) {
await cfFetch(`/zones/${zoneId}/dns_records/${recordId}`, { method: "DELETE", config });
}
async function deleteRecordByNameType(zoneId, name, type, config) {
const existing = await cfFetch(`/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(name)}`, { config });
for (const r of existing ?? []) {
await deleteRecord(zoneId, r.id, config);
}
}
function zoneToDomainInfo(zone) {
return {
domain: zone.name,
registrar: "Cloudflare DNS",
created: "",
expires: "",
nameservers: zone.nameservers,
status: zone.status === "active" ? "active" : "discovered",
auto_renew: false
};
}
function withCloudflareMetadata(existing, zone) {
return {
...existing,
cloudflare: {
zone_id: zone.id,
zone_status: zone.status,
source: "cloudflare:zones",
synced_at: new Date().toISOString()
}
};
}
function createCloudflareProvider(config) {
const cfg = config ?? getConfig();
return {
name: "cloudflare",
dnsWriteScope: "changed-groups",
async listDomains() {
const zones = await listZones(cfg);
return zones.map(zoneToDomainInfo);
},
async syncToLocalDb(dbFns) {
const zones = await listZones(cfg);
let synced = 0;
let created = 0;
let updated = 0;
const errors = [];
for (const zone of zones) {
try {
const info = zoneToDomainInfo(zone);
const existing = await dbFns.getDomainByName(zone.name);
if (existing) {
await dbFns.updateDomain(existing.id, {
...existing.registrar === "Cloudflare DNS" ? { registrar: null } : {},
status: existing.status === "discovered" && info.status === "active" ? "active" : existing.status,
nameservers: zone.nameservers,
metadata: withCloudflareMetadata(existing.metadata, zone)
});
updated++;
} else {
await dbFns.createDomain({
name: zone.name,
status: info.status === "active" ? "active" : "discovered",
auto_renew: false,
nameservers: zone.nameservers,
notes: "Discovered from Cloudflare zones; registrar ownership was not inferred.",
metadata: withCloudflareMetadata({}, zone)
});
created++;
}
synced++;
} catch (err) {
errors.push(`${zone.name}: ${err instanceof Error ? err.message : String(err)}`);
}
}
return { synced, created, updated, errors };
},
async getDnsRecords(domain) {
const zone = await getZone(domain, cfg);
if (!zone)
return [];
const records = await listRecords(zone.id, cfg);
return records.map((r) => ({
type: r.type,
name: r.name,
value: r.content,
ttl: r.ttl === 1 ? 0 : r.ttl,
priority: r.priority,
...r.proxied === undefined ? {} : { proxied: r.proxied }
}));
},
async setDnsRecords(domain, records) {
const zone = await getZone(domain, cfg);
if (!zone)
throw new Error(`No Cloudflare zone found for ${domain}`);
await reconcileRecords(zone.id, records.map((r) => ({
type: r.type,
name: r.name,
content: r.value,
ttl: r.ttl || 1,
priority: r.priority,
...r.proxied === undefined ? {} : { proxied: r.proxied }
})), cfg);
return true;
},
async deleteDnsRecords(domain, records) {
const zone = await getZone(domain, cfg);
if (!zone)
throw new Error(`No Cloudflare zone found for ${domain}`);
const grouped = new Map;
for (const record of records) {
const key = `${record.type}|${record.name}`;
const group = grouped.get(key) ?? [];
group.push(record);
grouped.set(key, group);
}
for (const group of grouped.values()) {
const { type, name } = group[0];
const existing = await listRecordsByNameType(zone.id, type, name, cfg);
for (const record of group) {
const match = existing.find((candidate) => candidate.content === record.value && (candidate.priority ?? undefined) === (record.priority ?? undefined));
if (match?.id) {
await deleteRecord(zone.id, match.id, cfg);
}
}
}
return true;
}
};
}
async function listWorkerCustomDomains(config) {
const cfg = config ?? getConfig();
if (!cfg.accountId)
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Worker Custom Domains.");
return cfFetch(`/accounts/${encodeURIComponent(cfg.accountId)}/workers/domains`, { config: cfg });
}
async function bindWorkerCustomDomain(hostname, zoneId, workerName = "shortlink-router", config) {
const cfg = config ?? getConfig();
if (!cfg.accountId)
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Worker Custom Domains.");
if (!zoneId.trim())
throw new Error("Cloudflare zone id is required for Worker Custom Domains.");
if (!/^[A-Za-z0-9_-]{1,64}$/.test(workerName))
throw new Error("Invalid Cloudflare Worker name.");
return cfFetch(`/accounts/${encodeURIComponent(cfg.accountId)}/workers/domains`, {
method: "PUT",
body: { hostname, service: workerName, zone_id: zoneId },
config: cfg
});
}
async function workerCustomDomainReady(hostname, zoneId, workerName = "shortlink-router", config, fetchImpl = fetch, readiness = {}) {
const domains = await listWorkerCustomDomains(config);
const bound = domains.some((domain) => domain.hostname.toLowerCase() === hostname.toLowerCase() && domain.service === workerName && (domain.zone_id === undefined || domain.zone_id === zoneId) && domain.enabled !== false);
if (!bound)
return false;
try {
const path = readiness.path ?? "/.well-known/shortlink-router";
const header = readiness.header ?? "x-shortlink-router";
const expectedValue = readiness.value ?? workerName;
const response = await fetchImpl(`https://${hostname}${path}`, {
method: "GET",
redirect: "manual",
headers: { accept: "application/json" }
});
const ready = response.status === 200 && response.headers.get(header) === expectedValue;
await response.body?.cancel();
return ready;
} catch {
return false;
}
}
export {
CLOUDFLARE_ZONE_SETTINGS,
CloudflareApiError,
CloudflareDnsPaginationError,
DNS_MAIL_RECONCILIATION_TIMEOUT_MS,
bindWorkerCustomDomain,
boundedResponseText,
createCloudflareProvider,
createRecordIfAbsent,
createZone,
deleteRecord,
deleteRecordByNameType,
deleteZone,
ensureZone,
ensureZoneOriginTlsMode,
ensureZoneSetting,
getConfig,
getDnsRecordGroupState,
getZone,
getZoneOriginTlsMode,
getZoneSetting,
listRecords,
listWorkerCustomDomains,
listZones,
reconcileGuardedRecords,
reconcileMailRecords,
reconcileRecords,
updateRecordInPlace,
upsertRecord,
workerCustomDomainReady
};