@sakupa/mcp 0.7.16 → 0.7.18
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 +32 -97
- package/dist/index.js +32 -97
- 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.18";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -603,20 +603,6 @@ var HttpApiClient = class {
|
|
|
603
603
|
}
|
|
604
604
|
);
|
|
605
605
|
}
|
|
606
|
-
async unbindDomain(siteId, credential, req) {
|
|
607
|
-
return this.call(
|
|
608
|
-
"POST",
|
|
609
|
-
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
|
|
610
|
-
{ credential, body: req }
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
async previewUnbindDomain(siteId, credential, req) {
|
|
614
|
-
return this.call(
|
|
615
|
-
"POST",
|
|
616
|
-
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
|
|
617
|
-
{ credential, body: req }
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
606
|
async recoverDomain(req) {
|
|
621
607
|
return this.call("POST", "/v1/domains/recover", { body: req });
|
|
622
608
|
}
|
|
@@ -1497,8 +1483,10 @@ function requireSiteFile(ctx) {
|
|
|
1497
1483
|
}
|
|
1498
1484
|
var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
|
|
1499
1485
|
function toolError(e) {
|
|
1500
|
-
const
|
|
1501
|
-
const
|
|
1486
|
+
const isSakupa = isSakupaError(e);
|
|
1487
|
+
const errorCode = isSakupa ? e.code : "internal";
|
|
1488
|
+
const retryable = errorCode === "rate_limited" || isSakupa && errorCode === "internal";
|
|
1489
|
+
const opaqueUnclassified = errorCode === "internal" && !retryable;
|
|
1502
1490
|
const safeDetailKeys = /* @__PURE__ */ new Set([
|
|
1503
1491
|
"retryAfterSeconds",
|
|
1504
1492
|
"reasonCode",
|
|
@@ -1515,7 +1503,7 @@ function toolError(e) {
|
|
|
1515
1503
|
) : void 0;
|
|
1516
1504
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1517
1505
|
const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
|
|
1518
|
-
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
|
|
1506
|
+
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying will not help. Tell the user briefly, then offer to file it: run the report tool to send Sakupa a diagnostic report of what was being attempted so we can investigate." : "The operation failed; no server-internal details are exposed.");
|
|
1519
1507
|
const result = structuredToolResult({
|
|
1520
1508
|
schemaVersion: 1,
|
|
1521
1509
|
outcome: "failed",
|
|
@@ -2095,9 +2083,12 @@ ${res2.message}
|
|
|
2095
2083
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
2096
2084
|
const apex = res.apexDomain;
|
|
2097
2085
|
const txtShort = shortHostFor(res.verificationRecord.name, apex);
|
|
2086
|
+
const switchNotice = res.supersedes ? `NOTE FOR THE USER: this site already serves ${res.supersedes}. ${res.supersedes} keeps working the whole time; once www.${apex} is confirmed live, it automatically REPLACES ${res.supersedes} (zero downtime). Tell the user this before they start.
|
|
2087
|
+
|
|
2088
|
+
` : "";
|
|
2098
2089
|
return text(
|
|
2099
2090
|
"domain_verification_started",
|
|
2100
|
-
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2091
|
+
switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2101
2092
|
|
|
2102
2093
|
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2103
2094
|
|
|
@@ -2118,6 +2109,7 @@ Drive the whole flow with bind "status": it live-checks every record and names t
|
|
|
2118
2109
|
includedHostnames: res.includedHostnames,
|
|
2119
2110
|
verificationRecord: res.verificationRecord,
|
|
2120
2111
|
servingTarget: res.servingTarget,
|
|
2112
|
+
...res.supersedes ? { supersedes: res.supersedes } : {},
|
|
2121
2113
|
requiredRecords: [
|
|
2122
2114
|
{
|
|
2123
2115
|
type: "TXT",
|
|
@@ -2517,6 +2509,7 @@ function registerBillingTools(server, baseCtx) {
|
|
|
2517
2509
|
}
|
|
2518
2510
|
|
|
2519
2511
|
// src/tools/lifecycle.ts
|
|
2512
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2520
2513
|
import { z as z5 } from "zod";
|
|
2521
2514
|
var deleteConfirmation = z5.object({
|
|
2522
2515
|
siteId: z5.string().min(1),
|
|
@@ -2533,15 +2526,6 @@ var deleteConfirmation = z5.object({
|
|
|
2533
2526
|
expectedBoundHostnames: z5.array(z5.string()),
|
|
2534
2527
|
acknowledge: z5.literal("delete_and_cancel_renewal")
|
|
2535
2528
|
});
|
|
2536
|
-
var unbindConfirmation = z5.object({
|
|
2537
|
-
siteId: z5.string().min(1),
|
|
2538
|
-
bindingId: z5.string().min(1),
|
|
2539
|
-
expectedBindingUpdatedAt: z5.string().datetime(),
|
|
2540
|
-
expectedBindingStatus: z5.enum(["provisioning", "active"]),
|
|
2541
|
-
apexDomain: z5.string().min(1),
|
|
2542
|
-
expectedBoundHostnames: z5.array(z5.string()),
|
|
2543
|
-
acknowledge: z5.literal("unbind_and_remove_custom_hostnames")
|
|
2544
|
-
});
|
|
2545
2529
|
function registerLifecycleTools(server, baseCtx) {
|
|
2546
2530
|
server.registerTool(
|
|
2547
2531
|
"delete",
|
|
@@ -2560,28 +2544,31 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2560
2544
|
try {
|
|
2561
2545
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2562
2546
|
const site = requireSiteFile(ctx);
|
|
2563
|
-
|
|
2564
|
-
throw new Error("operationId is required for delete");
|
|
2565
|
-
}
|
|
2547
|
+
const operationId = args.operationId ?? randomUUID2();
|
|
2566
2548
|
if (args.action === "preview") {
|
|
2567
2549
|
const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
|
|
2568
|
-
operationId
|
|
2550
|
+
operationId
|
|
2569
2551
|
});
|
|
2570
2552
|
return structuredToolResult({
|
|
2571
2553
|
schemaVersion: 1,
|
|
2572
2554
|
outcome: "waiting_user",
|
|
2573
2555
|
resultCode: "delete_confirmation_required",
|
|
2574
|
-
operationId
|
|
2575
|
-
summary:
|
|
2556
|
+
operationId,
|
|
2557
|
+
summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete again with action:"confirm", operationId:"${operationId}", and the confirmation object from data.preview.confirmation.`,
|
|
2576
2558
|
data: { preview },
|
|
2577
2559
|
nextActions: [
|
|
2578
2560
|
{ tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2579
2561
|
]
|
|
2580
2562
|
});
|
|
2581
2563
|
}
|
|
2582
|
-
if (!args.confirmation)
|
|
2564
|
+
if (!args.confirmation) {
|
|
2565
|
+
throw new LocalGuidanceError(
|
|
2566
|
+
"invalid_request",
|
|
2567
|
+
'To CONFIRM deletion, first call delete with action:"preview" to get the exact confirmation object bound to the current site state, then call again with action:"confirm", the same operationId, and that confirmation object.'
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2583
2570
|
const result = await ctx.client.deleteSite(site.siteId, site.credential, {
|
|
2584
|
-
operationId
|
|
2571
|
+
operationId,
|
|
2585
2572
|
confirmation: args.confirmation
|
|
2586
2573
|
});
|
|
2587
2574
|
deleteSiteFile(ctx.projectDir);
|
|
@@ -2590,7 +2577,7 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2590
2577
|
schemaVersion: 1,
|
|
2591
2578
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2592
2579
|
resultCode: "site_deleted",
|
|
2593
|
-
operationId
|
|
2580
|
+
operationId,
|
|
2594
2581
|
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2595
2582
|
data: { result, projectDir: ctx.projectDir },
|
|
2596
2583
|
nextActions: []
|
|
@@ -2600,61 +2587,6 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2600
2587
|
}
|
|
2601
2588
|
}
|
|
2602
2589
|
);
|
|
2603
|
-
server.registerTool(
|
|
2604
|
-
"unbind",
|
|
2605
|
-
{
|
|
2606
|
-
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2607
|
-
inputSchema: {
|
|
2608
|
-
projectDir: projectDirInput,
|
|
2609
|
-
action: z5.enum(["preview", "confirm"]),
|
|
2610
|
-
operationId: z5.string().min(1).optional(),
|
|
2611
|
-
confirmation: unbindConfirmation.optional()
|
|
2612
|
-
},
|
|
2613
|
-
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2614
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2615
|
-
},
|
|
2616
|
-
async (args) => {
|
|
2617
|
-
try {
|
|
2618
|
-
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2619
|
-
const site = requireSiteFile(ctx);
|
|
2620
|
-
if (!args.operationId) throw new Error("operationId is required for unbind");
|
|
2621
|
-
if (args.action === "preview") {
|
|
2622
|
-
const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
|
|
2623
|
-
operationId: args.operationId
|
|
2624
|
-
});
|
|
2625
|
-
return structuredToolResult({
|
|
2626
|
-
schemaVersion: 1,
|
|
2627
|
-
outcome: "waiting_user",
|
|
2628
|
-
resultCode: "unbind_confirmation_required",
|
|
2629
|
-
operationId: args.operationId,
|
|
2630
|
-
summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
|
|
2631
|
-
data: { preview },
|
|
2632
|
-
nextActions: [
|
|
2633
|
-
{ tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2634
|
-
]
|
|
2635
|
-
});
|
|
2636
|
-
}
|
|
2637
|
-
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2638
|
-
const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
|
|
2639
|
-
operationId: args.operationId,
|
|
2640
|
-
confirmation: args.confirmation
|
|
2641
|
-
});
|
|
2642
|
-
const { boundDomain: _removed, ...remaining } = site;
|
|
2643
|
-
writeSiteFile(ctx.projectDir, remaining);
|
|
2644
|
-
return structuredToolResult({
|
|
2645
|
-
schemaVersion: 1,
|
|
2646
|
-
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2647
|
-
resultCode: "domain_unbound",
|
|
2648
|
-
operationId: args.operationId,
|
|
2649
|
-
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2650
|
-
data: { result },
|
|
2651
|
-
nextActions: [{ tool: "status", allowed: true }]
|
|
2652
|
-
});
|
|
2653
|
-
} catch (error) {
|
|
2654
|
-
return toolError(error);
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
);
|
|
2658
2590
|
}
|
|
2659
2591
|
|
|
2660
2592
|
// src/transport.ts
|
|
@@ -2722,8 +2654,10 @@ var FetchTransport = class {
|
|
|
2722
2654
|
});
|
|
2723
2655
|
if (!res.ok) {
|
|
2724
2656
|
const text2 = await res.text().catch(() => "");
|
|
2725
|
-
|
|
2726
|
-
|
|
2657
|
+
const detail = `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`;
|
|
2658
|
+
throw new SakupaError(
|
|
2659
|
+
res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
|
|
2660
|
+
detail
|
|
2727
2661
|
);
|
|
2728
2662
|
}
|
|
2729
2663
|
}
|
|
@@ -2751,8 +2685,9 @@ Workflow:
|
|
|
2751
2685
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2752
2686
|
first verified request wins; unverified requests expire after 72 hours. billing,
|
|
2753
2687
|
upgrade, portal and recover manage the paid
|
|
2754
|
-
lifecycle;
|
|
2755
|
-
|
|
2688
|
+
lifecycle; delete tears the whole site down after explicit confirmation. Binding a
|
|
2689
|
+
NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
|
|
2690
|
+
until the new domain's www is confirmed live, then it is replaced automatically.
|
|
2756
2691
|
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2757
2692
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2758
2693
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
@@ -2766,7 +2701,7 @@ the user opened (for framework projects, where package.json lives \u2014 never t
|
|
|
2766
2701
|
build folder; output is auto-detected). The server NEVER guesses a directory and refuses
|
|
2767
2702
|
calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2768
2703
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
2769
|
-
refresh
|
|
2704
|
+
refresh and delete echo
|
|
2770
2705
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2771
2706
|
|
|
2772
2707
|
When the same operation fails twice in a row, or the user is clearly stuck or
|
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.18";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -546,8 +546,10 @@ var FetchTransport = class {
|
|
|
546
546
|
});
|
|
547
547
|
if (!res.ok) {
|
|
548
548
|
const text2 = await res.text().catch(() => "");
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
const detail = `Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`;
|
|
550
|
+
throw new SakupaError(
|
|
551
|
+
res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
|
|
552
|
+
detail
|
|
551
553
|
);
|
|
552
554
|
}
|
|
553
555
|
}
|
|
@@ -671,20 +673,6 @@ var HttpApiClient = class {
|
|
|
671
673
|
}
|
|
672
674
|
);
|
|
673
675
|
}
|
|
674
|
-
async unbindDomain(siteId, credential, req) {
|
|
675
|
-
return this.call(
|
|
676
|
-
"POST",
|
|
677
|
-
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
|
|
678
|
-
{ credential, body: req }
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
async previewUnbindDomain(siteId, credential, req) {
|
|
682
|
-
return this.call(
|
|
683
|
-
"POST",
|
|
684
|
-
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
|
|
685
|
-
{ credential, body: req }
|
|
686
|
-
);
|
|
687
|
-
}
|
|
688
676
|
async recoverDomain(req) {
|
|
689
677
|
return this.call("POST", "/v1/domains/recover", { body: req });
|
|
690
678
|
}
|
|
@@ -1378,8 +1366,10 @@ function requireSiteFile(ctx) {
|
|
|
1378
1366
|
}
|
|
1379
1367
|
var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
|
|
1380
1368
|
function toolError(e) {
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1369
|
+
const isSakupa = isSakupaError(e);
|
|
1370
|
+
const errorCode = isSakupa ? e.code : "internal";
|
|
1371
|
+
const retryable = errorCode === "rate_limited" || isSakupa && errorCode === "internal";
|
|
1372
|
+
const opaqueUnclassified = errorCode === "internal" && !retryable;
|
|
1383
1373
|
const safeDetailKeys = /* @__PURE__ */ new Set([
|
|
1384
1374
|
"retryAfterSeconds",
|
|
1385
1375
|
"reasonCode",
|
|
@@ -1396,7 +1386,7 @@ function toolError(e) {
|
|
|
1396
1386
|
) : void 0;
|
|
1397
1387
|
const minimumVersion = rawDetails && typeof rawDetails["minimumVersion"] === "string" ? rawDetails["minimumVersion"] : void 0;
|
|
1398
1388
|
const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
|
|
1399
|
-
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : "The operation failed; no server-internal details are exposed.");
|
|
1389
|
+
const safeSummary = e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying will not help. Tell the user briefly, then offer to file it: run the report tool to send Sakupa a diagnostic report of what was being attempted so we can investigate." : "The operation failed; no server-internal details are exposed.");
|
|
1400
1390
|
const result = structuredToolResult({
|
|
1401
1391
|
schemaVersion: 1,
|
|
1402
1392
|
outcome: "failed",
|
|
@@ -2159,9 +2149,12 @@ ${res2.message}
|
|
|
2159
2149
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
2160
2150
|
const apex = res.apexDomain;
|
|
2161
2151
|
const txtShort = shortHostFor(res.verificationRecord.name, apex);
|
|
2152
|
+
const switchNotice = res.supersedes ? `NOTE FOR THE USER: this site already serves ${res.supersedes}. ${res.supersedes} keeps working the whole time; once www.${apex} is confirmed live, it automatically REPLACES ${res.supersedes} (zero downtime). Tell the user this before they start.
|
|
2153
|
+
|
|
2154
|
+
` : "";
|
|
2162
2155
|
return text(
|
|
2163
2156
|
"domain_verification_started",
|
|
2164
|
-
`Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2157
|
+
switchNotice + `Domain binding started for ${apex} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
2165
2158
|
|
|
2166
2159
|
STEP 1 of 2 \u2014 add ALL THREE records NOW:
|
|
2167
2160
|
|
|
@@ -2182,6 +2175,7 @@ Drive the whole flow with bind "status": it live-checks every record and names t
|
|
|
2182
2175
|
includedHostnames: res.includedHostnames,
|
|
2183
2176
|
verificationRecord: res.verificationRecord,
|
|
2184
2177
|
servingTarget: res.servingTarget,
|
|
2178
|
+
...res.supersedes ? { supersedes: res.supersedes } : {},
|
|
2185
2179
|
requiredRecords: [
|
|
2186
2180
|
{
|
|
2187
2181
|
type: "TXT",
|
|
@@ -2509,6 +2503,7 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2509
2503
|
}
|
|
2510
2504
|
|
|
2511
2505
|
// src/tools/lifecycle.ts
|
|
2506
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2512
2507
|
import { z as z4 } from "zod";
|
|
2513
2508
|
var deleteConfirmation = z4.object({
|
|
2514
2509
|
siteId: z4.string().min(1),
|
|
@@ -2525,15 +2520,6 @@ var deleteConfirmation = z4.object({
|
|
|
2525
2520
|
expectedBoundHostnames: z4.array(z4.string()),
|
|
2526
2521
|
acknowledge: z4.literal("delete_and_cancel_renewal")
|
|
2527
2522
|
});
|
|
2528
|
-
var unbindConfirmation = z4.object({
|
|
2529
|
-
siteId: z4.string().min(1),
|
|
2530
|
-
bindingId: z4.string().min(1),
|
|
2531
|
-
expectedBindingUpdatedAt: z4.string().datetime(),
|
|
2532
|
-
expectedBindingStatus: z4.enum(["provisioning", "active"]),
|
|
2533
|
-
apexDomain: z4.string().min(1),
|
|
2534
|
-
expectedBoundHostnames: z4.array(z4.string()),
|
|
2535
|
-
acknowledge: z4.literal("unbind_and_remove_custom_hostnames")
|
|
2536
|
-
});
|
|
2537
2523
|
function registerLifecycleTools(server, baseCtx) {
|
|
2538
2524
|
server.registerTool(
|
|
2539
2525
|
"delete",
|
|
@@ -2552,28 +2538,31 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2552
2538
|
try {
|
|
2553
2539
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2554
2540
|
const site = requireSiteFile(ctx);
|
|
2555
|
-
|
|
2556
|
-
throw new Error("operationId is required for delete");
|
|
2557
|
-
}
|
|
2541
|
+
const operationId = args.operationId ?? randomUUID2();
|
|
2558
2542
|
if (args.action === "preview") {
|
|
2559
2543
|
const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
|
|
2560
|
-
operationId
|
|
2544
|
+
operationId
|
|
2561
2545
|
});
|
|
2562
2546
|
return structuredToolResult({
|
|
2563
2547
|
schemaVersion: 1,
|
|
2564
2548
|
outcome: "waiting_user",
|
|
2565
2549
|
resultCode: "delete_confirmation_required",
|
|
2566
|
-
operationId
|
|
2567
|
-
summary:
|
|
2550
|
+
operationId,
|
|
2551
|
+
summary: `Deletion consequences returned, bound to the current site and billing state; after confirmation the content and the permanent URL are unrecoverable. To proceed, call delete again with action:"confirm", operationId:"${operationId}", and the confirmation object from data.preview.confirmation.`,
|
|
2568
2552
|
data: { preview },
|
|
2569
2553
|
nextActions: [
|
|
2570
2554
|
{ tool: "delete", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2571
2555
|
]
|
|
2572
2556
|
});
|
|
2573
2557
|
}
|
|
2574
|
-
if (!args.confirmation)
|
|
2558
|
+
if (!args.confirmation) {
|
|
2559
|
+
throw new LocalGuidanceError(
|
|
2560
|
+
"invalid_request",
|
|
2561
|
+
'To CONFIRM deletion, first call delete with action:"preview" to get the exact confirmation object bound to the current site state, then call again with action:"confirm", the same operationId, and that confirmation object.'
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2575
2564
|
const result = await ctx.client.deleteSite(site.siteId, site.credential, {
|
|
2576
|
-
operationId
|
|
2565
|
+
operationId,
|
|
2577
2566
|
confirmation: args.confirmation
|
|
2578
2567
|
});
|
|
2579
2568
|
deleteSiteFile(ctx.projectDir);
|
|
@@ -2582,7 +2571,7 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2582
2571
|
schemaVersion: 1,
|
|
2583
2572
|
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2584
2573
|
resultCode: "site_deleted",
|
|
2585
|
-
operationId
|
|
2574
|
+
operationId,
|
|
2586
2575
|
summary: `Site deleted; the local management credential file was removed from ${ctx.projectDir}.`,
|
|
2587
2576
|
data: { result, projectDir: ctx.projectDir },
|
|
2588
2577
|
nextActions: []
|
|
@@ -2592,61 +2581,6 @@ function registerLifecycleTools(server, baseCtx) {
|
|
|
2592
2581
|
}
|
|
2593
2582
|
}
|
|
2594
2583
|
);
|
|
2595
|
-
server.registerTool(
|
|
2596
|
-
"unbind",
|
|
2597
|
-
{
|
|
2598
|
-
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2599
|
-
inputSchema: {
|
|
2600
|
-
projectDir: projectDirInput,
|
|
2601
|
-
action: z4.enum(["preview", "confirm"]),
|
|
2602
|
-
operationId: z4.string().min(1).optional(),
|
|
2603
|
-
confirmation: unbindConfirmation.optional()
|
|
2604
|
-
},
|
|
2605
|
-
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2606
|
-
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2607
|
-
},
|
|
2608
|
-
async (args) => {
|
|
2609
|
-
try {
|
|
2610
|
-
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2611
|
-
const site = requireSiteFile(ctx);
|
|
2612
|
-
if (!args.operationId) throw new Error("operationId is required for unbind");
|
|
2613
|
-
if (args.action === "preview") {
|
|
2614
|
-
const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
|
|
2615
|
-
operationId: args.operationId
|
|
2616
|
-
});
|
|
2617
|
-
return structuredToolResult({
|
|
2618
|
-
schemaVersion: 1,
|
|
2619
|
-
outcome: "waiting_user",
|
|
2620
|
-
resultCode: "unbind_confirmation_required",
|
|
2621
|
-
operationId: args.operationId,
|
|
2622
|
-
summary: "Exact binding snapshot returned; unbinding removes ONLY the custom domain \u2014 the subscription, content, and permanent URL stay unchanged.",
|
|
2623
|
-
data: { preview },
|
|
2624
|
-
nextActions: [
|
|
2625
|
-
{ tool: "unbind", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2626
|
-
]
|
|
2627
|
-
});
|
|
2628
|
-
}
|
|
2629
|
-
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2630
|
-
const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
|
|
2631
|
-
operationId: args.operationId,
|
|
2632
|
-
confirmation: args.confirmation
|
|
2633
|
-
});
|
|
2634
|
-
const { boundDomain: _removed, ...remaining } = site;
|
|
2635
|
-
writeSiteFile(ctx.projectDir, remaining);
|
|
2636
|
-
return structuredToolResult({
|
|
2637
|
-
schemaVersion: 1,
|
|
2638
|
-
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2639
|
-
resultCode: "domain_unbound",
|
|
2640
|
-
operationId: args.operationId,
|
|
2641
|
-
summary: `Custom domain unbound; the subscription, deployed content, and permanent Sakupa URL are unchanged. (project: ${ctx.projectDir})`,
|
|
2642
|
-
data: { result },
|
|
2643
|
-
nextActions: [{ tool: "status", allowed: true }]
|
|
2644
|
-
});
|
|
2645
|
-
} catch (error) {
|
|
2646
|
-
return toolError(error);
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
);
|
|
2650
2584
|
}
|
|
2651
2585
|
|
|
2652
2586
|
// src/server.ts
|
|
@@ -2746,8 +2680,9 @@ Workflow:
|
|
|
2746
2680
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2747
2681
|
first verified request wins; unverified requests expire after 72 hours. billing,
|
|
2748
2682
|
upgrade, portal and recover manage the paid
|
|
2749
|
-
lifecycle;
|
|
2750
|
-
|
|
2683
|
+
lifecycle; delete tears the whole site down after explicit confirmation. Binding a
|
|
2684
|
+
NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
|
|
2685
|
+
until the new domain's www is confirmed live, then it is replaced automatically.
|
|
2751
2686
|
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2752
2687
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2753
2688
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
@@ -2761,7 +2696,7 @@ the user opened (for framework projects, where package.json lives \u2014 never t
|
|
|
2761
2696
|
build folder; output is auto-detected). The server NEVER guesses a directory and refuses
|
|
2762
2697
|
calls without one: only you can see which directory the user is actually in. After every deploy, TELL the user which environment it went to (deploy results carry an
|
|
2763
2698
|
Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
|
|
2764
|
-
refresh
|
|
2699
|
+
refresh and delete echo
|
|
2765
2700
|
the directory they acted on \u2014 verify it matches the user's active project.
|
|
2766
2701
|
|
|
2767
2702
|
When the same operation fails twice in a row, or the user is clearly stuck or
|