'use strict'; // ============================================================================ // Porkbun DNS 공급자 (v1.5.36, 요구 5) — Porkbun 에 등록(또는 Porkbun 네임서버를 쓰는) 도메인의 레코드 관리 // // 접속 정보: 보관소 종류 'porkbun' — 키(secret) = Secret API Key, 설정 API_KEY=… (공개 API 키). 도메인마다 Porkbun 화면에서 "API Access" 를 켜야 한다. // API: https://api.porkbun.com/api/json/v3/ (JSON, 모든 호출이 POST 이고 본문에 apikey·secretapikey 를 넣는다). 시험용 대체 주소: PORKBUN_API_BASE. // 영역은 "계정에 있는 도메인" 그 자체라 따로 만들지 않는다(createZone = 계정 보유 확인). 레코드마다 id 가 있어 지울 때 이름·종류·값으로 id 를 찾는다. // ============================================================================ const httpError = (m, s = 400) => Object.assign(new Error(m), { status: s }); const base = () => (process.env.PORKBUN_API_BASE || 'https://api.porkbun.com/api/json/v3').replace(/\/+$/, ''); function creds() { let x = null; try { x = require('../vault').resolve('porkbun'); } catch (_) {} const key = x && x.settings && (x.settings.API_KEY || x.settings.PORKBUN_API_KEY); if (!x || !x.secret || !key) return null; return { apikey: key, secretapikey: x.secret }; } const ready = () => !!creds(); /** 호출: 응답 { status: 'SUCCESS'|'ERROR', message, ... } */ async function call(path, body) { const c = creds(); if (!c) throw httpError('Porkbun 접속 정보가 없습니다 (관리자 콘솔 › 설정 › 외부 연동 접속 정보 › Porkbun: API_KEY 설정과 Secret API Key)', 503); let r; try { r = await fetch(`${base()}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...c, ...(body || {}) }), signal: AbortSignal.timeout(25000) }); } catch (e) { throw httpError(`Porkbun 연결 실패: ${e.message}`, 502); } const j = await r.json().catch(() => ({})); if (!r.ok || j.status !== 'SUCCESS') throw httpError(`Porkbun 오류: ${j.message || `HTTP ${r.status}`}`, r.status === 403 || /invalid api|not authorized|api access/i.test(j.message || '') ? 403 : r.status >= 500 ? 502 : 400); return j; } const low = (d) => String(d).toLowerCase().replace(/\.$/, ''); async function listZones() { const j = await call('/domain/listAll', { start: '0', includeLabels: 'no' }); return (j.domains || []).map((d) => ({ id: d.domain, name: low(d.domain), status: d.status, expires: d.expireDate || null, nameServers: [] })); } async function getZone(domain) { const z = (await listZones()).find((x) => x.name === low(domain)); if (!z) return null; try { const ns = await call(`/domain/getNs/${low(domain)}`); z.nameServers = ns.ns || []; } catch (_) {} return z; } async function createZone(domain) { const z = await getZone(domain); if (!z) throw httpError(`Porkbun 계정에 ${domain} 이(가) 없습니다 — Porkbun 에서 등록한(또는 이전한) 도메인만 관리할 수 있습니다`, 404); return z; } async function deleteZone(domain) { const recs = await rawRecords(domain); for (const r of recs) if (!(r.type === 'NS' && r.rel === '')) await call(`/dns/delete/${low(domain)}/${r.id}`); return { ok: true, existed: true, cleared: recs.length }; } async function rawRecords(domain) { const j = await call(`/dns/retrieve/${low(domain)}`); const d = low(domain); return (j.records || []).map((r) => { const n = low(r.name); return { id: r.id, rel: n === d ? '' : n.endsWith('.' + d) ? n.slice(0, -(d.length + 1)) : n, type: r.type, value: r.content, ttl: Number(r.ttl) || 600, prio: r.prio != null && r.prio !== '' ? Number(r.prio) : undefined }; }); } async function listRecords(domain) { return (await rawRecords(domain)).map(({ rel, type, value, ttl, prio }) => ({ name: rel, type, value, ttl, prio })); } async function updateRecords(domain, { add = [], remove = [] }) { let n = 0; if (remove.length) { const cur = await rawRecords(domain); for (const r of remove) { const hit = cur.find((x) => x.rel === String(r.name || '') && x.type === r.type && String(x.value) === String(r.value)); if (hit) { await call(`/dns/delete/${low(domain)}/${hit.id}`); n += 1; } } } for (const r of add) { await call(`/dns/create/${low(domain)}`, { name: r.name || '', type: r.type, content: r.value, ttl: String(Math.max(600, r.ttl || 600)), ...(r.prio != null ? { prio: String(r.prio) } : {}) }); n += 1; } return { ok: true, changes: n }; } async function nameServers(domain) { const j = await call(`/domain/getNs/${low(domain)}`); return j.ns || []; } async function setNameServers(domain, ns) { await call(`/domain/updateNs/${low(domain)}`, { ns }); return { ok: true }; } module.exports = { id: 'porkbun', label: 'Porkbun', ready, creds, call, listZones, getZone, createZone, deleteZone, listRecords, updateRecords, nameServers, setNameServers };