diff --git a/ts/classes/database.ts b/ts/classes/database.ts index fcc439d..03238a3 100644 --- a/ts/classes/database.ts +++ b/ts/classes/database.ts @@ -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 { diff --git a/ts/classes/dns.ts b/ts/classes/dns.ts index 4a9fa24..fe2cf39 100644 --- a/ts/classes/dns.ts +++ b/ts/classes/dns.ts @@ -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 = 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 '); - logger.info('Configure with: onebox config set cloudflareEmail '); 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 '); - 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 '); } + // 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}`); + } } } diff --git a/ts/plugins.ts b/ts/plugins.ts index bfe2873..904bf6b 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -26,8 +26,8 @@ import { DockerHost } from '@apiclient.xyz/docker'; export const docker = { Docker: DockerHost }; // Cloudflare DNS Management -import * as cloudflare from '@apiclient.xyz/cloudflare'; -export { cloudflare }; +import Cloudflare from 'npm:cloudflare@5.2.0'; +export const cloudflare = { Cloudflare }; // Let's Encrypt / ACME import * as smartacme from '@push.rocks/smartacme';