@sakupa/mcp 0.7.8 → 0.7.10
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/bin.js +243 -33
- package/dist/index.js +243 -33
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -129,7 +129,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
129
129
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
130
130
|
|
|
131
131
|
// ../core/dist/domain/version.js
|
|
132
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
132
|
+
var SAKUPA_MCP_VERSION = "0.7.10";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -458,10 +458,10 @@ var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
|
458
458
|
// src/config.ts
|
|
459
459
|
var TEST_API_BASE_URL = "https://api-test.sakupa.com";
|
|
460
460
|
function previewHostPatternFor(apiBaseUrl) {
|
|
461
|
-
return apiBaseUrl ===
|
|
461
|
+
return environmentFor(apiBaseUrl) === "test" ? "{shortId}-test.sakupa.com" : "{shortId}.sakupa.com";
|
|
462
462
|
}
|
|
463
463
|
function loadMcpRuntimeConfig(env = process.env, cwd = process.cwd()) {
|
|
464
|
-
const apiBaseUrl = (env["SAKUPA_API_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
464
|
+
const apiBaseUrl = (env["SAKUPA_API_URL"] ?? env["SAKUPA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
465
465
|
const projectDir = env["SAKUPA_PROJECT_DIR"] ?? cwd;
|
|
466
466
|
const testAccessToken = env["SAKUPA_TEST_ACCESS_TOKEN"]?.trim() ?? "";
|
|
467
467
|
if (apiBaseUrl === TEST_API_BASE_URL) {
|
|
@@ -479,6 +479,9 @@ function loadMcpRuntimeConfig(env = process.env, cwd = process.cwd()) {
|
|
|
479
479
|
}
|
|
480
480
|
return { apiBaseUrl, projectDir };
|
|
481
481
|
}
|
|
482
|
+
function environmentFor(apiBaseUrl) {
|
|
483
|
+
return apiBaseUrl === TEST_API_BASE_URL ? "test" : "production";
|
|
484
|
+
}
|
|
482
485
|
|
|
483
486
|
// src/server.ts
|
|
484
487
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -591,11 +594,14 @@ var HttpApiClient = class {
|
|
|
591
594
|
async bindDomain(credential, req) {
|
|
592
595
|
return this.call("POST", "/v1/domains/bind", { credential, body: req });
|
|
593
596
|
}
|
|
594
|
-
async checkVerification(verificationId, credential) {
|
|
597
|
+
async checkVerification(verificationId, credential, latestForSiteId) {
|
|
595
598
|
return this.call(
|
|
596
599
|
"POST",
|
|
597
600
|
`/v1/domains/verifications/${encodeURIComponent(verificationId)}/check`,
|
|
598
|
-
{
|
|
601
|
+
{
|
|
602
|
+
credential,
|
|
603
|
+
...latestForSiteId !== void 0 ? { body: { siteId: latestForSiteId } } : {}
|
|
604
|
+
}
|
|
599
605
|
);
|
|
600
606
|
}
|
|
601
607
|
async unbindDomain(siteId, credential, req) {
|
|
@@ -1248,6 +1254,131 @@ function removeCreation(siteId) {
|
|
|
1248
1254
|
if (rest.length !== all.length) writeAll(rest);
|
|
1249
1255
|
}
|
|
1250
1256
|
|
|
1257
|
+
// src/dns-doh.ts
|
|
1258
|
+
var dohFetch = (input, init) => fetch(input, init);
|
|
1259
|
+
var TYPE_CODES = { TXT: 16, CNAME: 5, A: 1 };
|
|
1260
|
+
async function resolveDns(name, type) {
|
|
1261
|
+
const endpoints = [
|
|
1262
|
+
`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=${type}`,
|
|
1263
|
+
`https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${type}`
|
|
1264
|
+
];
|
|
1265
|
+
for (const url of endpoints) {
|
|
1266
|
+
try {
|
|
1267
|
+
const res = await dohFetch(url, { headers: { accept: "application/dns-json" } });
|
|
1268
|
+
if (!res.ok) continue;
|
|
1269
|
+
const body = await res.json();
|
|
1270
|
+
return (body.Answer ?? []).filter((a) => a.type === TYPE_CODES[type]).map((a) => a.data.replace(/^"|"$/g, "").replace(/"\s+"/g, "")).map((v) => type === "CNAME" ? v.replace(/\.$/, "").toLowerCase() : v);
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return [];
|
|
1275
|
+
}
|
|
1276
|
+
function shortHostFor(fullName, apexDomain) {
|
|
1277
|
+
const suffix = `.${apexDomain}`;
|
|
1278
|
+
if (fullName === apexDomain) return "@";
|
|
1279
|
+
return fullName.endsWith(suffix) ? fullName.slice(0, -suffix.length) : fullName;
|
|
1280
|
+
}
|
|
1281
|
+
function matches(record, values) {
|
|
1282
|
+
if (record.type === "CNAME") {
|
|
1283
|
+
const want = record.value.replace(/\.$/, "").toLowerCase();
|
|
1284
|
+
return values.some((v) => v === want);
|
|
1285
|
+
}
|
|
1286
|
+
return values.includes(record.value);
|
|
1287
|
+
}
|
|
1288
|
+
async function checkRecord(record, apexDomain) {
|
|
1289
|
+
const shortHost = shortHostFor(record.name, apexDomain);
|
|
1290
|
+
const found = await resolveDns(record.name, record.type);
|
|
1291
|
+
if (matches(record, found)) {
|
|
1292
|
+
return { record, shortHost, state: "ok", found, fix: "" };
|
|
1293
|
+
}
|
|
1294
|
+
const doubled = await resolveDns(`${record.name}.${apexDomain}`, record.type);
|
|
1295
|
+
if (matches(record, doubled)) {
|
|
1296
|
+
return {
|
|
1297
|
+
record,
|
|
1298
|
+
shortHost,
|
|
1299
|
+
state: "double_domain",
|
|
1300
|
+
found: doubled,
|
|
1301
|
+
fix: `The record exists at ${record.name}.${apexDomain} \u2014 the host field was filled with the full name and your DNS panel appended ${apexDomain} again. Edit that record's host to exactly: ${shortHost}`
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
if (found.length > 0) {
|
|
1305
|
+
return {
|
|
1306
|
+
record,
|
|
1307
|
+
shortHost,
|
|
1308
|
+
state: "wrong_value",
|
|
1309
|
+
found,
|
|
1310
|
+
fix: `A ${record.type} record exists at ${record.name} but its value is ${JSON.stringify(found)} instead of "${record.value}". Update the value exactly.`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
record,
|
|
1315
|
+
shortHost,
|
|
1316
|
+
state: "missing",
|
|
1317
|
+
found,
|
|
1318
|
+
fix: `Create it now \u2014 type: ${record.type}, host: ${shortHost} (most panels append .${apexDomain} automatically; if yours wants the full name use ${record.name}), value: ${record.value}`
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function renderCheck(c) {
|
|
1322
|
+
const label = `${c.record.type} ${c.shortHost}`;
|
|
1323
|
+
switch (c.state) {
|
|
1324
|
+
case "ok":
|
|
1325
|
+
return ` [OK] ${label} \u2014 live on public DNS.`;
|
|
1326
|
+
case "double_domain":
|
|
1327
|
+
return ` [FIX] ${label} \u2014 ${c.fix}`;
|
|
1328
|
+
case "wrong_value":
|
|
1329
|
+
return ` [FIX] ${label} \u2014 ${c.fix}`;
|
|
1330
|
+
case "missing":
|
|
1331
|
+
return ` [MISSING] ${label} \u2014 ${c.fix}`;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
async function diagnoseBinding(input) {
|
|
1335
|
+
const apex = input.apexDomain;
|
|
1336
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1337
|
+
if (input.verificationRecord) {
|
|
1338
|
+
byKey.set(`TXT:${input.verificationRecord.name}`, input.verificationRecord);
|
|
1339
|
+
}
|
|
1340
|
+
const www = {
|
|
1341
|
+
name: `www.${apex}`,
|
|
1342
|
+
type: "CNAME",
|
|
1343
|
+
value: input.servingTarget
|
|
1344
|
+
};
|
|
1345
|
+
byKey.set(`CNAME:${www.name}`, www);
|
|
1346
|
+
for (const rec of input.pendingDnsRecords) {
|
|
1347
|
+
const key = `${rec.type}:${rec.name}`;
|
|
1348
|
+
if (!byKey.has(key)) byKey.set(key, rec);
|
|
1349
|
+
}
|
|
1350
|
+
const [checks, apexAnswers] = await Promise.all([
|
|
1351
|
+
Promise.all([...byKey.values()].map((rec) => checkRecord(rec, apex))),
|
|
1352
|
+
resolveDns(apex, "A")
|
|
1353
|
+
]);
|
|
1354
|
+
const apexResolves = apexAnswers.length > 0;
|
|
1355
|
+
const allOk = checks.every((c) => c.state === "ok");
|
|
1356
|
+
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1357
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet: point it at ${input.servingTarget} using your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).`);
|
|
1358
|
+
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1359
|
+
return { checks, apexResolves, allOk, checklist, layers };
|
|
1360
|
+
}
|
|
1361
|
+
var DNS_RETRY_AFTER_SECONDS = 300;
|
|
1362
|
+
var DNS_MAX_ATTEMPTS = 10;
|
|
1363
|
+
function toDnsChecklist(checks) {
|
|
1364
|
+
return checks.map((c) => ({
|
|
1365
|
+
name: c.record.name,
|
|
1366
|
+
type: c.record.type,
|
|
1367
|
+
shortHost: c.shortHost,
|
|
1368
|
+
state: c.state,
|
|
1369
|
+
...c.fix ? { fix: c.fix } : {}
|
|
1370
|
+
}));
|
|
1371
|
+
}
|
|
1372
|
+
function renderChecklistBlock(diag, fixTail) {
|
|
1373
|
+
const cadence = `Re-check every ${DNS_RETRY_AFTER_SECONDS / 60} minutes, up to ${DNS_MAX_ATTEMPTS} times.`;
|
|
1374
|
+
return `Live DNS checklist (host values are the SHORT panel form):
|
|
1375
|
+
${diag.checklist}
|
|
1376
|
+
|
|
1377
|
+
${diag.layers}
|
|
1378
|
+
|
|
1379
|
+
` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1251
1382
|
// src/version.ts
|
|
1252
1383
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
1253
1384
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
@@ -1498,6 +1629,38 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1498
1629
|
}
|
|
1499
1630
|
return targets.length;
|
|
1500
1631
|
}
|
|
1632
|
+
async function describePendingBinding(client, credential, pb) {
|
|
1633
|
+
const framing = `
|
|
1634
|
+
Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provisioning" ? "ownership verified; certificates/serving are provisioning." : `awaiting DNS verification (challenge valid until ${pb.verificationExpiresAt}).`);
|
|
1635
|
+
let check;
|
|
1636
|
+
try {
|
|
1637
|
+
check = await client.checkVerification(pb.verificationId, credential);
|
|
1638
|
+
} catch {
|
|
1639
|
+
return {
|
|
1640
|
+
note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
try {
|
|
1644
|
+
const diag = await diagnoseBinding({
|
|
1645
|
+
apexDomain: check.apexDomain,
|
|
1646
|
+
servingTarget: check.servingTarget,
|
|
1647
|
+
...check.verificationRecord ? { verificationRecord: check.verificationRecord } : {},
|
|
1648
|
+
pendingDnsRecords: check.pendingDnsRecords,
|
|
1649
|
+
verificationStatus: check.status,
|
|
1650
|
+
provisioning: pb.phase === "provisioning"
|
|
1651
|
+
});
|
|
1652
|
+
const block = renderChecklistBlock(
|
|
1653
|
+
diag,
|
|
1654
|
+
"Relay every [MISSING]/[FIX] line to the user with its exact fix."
|
|
1655
|
+
);
|
|
1656
|
+
return { note: `${framing}
|
|
1657
|
+
${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
1658
|
+
} catch {
|
|
1659
|
+
return {
|
|
1660
|
+
note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1501
1664
|
function freeSiteCreationBarrier() {
|
|
1502
1665
|
const recent = listRecentCreations(Date.now());
|
|
1503
1666
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1628,6 +1791,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1628
1791
|
return text(
|
|
1629
1792
|
"site_published",
|
|
1630
1793
|
`Site published: ${finalized2.url}
|
|
1794
|
+
Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
1631
1795
|
Project directory: ${ctx.projectDir}
|
|
1632
1796
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1633
1797
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
@@ -1647,7 +1811,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1647
1811
|
totalBytes: finalized2.totalBytes,
|
|
1648
1812
|
warnings: finalized2.warnings,
|
|
1649
1813
|
credentialStoredLocally: true,
|
|
1650
|
-
projectDir: ctx.projectDir
|
|
1814
|
+
projectDir: ctx.projectDir,
|
|
1815
|
+
environment: environmentFor(ctx.apiBaseUrl)
|
|
1651
1816
|
}
|
|
1652
1817
|
);
|
|
1653
1818
|
}
|
|
@@ -1693,6 +1858,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1693
1858
|
return text(
|
|
1694
1859
|
"site_updated",
|
|
1695
1860
|
`Site updated: ${finalized.url}
|
|
1861
|
+
Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
1696
1862
|
Project directory: ${ctx.projectDir}
|
|
1697
1863
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1698
1864
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
@@ -1706,6 +1872,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1706
1872
|
url: finalized.url,
|
|
1707
1873
|
mode: finalized.mode,
|
|
1708
1874
|
projectDir: ctx.projectDir,
|
|
1875
|
+
environment: environmentFor(ctx.apiBaseUrl),
|
|
1709
1876
|
expiresAt: finalized.expiresAt,
|
|
1710
1877
|
filesUploaded: uploaded,
|
|
1711
1878
|
totalBytes: finalized.totalBytes,
|
|
@@ -1754,9 +1921,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1754
1921
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1755
1922
|
const site = requireSiteFile(ctx);
|
|
1756
1923
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1757
|
-
|
|
1924
|
+
const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
|
|
1925
|
+
return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
|
|
1758
1926
|
...res,
|
|
1759
|
-
projectDir: ctx.projectDir
|
|
1927
|
+
projectDir: ctx.projectDir,
|
|
1928
|
+
...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
|
|
1760
1929
|
});
|
|
1761
1930
|
} catch (e) {
|
|
1762
1931
|
return toolError(e);
|
|
@@ -1820,7 +1989,9 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1820
1989
|
projectDir: projectDirInput,
|
|
1821
1990
|
action: z3.enum(["start", "status"]),
|
|
1822
1991
|
hostname: z3.string().optional().describe("Required for start."),
|
|
1823
|
-
verificationId: z3.string().optional().describe(
|
|
1992
|
+
verificationId: z3.string().optional().describe(
|
|
1993
|
+
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
1994
|
+
)
|
|
1824
1995
|
}
|
|
1825
1996
|
},
|
|
1826
1997
|
async (args) => {
|
|
@@ -1828,30 +1999,42 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1828
1999
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1829
2000
|
const site = requireSiteFile(ctx);
|
|
1830
2001
|
if (args.action === "status") {
|
|
1831
|
-
|
|
1832
|
-
throw new SakupaError("invalid_request", "verificationId is required for status");
|
|
1833
|
-
}
|
|
1834
|
-
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
2002
|
+
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
1835
2003
|
if (res2.status === "verified") {
|
|
1836
2004
|
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1837
2005
|
}
|
|
2006
|
+
const apex2 = res2.apexDomain;
|
|
2007
|
+
const diag = await diagnoseBinding({
|
|
2008
|
+
apexDomain: apex2,
|
|
2009
|
+
servingTarget: res2.servingTarget,
|
|
2010
|
+
...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
|
|
2011
|
+
pendingDnsRecords: res2.pendingDnsRecords,
|
|
2012
|
+
verificationStatus: res2.status,
|
|
2013
|
+
provisioning: res2.provisioningJobId !== void 0
|
|
2014
|
+
});
|
|
2015
|
+
const { apexResolves, allOk } = diag;
|
|
1838
2016
|
return text(
|
|
1839
2017
|
res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
|
|
1840
|
-
`
|
|
2018
|
+
`Domain binding status for ${apex2}: ${res2.status}
|
|
1841
2019
|
${res2.message}
|
|
1842
|
-
|
|
1843
|
-
`
|
|
1844
|
-
|
|
1845
|
-
|
|
2020
|
+
|
|
2021
|
+
` + renderChecklistBlock(
|
|
2022
|
+
diag,
|
|
2023
|
+
"Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
|
|
2024
|
+
),
|
|
1846
2025
|
{
|
|
1847
2026
|
verificationId: res2.verificationId,
|
|
1848
2027
|
status: res2.status,
|
|
1849
|
-
apexDomain:
|
|
2028
|
+
apexDomain: apex2,
|
|
1850
2029
|
provisioningJobId: res2.provisioningJobId,
|
|
1851
|
-
|
|
2030
|
+
servingTarget: res2.servingTarget,
|
|
2031
|
+
dnsChecklist: toDnsChecklist(diag.checks),
|
|
2032
|
+
apexResolves,
|
|
2033
|
+
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
2034
|
+
maxAttempts: DNS_MAX_ATTEMPTS,
|
|
1852
2035
|
message: res2.message
|
|
1853
2036
|
},
|
|
1854
|
-
res2.status === "verified" ? "
|
|
2037
|
+
res2.status === "verified" ? "pending_provider" : "waiting_user"
|
|
1855
2038
|
);
|
|
1856
2039
|
}
|
|
1857
2040
|
if (!args.hostname) {
|
|
@@ -1862,25 +2045,42 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : ""),
|
|
|
1862
2045
|
hostname: args.hostname
|
|
1863
2046
|
};
|
|
1864
2047
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
2048
|
+
const apex = res.apexDomain;
|
|
2049
|
+
const txtShort = shortHostFor(res.verificationRecord.name, apex);
|
|
1865
2050
|
return text(
|
|
1866
2051
|
"domain_verification_started",
|
|
1867
|
-
`Domain binding started for ${
|
|
2052
|
+
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
1868
2053
|
|
|
1869
|
-
|
|
1870
|
-
name: ${res.verificationRecord.name}
|
|
1871
|
-
type: ${res.verificationRecord.type}
|
|
1872
|
-
value: ${res.verificationRecord.value}
|
|
1873
|
-
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the domain \u2014 the first verified request wins, and this challenge expires after 72 hours.
|
|
2054
|
+
Add ALL THREE DNS records NOW (adding them together lets verification, certificate issuance and serving complete without further record changes):
|
|
1874
2055
|
|
|
1875
|
-
|
|
2056
|
+
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2057
|
+
2) CNAME host: www value: ${res.servingTarget}
|
|
2058
|
+
3) APEX host: @ -> ${res.servingTarget} via your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).
|
|
1876
2059
|
|
|
1877
|
-
|
|
2060
|
+
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2061
|
+
|
|
2062
|
+
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2063
|
+
|
|
2064
|
+
Then run bind_domain with action "status" \u2014 it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
1878
2065
|
{
|
|
1879
2066
|
verificationId: res.verificationId,
|
|
1880
|
-
apexDomain:
|
|
2067
|
+
apexDomain: apex,
|
|
1881
2068
|
includedHostnames: res.includedHostnames,
|
|
1882
2069
|
verificationRecord: res.verificationRecord,
|
|
1883
|
-
|
|
2070
|
+
servingTarget: res.servingTarget,
|
|
2071
|
+
requiredRecords: [
|
|
2072
|
+
{
|
|
2073
|
+
type: "TXT",
|
|
2074
|
+
shortHost: txtShort,
|
|
2075
|
+
name: res.verificationRecord.name,
|
|
2076
|
+
value: res.verificationRecord.value
|
|
2077
|
+
},
|
|
2078
|
+
{ type: "CNAME", shortHost: "www", name: `www.${apex}`, value: res.servingTarget },
|
|
2079
|
+
{ type: "ALIAS", shortHost: "@", name: apex, value: res.servingTarget }
|
|
2080
|
+
],
|
|
2081
|
+
servingInstructions: res.servingInstructions,
|
|
2082
|
+
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
2083
|
+
maxAttempts: DNS_MAX_ATTEMPTS
|
|
1884
2084
|
},
|
|
1885
2085
|
"waiting_user"
|
|
1886
2086
|
);
|
|
@@ -2136,6 +2336,12 @@ ${res.archiveUrl}`,
|
|
|
2136
2336
|
deploymentId: z3.string().optional(),
|
|
2137
2337
|
severity: severityEnum.optional(),
|
|
2138
2338
|
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2339
|
+
agentContext: z3.string().optional().describe(
|
|
2340
|
+
"YOUR OWN factual account of the session as the AI: which tools you called, what they returned, expected vs actual. Write it yourself from your observations \u2014 never ask the user to compose it, and do not read it back to them; it travels alongside the user's description as a second witness. No secrets, no file contents."
|
|
2341
|
+
),
|
|
2342
|
+
contactEmail: z3.string().optional().describe(
|
|
2343
|
+
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
2344
|
+
),
|
|
2139
2345
|
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2140
2346
|
}
|
|
2141
2347
|
},
|
|
@@ -2160,7 +2366,9 @@ ${res.archiveUrl}`,
|
|
|
2160
2366
|
...site ? { siteId: site.siteId } : {},
|
|
2161
2367
|
...args.severity !== void 0 ? { severity: args.severity } : {},
|
|
2162
2368
|
diagnostics,
|
|
2163
|
-
...args.description !== void 0 ? { description: args.description } : {}
|
|
2369
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
2370
|
+
...args.agentContext !== void 0 ? { agentContext: args.agentContext } : {},
|
|
2371
|
+
...args.contactEmail !== void 0 ? { contactEmail: args.contactEmail } : {}
|
|
2164
2372
|
};
|
|
2165
2373
|
if (args.confirmSubmit !== true) {
|
|
2166
2374
|
return textJson(
|
|
@@ -2503,7 +2711,9 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
|
|
|
2503
2711
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2504
2712
|
the directory the user is currently working in, on every call. Without it the server falls
|
|
2505
2713
|
back to its startup directory, which may be a different project than the one the user is
|
|
2506
|
-
looking at.
|
|
2714
|
+
looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2715
|
+
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2716
|
+
refresh_site, delete_site and unbind_domain echo
|
|
2507
2717
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2508
2718
|
|
|
2509
2719
|
Safety boundaries:
|
package/dist/index.js
CHANGED
|
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.10";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -457,10 +457,10 @@ var CLIENT_TYPE = "sakupa-mcp";
|
|
|
457
457
|
// src/config.ts
|
|
458
458
|
var TEST_API_BASE_URL = "https://api-test.sakupa.com";
|
|
459
459
|
function previewHostPatternFor(apiBaseUrl) {
|
|
460
|
-
return apiBaseUrl ===
|
|
460
|
+
return environmentFor(apiBaseUrl) === "test" ? "{shortId}-test.sakupa.com" : "{shortId}.sakupa.com";
|
|
461
461
|
}
|
|
462
462
|
function loadMcpRuntimeConfig(env = process.env, cwd = process.cwd()) {
|
|
463
|
-
const apiBaseUrl = (env["SAKUPA_API_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
463
|
+
const apiBaseUrl = (env["SAKUPA_API_URL"] ?? env["SAKUPA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
|
|
464
464
|
const projectDir = env["SAKUPA_PROJECT_DIR"] ?? cwd;
|
|
465
465
|
const testAccessToken = env["SAKUPA_TEST_ACCESS_TOKEN"]?.trim() ?? "";
|
|
466
466
|
if (apiBaseUrl === TEST_API_BASE_URL) {
|
|
@@ -478,6 +478,9 @@ function loadMcpRuntimeConfig(env = process.env, cwd = process.cwd()) {
|
|
|
478
478
|
}
|
|
479
479
|
return { apiBaseUrl, projectDir };
|
|
480
480
|
}
|
|
481
|
+
function environmentFor(apiBaseUrl) {
|
|
482
|
+
return apiBaseUrl === TEST_API_BASE_URL ? "test" : "production";
|
|
483
|
+
}
|
|
481
484
|
|
|
482
485
|
// src/transport.ts
|
|
483
486
|
var FetchTransport = class {
|
|
@@ -659,11 +662,14 @@ var HttpApiClient = class {
|
|
|
659
662
|
async bindDomain(credential, req) {
|
|
660
663
|
return this.call("POST", "/v1/domains/bind", { credential, body: req });
|
|
661
664
|
}
|
|
662
|
-
async checkVerification(verificationId, credential) {
|
|
665
|
+
async checkVerification(verificationId, credential, latestForSiteId) {
|
|
663
666
|
return this.call(
|
|
664
667
|
"POST",
|
|
665
668
|
`/v1/domains/verifications/${encodeURIComponent(verificationId)}/check`,
|
|
666
|
-
{
|
|
669
|
+
{
|
|
670
|
+
credential,
|
|
671
|
+
...latestForSiteId !== void 0 ? { body: { siteId: latestForSiteId } } : {}
|
|
672
|
+
}
|
|
667
673
|
);
|
|
668
674
|
}
|
|
669
675
|
async unbindDomain(siteId, credential, req) {
|
|
@@ -1456,6 +1462,131 @@ function removeCreation(siteId) {
|
|
|
1456
1462
|
if (rest.length !== all.length) writeAll(rest);
|
|
1457
1463
|
}
|
|
1458
1464
|
|
|
1465
|
+
// src/dns-doh.ts
|
|
1466
|
+
var dohFetch = (input, init) => fetch(input, init);
|
|
1467
|
+
var TYPE_CODES = { TXT: 16, CNAME: 5, A: 1 };
|
|
1468
|
+
async function resolveDns(name, type) {
|
|
1469
|
+
const endpoints = [
|
|
1470
|
+
`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=${type}`,
|
|
1471
|
+
`https://dns.google/resolve?name=${encodeURIComponent(name)}&type=${type}`
|
|
1472
|
+
];
|
|
1473
|
+
for (const url of endpoints) {
|
|
1474
|
+
try {
|
|
1475
|
+
const res = await dohFetch(url, { headers: { accept: "application/dns-json" } });
|
|
1476
|
+
if (!res.ok) continue;
|
|
1477
|
+
const body = await res.json();
|
|
1478
|
+
return (body.Answer ?? []).filter((a) => a.type === TYPE_CODES[type]).map((a) => a.data.replace(/^"|"$/g, "").replace(/"\s+"/g, "")).map((v) => type === "CNAME" ? v.replace(/\.$/, "").toLowerCase() : v);
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return [];
|
|
1483
|
+
}
|
|
1484
|
+
function shortHostFor(fullName, apexDomain) {
|
|
1485
|
+
const suffix = `.${apexDomain}`;
|
|
1486
|
+
if (fullName === apexDomain) return "@";
|
|
1487
|
+
return fullName.endsWith(suffix) ? fullName.slice(0, -suffix.length) : fullName;
|
|
1488
|
+
}
|
|
1489
|
+
function matches(record, values) {
|
|
1490
|
+
if (record.type === "CNAME") {
|
|
1491
|
+
const want = record.value.replace(/\.$/, "").toLowerCase();
|
|
1492
|
+
return values.some((v) => v === want);
|
|
1493
|
+
}
|
|
1494
|
+
return values.includes(record.value);
|
|
1495
|
+
}
|
|
1496
|
+
async function checkRecord(record, apexDomain) {
|
|
1497
|
+
const shortHost = shortHostFor(record.name, apexDomain);
|
|
1498
|
+
const found = await resolveDns(record.name, record.type);
|
|
1499
|
+
if (matches(record, found)) {
|
|
1500
|
+
return { record, shortHost, state: "ok", found, fix: "" };
|
|
1501
|
+
}
|
|
1502
|
+
const doubled = await resolveDns(`${record.name}.${apexDomain}`, record.type);
|
|
1503
|
+
if (matches(record, doubled)) {
|
|
1504
|
+
return {
|
|
1505
|
+
record,
|
|
1506
|
+
shortHost,
|
|
1507
|
+
state: "double_domain",
|
|
1508
|
+
found: doubled,
|
|
1509
|
+
fix: `The record exists at ${record.name}.${apexDomain} \u2014 the host field was filled with the full name and your DNS panel appended ${apexDomain} again. Edit that record's host to exactly: ${shortHost}`
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
if (found.length > 0) {
|
|
1513
|
+
return {
|
|
1514
|
+
record,
|
|
1515
|
+
shortHost,
|
|
1516
|
+
state: "wrong_value",
|
|
1517
|
+
found,
|
|
1518
|
+
fix: `A ${record.type} record exists at ${record.name} but its value is ${JSON.stringify(found)} instead of "${record.value}". Update the value exactly.`
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
return {
|
|
1522
|
+
record,
|
|
1523
|
+
shortHost,
|
|
1524
|
+
state: "missing",
|
|
1525
|
+
found,
|
|
1526
|
+
fix: `Create it now \u2014 type: ${record.type}, host: ${shortHost} (most panels append .${apexDomain} automatically; if yours wants the full name use ${record.name}), value: ${record.value}`
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function renderCheck(c) {
|
|
1530
|
+
const label = `${c.record.type} ${c.shortHost}`;
|
|
1531
|
+
switch (c.state) {
|
|
1532
|
+
case "ok":
|
|
1533
|
+
return ` [OK] ${label} \u2014 live on public DNS.`;
|
|
1534
|
+
case "double_domain":
|
|
1535
|
+
return ` [FIX] ${label} \u2014 ${c.fix}`;
|
|
1536
|
+
case "wrong_value":
|
|
1537
|
+
return ` [FIX] ${label} \u2014 ${c.fix}`;
|
|
1538
|
+
case "missing":
|
|
1539
|
+
return ` [MISSING] ${label} \u2014 ${c.fix}`;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async function diagnoseBinding(input) {
|
|
1543
|
+
const apex = input.apexDomain;
|
|
1544
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1545
|
+
if (input.verificationRecord) {
|
|
1546
|
+
byKey.set(`TXT:${input.verificationRecord.name}`, input.verificationRecord);
|
|
1547
|
+
}
|
|
1548
|
+
const www = {
|
|
1549
|
+
name: `www.${apex}`,
|
|
1550
|
+
type: "CNAME",
|
|
1551
|
+
value: input.servingTarget
|
|
1552
|
+
};
|
|
1553
|
+
byKey.set(`CNAME:${www.name}`, www);
|
|
1554
|
+
for (const rec of input.pendingDnsRecords) {
|
|
1555
|
+
const key = `${rec.type}:${rec.name}`;
|
|
1556
|
+
if (!byKey.has(key)) byKey.set(key, rec);
|
|
1557
|
+
}
|
|
1558
|
+
const [checks, apexAnswers] = await Promise.all([
|
|
1559
|
+
Promise.all([...byKey.values()].map((rec) => checkRecord(rec, apex))),
|
|
1560
|
+
resolveDns(apex, "A")
|
|
1561
|
+
]);
|
|
1562
|
+
const apexResolves = apexAnswers.length > 0;
|
|
1563
|
+
const allOk = checks.every((c) => c.state === "ok");
|
|
1564
|
+
const checklist = checks.map(renderCheck).join("\n") + `
|
|
1565
|
+
[${apexResolves ? "OK" : "MISSING"}] APEX ${apex} \u2014 ` + (apexResolves ? "resolves." : `does not resolve yet: point it at ${input.servingTarget} using your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).`);
|
|
1566
|
+
const layers = `Pipeline: [1] public DNS (checked LIVE above) -> [2] Sakupa ownership verification: ${input.verificationStatus} -> [3] HTTPS certificate & serving: ` + (input.provisioning ? "provisioning (Cloudflare validates and issues within minutes once the records above are all OK; Sakupa retries automatically every ~5 minutes)." : "starts after verification.");
|
|
1567
|
+
return { checks, apexResolves, allOk, checklist, layers };
|
|
1568
|
+
}
|
|
1569
|
+
var DNS_RETRY_AFTER_SECONDS = 300;
|
|
1570
|
+
var DNS_MAX_ATTEMPTS = 10;
|
|
1571
|
+
function toDnsChecklist(checks) {
|
|
1572
|
+
return checks.map((c) => ({
|
|
1573
|
+
name: c.record.name,
|
|
1574
|
+
type: c.record.type,
|
|
1575
|
+
shortHost: c.shortHost,
|
|
1576
|
+
state: c.state,
|
|
1577
|
+
...c.fix ? { fix: c.fix } : {}
|
|
1578
|
+
}));
|
|
1579
|
+
}
|
|
1580
|
+
function renderChecklistBlock(diag, fixTail) {
|
|
1581
|
+
const cadence = `Re-check every ${DNS_RETRY_AFTER_SECONDS / 60} minutes, up to ${DNS_MAX_ATTEMPTS} times.`;
|
|
1582
|
+
return `Live DNS checklist (host values are the SHORT panel form):
|
|
1583
|
+
${diag.checklist}
|
|
1584
|
+
|
|
1585
|
+
${diag.layers}
|
|
1586
|
+
|
|
1587
|
+
` + (diag.allOk ? "All records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1459
1590
|
// src/tools/definitions.ts
|
|
1460
1591
|
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
1461
1592
|
return structuredToolResult({
|
|
@@ -1562,6 +1693,38 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1562
1693
|
}
|
|
1563
1694
|
return targets.length;
|
|
1564
1695
|
}
|
|
1696
|
+
async function describePendingBinding(client, credential, pb) {
|
|
1697
|
+
const framing = `
|
|
1698
|
+
Domain binding IN PROGRESS: ${pb.apexDomain} \u2014 ` + (pb.phase === "provisioning" ? "ownership verified; certificates/serving are provisioning." : `awaiting DNS verification (challenge valid until ${pb.verificationExpiresAt}).`);
|
|
1699
|
+
let check;
|
|
1700
|
+
try {
|
|
1701
|
+
check = await client.checkVerification(pb.verificationId, credential);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return {
|
|
1704
|
+
note: framing + "\n(Couldn't refresh binding progress from the server just now \u2014 try site_status again shortly.)"
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
try {
|
|
1708
|
+
const diag = await diagnoseBinding({
|
|
1709
|
+
apexDomain: check.apexDomain,
|
|
1710
|
+
servingTarget: check.servingTarget,
|
|
1711
|
+
...check.verificationRecord ? { verificationRecord: check.verificationRecord } : {},
|
|
1712
|
+
pendingDnsRecords: check.pendingDnsRecords,
|
|
1713
|
+
verificationStatus: check.status,
|
|
1714
|
+
provisioning: pb.phase === "provisioning"
|
|
1715
|
+
});
|
|
1716
|
+
const block = renderChecklistBlock(
|
|
1717
|
+
diag,
|
|
1718
|
+
"Relay every [MISSING]/[FIX] line to the user with its exact fix."
|
|
1719
|
+
);
|
|
1720
|
+
return { note: `${framing}
|
|
1721
|
+
${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
1722
|
+
} catch {
|
|
1723
|
+
return {
|
|
1724
|
+
note: framing + '\n(Live DNS lookups are unavailable right now \u2014 run bind_domain with action "status" for the per-record checklist.)'
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1565
1728
|
function freeSiteCreationBarrier() {
|
|
1566
1729
|
const recent = listRecentCreations(Date.now());
|
|
1567
1730
|
if (recent.length < FREE_ACTIVE_SITES_PER_IP) return null;
|
|
@@ -1692,6 +1855,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1692
1855
|
return text(
|
|
1693
1856
|
"site_published",
|
|
1694
1857
|
`Site published: ${finalized2.url}
|
|
1858
|
+
Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
1695
1859
|
Project directory: ${ctx.projectDir}
|
|
1696
1860
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1697
1861
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
@@ -1711,7 +1875,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1711
1875
|
totalBytes: finalized2.totalBytes,
|
|
1712
1876
|
warnings: finalized2.warnings,
|
|
1713
1877
|
credentialStoredLocally: true,
|
|
1714
|
-
projectDir: ctx.projectDir
|
|
1878
|
+
projectDir: ctx.projectDir,
|
|
1879
|
+
environment: environmentFor(ctx.apiBaseUrl)
|
|
1715
1880
|
}
|
|
1716
1881
|
);
|
|
1717
1882
|
}
|
|
@@ -1757,6 +1922,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
|
1757
1922
|
return text(
|
|
1758
1923
|
"site_updated",
|
|
1759
1924
|
`Site updated: ${finalized.url}
|
|
1925
|
+
Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
|
|
1760
1926
|
Project directory: ${ctx.projectDir}
|
|
1761
1927
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1762
1928
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
@@ -1770,6 +1936,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
1770
1936
|
url: finalized.url,
|
|
1771
1937
|
mode: finalized.mode,
|
|
1772
1938
|
projectDir: ctx.projectDir,
|
|
1939
|
+
environment: environmentFor(ctx.apiBaseUrl),
|
|
1773
1940
|
expiresAt: finalized.expiresAt,
|
|
1774
1941
|
filesUploaded: uploaded,
|
|
1775
1942
|
totalBytes: finalized.totalBytes,
|
|
@@ -1818,9 +1985,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
1818
1985
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1819
1986
|
const site = requireSiteFile(ctx);
|
|
1820
1987
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1821
|
-
|
|
1988
|
+
const binding = res.pendingDomainBinding ? await describePendingBinding(ctx.client, site.credential, res.pendingDomainBinding) : void 0;
|
|
1989
|
+
return textJson("site_status_returned", `Site status:${binding?.note ?? ""}`, {
|
|
1822
1990
|
...res,
|
|
1823
|
-
projectDir: ctx.projectDir
|
|
1991
|
+
projectDir: ctx.projectDir,
|
|
1992
|
+
...binding?.checklist ? { dnsChecklist: binding.checklist } : {}
|
|
1824
1993
|
});
|
|
1825
1994
|
} catch (e) {
|
|
1826
1995
|
return toolError(e);
|
|
@@ -1884,7 +2053,9 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1884
2053
|
projectDir: projectDirInput,
|
|
1885
2054
|
action: z3.enum(["start", "status"]),
|
|
1886
2055
|
hostname: z3.string().optional().describe("Required for start."),
|
|
1887
|
-
verificationId: z3.string().optional().describe(
|
|
2056
|
+
verificationId: z3.string().optional().describe(
|
|
2057
|
+
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
2058
|
+
)
|
|
1888
2059
|
}
|
|
1889
2060
|
},
|
|
1890
2061
|
async (args) => {
|
|
@@ -1892,30 +2063,42 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1892
2063
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
1893
2064
|
const site = requireSiteFile(ctx);
|
|
1894
2065
|
if (args.action === "status") {
|
|
1895
|
-
|
|
1896
|
-
throw new SakupaError("invalid_request", "verificationId is required for status");
|
|
1897
|
-
}
|
|
1898
|
-
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
2066
|
+
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
1899
2067
|
if (res2.status === "verified") {
|
|
1900
2068
|
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1901
2069
|
}
|
|
2070
|
+
const apex2 = res2.apexDomain;
|
|
2071
|
+
const diag = await diagnoseBinding({
|
|
2072
|
+
apexDomain: apex2,
|
|
2073
|
+
servingTarget: res2.servingTarget,
|
|
2074
|
+
...res2.verificationRecord ? { verificationRecord: res2.verificationRecord } : {},
|
|
2075
|
+
pendingDnsRecords: res2.pendingDnsRecords,
|
|
2076
|
+
verificationStatus: res2.status,
|
|
2077
|
+
provisioning: res2.provisioningJobId !== void 0
|
|
2078
|
+
});
|
|
2079
|
+
const { apexResolves, allOk } = diag;
|
|
1902
2080
|
return text(
|
|
1903
2081
|
res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
|
|
1904
|
-
`
|
|
2082
|
+
`Domain binding status for ${apex2}: ${res2.status}
|
|
1905
2083
|
${res2.message}
|
|
1906
|
-
|
|
1907
|
-
`
|
|
1908
|
-
|
|
1909
|
-
|
|
2084
|
+
|
|
2085
|
+
` + renderChecklistBlock(
|
|
2086
|
+
diag,
|
|
2087
|
+
"Fix any [MISSING]/[FIX] lines above, then re-run bind_domain status; if still failing after the attempts below, show the user this checklist."
|
|
2088
|
+
),
|
|
1910
2089
|
{
|
|
1911
2090
|
verificationId: res2.verificationId,
|
|
1912
2091
|
status: res2.status,
|
|
1913
|
-
apexDomain:
|
|
2092
|
+
apexDomain: apex2,
|
|
1914
2093
|
provisioningJobId: res2.provisioningJobId,
|
|
1915
|
-
|
|
2094
|
+
servingTarget: res2.servingTarget,
|
|
2095
|
+
dnsChecklist: toDnsChecklist(diag.checks),
|
|
2096
|
+
apexResolves,
|
|
2097
|
+
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
2098
|
+
maxAttempts: DNS_MAX_ATTEMPTS,
|
|
1916
2099
|
message: res2.message
|
|
1917
2100
|
},
|
|
1918
|
-
res2.status === "verified" ? "
|
|
2101
|
+
res2.status === "verified" ? "pending_provider" : "waiting_user"
|
|
1919
2102
|
);
|
|
1920
2103
|
}
|
|
1921
2104
|
if (!args.hostname) {
|
|
@@ -1926,25 +2109,42 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : ""),
|
|
|
1926
2109
|
hostname: args.hostname
|
|
1927
2110
|
};
|
|
1928
2111
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
2112
|
+
const apex = res.apexDomain;
|
|
2113
|
+
const txtShort = shortHostFor(res.verificationRecord.name, apex);
|
|
1929
2114
|
return text(
|
|
1930
2115
|
"domain_verification_started",
|
|
1931
|
-
`Domain binding started for ${
|
|
2116
|
+
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
1932
2117
|
|
|
1933
|
-
|
|
1934
|
-
name: ${res.verificationRecord.name}
|
|
1935
|
-
type: ${res.verificationRecord.type}
|
|
1936
|
-
value: ${res.verificationRecord.value}
|
|
1937
|
-
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the domain \u2014 the first verified request wins, and this challenge expires after 72 hours.
|
|
2118
|
+
Add ALL THREE DNS records NOW (adding them together lets verification, certificate issuance and serving complete without further record changes):
|
|
1938
2119
|
|
|
1939
|
-
|
|
2120
|
+
1) TXT host: ${txtShort} value: ${res.verificationRecord.value}
|
|
2121
|
+
2) CNAME host: www value: ${res.servingTarget}
|
|
2122
|
+
3) APEX host: @ -> ${res.servingTarget} via your DNS panel's ALIAS / ANAME / CNAME-flattening feature (an apex cannot use a plain CNAME).
|
|
1940
2123
|
|
|
1941
|
-
|
|
2124
|
+
Host fields above are the SHORT form: most DNS panels append the domain automatically. After saving, the record list must NOT show ${apex} twice in one name \u2014 that means the full name was pasted into an auto-appending field.
|
|
2125
|
+
|
|
2126
|
+
Ownership comes ONLY from DNS control; paying never grants it. The first verified request wins and this challenge expires after 72 hours.
|
|
2127
|
+
|
|
2128
|
+
Then run bind_domain with action "status" \u2014 it live-checks every record and names the exact fix for anything wrong. Re-check every 5 minutes (up to 10 times). Any later session can resume with action "status" alone; the verificationId is optional.`,
|
|
1942
2129
|
{
|
|
1943
2130
|
verificationId: res.verificationId,
|
|
1944
|
-
apexDomain:
|
|
2131
|
+
apexDomain: apex,
|
|
1945
2132
|
includedHostnames: res.includedHostnames,
|
|
1946
2133
|
verificationRecord: res.verificationRecord,
|
|
1947
|
-
|
|
2134
|
+
servingTarget: res.servingTarget,
|
|
2135
|
+
requiredRecords: [
|
|
2136
|
+
{
|
|
2137
|
+
type: "TXT",
|
|
2138
|
+
shortHost: txtShort,
|
|
2139
|
+
name: res.verificationRecord.name,
|
|
2140
|
+
value: res.verificationRecord.value
|
|
2141
|
+
},
|
|
2142
|
+
{ type: "CNAME", shortHost: "www", name: `www.${apex}`, value: res.servingTarget },
|
|
2143
|
+
{ type: "ALIAS", shortHost: "@", name: apex, value: res.servingTarget }
|
|
2144
|
+
],
|
|
2145
|
+
servingInstructions: res.servingInstructions,
|
|
2146
|
+
retryAfterSeconds: DNS_RETRY_AFTER_SECONDS,
|
|
2147
|
+
maxAttempts: DNS_MAX_ATTEMPTS
|
|
1948
2148
|
},
|
|
1949
2149
|
"waiting_user"
|
|
1950
2150
|
);
|
|
@@ -2200,6 +2400,12 @@ ${res.archiveUrl}`,
|
|
|
2200
2400
|
deploymentId: z3.string().optional(),
|
|
2201
2401
|
severity: severityEnum.optional(),
|
|
2202
2402
|
description: z3.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
2403
|
+
agentContext: z3.string().optional().describe(
|
|
2404
|
+
"YOUR OWN factual account of the session as the AI: which tools you called, what they returned, expected vs actual. Write it yourself from your observations \u2014 never ask the user to compose it, and do not read it back to them; it travels alongside the user's description as a second witness. No secrets, no file contents."
|
|
2405
|
+
),
|
|
2406
|
+
contactEmail: z3.string().optional().describe(
|
|
2407
|
+
"OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
|
|
2408
|
+
),
|
|
2203
2409
|
confirmSubmit: z3.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
2204
2410
|
}
|
|
2205
2411
|
},
|
|
@@ -2224,7 +2430,9 @@ ${res.archiveUrl}`,
|
|
|
2224
2430
|
...site ? { siteId: site.siteId } : {},
|
|
2225
2431
|
...args.severity !== void 0 ? { severity: args.severity } : {},
|
|
2226
2432
|
diagnostics,
|
|
2227
|
-
...args.description !== void 0 ? { description: args.description } : {}
|
|
2433
|
+
...args.description !== void 0 ? { description: args.description } : {},
|
|
2434
|
+
...args.agentContext !== void 0 ? { agentContext: args.agentContext } : {},
|
|
2435
|
+
...args.contactEmail !== void 0 ? { contactEmail: args.contactEmail } : {}
|
|
2228
2436
|
};
|
|
2229
2437
|
if (args.confirmSubmit !== true) {
|
|
2230
2438
|
return textJson(
|
|
@@ -2498,7 +2706,9 @@ Project directory contract: ONE directory = ONE site (its .sakupa/site.json hold
|
|
|
2498
2706
|
binding). Every project-scoped tool accepts projectDir \u2014 ALWAYS pass the absolute path of
|
|
2499
2707
|
the directory the user is currently working in, on every call. Without it the server falls
|
|
2500
2708
|
back to its startup directory, which may be a different project than the one the user is
|
|
2501
|
-
looking at.
|
|
2709
|
+
looking at. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2710
|
+
Explicit Environment line: TEST vs PRODUCTION). analyze_site, deploy_site, site_status,
|
|
2711
|
+
refresh_site, delete_site and unbind_domain echo
|
|
2502
2712
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2503
2713
|
|
|
2504
2714
|
Safety boundaries:
|