@mgsoftwarebv/mg-dashboard-mcp 6.5.0 → 6.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +270 -79
- package/dist/index.js.map +1 -1
- package/package.json +50 -50
package/dist/index.js
CHANGED
|
@@ -17,6 +17,9 @@ import { drizzle } from 'drizzle-orm/postgres-js';
|
|
|
17
17
|
import postgres from 'postgres';
|
|
18
18
|
import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
|
|
19
19
|
import { tmpdir } from 'os';
|
|
20
|
+
import { once } from 'events';
|
|
21
|
+
import https from 'https';
|
|
22
|
+
import { connect } from 'tls';
|
|
20
23
|
import { HeadObjectCommand, S3Client, ListObjectsV2Command, DeleteObjectsCommand, DeleteObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, PutObjectCommand, GetObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3';
|
|
21
24
|
|
|
22
25
|
var __defProp = Object.defineProperty;
|
|
@@ -314,7 +317,7 @@ function rewriteUrl(originalUrl, localPort) {
|
|
|
314
317
|
parsed.port = String(localPort);
|
|
315
318
|
return parsed.toString();
|
|
316
319
|
}
|
|
317
|
-
async function
|
|
320
|
+
async function connectSsh2(opts) {
|
|
318
321
|
return await new Promise((resolve, reject) => {
|
|
319
322
|
const conn = new Client();
|
|
320
323
|
const onError = (err) => {
|
|
@@ -374,7 +377,7 @@ async function fetchRemoteEnvValue(options) {
|
|
|
374
377
|
const privateKey = readFileSync(privateKeyPath);
|
|
375
378
|
const keepaliveIntervalMs = options.keepaliveIntervalMs ?? 3e4;
|
|
376
379
|
const envKey = options.envKey ?? "DATABASE_PRIMARY_URL";
|
|
377
|
-
const conn = await
|
|
380
|
+
const conn = await connectSsh2({ host, port, username, privateKey, keepaliveIntervalMs });
|
|
378
381
|
try {
|
|
379
382
|
const safePath = options.remoteEnvPath.replace(/'/g, "'\\''");
|
|
380
383
|
const result = await execOverSsh(conn, `cat -- '${safePath}'`);
|
|
@@ -400,7 +403,7 @@ async function openDbSshTunnel(options) {
|
|
|
400
403
|
const parsedDbUrl = new URL(options.databaseUrl);
|
|
401
404
|
const remoteHost = parsedDbUrl.hostname;
|
|
402
405
|
const remotePort = Number(parsedDbUrl.port || "5432");
|
|
403
|
-
const conn = await
|
|
406
|
+
const conn = await connectSsh2({ host, port, username, privateKey, keepaliveIntervalMs });
|
|
404
407
|
conn.on("error", (err) => {
|
|
405
408
|
console.error(`[mcp][db-ssh-tunnel] ssh connection error: ${err.message}`);
|
|
406
409
|
});
|
|
@@ -460,7 +463,7 @@ var connectionConfig = {
|
|
|
460
463
|
}
|
|
461
464
|
},
|
|
462
465
|
connection: {
|
|
463
|
-
application_name: `mg-dashboard-${process.env.
|
|
466
|
+
application_name: `mg-dashboard-${process.env.TRIGGER_RUN_ID ? "trigger" : "local"}`
|
|
464
467
|
}
|
|
465
468
|
};
|
|
466
469
|
function getDatabaseUrl() {
|
|
@@ -1525,6 +1528,97 @@ async function handleRepoTool(name, args2, deps) {
|
|
|
1525
1528
|
return { content: [{ type: "text", text: `Unknown repo tool: ${name}` }] };
|
|
1526
1529
|
}
|
|
1527
1530
|
}
|
|
1531
|
+
function connectSsh(opts) {
|
|
1532
|
+
return new Promise((resolve, reject) => {
|
|
1533
|
+
const client = new Client();
|
|
1534
|
+
client.once("ready", () => resolve(client));
|
|
1535
|
+
client.once("error", reject);
|
|
1536
|
+
client.connect({
|
|
1537
|
+
host: opts.hostname,
|
|
1538
|
+
port: opts.port,
|
|
1539
|
+
username: opts.username,
|
|
1540
|
+
password: opts.password,
|
|
1541
|
+
privateKey: opts.privateKey,
|
|
1542
|
+
passphrase: opts.passphrase,
|
|
1543
|
+
readyTimeout: opts.timeout ?? 3e4
|
|
1544
|
+
});
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
function forwardOut(client, host, port) {
|
|
1548
|
+
return new Promise((resolve, reject) => {
|
|
1549
|
+
client.forwardOut("127.0.0.1", 0, host, port, (err, stream) => {
|
|
1550
|
+
if (err) reject(err);
|
|
1551
|
+
else resolve(stream);
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
async function fetchViaSshProxy(options) {
|
|
1556
|
+
const parsedUrl = new URL(options.url);
|
|
1557
|
+
if (parsedUrl.protocol !== "https:") {
|
|
1558
|
+
throw new Error(`fetchViaSshProxy only supports https URLs, got ${parsedUrl.protocol}`);
|
|
1559
|
+
}
|
|
1560
|
+
const targetHost = parsedUrl.hostname;
|
|
1561
|
+
const targetPort = parsedUrl.port ? Number(parsedUrl.port) : 443;
|
|
1562
|
+
const method = options.method ?? "GET";
|
|
1563
|
+
const body = options.body ?? "";
|
|
1564
|
+
const headers = { ...options.headers };
|
|
1565
|
+
if (body && !headers["Content-Length"]) {
|
|
1566
|
+
headers["Content-Length"] = String(Buffer.byteLength(body));
|
|
1567
|
+
}
|
|
1568
|
+
headers.Host = parsedUrl.host;
|
|
1569
|
+
const ssh = await connectSsh(options.proxy);
|
|
1570
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
1571
|
+
let timer;
|
|
1572
|
+
try {
|
|
1573
|
+
const stream = await forwardOut(ssh, targetHost, targetPort);
|
|
1574
|
+
const tlsSocket = connect({
|
|
1575
|
+
socket: stream,
|
|
1576
|
+
servername: targetHost,
|
|
1577
|
+
ALPNProtocols: ["http/1.1"]
|
|
1578
|
+
});
|
|
1579
|
+
await Promise.race([
|
|
1580
|
+
once(tlsSocket, "secureConnect"),
|
|
1581
|
+
new Promise((_, reject) => {
|
|
1582
|
+
timer = setTimeout(
|
|
1583
|
+
() => reject(new Error(`mijn.host fetch via SSH proxy timed out after ${timeoutMs}ms`)),
|
|
1584
|
+
timeoutMs
|
|
1585
|
+
);
|
|
1586
|
+
})
|
|
1587
|
+
]);
|
|
1588
|
+
if (timer) clearTimeout(timer);
|
|
1589
|
+
return await new Promise((resolve, reject) => {
|
|
1590
|
+
const req = https.request(
|
|
1591
|
+
{
|
|
1592
|
+
createConnection: () => tlsSocket,
|
|
1593
|
+
hostname: targetHost,
|
|
1594
|
+
port: targetPort,
|
|
1595
|
+
path: `${parsedUrl.pathname}${parsedUrl.search}`,
|
|
1596
|
+
method,
|
|
1597
|
+
headers,
|
|
1598
|
+
agent: false
|
|
1599
|
+
},
|
|
1600
|
+
(res) => {
|
|
1601
|
+
const chunks = [];
|
|
1602
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
1603
|
+
res.on("end", () => {
|
|
1604
|
+
resolve({
|
|
1605
|
+
status: res.statusCode ?? 0,
|
|
1606
|
+
statusText: res.statusMessage ?? "",
|
|
1607
|
+
body: Buffer.concat(chunks).toString("utf8")
|
|
1608
|
+
});
|
|
1609
|
+
});
|
|
1610
|
+
res.on("error", reject);
|
|
1611
|
+
}
|
|
1612
|
+
);
|
|
1613
|
+
req.on("error", reject);
|
|
1614
|
+
if (body) req.write(body);
|
|
1615
|
+
req.end();
|
|
1616
|
+
});
|
|
1617
|
+
} finally {
|
|
1618
|
+
if (timer) clearTimeout(timer);
|
|
1619
|
+
ssh.end();
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1528
1622
|
var args = process.argv.slice(2);
|
|
1529
1623
|
function getArg2(name) {
|
|
1530
1624
|
return args.find((a) => a.startsWith(`--${name}=`))?.split("=").slice(1).join("=");
|
|
@@ -1540,6 +1634,14 @@ var sshKeyPath = getArg2("ssh-key") || process.env.MG_DASHBOARD_SSH_KEY;
|
|
|
1540
1634
|
var databaseUrl = getArg2("database-url") || process.env.DATABASE_PRIMARY_POOLER_URL || process.env.DATABASE_PRIMARY_URL;
|
|
1541
1635
|
var encryptionKey = getArg2("encryption-key") || process.env.ENCRYPTION_KEY;
|
|
1542
1636
|
var mijnhostApiKey = getArg2("mijnhost-api-key") || process.env.MIJNHOST_API_KEY;
|
|
1637
|
+
function isMijnhostViaSshProxyEnabled() {
|
|
1638
|
+
const arg = getArg2("mijnhost-via-ssh-proxy");
|
|
1639
|
+
if (arg === "false" || arg === "0") return false;
|
|
1640
|
+
const env = process.env.MG_DASHBOARD_MIJNHOST_VIA_SSH_PROXY;
|
|
1641
|
+
if (env === "0" || env === "false") return false;
|
|
1642
|
+
return true;
|
|
1643
|
+
}
|
|
1644
|
+
var mijnhostViaSshProxy = isMijnhostViaSshProxyEnabled();
|
|
1543
1645
|
var dbSshTunnel = getArg2("db-ssh-tunnel") || process.env.MG_DASHBOARD_DB_SSH_TUNNEL;
|
|
1544
1646
|
var dbRemoteEnvFile = getArg2("db-remote-env-file") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_FILE;
|
|
1545
1647
|
var dbRemoteEnvKey = getArg2("db-remote-env-key") || process.env.MG_DASHBOARD_DB_REMOTE_ENV_KEY || "DATABASE_PRIMARY_URL";
|
|
@@ -1577,6 +1679,9 @@ if (dbSshTunnel) {
|
|
|
1577
1679
|
process.env.DATABASE_PRIMARY_URL = databaseUrl;
|
|
1578
1680
|
process.env.DATABASE_PRIMARY_POOLER_URL = databaseUrl;
|
|
1579
1681
|
var db = getDb();
|
|
1682
|
+
if (mijnhostApiKey && mijnhostViaSshProxy) {
|
|
1683
|
+
console.error("[mcp][mijnhost] Routing mijn.host API via SSH proxy");
|
|
1684
|
+
}
|
|
1580
1685
|
var RateLimiter = class {
|
|
1581
1686
|
buckets = /* @__PURE__ */ new Map();
|
|
1582
1687
|
maxAttempts;
|
|
@@ -3918,23 +4023,81 @@ function describeDnsCandidates(records, type, name, attemptedValue) {
|
|
|
3918
4023
|
}
|
|
3919
4024
|
async function mijnhostFetch(path, options = {}) {
|
|
3920
4025
|
const key = requireMijnhostApiKey();
|
|
3921
|
-
const
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
4026
|
+
const headers = {
|
|
4027
|
+
"API-Key": key,
|
|
4028
|
+
"Accept": "application/json",
|
|
4029
|
+
"Content-Type": "application/json",
|
|
4030
|
+
"User-Agent": "mg-dashboard-mcp/6.6.0",
|
|
4031
|
+
...options.headers || {}
|
|
4032
|
+
};
|
|
4033
|
+
const method = options.method ?? "GET";
|
|
4034
|
+
const requestBody = typeof options.body === "string" ? options.body : options.body != null ? String(options.body) : void 0;
|
|
4035
|
+
let status;
|
|
4036
|
+
let responseText;
|
|
4037
|
+
if (mijnhostViaSshProxy) {
|
|
4038
|
+
const proxy = await getProxyConnection();
|
|
4039
|
+
const proxied = await fetchViaSshProxy({
|
|
4040
|
+
proxy,
|
|
4041
|
+
url: `${MIJNHOST_BASE_URL}${path}`,
|
|
4042
|
+
method,
|
|
4043
|
+
headers,
|
|
4044
|
+
body: requestBody,
|
|
4045
|
+
timeoutMs: 6e4
|
|
4046
|
+
});
|
|
4047
|
+
status = proxied.status;
|
|
4048
|
+
responseText = proxied.body;
|
|
4049
|
+
} else {
|
|
4050
|
+
const res = await fetch(`${MIJNHOST_BASE_URL}${path}`, {
|
|
4051
|
+
...options,
|
|
4052
|
+
headers
|
|
4053
|
+
});
|
|
4054
|
+
status = res.status;
|
|
4055
|
+
responseText = await res.text();
|
|
4056
|
+
}
|
|
4057
|
+
let json;
|
|
4058
|
+
try {
|
|
4059
|
+
json = JSON.parse(responseText);
|
|
4060
|
+
} catch {
|
|
4061
|
+
throw new Error(
|
|
4062
|
+
`mijn.host API returned non-JSON (${status}): ${responseText.slice(0, 300)}`
|
|
4063
|
+
);
|
|
4064
|
+
}
|
|
3932
4065
|
const body = json;
|
|
3933
|
-
if (
|
|
3934
|
-
throw new Error(body?.status_description || `mijn.host API error: ${
|
|
4066
|
+
if (status < 200 || status >= 300) {
|
|
4067
|
+
throw new Error(body?.status_description || `mijn.host API error: ${status}`);
|
|
3935
4068
|
}
|
|
3936
4069
|
return body;
|
|
3937
4070
|
}
|
|
4071
|
+
var DNS_MIN_INTERVAL_MS = 750;
|
|
4072
|
+
var DNS_RETRY_BACKOFF_MS = 1500;
|
|
4073
|
+
var dnsZoneQueue = /* @__PURE__ */ new Map();
|
|
4074
|
+
var dnsLastPutAt = /* @__PURE__ */ new Map();
|
|
4075
|
+
function sleep(ms) {
|
|
4076
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4077
|
+
}
|
|
4078
|
+
async function withDnsZoneLock(domain, fn) {
|
|
4079
|
+
const key = domain.toLowerCase();
|
|
4080
|
+
const previous = dnsZoneQueue.get(key) ?? Promise.resolve();
|
|
4081
|
+
const run = previous.then(async () => {
|
|
4082
|
+
const last = dnsLastPutAt.get(key) ?? 0;
|
|
4083
|
+
const wait = DNS_MIN_INTERVAL_MS - (Date.now() - last);
|
|
4084
|
+
if (wait > 0) await sleep(wait);
|
|
4085
|
+
return fn();
|
|
4086
|
+
});
|
|
4087
|
+
const queued = run.finally(() => {
|
|
4088
|
+
if (dnsZoneQueue.get(key) === queued) dnsZoneQueue.delete(key);
|
|
4089
|
+
});
|
|
4090
|
+
dnsZoneQueue.set(key, queued.catch(() => {
|
|
4091
|
+
}));
|
|
4092
|
+
return run;
|
|
4093
|
+
}
|
|
4094
|
+
function markDnsPut(domain) {
|
|
4095
|
+
dnsLastPutAt.set(domain.toLowerCase(), Date.now());
|
|
4096
|
+
}
|
|
4097
|
+
function isTransientMijnhostError(err) {
|
|
4098
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4099
|
+
return /Internal Server Error|API error: 5\d\d|fetch failed|ETIMEDOUT|ECONNRESET/i.test(msg);
|
|
4100
|
+
}
|
|
3938
4101
|
var TOOLS = [
|
|
3939
4102
|
{
|
|
3940
4103
|
name: "list-servers",
|
|
@@ -4298,7 +4461,7 @@ var TOOLS = [
|
|
|
4298
4461
|
},
|
|
4299
4462
|
{
|
|
4300
4463
|
name: "dns-record",
|
|
4301
|
-
description: 'Mutate a single DNS record on a mijn.host domain. Pick the mutation with `action`:\n- "create": add a new record. Required: type, name, value. Optional: ttl (default 3600).\n- "update": replace an existing record. Required: type, name, oldValue, newValue. Optional: ttl.\n- "delete": remove a record. Required: type, name, value.\n\nAlways pass `dryRun: true` first when touching MX / SPF / DKIM / DMARC \u2014 returns a full before/after diff with a mail-auth warning and applies nothing. Re-run without dryRun once the diff looks correct.\n\nUse `dns-list` to inspect the current zone first if you need to identify the right `oldValue`. Requires MIJNHOST_API_KEY.',
|
|
4464
|
+
description: 'Mutate a single DNS record on a mijn.host domain. Pick the mutation with `action`:\n- "create": add a new record. Required: type, name, value. Optional: ttl (default 3600).\n- "update": replace an existing record. Required: type, name, oldValue, newValue. Optional: ttl.\n- "delete": remove a record. Required: type, name, value.\n\nAlways pass `dryRun: true` first when touching MX / SPF / DKIM / DMARC \u2014 returns a full before/after diff with a mail-auth warning and applies nothing. Re-run without dryRun once the diff looks correct.\n\nUse `dns-list` to inspect the current zone first if you need to identify the right `oldValue`. Requires MIJNHOST_API_KEY.\n\nConcurrency: calls are automatically serialized per domain (mijn.host\'s API does GET-then-PUT-the-whole-zone, so parallel calls would race and silently drop records). A minimum 750 ms gap is enforced between PUTs to the same zone, transient 5xx errors are retried once, and a stale-snapshot guard refuses to PUT a zone that suddenly shrunk. You can still fire calls in parallel \u2014 they just queue safely behind each other per domain.',
|
|
4302
4465
|
inputSchema: {
|
|
4303
4466
|
type: "object",
|
|
4304
4467
|
properties: {
|
|
@@ -4320,7 +4483,7 @@ var TOOLS = [
|
|
|
4320
4483
|
// ----- Repo reference -----
|
|
4321
4484
|
...REPO_TOOLS
|
|
4322
4485
|
];
|
|
4323
|
-
var MCP_VERSION = "6.
|
|
4486
|
+
var MCP_VERSION = "6.6.0";
|
|
4324
4487
|
async function handleListTools() {
|
|
4325
4488
|
if (!authContext) return { tools: TOOLS };
|
|
4326
4489
|
const accessible = TOOLS.filter((tool) => {
|
|
@@ -5985,74 +6148,102 @@ ${lines.join("\n")}` }] };
|
|
|
5985
6148
|
if (!domain || !type || !dnsName) {
|
|
5986
6149
|
throw new Error("domain, type, and name are required");
|
|
5987
6150
|
}
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
let nextRecords;
|
|
5992
|
-
let diffArgs;
|
|
5993
|
-
let okMessage;
|
|
5994
|
-
if (action === "create") {
|
|
5995
|
-
const value = typeof a.value === "string" ? a.value : "";
|
|
5996
|
-
if (!value) throw new Error('action="create" requires value');
|
|
5997
|
-
const ttl = Number(a.ttl) || 3600;
|
|
5998
|
-
const newRecord = { type, name: dnsName, value, ttl };
|
|
5999
|
-
nextRecords = [...current.data.records, newRecord];
|
|
6000
|
-
diffArgs = { verb: "create", added: [newRecord] };
|
|
6001
|
-
okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
|
|
6002
|
-
} else if (action === "update") {
|
|
6003
|
-
const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
|
|
6004
|
-
const newValue = typeof a.newValue === "string" ? a.newValue : "";
|
|
6005
|
-
if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
|
|
6006
|
-
const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
|
|
6007
|
-
const idx = current.data.records.findIndex(
|
|
6008
|
-
(r) => r.type === type && r.name === dnsName && r.value === oldValue
|
|
6151
|
+
return withDnsZoneLock(domain, async () => {
|
|
6152
|
+
const fetchZone = () => mijnhostFetch(
|
|
6153
|
+
`/domains/${encodeURIComponent(domain)}/dns`
|
|
6009
6154
|
);
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6155
|
+
let current = await fetchZone();
|
|
6156
|
+
if (current.data.records.length === 0) {
|
|
6157
|
+
await sleep(DNS_RETRY_BACKOFF_MS);
|
|
6158
|
+
current = await fetchZone();
|
|
6159
|
+
}
|
|
6160
|
+
let nextRecords;
|
|
6161
|
+
let diffArgs;
|
|
6162
|
+
let okMessage;
|
|
6163
|
+
if (action === "create") {
|
|
6164
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
6165
|
+
if (!value) throw new Error('action="create" requires value');
|
|
6166
|
+
const ttl = Number(a.ttl) || 3600;
|
|
6167
|
+
const newRecord = { type, name: dnsName, value, ttl };
|
|
6168
|
+
nextRecords = [...current.data.records, newRecord];
|
|
6169
|
+
diffArgs = { verb: "create", added: [newRecord] };
|
|
6170
|
+
okMessage = `DNS record created: ${type} ${dnsName} \u2192 ${value} (TTL: ${ttl})`;
|
|
6171
|
+
} else if (action === "update") {
|
|
6172
|
+
const oldValue = typeof a.oldValue === "string" ? a.oldValue : "";
|
|
6173
|
+
const newValue = typeof a.newValue === "string" ? a.newValue : "";
|
|
6174
|
+
if (!oldValue || !newValue) throw new Error('action="update" requires oldValue and newValue');
|
|
6175
|
+
const ttlArg = a.ttl !== void 0 ? Number(a.ttl) : void 0;
|
|
6176
|
+
const idx = current.data.records.findIndex(
|
|
6177
|
+
(r) => r.type === type && r.name === dnsName && r.value === oldValue
|
|
6178
|
+
);
|
|
6179
|
+
if (idx === -1) {
|
|
6180
|
+
throw new Error(
|
|
6181
|
+
`No matching DNS record found: ${type} ${dnsName} = ${oldValue}
|
|
6013
6182
|
` + describeDnsCandidates(current.data.records, type, dnsName, oldValue)
|
|
6183
|
+
);
|
|
6184
|
+
}
|
|
6185
|
+
const updated = [...current.data.records];
|
|
6186
|
+
const before = updated[idx];
|
|
6187
|
+
const after = {
|
|
6188
|
+
type,
|
|
6189
|
+
name: dnsName,
|
|
6190
|
+
value: newValue,
|
|
6191
|
+
ttl: ttlArg ?? before.ttl
|
|
6192
|
+
};
|
|
6193
|
+
updated[idx] = after;
|
|
6194
|
+
nextRecords = updated;
|
|
6195
|
+
diffArgs = { verb: "update", removed: [before], added: [after] };
|
|
6196
|
+
okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
|
|
6197
|
+
} else {
|
|
6198
|
+
const value = typeof a.value === "string" ? a.value : "";
|
|
6199
|
+
if (!value) throw new Error('action="delete" requires value');
|
|
6200
|
+
const removed = current.data.records.filter(
|
|
6201
|
+
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
6202
|
+
);
|
|
6203
|
+
const remaining = current.data.records.filter(
|
|
6204
|
+
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
6014
6205
|
);
|
|
6206
|
+
if (removed.length === 0) {
|
|
6207
|
+
throw new Error(
|
|
6208
|
+
`No matching DNS record found: ${type} ${dnsName} = ${value}
|
|
6209
|
+
` + describeDnsCandidates(current.data.records, type, dnsName, value)
|
|
6210
|
+
);
|
|
6211
|
+
}
|
|
6212
|
+
nextRecords = remaining;
|
|
6213
|
+
diffArgs = { verb: "delete", removed };
|
|
6214
|
+
okMessage = `DNS record deleted: ${type} ${dnsName} = ${value} (${remaining.length} records remaining)`;
|
|
6015
6215
|
}
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
value: newValue,
|
|
6022
|
-
ttl: ttlArg ?? before.ttl
|
|
6023
|
-
};
|
|
6024
|
-
updated[idx] = after;
|
|
6025
|
-
nextRecords = updated;
|
|
6026
|
-
diffArgs = { verb: "update", removed: [before], added: [after] };
|
|
6027
|
-
okMessage = `DNS record updated: ${type} ${dnsName} \u2192 ${newValue}${ttlArg ? ` (TTL: ${ttlArg})` : ""}`;
|
|
6028
|
-
} else {
|
|
6029
|
-
const value = typeof a.value === "string" ? a.value : "";
|
|
6030
|
-
if (!value) throw new Error('action="delete" requires value');
|
|
6031
|
-
const removed = current.data.records.filter(
|
|
6032
|
-
(r) => r.type === type && r.name === dnsName && r.value === value
|
|
6033
|
-
);
|
|
6034
|
-
const remaining = current.data.records.filter(
|
|
6035
|
-
(r) => !(r.type === type && r.name === dnsName && r.value === value)
|
|
6036
|
-
);
|
|
6037
|
-
if (removed.length === 0) {
|
|
6216
|
+
if (dryRun) {
|
|
6217
|
+
const diff = formatDnsDiff(domain, current.data.records, nextRecords, diffArgs);
|
|
6218
|
+
return { content: [{ type: "text", text: diff }] };
|
|
6219
|
+
}
|
|
6220
|
+
if (nextRecords.length < 3 && current.data.records.length >= 5 && action !== "delete") {
|
|
6038
6221
|
throw new Error(
|
|
6039
|
-
`
|
|
6040
|
-
` + describeDnsCandidates(current.data.records, type, dnsName, value)
|
|
6222
|
+
`Aborting PUT: would shrink ${domain} from ${current.data.records.length} to ${nextRecords.length} records. This usually indicates a stale zone snapshot from mijn.host. Run dns-list and retry.`
|
|
6041
6223
|
);
|
|
6042
6224
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6225
|
+
try {
|
|
6226
|
+
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
6227
|
+
method: "PUT",
|
|
6228
|
+
body: JSON.stringify({ records: nextRecords })
|
|
6229
|
+
});
|
|
6230
|
+
} catch (err) {
|
|
6231
|
+
if (!isTransientMijnhostError(err)) throw err;
|
|
6232
|
+
await sleep(DNS_RETRY_BACKOFF_MS);
|
|
6233
|
+
const refetched = await fetchZone();
|
|
6234
|
+
const stillNeedsApply = action === "create" ? !refetched.data.records.some((r) => r.type === type && r.name === dnsName && r.value === (typeof a.value === "string" ? a.value : "")) : true;
|
|
6235
|
+
if (!stillNeedsApply) {
|
|
6236
|
+
markDnsPut(domain);
|
|
6237
|
+
return { content: [{ type: "text", text: `${okMessage} (already applied; recovered from transient error)` }] };
|
|
6238
|
+
}
|
|
6239
|
+
await mijnhostFetch(`/domains/${encodeURIComponent(domain)}/dns`, {
|
|
6240
|
+
method: "PUT",
|
|
6241
|
+
body: JSON.stringify({ records: nextRecords })
|
|
6242
|
+
});
|
|
6243
|
+
}
|
|
6244
|
+
markDnsPut(domain);
|
|
6245
|
+
return { content: [{ type: "text", text: okMessage }] };
|
|
6054
6246
|
});
|
|
6055
|
-
return { content: [{ type: "text", text: okMessage }] };
|
|
6056
6247
|
}
|
|
6057
6248
|
default:
|
|
6058
6249
|
if (TRIGGER_TOOL_NAMES.has(name)) {
|