@sakupa/mcp 0.7.41 → 0.7.42
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 +445 -35
- package/dist/index.js +450 -40
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -258,6 +258,7 @@ var SERVICE_DOMAIN = "sakupa.com";
|
|
|
258
258
|
var DEFAULT_API_BASE_URL = "https://api.sakupa.com";
|
|
259
259
|
var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
260
260
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
261
|
+
var CREDENTIAL_ROTATION_RECOMMEND_AFTER_SECONDS = 7 * 24 * 60 * 60;
|
|
261
262
|
var FREE_SITE_TTL_HOURS = 24;
|
|
262
263
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
263
264
|
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
@@ -379,7 +380,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
379
380
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
380
381
|
|
|
381
382
|
// ../core/dist/domain/version.js
|
|
382
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
383
|
+
var SAKUPA_MCP_VERSION = "0.7.42";
|
|
383
384
|
|
|
384
385
|
// ../core/dist/domain/errors.js
|
|
385
386
|
var HTTP_STATUS = {
|
|
@@ -835,6 +836,20 @@ var HttpApiClient = class {
|
|
|
835
836
|
credential
|
|
836
837
|
});
|
|
837
838
|
}
|
|
839
|
+
async getCredentialStatus(siteId, credential) {
|
|
840
|
+
return this.call(
|
|
841
|
+
"GET",
|
|
842
|
+
`/v1/sites/${encodeURIComponent(siteId)}/credential`,
|
|
843
|
+
{ credential }
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
async rotateCredential(siteId, credential, req) {
|
|
847
|
+
return this.call(
|
|
848
|
+
"POST",
|
|
849
|
+
`/v1/sites/${encodeURIComponent(siteId)}/credential/rotate`,
|
|
850
|
+
{ credential, body: req }
|
|
851
|
+
);
|
|
852
|
+
}
|
|
838
853
|
async getSiteArchive(siteId, credential) {
|
|
839
854
|
return this.call(
|
|
840
855
|
"GET",
|
|
@@ -938,9 +953,9 @@ var HttpApiClient = class {
|
|
|
938
953
|
};
|
|
939
954
|
|
|
940
955
|
// src/tools/definitions.ts
|
|
941
|
-
import { randomUUID as
|
|
956
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
942
957
|
import { promises as fs2 } from "node:fs";
|
|
943
|
-
import { join as
|
|
958
|
+
import { join as join8, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
944
959
|
import { z as z2 } from "zod";
|
|
945
960
|
|
|
946
961
|
// src/analyze/analyzer.ts
|
|
@@ -1378,10 +1393,12 @@ import {
|
|
|
1378
1393
|
existsSync as existsSync2,
|
|
1379
1394
|
mkdirSync as mkdirSync2,
|
|
1380
1395
|
readFileSync as readFileSync2,
|
|
1396
|
+
renameSync as renameSync2,
|
|
1381
1397
|
rmdirSync as rmdirSync2,
|
|
1382
1398
|
rmSync,
|
|
1383
1399
|
writeFileSync as writeFileSync2
|
|
1384
1400
|
} from "node:fs";
|
|
1401
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1385
1402
|
import { dirname, join as join3 } from "node:path";
|
|
1386
1403
|
var SITE_DIR = ".sakupa";
|
|
1387
1404
|
var SITE_FILE = "site.json";
|
|
@@ -1492,12 +1509,22 @@ function writeSiteFile(projectDir, file, opts = {}) {
|
|
|
1492
1509
|
const dir = join3(projectDir, SITE_DIR);
|
|
1493
1510
|
mkdirSync2(dir, { recursive: true });
|
|
1494
1511
|
const path = join3(dir, SITE_FILE);
|
|
1495
|
-
|
|
1496
|
-
|
|
1512
|
+
const temporary = join3(dir, `.site-${randomUUID2()}.tmp`);
|
|
1513
|
+
writeFileSync2(temporary, `${JSON.stringify(file, null, 2)}
|
|
1514
|
+
`, {
|
|
1515
|
+
encoding: "utf8",
|
|
1516
|
+
mode: 384
|
|
1517
|
+
});
|
|
1497
1518
|
try {
|
|
1498
|
-
chmodSync2(
|
|
1519
|
+
chmodSync2(temporary, 384);
|
|
1499
1520
|
} catch {
|
|
1500
1521
|
}
|
|
1522
|
+
try {
|
|
1523
|
+
renameSync2(temporary, path);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
rmSync(temporary, { force: true });
|
|
1526
|
+
throw error;
|
|
1527
|
+
}
|
|
1501
1528
|
}
|
|
1502
1529
|
function deleteSiteFile(projectDir) {
|
|
1503
1530
|
const path = siteFilePath(projectDir);
|
|
@@ -1959,15 +1986,15 @@ function strFromU8(dat, latin1) {
|
|
|
1959
1986
|
var slzh = function(d, b) {
|
|
1960
1987
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1961
1988
|
};
|
|
1962
|
-
var zh = function(d, b,
|
|
1989
|
+
var zh = function(d, b, z6) {
|
|
1963
1990
|
var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
|
|
1964
|
-
var _a2 = z64hs(d, es, efl,
|
|
1991
|
+
var _a2 = z64hs(d, es, efl, z6, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
|
|
1965
1992
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
1966
1993
|
};
|
|
1967
|
-
var z64hs = function(d, b, l,
|
|
1994
|
+
var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
1968
1995
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
1969
1996
|
var nf = nsc + nsu + noff;
|
|
1970
|
-
if (
|
|
1997
|
+
if (z6 && nf) {
|
|
1971
1998
|
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
1972
1999
|
if (b2(d, b) == 1) {
|
|
1973
2000
|
return [
|
|
@@ -1978,7 +2005,7 @@ var z64hs = function(d, b, l, z5, sc, su, off) {
|
|
|
1978
2005
|
];
|
|
1979
2006
|
}
|
|
1980
2007
|
}
|
|
1981
|
-
if (
|
|
2008
|
+
if (z6 < 2)
|
|
1982
2009
|
err(13);
|
|
1983
2010
|
}
|
|
1984
2011
|
return [sc, su, off, 0];
|
|
@@ -1995,18 +2022,18 @@ function unzipSync(data, opts) {
|
|
|
1995
2022
|
if (!c)
|
|
1996
2023
|
return {};
|
|
1997
2024
|
var o = b4(data, e + 16);
|
|
1998
|
-
var
|
|
1999
|
-
if (
|
|
2025
|
+
var z6 = b4(data, e - 20) == 117853008;
|
|
2026
|
+
if (z6) {
|
|
2000
2027
|
var ze = b4(data, e - 12);
|
|
2001
|
-
|
|
2002
|
-
if (
|
|
2028
|
+
z6 = b4(data, ze) == 101075792;
|
|
2029
|
+
if (z6) {
|
|
2003
2030
|
c = b4(data, ze + 32);
|
|
2004
2031
|
o = b4(data, ze + 48);
|
|
2005
2032
|
}
|
|
2006
2033
|
}
|
|
2007
2034
|
var fltr = opts && opts.filter;
|
|
2008
2035
|
for (var i = 0; i < c; ++i) {
|
|
2009
|
-
var _a2 = zh(data, o,
|
|
2036
|
+
var _a2 = zh(data, o, z6), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
|
|
2010
2037
|
o = no;
|
|
2011
2038
|
if (!fltr || fltr({
|
|
2012
2039
|
name: fn,
|
|
@@ -2518,6 +2545,172 @@ ${diag.layers}
|
|
|
2518
2545
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
2519
2546
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
2520
2547
|
|
|
2548
|
+
// src/credential-rotation.ts
|
|
2549
|
+
import {
|
|
2550
|
+
chmodSync as chmodSync3,
|
|
2551
|
+
existsSync as existsSync6,
|
|
2552
|
+
mkdirSync as mkdirSync5,
|
|
2553
|
+
readFileSync as readFileSync4,
|
|
2554
|
+
renameSync as renameSync3,
|
|
2555
|
+
rmSync as rmSync2,
|
|
2556
|
+
writeFileSync as writeFileSync5
|
|
2557
|
+
} from "node:fs";
|
|
2558
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
2559
|
+
import { join as join7 } from "node:path";
|
|
2560
|
+
var ROTATION_FILE = "rotation.json";
|
|
2561
|
+
function credentialRotationPath(projectDir) {
|
|
2562
|
+
return join7(projectDir, ".sakupa", ROTATION_FILE);
|
|
2563
|
+
}
|
|
2564
|
+
function loadCredentialRotation(projectDir) {
|
|
2565
|
+
const path = credentialRotationPath(projectDir);
|
|
2566
|
+
if (!existsSync6(path)) return { kind: "absent" };
|
|
2567
|
+
let parsed;
|
|
2568
|
+
try {
|
|
2569
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
2570
|
+
} catch (error) {
|
|
2571
|
+
return {
|
|
2572
|
+
kind: "corrupted",
|
|
2573
|
+
problem: `the file cannot be read as JSON (${error instanceof Error ? error.message : String(error)})`
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
|
|
2577
|
+
return { kind: "corrupted", problem: "siteId is missing or empty" };
|
|
2578
|
+
}
|
|
2579
|
+
if (typeof parsed.candidateCredential !== "string" || !CREDENTIAL_PATTERN.test(parsed.candidateCredential)) {
|
|
2580
|
+
return { kind: "corrupted", problem: "candidateCredential has an invalid shape" };
|
|
2581
|
+
}
|
|
2582
|
+
if (typeof parsed.createdAt !== "string" || !Number.isFinite(Date.parse(parsed.createdAt))) {
|
|
2583
|
+
return { kind: "corrupted", problem: "createdAt is missing or invalid" };
|
|
2584
|
+
}
|
|
2585
|
+
if (typeof parsed.apiBaseUrl !== "string" || parsed.apiBaseUrl.length === 0) {
|
|
2586
|
+
return { kind: "corrupted", problem: "apiBaseUrl is missing or empty" };
|
|
2587
|
+
}
|
|
2588
|
+
return {
|
|
2589
|
+
kind: "ok",
|
|
2590
|
+
file: {
|
|
2591
|
+
siteId: parsed.siteId,
|
|
2592
|
+
candidateCredential: parsed.candidateCredential,
|
|
2593
|
+
createdAt: parsed.createdAt,
|
|
2594
|
+
apiBaseUrl: parsed.apiBaseUrl
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
function writeCredentialRotation(projectDir, file) {
|
|
2599
|
+
if (!CREDENTIAL_PATTERN.test(file.candidateCredential)) {
|
|
2600
|
+
throw new Error("Refusing to persist an invalid credential rotation candidate.");
|
|
2601
|
+
}
|
|
2602
|
+
const current = loadCredentialRotation(projectDir);
|
|
2603
|
+
if (current.kind === "corrupted") {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
`Refusing to overwrite damaged credential rotation state: ${current.problem}. Run help.`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
if (current.kind === "ok") {
|
|
2609
|
+
if (current.file.siteId === file.siteId && current.file.candidateCredential === file.candidateCredential && current.file.apiBaseUrl === file.apiBaseUrl) {
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
throw new Error(
|
|
2613
|
+
"A different credential rotation is already pending. Run rotate to resume it; no state was overwritten."
|
|
2614
|
+
);
|
|
2615
|
+
}
|
|
2616
|
+
const directory = join7(projectDir, ".sakupa");
|
|
2617
|
+
mkdirSync5(directory, { recursive: true, mode: 448 });
|
|
2618
|
+
const target = credentialRotationPath(projectDir);
|
|
2619
|
+
const temporary = join7(directory, `.rotation-${randomUUID3()}.tmp`);
|
|
2620
|
+
writeFileSync5(temporary, `${JSON.stringify(file, null, 2)}
|
|
2621
|
+
`, {
|
|
2622
|
+
encoding: "utf8",
|
|
2623
|
+
mode: 384
|
|
2624
|
+
});
|
|
2625
|
+
try {
|
|
2626
|
+
chmodSync3(temporary, 384);
|
|
2627
|
+
} catch {
|
|
2628
|
+
}
|
|
2629
|
+
try {
|
|
2630
|
+
renameSync3(temporary, target);
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
rmSync2(temporary, { force: true });
|
|
2633
|
+
throw error;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function deleteCredentialRotation(projectDir) {
|
|
2637
|
+
rmSync2(credentialRotationPath(projectDir), { force: true });
|
|
2638
|
+
}
|
|
2639
|
+
function promoteRotatedCredential(projectDir, site, rotation, credentialCreatedAt) {
|
|
2640
|
+
if (rotation.siteId !== site.siteId || rotation.apiBaseUrl !== site.apiBaseUrl) {
|
|
2641
|
+
throw new Error(
|
|
2642
|
+
"Credential rotation state belongs to a different site or Sakupa environment; no file was changed."
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
if (!Number.isFinite(Date.parse(credentialCreatedAt))) {
|
|
2646
|
+
throw new Error(
|
|
2647
|
+
"The server returned an invalid credential creation time; no file was changed."
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const updated = {
|
|
2651
|
+
...site,
|
|
2652
|
+
credential: rotation.candidateCredential,
|
|
2653
|
+
createdAt: credentialCreatedAt
|
|
2654
|
+
};
|
|
2655
|
+
writeSiteFile(projectDir, updated);
|
|
2656
|
+
deleteCredentialRotation(projectDir);
|
|
2657
|
+
return updated;
|
|
2658
|
+
}
|
|
2659
|
+
async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
|
|
2660
|
+
const state = loadCredentialRotation(projectDir);
|
|
2661
|
+
if (state.kind === "absent") return null;
|
|
2662
|
+
if (state.kind === "corrupted") {
|
|
2663
|
+
throw new Error(
|
|
2664
|
+
`Credential rotation state is damaged (${state.problem}). Nothing was overwritten; run help.`
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
const pending = state.file;
|
|
2668
|
+
const siteEnvironment = site.apiBaseUrl || apiBaseUrl;
|
|
2669
|
+
if (pending.siteId !== site.siteId || pending.apiBaseUrl !== apiBaseUrl || siteEnvironment !== apiBaseUrl) {
|
|
2670
|
+
throw new Error(
|
|
2671
|
+
"Credential rotation state belongs to a different site or Sakupa environment. Nothing was changed; run help."
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
try {
|
|
2675
|
+
const status = await client.getCredentialStatus(site.siteId, pending.candidateCredential);
|
|
2676
|
+
return {
|
|
2677
|
+
site: promoteRotatedCredential(
|
|
2678
|
+
projectDir,
|
|
2679
|
+
{ ...site, apiBaseUrl },
|
|
2680
|
+
pending,
|
|
2681
|
+
status.credentialCreatedAt
|
|
2682
|
+
),
|
|
2683
|
+
status,
|
|
2684
|
+
rotation: null,
|
|
2685
|
+
resumed: true
|
|
2686
|
+
};
|
|
2687
|
+
} catch (error) {
|
|
2688
|
+
if (!isSakupaError(error) || error.code !== "unauthorized") throw error;
|
|
2689
|
+
}
|
|
2690
|
+
try {
|
|
2691
|
+
await client.getCredentialStatus(site.siteId, site.credential);
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
if (!isSakupaError(error) || error.code !== "unauthorized") throw error;
|
|
2694
|
+
throw new Error(
|
|
2695
|
+
"Neither the current nor pending credential is accepted. No local file was overwritten; run help before retrying."
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
const rotation = await client.rotateCredential(site.siteId, site.credential, {
|
|
2699
|
+
newCredential: pending.candidateCredential
|
|
2700
|
+
});
|
|
2701
|
+
return {
|
|
2702
|
+
site: promoteRotatedCredential(
|
|
2703
|
+
projectDir,
|
|
2704
|
+
{ ...site, apiBaseUrl },
|
|
2705
|
+
pending,
|
|
2706
|
+
rotation.credentialCreatedAt
|
|
2707
|
+
),
|
|
2708
|
+
status: rotation,
|
|
2709
|
+
rotation,
|
|
2710
|
+
resumed: true
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2521
2714
|
// src/project-binding.ts
|
|
2522
2715
|
import { fileURLToPath } from "node:url";
|
|
2523
2716
|
import { resolve as resolve4 } from "node:path";
|
|
@@ -2820,7 +3013,7 @@ function structuredToolResult(envelope) {
|
|
|
2820
3013
|
}
|
|
2821
3014
|
|
|
2822
3015
|
// src/tools/context.ts
|
|
2823
|
-
import { randomUUID as
|
|
3016
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
2824
3017
|
var LocalGuidanceError = class extends SakupaError {
|
|
2825
3018
|
constructor(code, message) {
|
|
2826
3019
|
super(code, message);
|
|
@@ -2899,7 +3092,7 @@ function reportAuthorizationStore(ctx) {
|
|
|
2899
3092
|
return store;
|
|
2900
3093
|
}
|
|
2901
3094
|
function issueReportAuthorization(ctx, failedTool) {
|
|
2902
|
-
const token =
|
|
3095
|
+
const token = randomUUID4();
|
|
2903
3096
|
reportAuthorizationStore(ctx).set(token, {
|
|
2904
3097
|
failedTool,
|
|
2905
3098
|
expiresAt: Date.now() + 10 * 60 * 1e3
|
|
@@ -2978,14 +3171,14 @@ function toolError(e) {
|
|
|
2978
3171
|
}
|
|
2979
3172
|
|
|
2980
3173
|
// src/tools/definitions.ts
|
|
2981
|
-
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
3174
|
+
function text(resultCode, t, data = {}, outcome = "completed", nextActions = []) {
|
|
2982
3175
|
return structuredToolResult({
|
|
2983
3176
|
schemaVersion: 1,
|
|
2984
3177
|
outcome,
|
|
2985
3178
|
resultCode,
|
|
2986
3179
|
summary: t,
|
|
2987
3180
|
data,
|
|
2988
|
-
nextActions
|
|
3181
|
+
nextActions
|
|
2989
3182
|
});
|
|
2990
3183
|
}
|
|
2991
3184
|
function textJson(resultCode, header, obj, outcome = "completed") {
|
|
@@ -3053,7 +3246,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
3053
3246
|
async function buildHashedManifest(files, outputAbs) {
|
|
3054
3247
|
const manifest = [];
|
|
3055
3248
|
for (const file of files) {
|
|
3056
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
3249
|
+
const bytes = new Uint8Array(await fs2.readFile(join8(outputAbs, file.path)));
|
|
3057
3250
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
3058
3251
|
}
|
|
3059
3252
|
return manifest;
|
|
@@ -3072,7 +3265,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
3072
3265
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
3073
3266
|
);
|
|
3074
3267
|
}
|
|
3075
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
3268
|
+
const bytes = new Uint8Array(await fs2.readFile(join8(outputAbs, match.path)));
|
|
3076
3269
|
if (bytes.byteLength !== match.size) {
|
|
3077
3270
|
throw new SakupaError(
|
|
3078
3271
|
"validation_failed",
|
|
@@ -3165,14 +3358,14 @@ function outputDirectoryChain(projectRoot, outputAbs) {
|
|
|
3165
3358
|
const chain = [];
|
|
3166
3359
|
let cursor = projectRoot;
|
|
3167
3360
|
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
3168
|
-
cursor =
|
|
3361
|
+
cursor = join8(cursor, part);
|
|
3169
3362
|
chain.push(cursor);
|
|
3170
3363
|
}
|
|
3171
3364
|
return chain;
|
|
3172
3365
|
}
|
|
3173
3366
|
async function sakupaDirectoryEntries(projectDir) {
|
|
3174
3367
|
try {
|
|
3175
|
-
return await fs2.readdir(
|
|
3368
|
+
return await fs2.readdir(join8(projectDir, ".sakupa"));
|
|
3176
3369
|
} catch (error) {
|
|
3177
3370
|
const code = error.code;
|
|
3178
3371
|
if (code === "ENOENT") return [];
|
|
@@ -3283,6 +3476,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3283
3476
|
}
|
|
3284
3477
|
let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
3285
3478
|
let handoff = null;
|
|
3479
|
+
let credentialSecurity = null;
|
|
3480
|
+
let credentialRotationResumed = false;
|
|
3286
3481
|
const resumedHandoffCleanup = existing ? resumeLocalSiteHandoff(ctx.projectDir, existing, Date.now()) : null;
|
|
3287
3482
|
const credentialRelocatedFrom = [];
|
|
3288
3483
|
const markerRelocatedFrom = [];
|
|
@@ -3320,8 +3515,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3320
3515
|
summary: `A nested Sakupa project marker exists at ${candidateDir}/.sakupa, but the active MCP Root is ${ctx.projectDir}. Nothing was moved or deployed. Show both paths to the user; after confirmation retry deploy with sakupaRelocationConfirmed:true. Sakupa will preserve credentials and refuse conflicts.`,
|
|
3321
3516
|
data: {
|
|
3322
3517
|
projectRoot: ctx.projectDir,
|
|
3323
|
-
misplacedSakupaDirectory:
|
|
3324
|
-
targetSakupaDirectory:
|
|
3518
|
+
misplacedSakupaDirectory: join8(candidateDir, ".sakupa"),
|
|
3519
|
+
targetSakupaDirectory: join8(ctx.projectDir, ".sakupa"),
|
|
3325
3520
|
confirmationField: "sakupaRelocationConfirmed"
|
|
3326
3521
|
},
|
|
3327
3522
|
nextActions: [
|
|
@@ -3493,6 +3688,17 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3493
3688
|
ctx.apiBaseUrl
|
|
3494
3689
|
);
|
|
3495
3690
|
releaseHandoffLock = acquireSiteHandoffLock(handoff.site.siteId);
|
|
3691
|
+
const resumedSourceRotation = await resumeCredentialRotation(
|
|
3692
|
+
ctx.client,
|
|
3693
|
+
handoff.sourceProjectDir,
|
|
3694
|
+
handoff.site,
|
|
3695
|
+
ctx.apiBaseUrl
|
|
3696
|
+
);
|
|
3697
|
+
if (resumedSourceRotation) {
|
|
3698
|
+
handoff = { ...handoff, site: resumedSourceRotation.site };
|
|
3699
|
+
credentialSecurity = resumedSourceRotation.status;
|
|
3700
|
+
credentialRotationResumed = true;
|
|
3701
|
+
}
|
|
3496
3702
|
const cloud = await ctx.client.getSiteStatus(
|
|
3497
3703
|
handoff.site.siteId,
|
|
3498
3704
|
handoff.site.credential
|
|
@@ -3529,6 +3735,39 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3529
3735
|
);
|
|
3530
3736
|
}
|
|
3531
3737
|
}
|
|
3738
|
+
if (existing) {
|
|
3739
|
+
try {
|
|
3740
|
+
if (!handoff) {
|
|
3741
|
+
const resumed = await resumeCredentialRotation(
|
|
3742
|
+
ctx.client,
|
|
3743
|
+
ctx.projectDir,
|
|
3744
|
+
existing,
|
|
3745
|
+
ctx.apiBaseUrl
|
|
3746
|
+
);
|
|
3747
|
+
if (resumed) {
|
|
3748
|
+
existing = resumed.site;
|
|
3749
|
+
credentialSecurity = resumed.status;
|
|
3750
|
+
credentialRotationResumed = true;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
credentialSecurity ??= await ctx.client.getCredentialStatus(
|
|
3754
|
+
existing.siteId,
|
|
3755
|
+
existing.credential
|
|
3756
|
+
);
|
|
3757
|
+
} catch (error) {
|
|
3758
|
+
if (isSakupaError(error) && error.code === "unauthorized") {
|
|
3759
|
+
return text(
|
|
3760
|
+
"credential_mismatch",
|
|
3761
|
+
`The server rejected the credential in .sakupa/site.json for site ${existing.siteId}.
|
|
3762
|
+
|
|
3763
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
3764
|
+
{ siteId: existing.siteId },
|
|
3765
|
+
"blocked"
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
throw error;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3532
3771
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
3533
3772
|
if (!existing) {
|
|
3534
3773
|
const created = await ctx.client.createSite({
|
|
@@ -3658,11 +3897,13 @@ Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
|
3658
3897
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
3659
3898
|
` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
|
|
3660
3899
|
` : "") + (handoff ? `Reusable free-site slot transferred to the current project. The cloud site was NOT deleted; its content was replaced. Previous project: ${handoff.sourceProjectDir}. ` + (handoffCleanup?.sourceCredentialRemoved ? "Its matching .sakupa/site.json credential was removed. Do not use that previous project to manage this URL.\n" : handoffCleanup?.sourceRemovalState === "absent" ? "Its .sakupa/site.json credential was already absent. Do not use that previous project to manage this URL.\n" : `Its credential could not be safely removed because the file was ${handoffCleanup?.sourceRemovalState}. Do not use the previous project to manage this URL; run help before touching its .sakupa directory.
|
|
3661
|
-
`) : "") + (finalized.mode === "free" ? `
|
|
3900
|
+
`) : "") + (credentialRotationResumed ? "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked.\n" : "") + (finalized.mode === "free" ? `
|
|
3662
3901
|
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
|
|
3663
3902
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
3664
3903
|
Warnings:
|
|
3665
|
-
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
3904
|
+
${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
|
|
3905
|
+
|
|
3906
|
+
Optional security recommendation: this management credential was created at ${credentialSecurity.credentialCreatedAt} and is older than 7 days. The deploy SUCCEEDED and rotation is not required. Ask the user whether they want to rotate; call rotate without confirmed:true to show the exact revocation preview. Never rotate automatically.` : ""),
|
|
3666
3907
|
{
|
|
3667
3908
|
siteId: existing.siteId,
|
|
3668
3909
|
url: finalized.url,
|
|
@@ -3673,6 +3914,13 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
3673
3914
|
filesUploaded: uploaded,
|
|
3674
3915
|
totalBytes: finalized.totalBytes,
|
|
3675
3916
|
warnings: finalized.warnings,
|
|
3917
|
+
credentialSecurity: credentialSecurity ? {
|
|
3918
|
+
credentialCreatedAt: credentialSecurity.credentialCreatedAt,
|
|
3919
|
+
ageSeconds: credentialSecurity.ageSeconds,
|
|
3920
|
+
rotationRecommended: credentialSecurity.rotationRecommended,
|
|
3921
|
+
optional: true,
|
|
3922
|
+
resumedAfterInterruption: credentialRotationResumed
|
|
3923
|
+
} : null,
|
|
3676
3924
|
...credentialRelocatedFrom.length > 0 ? { credentialRelocatedFrom } : {},
|
|
3677
3925
|
...handoff ? {
|
|
3678
3926
|
handoff: {
|
|
@@ -3685,7 +3933,15 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
3685
3933
|
sourceRemovalState: handoffCleanup?.sourceRemovalState
|
|
3686
3934
|
}
|
|
3687
3935
|
} : {}
|
|
3688
|
-
}
|
|
3936
|
+
},
|
|
3937
|
+
"completed",
|
|
3938
|
+
credentialSecurity?.rotationRecommended ? [
|
|
3939
|
+
{
|
|
3940
|
+
tool: "rotate",
|
|
3941
|
+
allowed: true,
|
|
3942
|
+
reasonCode: "credential_older_than_seven_days_optional_rotation"
|
|
3943
|
+
}
|
|
3944
|
+
] : []
|
|
3689
3945
|
);
|
|
3690
3946
|
} catch (e) {
|
|
3691
3947
|
return toolError(e);
|
|
@@ -3778,7 +4034,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3778
4034
|
{
|
|
3779
4035
|
siteId: site.siteId,
|
|
3780
4036
|
plan: args.plan,
|
|
3781
|
-
idempotencyKey:
|
|
4037
|
+
idempotencyKey: randomUUID5()
|
|
3782
4038
|
},
|
|
3783
4039
|
site.credential
|
|
3784
4040
|
);
|
|
@@ -4503,7 +4759,7 @@ function registerBillingTools(server, baseCtx) {
|
|
|
4503
4759
|
}
|
|
4504
4760
|
|
|
4505
4761
|
// src/tools/help.ts
|
|
4506
|
-
import { join as
|
|
4762
|
+
import { join as join9 } from "node:path";
|
|
4507
4763
|
import { z as z4 } from "zod";
|
|
4508
4764
|
var TOOL_TOPICS = [
|
|
4509
4765
|
"init",
|
|
@@ -4511,6 +4767,7 @@ var TOOL_TOPICS = [
|
|
|
4511
4767
|
"deploy",
|
|
4512
4768
|
"refresh",
|
|
4513
4769
|
"status",
|
|
4770
|
+
"rotate",
|
|
4514
4771
|
"plans",
|
|
4515
4772
|
"subscribe",
|
|
4516
4773
|
"bind",
|
|
@@ -4572,6 +4829,14 @@ var TOOL_MANUALS = {
|
|
|
4572
4829
|
warnings: ["Billing truth comes from billing, not inferred status text."],
|
|
4573
4830
|
nextStep: "Follow only the returned real tool names."
|
|
4574
4831
|
},
|
|
4832
|
+
rotate: {
|
|
4833
|
+
purpose: "Replace the current site management credential after explicit confirmation.",
|
|
4834
|
+
sideEffects: "Confirmed rotation revokes every previous credential for this site.",
|
|
4835
|
+
preconditions: "A valid local site credential; preview is required before confirmation.",
|
|
4836
|
+
parameters: "confirmed=true only from the exact preview resume arguments.",
|
|
4837
|
+
warnings: ["Rotation is optional and never blocks deploy.", "Never expose credential values."],
|
|
4838
|
+
nextStep: "Use the preview resumeWith arguments only after the user confirms."
|
|
4839
|
+
},
|
|
4575
4840
|
plans: {
|
|
4576
4841
|
purpose: "Read the authoritative hosting plan catalog and rules.",
|
|
4577
4842
|
sideEffects: "Read-only public API request.",
|
|
@@ -4676,7 +4941,7 @@ function registerHelpTools(server, baseCtx) {
|
|
|
4676
4941
|
throw new Error("init postcondition failed: project marker missing");
|
|
4677
4942
|
const site = loadSiteFile(ctx.projectDir);
|
|
4678
4943
|
const recovery = loadRecoveryFile(ctx.projectDir);
|
|
4679
|
-
const sakupaDirectory =
|
|
4944
|
+
const sakupaDirectory = join9(ctx.projectDir, ".sakupa");
|
|
4680
4945
|
return structuredToolResult({
|
|
4681
4946
|
schemaVersion: 1,
|
|
4682
4947
|
outcome: "completed",
|
|
@@ -4750,12 +5015,26 @@ Next: ${manual.nextStep}`,
|
|
|
4750
5015
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
4751
5016
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
4752
5017
|
let recoveryState = "absent";
|
|
5018
|
+
let credentialRotationState = "absent";
|
|
5019
|
+
let credentialRotationDetails;
|
|
4753
5020
|
if (selected) {
|
|
4754
5021
|
try {
|
|
4755
5022
|
recoveryState = loadRecoveryFile(selected) === null ? "absent" : "ok";
|
|
4756
5023
|
} catch {
|
|
4757
5024
|
recoveryState = "corrupted";
|
|
4758
5025
|
}
|
|
5026
|
+
const rotation = loadCredentialRotation(selected);
|
|
5027
|
+
if (rotation.kind === "ok") {
|
|
5028
|
+
credentialRotationState = "pending";
|
|
5029
|
+
credentialRotationDetails = {
|
|
5030
|
+
siteId: rotation.file.siteId,
|
|
5031
|
+
createdAt: rotation.file.createdAt,
|
|
5032
|
+
apiBaseUrl: rotation.file.apiBaseUrl
|
|
5033
|
+
};
|
|
5034
|
+
} else if (rotation.kind === "corrupted") {
|
|
5035
|
+
credentialRotationState = "corrupted";
|
|
5036
|
+
credentialRotationDetails = { problem: rotation.problem };
|
|
5037
|
+
}
|
|
4759
5038
|
}
|
|
4760
5039
|
const opaqueFailure = args.errorCode === "internal" || args.resultCode === "error_internal";
|
|
4761
5040
|
const reportRecommended = opaqueFailure && (diagnosis.diagnosisCode === "project_bound" || diagnosis.diagnosisCode === "roots_request_failed");
|
|
@@ -4772,8 +5051,9 @@ Next: ${manual.nextStep}`,
|
|
|
4772
5051
|
allowed: true,
|
|
4773
5052
|
reasonCode: "help_confirmed_last_resort"
|
|
4774
5053
|
}
|
|
4775
|
-
] : diagnosis.diagnosisCode === "workspace_not_initialized" ? [{ tool: "init", allowed: true, reasonCode: "initialize_active_root" }] : [];
|
|
4776
|
-
const
|
|
5054
|
+
] : credentialRotationState === "pending" ? [{ tool: "rotate", allowed: true, reasonCode: "resume_confirmed_rotation" }] : diagnosis.diagnosisCode === "workspace_not_initialized" ? [{ tool: "init", allowed: true, reasonCode: "initialize_active_root" }] : [];
|
|
5055
|
+
const rotationGuidance = credentialRotationState === "pending" ? "A previously confirmed credential rotation is pending; call rotate with no arguments to resume it. The candidate credential is intentionally hidden." : credentialRotationState === "corrupted" ? "The local credential rotation journal is damaged. Preserve .sakupa/rotation.json, do not print, edit or delete it, and do not retry deploy or rotate until the file is recovered from a trusted backup or Sakupa support confirms the recovery path." : "";
|
|
5056
|
+
const summary = `Help diagnosis: ${diagnosis.diagnosisCode}. ${diagnosis.guidance} MCP version: ${MCP_VERSION}. ${rotationGuidance} ` + (reportRecommended ? diagnosis.diagnosisCode === "project_bound" ? "Local project binding is healthy but the failure is an unclassified internal error. report is now available as the last resort; preview it before submission." : "The MCP Roots request itself failed with an unclassified internal error. report is now available as the last resort; preview it before submission." : "Do not submit report for this diagnosis; follow the guidance and retry help.");
|
|
4777
5057
|
return structuredToolResult({
|
|
4778
5058
|
schemaVersion: 1,
|
|
4779
5059
|
outcome: diagnosis.diagnosisCode === "project_bound" ? "completed" : "blocked",
|
|
@@ -4785,6 +5065,8 @@ Next: ${manual.nextStep}`,
|
|
|
4785
5065
|
projectMarkerState: marker.kind,
|
|
4786
5066
|
siteState: site.kind,
|
|
4787
5067
|
recoveryState,
|
|
5068
|
+
credentialRotationState,
|
|
5069
|
+
...credentialRotationDetails !== void 0 ? { credentialRotationDetails } : {},
|
|
4788
5070
|
reportRecommended,
|
|
4789
5071
|
...helpAuthorization !== void 0 ? { helpAuthorization } : {},
|
|
4790
5072
|
...args.failedTool !== void 0 ? { failedTool: args.failedTool } : {},
|
|
@@ -4800,6 +5082,128 @@ Next: ${manual.nextStep}`,
|
|
|
4800
5082
|
);
|
|
4801
5083
|
}
|
|
4802
5084
|
|
|
5085
|
+
// src/tools/credential.ts
|
|
5086
|
+
import { z as z5 } from "zod";
|
|
5087
|
+
function registerCredentialTools(server, baseCtx) {
|
|
5088
|
+
server.registerTool(
|
|
5089
|
+
"rotate",
|
|
5090
|
+
{
|
|
5091
|
+
description: "Optionally replace this site management credential. The first call is a read-only preview. Only confirmed:true after explicit user approval installs a locally generated new credential and revokes every previous credential. Rotation is never required to deploy.",
|
|
5092
|
+
inputSchema: {
|
|
5093
|
+
confirmed: z5.boolean().optional().describe(
|
|
5094
|
+
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
5095
|
+
)
|
|
5096
|
+
},
|
|
5097
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5098
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
5099
|
+
},
|
|
5100
|
+
async (args) => {
|
|
5101
|
+
let releaseLock;
|
|
5102
|
+
try {
|
|
5103
|
+
const ctx = await withProjectDir(baseCtx);
|
|
5104
|
+
let site = requireSiteFile(ctx);
|
|
5105
|
+
const pending = loadCredentialRotation(ctx.projectDir);
|
|
5106
|
+
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
5107
|
+
releaseLock = acquireSiteHandoffLock(site.siteId);
|
|
5108
|
+
}
|
|
5109
|
+
if (pending.kind !== "absent") {
|
|
5110
|
+
const resumed = await resumeCredentialRotation(
|
|
5111
|
+
ctx.client,
|
|
5112
|
+
ctx.projectDir,
|
|
5113
|
+
site,
|
|
5114
|
+
ctx.apiBaseUrl
|
|
5115
|
+
);
|
|
5116
|
+
if (!resumed) throw new Error("Credential rotation resume state disappeared.");
|
|
5117
|
+
site = resumed.site;
|
|
5118
|
+
return structuredToolResult({
|
|
5119
|
+
schemaVersion: 1,
|
|
5120
|
+
outcome: "completed",
|
|
5121
|
+
resultCode: "credential_rotation_resumed",
|
|
5122
|
+
summary: `Credential rotation resumed and completed for ${site.url ?? site.siteId}. The new credential is stored only in this project .sakupa/site.json. Every previous credential is revoked; old project folders and backup copies can no longer manage this site. No credential value is shown.`,
|
|
5123
|
+
data: {
|
|
5124
|
+
siteId: site.siteId,
|
|
5125
|
+
credentialCreatedAt: resumed.status.credentialCreatedAt,
|
|
5126
|
+
rotationRecommended: false,
|
|
5127
|
+
previousCredentialsRevoked: true,
|
|
5128
|
+
resumedAfterInterruption: true,
|
|
5129
|
+
credentialStoredLocally: true
|
|
5130
|
+
},
|
|
5131
|
+
nextActions: [{ tool: "status", allowed: true }]
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
const status = await ctx.client.getCredentialStatus(site.siteId, site.credential);
|
|
5135
|
+
const confirmation = { confirmed: true };
|
|
5136
|
+
if (args.confirmed !== true) {
|
|
5137
|
+
return structuredToolResult({
|
|
5138
|
+
schemaVersion: 1,
|
|
5139
|
+
outcome: "waiting_user",
|
|
5140
|
+
resultCode: "credential_rotation_confirmation_required",
|
|
5141
|
+
summary: `Nothing was changed. Rotating the management credential for ${site.url ?? site.siteId} will generate a new credential locally, replace the one in this project .sakupa/site.json, and revoke EVERY previous credential for this site\u2014including copies in old folders and backups. Rotation is optional and deploy remains available. Current credential created at: ${status.credentialCreatedAt}. Exact confirm arguments: ${JSON.stringify(confirmation)}. Ask the user for explicit approval; never expose credential values.`,
|
|
5142
|
+
data: {
|
|
5143
|
+
siteId: site.siteId,
|
|
5144
|
+
credentialCreatedAt: status.credentialCreatedAt,
|
|
5145
|
+
credentialAgeSeconds: status.ageSeconds,
|
|
5146
|
+
rotationRecommended: status.rotationRecommended,
|
|
5147
|
+
confirmation,
|
|
5148
|
+
confirmArguments: confirmation,
|
|
5149
|
+
previousCredentialsWillBeRevoked: true,
|
|
5150
|
+
optional: true
|
|
5151
|
+
},
|
|
5152
|
+
userAction: {
|
|
5153
|
+
type: "confirm_in_mcp",
|
|
5154
|
+
provider: "sakupa",
|
|
5155
|
+
expectedOutcome: "Generate one new local credential and revoke every previous credential for this site.",
|
|
5156
|
+
resumeWith: { tool: "rotate", arguments: confirmation }
|
|
5157
|
+
},
|
|
5158
|
+
nextActions: [
|
|
5159
|
+
{
|
|
5160
|
+
tool: "rotate",
|
|
5161
|
+
arguments: confirmation,
|
|
5162
|
+
allowed: true,
|
|
5163
|
+
reasonCode: "explicit_credential_rotation_confirmation"
|
|
5164
|
+
}
|
|
5165
|
+
]
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
writeCredentialRotation(ctx.projectDir, {
|
|
5169
|
+
siteId: site.siteId,
|
|
5170
|
+
candidateCredential: generateCredential(),
|
|
5171
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5172
|
+
apiBaseUrl: ctx.apiBaseUrl
|
|
5173
|
+
});
|
|
5174
|
+
const completed = await resumeCredentialRotation(
|
|
5175
|
+
ctx.client,
|
|
5176
|
+
ctx.projectDir,
|
|
5177
|
+
site,
|
|
5178
|
+
ctx.apiBaseUrl
|
|
5179
|
+
);
|
|
5180
|
+
if (!completed) throw new Error("Credential rotation did not produce resumable state.");
|
|
5181
|
+
site = completed.site;
|
|
5182
|
+
return structuredToolResult({
|
|
5183
|
+
schemaVersion: 1,
|
|
5184
|
+
outcome: "completed",
|
|
5185
|
+
resultCode: "credential_rotated",
|
|
5186
|
+
summary: `Management credential rotated for ${site.url ?? site.siteId}. The new credential is stored only in this project .sakupa/site.json. Every previous credential is revoked; old project folders and backup copies can no longer manage this site. No credential value is shown.`,
|
|
5187
|
+
data: {
|
|
5188
|
+
siteId: site.siteId,
|
|
5189
|
+
credentialCreatedAt: completed.status.credentialCreatedAt,
|
|
5190
|
+
rotationRecommended: false,
|
|
5191
|
+
previousCredentialsRevoked: true,
|
|
5192
|
+
revokedPreviousCredentials: completed.rotation?.revokedPreviousCredentials ?? null,
|
|
5193
|
+
resumedAfterInterruption: false,
|
|
5194
|
+
credentialStoredLocally: true
|
|
5195
|
+
},
|
|
5196
|
+
nextActions: [{ tool: "status", allowed: true }]
|
|
5197
|
+
});
|
|
5198
|
+
} catch (error) {
|
|
5199
|
+
return toolError(error);
|
|
5200
|
+
} finally {
|
|
5201
|
+
releaseLock?.();
|
|
5202
|
+
}
|
|
5203
|
+
}
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
|
|
4803
5207
|
// src/transport.ts
|
|
4804
5208
|
var FetchTransport = class {
|
|
4805
5209
|
baseUrl;
|
|
@@ -4909,7 +5313,9 @@ Workflow:
|
|
|
4909
5313
|
site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
|
|
4910
5314
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
4911
5315
|
its validity; refresh extends validity without uploading; status shows the
|
|
4912
|
-
current deployment and serving state at any time.
|
|
5316
|
+
current deployment and serving state at any time. Every update checks the credential's
|
|
5317
|
+
server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
|
|
5318
|
+
optional rotate tool; never rotate without the user's explicit confirmation.
|
|
4913
5319
|
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
|
|
4914
5320
|
shows the catalog; subscribe -> Stripe-hosted checkout;
|
|
4915
5321
|
water/personal/share/business). Paying makes the
|
|
@@ -4977,6 +5383,9 @@ Safety boundaries:
|
|
|
4977
5383
|
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
4978
5384
|
into the conversation copies the site's only key outside the protected local file. Read it
|
|
4979
5385
|
only through the tools.
|
|
5386
|
+
- rotate always previews first. confirmed:true revokes EVERY prior credential, including old
|
|
5387
|
+
folders and backups. Show that consequence and obtain explicit user approval; rotation is
|
|
5388
|
+
optional and never a condition for deploy.
|
|
4980
5389
|
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
4981
5390
|
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
4982
5391
|
wants no lang attribute.
|
|
@@ -5020,6 +5429,7 @@ function createSakupaMcpServer(opts) {
|
|
|
5020
5429
|
};
|
|
5021
5430
|
registerTools(server, ctx);
|
|
5022
5431
|
registerBillingTools(server, ctx);
|
|
5432
|
+
registerCredentialTools(server, ctx);
|
|
5023
5433
|
registerHelpTools(server, ctx);
|
|
5024
5434
|
return server;
|
|
5025
5435
|
}
|