This commit is contained in:
2025-11-18 00:36:23 +00:00
parent 8f538ab9c0
commit 1fe3cd7f14
3 changed files with 115 additions and 73 deletions

View File

@@ -120,6 +120,7 @@ export class OneboxDatabase {
type TEXT NOT NULL,
value TEXT NOT NULL,
cloudflare_id TEXT,
zone_id TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
@@ -437,7 +438,11 @@ export class OneboxDatabase {
if (!this.db) throw new Error('Database not initialized');
const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]);
return rows.length > 0 ? String(rows[0][0]) : null;
if (rows.length === 0) return null;
// @db/sqlite returns rows as objects with column names as keys
const value = (rows[0] as any).value || rows[0][0];
return value ? String(value) : null;
}
setSetting(key: string, value: string): void {

View File

@@ -4,15 +4,15 @@
* Manages DNS records via Cloudflare API
*/
import * as plugins from '../plugins.ts';
import Cloudflare from 'npm:cloudflare@5.2.0';
import { logger } from '../logging.ts';
import { OneboxDatabase } from './database.ts';
export class OneboxDnsManager {
private oneboxRef: any;
private database: OneboxDatabase;
private cloudflareClient: plugins.cloudflare.CloudflareAccount | null = null;
private zoneID: string | null = null;
private cloudflareClient: Cloudflare | null = null;
private zones: Map<string, string> = new Map(); // zone name -> zone ID
private serverIP: string | null = null;
constructor(oneboxRef: any) {
@@ -27,51 +27,42 @@ export class OneboxDnsManager {
try {
// Get Cloudflare credentials from settings
const apiKey = this.database.getSetting('cloudflareAPIKey');
const email = this.database.getSetting('cloudflareEmail');
const serverIP = this.database.getSetting('serverIP');
if (!apiKey || !email) {
if (!apiKey) {
logger.warn('Cloudflare credentials not configured. DNS management will be disabled.');
logger.info('Configure with: onebox config set cloudflareAPIKey <key>');
logger.info('Configure with: onebox config set cloudflareEmail <email>');
return;
}
this.serverIP = serverIP;
// Initialize Cloudflare client
// The CloudflareAccount class expects just the API key/token
this.cloudflareClient = new plugins.cloudflare.CloudflareAccount(apiKey);
// The official Cloudflare SDK expects the API token
logger.info(`Initializing Cloudflare client with token: ${apiKey.substring(0, 10)}...`);
this.cloudflareClient = new Cloudflare({
apiToken: apiKey,
});
logger.info('Cloudflare client initialized successfully');
// Try to get zone ID from settings, or auto-detect
let zoneID = this.database.getSetting('cloudflareZoneID');
if (!zoneID) {
// Auto-detect zones
logger.info('No zone ID configured, fetching available zones...');
const zones = await this.cloudflareClient.convenience.listZones();
if (zones.length === 0) {
logger.warn('No Cloudflare zones found for this account');
return;
} else if (zones.length === 1) {
// Auto-select the only zone
zoneID = zones[0].id;
this.database.setSetting('cloudflareZoneID', zoneID);
logger.success(`Auto-selected zone: ${zones[0].name} (${zoneID})`);
} else {
// Multiple zones found - log them for user to choose
logger.info(`Found ${zones.length} Cloudflare zones:`);
for (const zone of zones) {
logger.info(` - ${zone.name} (ID: ${zone.id})`);
}
logger.warn('Multiple zones found. Please set one with: onebox config set cloudflareZoneID <id>');
return;
}
// Load all available zones
logger.info('Loading Cloudflare zones...');
const zoneList = [];
for await (const zone of this.cloudflareClient.zones.list()) {
zoneList.push(zone);
this.zones.set(zone.name, zone.id);
}
this.zoneID = zoneID;
logger.info('DNS manager initialized with Cloudflare');
if (zoneList.length === 0) {
logger.warn('No Cloudflare zones found for this account');
return;
}
logger.info(`Loaded ${zoneList.length} Cloudflare zone(s):`);
for (const zone of zoneList) {
logger.info(` - ${zone.name} (ID: ${zone.id})`);
}
logger.success('DNS manager initialized with multi-zone support');
} catch (error) {
logger.error(`Failed to initialize DNS manager: ${error.message}`);
if (error.message && error.message.includes('Authorization header')) {
@@ -88,7 +79,29 @@ export class OneboxDnsManager {
* Check if DNS manager is configured
*/
isConfigured(): boolean {
return this.cloudflareClient !== null && this.zoneID !== null;
return this.cloudflareClient !== null && this.zones.size > 0;
}
/**
* Find the zone ID for a given domain
* Example: "api.task.vc" -> finds "task.vc" zone
*/
private findZoneForDomain(domain: string): string | null {
// Try exact match first
if (this.zones.has(domain)) {
return this.zones.get(domain)!;
}
// Try finding parent zone (e.g., "api.task.vc" -> "task.vc")
const parts = domain.split('.');
for (let i = 1; i < parts.length; i++) {
const candidate = parts.slice(i).join('.');
if (this.zones.has(candidate)) {
return this.zones.get(candidate)!;
}
}
return null;
}
/**
@@ -107,6 +120,14 @@ export class OneboxDnsManager {
throw new Error('Server IP not configured. Set with: onebox config set serverIP <ip>');
}
// Find the zone for this domain
const zoneID = this.findZoneForDomain(domain);
if (!zoneID) {
throw new Error(
`No Cloudflare zone found for domain ${domain}. Available zones: ${Array.from(this.zones.keys()).join(', ')}`
);
}
// Check if record already exists
const existing = await this.getDNSRecord(domain);
if (existing) {
@@ -115,7 +136,8 @@ export class OneboxDnsManager {
}
// Create A record
const response = await this.cloudflareClient!.zones.dns.records.create(this.zoneID!, {
const response = await this.cloudflareClient!.dns.records.create({
zone_id: zoneID,
type: 'A',
name: domain,
content: targetIP,
@@ -125,8 +147,8 @@ export class OneboxDnsManager {
// Store in database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.result.id, Date.now(), Date.now()]
'INSERT INTO dns_records (domain, type, value, cloudflare_id, zone_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[domain, 'A', targetIP, response.id, zoneID, Date.now(), Date.now()]
);
logger.success(`DNS record created for ${domain}${targetIP}`);
@@ -148,7 +170,7 @@ export class OneboxDnsManager {
logger.info(`Removing DNS record for ${domain}`);
// Get record from database
const rows = this.database.query('SELECT cloudflare_id FROM dns_records WHERE domain = ?', [
const rows = this.database.query('SELECT cloudflare_id, zone_id FROM dns_records WHERE domain = ?', [
domain,
]);
@@ -157,11 +179,14 @@ export class OneboxDnsManager {
return;
}
const cloudflareID = String(rows[0][0]);
const cloudflareID = String((rows[0] as any).cloudflare_id || rows[0][0]);
const zoneID = String((rows[0] as any).zone_id || rows[0][1]);
// Delete from Cloudflare
if (cloudflareID) {
await this.cloudflareClient!.zones.dns.records.delete(this.zoneID!, cloudflareID);
if (cloudflareID && zoneID) {
await this.cloudflareClient!.dns.records.delete(cloudflareID, {
zone_id: zoneID,
});
}
// Delete from database
@@ -187,11 +212,13 @@ export class OneboxDnsManager {
const rows = this.database.query('SELECT * FROM dns_records WHERE domain = ?', [domain]);
if (rows.length > 0) {
const row = rows[0] as any;
return {
domain: String(rows[0][1]),
type: String(rows[0][2]),
value: String(rows[0][3]),
cloudflareID: rows[0][4] ? String(rows[0][4]) : null,
domain: String(row.domain || row[1]),
type: String(row.type || row[2]),
value: String(row.value || row[3]),
cloudflareID: row.cloudflare_id ? String(row.cloudflare_id) : (row[4] ? String(row[4]) : null),
zoneID: row.zone_id ? String(row.zone_id) : (row[5] ? String(row[5]) : null),
};
}
@@ -209,14 +236,15 @@ export class OneboxDnsManager {
try {
const rows = this.database.query('SELECT * FROM dns_records ORDER BY created_at DESC');
return rows.map((row) => ({
id: Number(row[0]),
domain: String(row[1]),
type: String(row[2]),
value: String(row[3]),
cloudflareID: row[4] ? String(row[4]) : null,
createdAt: Number(row[5]),
updatedAt: Number(row[6]),
return rows.map((row: any) => ({
id: Number(row.id || row[0]),
domain: String(row.domain || row[1]),
type: String(row.type || row[2]),
value: String(row.value || row[3]),
cloudflareID: row.cloudflare_id ? String(row.cloudflare_id) : (row[4] ? String(row[4]) : null),
zoneID: row.zone_id ? String(row.zone_id) : (row[5] ? String(row[5]) : null),
createdAt: Number(row.created_at || row[6]),
updatedAt: Number(row.updated_at || row[7]),
}));
} catch (error) {
logger.error(`Failed to list DNS records: ${error.message}`);
@@ -233,26 +261,35 @@ export class OneboxDnsManager {
throw new Error('DNS manager not configured');
}
logger.info('Syncing DNS records from Cloudflare...');
logger.info('Syncing DNS records from all Cloudflare zones...');
const response = await this.cloudflareClient!.zones.dns.records.list(this.zoneID!);
const records = response.result;
// Sync records from all zones
for (const [zoneName, zoneID] of this.zones.entries()) {
logger.info(`Syncing zone: ${zoneName}`);
// Only sync A records
const aRecords = records.filter((r: any) => r.type === 'A');
const records = [];
for await (const record of this.cloudflareClient!.dns.records.list({
zone_id: zoneID,
})) {
records.push(record);
}
for (const record of aRecords) {
// Check if exists in database
const existing = await this.getDNSRecord(record.name);
// Only sync A records
const aRecords = records.filter((r) => r.type === 'A');
if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, Date.now(), Date.now()]
);
for (const record of aRecords) {
// Check if exists in database
const existing = await this.getDNSRecord(record.name);
logger.info(`Synced DNS record: ${record.name}`);
if (!existing) {
// Add to database
await this.database.query(
'INSERT INTO dns_records (domain, type, value, cloudflare_id, zone_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[record.name, record.type, record.content, record.id, zoneID, Date.now(), Date.now()]
);
logger.info(`Synced DNS record: ${record.name}`);
}
}
}