@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/index.js
CHANGED
|
@@ -3,6 +3,7 @@ var SERVICE_DOMAIN = "sakupa.com";
|
|
|
3
3
|
var DEFAULT_API_BASE_URL = "https://api.sakupa.com";
|
|
4
4
|
var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
5
5
|
var TEST_ACCESS_HEADER = "x-sakupa-test-token";
|
|
6
|
+
var CREDENTIAL_ROTATION_RECOMMEND_AFTER_SECONDS = 7 * 24 * 60 * 60;
|
|
6
7
|
var FREE_SITE_TTL_HOURS = 24;
|
|
7
8
|
var FREE_SITE_MAX_TOTAL_BYTES = 10 * 1024 * 1024;
|
|
8
9
|
var FREE_ACTIVE_SITES_PER_IP = 3;
|
|
@@ -124,7 +125,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
125
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
126
|
|
|
126
127
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
128
|
+
var SAKUPA_MCP_VERSION = "0.7.42";
|
|
128
129
|
|
|
129
130
|
// ../core/dist/domain/errors.js
|
|
130
131
|
var HTTP_STATUS = {
|
|
@@ -678,6 +679,20 @@ var HttpApiClient = class {
|
|
|
678
679
|
credential
|
|
679
680
|
});
|
|
680
681
|
}
|
|
682
|
+
async getCredentialStatus(siteId, credential) {
|
|
683
|
+
return this.call(
|
|
684
|
+
"GET",
|
|
685
|
+
`/v1/sites/${encodeURIComponent(siteId)}/credential`,
|
|
686
|
+
{ credential }
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
async rotateCredential(siteId, credential, req) {
|
|
690
|
+
return this.call(
|
|
691
|
+
"POST",
|
|
692
|
+
`/v1/sites/${encodeURIComponent(siteId)}/credential/rotate`,
|
|
693
|
+
{ credential, body: req }
|
|
694
|
+
);
|
|
695
|
+
}
|
|
681
696
|
async getSiteArchive(siteId, credential) {
|
|
682
697
|
return this.call(
|
|
683
698
|
"GET",
|
|
@@ -786,10 +801,12 @@ import {
|
|
|
786
801
|
existsSync,
|
|
787
802
|
mkdirSync,
|
|
788
803
|
readFileSync,
|
|
804
|
+
renameSync,
|
|
789
805
|
rmdirSync,
|
|
790
806
|
rmSync,
|
|
791
807
|
writeFileSync
|
|
792
808
|
} from "node:fs";
|
|
809
|
+
import { randomUUID } from "node:crypto";
|
|
793
810
|
import { dirname, join } from "node:path";
|
|
794
811
|
var SITE_DIR = ".sakupa";
|
|
795
812
|
var SITE_FILE = "site.json";
|
|
@@ -900,12 +917,22 @@ function writeSiteFile(projectDir, file, opts = {}) {
|
|
|
900
917
|
const dir = join(projectDir, SITE_DIR);
|
|
901
918
|
mkdirSync(dir, { recursive: true });
|
|
902
919
|
const path = join(dir, SITE_FILE);
|
|
903
|
-
|
|
904
|
-
|
|
920
|
+
const temporary = join(dir, `.site-${randomUUID()}.tmp`);
|
|
921
|
+
writeFileSync(temporary, `${JSON.stringify(file, null, 2)}
|
|
922
|
+
`, {
|
|
923
|
+
encoding: "utf8",
|
|
924
|
+
mode: 384
|
|
925
|
+
});
|
|
905
926
|
try {
|
|
906
|
-
chmodSync(
|
|
927
|
+
chmodSync(temporary, 384);
|
|
907
928
|
} catch {
|
|
908
929
|
}
|
|
930
|
+
try {
|
|
931
|
+
renameSync(temporary, path);
|
|
932
|
+
} catch (error) {
|
|
933
|
+
rmSync(temporary, { force: true });
|
|
934
|
+
throw error;
|
|
935
|
+
}
|
|
909
936
|
}
|
|
910
937
|
function deleteSiteFile(projectDir) {
|
|
911
938
|
const path = siteFilePath(projectDir);
|
|
@@ -1379,7 +1406,7 @@ import { fileURLToPath } from "node:url";
|
|
|
1379
1406
|
import { resolve as resolve3 } from "node:path";
|
|
1380
1407
|
|
|
1381
1408
|
// src/project-root.ts
|
|
1382
|
-
import { randomUUID } from "node:crypto";
|
|
1409
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1383
1410
|
import {
|
|
1384
1411
|
chmodSync as chmodSync2,
|
|
1385
1412
|
existsSync as existsSync2,
|
|
@@ -1387,7 +1414,7 @@ import {
|
|
|
1387
1414
|
mkdirSync as mkdirSync2,
|
|
1388
1415
|
readFileSync as readFileSync2,
|
|
1389
1416
|
realpathSync,
|
|
1390
|
-
renameSync,
|
|
1417
|
+
renameSync as renameSync2,
|
|
1391
1418
|
rmdirSync as rmdirSync2,
|
|
1392
1419
|
statSync,
|
|
1393
1420
|
unlinkSync,
|
|
@@ -1470,7 +1497,7 @@ function initializeProject(projectDir) {
|
|
|
1470
1497
|
}
|
|
1471
1498
|
const marker = {
|
|
1472
1499
|
schemaVersion: PROJECT_SCHEMA_VERSION,
|
|
1473
|
-
projectId:
|
|
1500
|
+
projectId: randomUUID2(),
|
|
1474
1501
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1475
1502
|
};
|
|
1476
1503
|
writeMarkerAtomically(canonical, marker);
|
|
@@ -1582,14 +1609,14 @@ function writeMarkerAtomically(projectDir, marker) {
|
|
|
1582
1609
|
const dir = join3(projectDir, SAKUPA_DIR);
|
|
1583
1610
|
mkdirSync2(dir, { recursive: true, mode: 448 });
|
|
1584
1611
|
const path = projectMarkerPath(projectDir);
|
|
1585
|
-
const temporary = `${path}.${process.pid}.${
|
|
1612
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
1586
1613
|
try {
|
|
1587
1614
|
writeFileSync2(temporary, `${JSON.stringify(marker, null, 2)}
|
|
1588
1615
|
`, {
|
|
1589
1616
|
encoding: "utf8",
|
|
1590
1617
|
mode: 384
|
|
1591
1618
|
});
|
|
1592
|
-
|
|
1619
|
+
renameSync2(temporary, path);
|
|
1593
1620
|
try {
|
|
1594
1621
|
chmodSync2(path, 384);
|
|
1595
1622
|
} catch {
|
|
@@ -1899,7 +1926,7 @@ function structuredToolResult(envelope) {
|
|
|
1899
1926
|
}
|
|
1900
1927
|
|
|
1901
1928
|
// src/tools/context.ts
|
|
1902
|
-
import { randomUUID as
|
|
1929
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1903
1930
|
var LocalGuidanceError = class extends SakupaError {
|
|
1904
1931
|
constructor(code, message) {
|
|
1905
1932
|
super(code, message);
|
|
@@ -1978,7 +2005,7 @@ function reportAuthorizationStore(ctx) {
|
|
|
1978
2005
|
return store;
|
|
1979
2006
|
}
|
|
1980
2007
|
function issueReportAuthorization(ctx, failedTool) {
|
|
1981
|
-
const token =
|
|
2008
|
+
const token = randomUUID3();
|
|
1982
2009
|
reportAuthorizationStore(ctx).set(token, {
|
|
1983
2010
|
failedTool,
|
|
1984
2011
|
expiresAt: Date.now() + 10 * 60 * 1e3
|
|
@@ -2057,9 +2084,9 @@ function toolError(e) {
|
|
|
2057
2084
|
}
|
|
2058
2085
|
|
|
2059
2086
|
// src/tools/definitions.ts
|
|
2060
|
-
import { randomUUID as
|
|
2087
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
2061
2088
|
import { promises as fs2 } from "node:fs";
|
|
2062
|
-
import { join as
|
|
2089
|
+
import { join as join8, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
|
|
2063
2090
|
import { z as z2 } from "zod";
|
|
2064
2091
|
|
|
2065
2092
|
// src/recovery-archive.ts
|
|
@@ -2484,15 +2511,15 @@ function strFromU8(dat, latin1) {
|
|
|
2484
2511
|
var slzh = function(d, b) {
|
|
2485
2512
|
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
2486
2513
|
};
|
|
2487
|
-
var zh = function(d, b,
|
|
2514
|
+
var zh = function(d, b, z6) {
|
|
2488
2515
|
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;
|
|
2489
|
-
var _a2 = z64hs(d, es, efl,
|
|
2516
|
+
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];
|
|
2490
2517
|
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
2491
2518
|
};
|
|
2492
|
-
var z64hs = function(d, b, l,
|
|
2519
|
+
var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
2493
2520
|
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
2494
2521
|
var nf = nsc + nsu + noff;
|
|
2495
|
-
if (
|
|
2522
|
+
if (z6 && nf) {
|
|
2496
2523
|
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
2497
2524
|
if (b2(d, b) == 1) {
|
|
2498
2525
|
return [
|
|
@@ -2503,7 +2530,7 @@ var z64hs = function(d, b, l, z5, sc, su, off) {
|
|
|
2503
2530
|
];
|
|
2504
2531
|
}
|
|
2505
2532
|
}
|
|
2506
|
-
if (
|
|
2533
|
+
if (z6 < 2)
|
|
2507
2534
|
err(13);
|
|
2508
2535
|
}
|
|
2509
2536
|
return [sc, su, off, 0];
|
|
@@ -2520,18 +2547,18 @@ function unzipSync(data, opts) {
|
|
|
2520
2547
|
if (!c)
|
|
2521
2548
|
return {};
|
|
2522
2549
|
var o = b4(data, e + 16);
|
|
2523
|
-
var
|
|
2524
|
-
if (
|
|
2550
|
+
var z6 = b4(data, e - 20) == 117853008;
|
|
2551
|
+
if (z6) {
|
|
2525
2552
|
var ze = b4(data, e - 12);
|
|
2526
|
-
|
|
2527
|
-
if (
|
|
2553
|
+
z6 = b4(data, ze) == 101075792;
|
|
2554
|
+
if (z6) {
|
|
2528
2555
|
c = b4(data, ze + 32);
|
|
2529
2556
|
o = b4(data, ze + 48);
|
|
2530
2557
|
}
|
|
2531
2558
|
}
|
|
2532
2559
|
var fltr = opts && opts.filter;
|
|
2533
2560
|
for (var i = 0; i < c; ++i) {
|
|
2534
|
-
var _a2 = zh(data, o,
|
|
2561
|
+
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);
|
|
2535
2562
|
o = no;
|
|
2536
2563
|
if (!fltr || fltr({
|
|
2537
2564
|
name: fn,
|
|
@@ -3039,15 +3066,181 @@ ${diag.layers}
|
|
|
3039
3066
|
` + (diag.allOk ? "All required records are live; certificate issuance completes automatically \u2014 re-check in a few minutes until the binding is active." : `${fixTail} ${cadence}`);
|
|
3040
3067
|
}
|
|
3041
3068
|
|
|
3069
|
+
// src/credential-rotation.ts
|
|
3070
|
+
import {
|
|
3071
|
+
chmodSync as chmodSync3,
|
|
3072
|
+
existsSync as existsSync6,
|
|
3073
|
+
mkdirSync as mkdirSync5,
|
|
3074
|
+
readFileSync as readFileSync4,
|
|
3075
|
+
renameSync as renameSync3,
|
|
3076
|
+
rmSync as rmSync2,
|
|
3077
|
+
writeFileSync as writeFileSync5
|
|
3078
|
+
} from "node:fs";
|
|
3079
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
3080
|
+
import { join as join7 } from "node:path";
|
|
3081
|
+
var ROTATION_FILE = "rotation.json";
|
|
3082
|
+
function credentialRotationPath(projectDir) {
|
|
3083
|
+
return join7(projectDir, ".sakupa", ROTATION_FILE);
|
|
3084
|
+
}
|
|
3085
|
+
function loadCredentialRotation(projectDir) {
|
|
3086
|
+
const path = credentialRotationPath(projectDir);
|
|
3087
|
+
if (!existsSync6(path)) return { kind: "absent" };
|
|
3088
|
+
let parsed;
|
|
3089
|
+
try {
|
|
3090
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
3091
|
+
} catch (error) {
|
|
3092
|
+
return {
|
|
3093
|
+
kind: "corrupted",
|
|
3094
|
+
problem: `the file cannot be read as JSON (${error instanceof Error ? error.message : String(error)})`
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
if (typeof parsed.siteId !== "string" || parsed.siteId.length === 0) {
|
|
3098
|
+
return { kind: "corrupted", problem: "siteId is missing or empty" };
|
|
3099
|
+
}
|
|
3100
|
+
if (typeof parsed.candidateCredential !== "string" || !CREDENTIAL_PATTERN.test(parsed.candidateCredential)) {
|
|
3101
|
+
return { kind: "corrupted", problem: "candidateCredential has an invalid shape" };
|
|
3102
|
+
}
|
|
3103
|
+
if (typeof parsed.createdAt !== "string" || !Number.isFinite(Date.parse(parsed.createdAt))) {
|
|
3104
|
+
return { kind: "corrupted", problem: "createdAt is missing or invalid" };
|
|
3105
|
+
}
|
|
3106
|
+
if (typeof parsed.apiBaseUrl !== "string" || parsed.apiBaseUrl.length === 0) {
|
|
3107
|
+
return { kind: "corrupted", problem: "apiBaseUrl is missing or empty" };
|
|
3108
|
+
}
|
|
3109
|
+
return {
|
|
3110
|
+
kind: "ok",
|
|
3111
|
+
file: {
|
|
3112
|
+
siteId: parsed.siteId,
|
|
3113
|
+
candidateCredential: parsed.candidateCredential,
|
|
3114
|
+
createdAt: parsed.createdAt,
|
|
3115
|
+
apiBaseUrl: parsed.apiBaseUrl
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
function writeCredentialRotation(projectDir, file) {
|
|
3120
|
+
if (!CREDENTIAL_PATTERN.test(file.candidateCredential)) {
|
|
3121
|
+
throw new Error("Refusing to persist an invalid credential rotation candidate.");
|
|
3122
|
+
}
|
|
3123
|
+
const current = loadCredentialRotation(projectDir);
|
|
3124
|
+
if (current.kind === "corrupted") {
|
|
3125
|
+
throw new Error(
|
|
3126
|
+
`Refusing to overwrite damaged credential rotation state: ${current.problem}. Run help.`
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
if (current.kind === "ok") {
|
|
3130
|
+
if (current.file.siteId === file.siteId && current.file.candidateCredential === file.candidateCredential && current.file.apiBaseUrl === file.apiBaseUrl) {
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
throw new Error(
|
|
3134
|
+
"A different credential rotation is already pending. Run rotate to resume it; no state was overwritten."
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
const directory = join7(projectDir, ".sakupa");
|
|
3138
|
+
mkdirSync5(directory, { recursive: true, mode: 448 });
|
|
3139
|
+
const target = credentialRotationPath(projectDir);
|
|
3140
|
+
const temporary = join7(directory, `.rotation-${randomUUID4()}.tmp`);
|
|
3141
|
+
writeFileSync5(temporary, `${JSON.stringify(file, null, 2)}
|
|
3142
|
+
`, {
|
|
3143
|
+
encoding: "utf8",
|
|
3144
|
+
mode: 384
|
|
3145
|
+
});
|
|
3146
|
+
try {
|
|
3147
|
+
chmodSync3(temporary, 384);
|
|
3148
|
+
} catch {
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
renameSync3(temporary, target);
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
rmSync2(temporary, { force: true });
|
|
3154
|
+
throw error;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
function deleteCredentialRotation(projectDir) {
|
|
3158
|
+
rmSync2(credentialRotationPath(projectDir), { force: true });
|
|
3159
|
+
}
|
|
3160
|
+
function promoteRotatedCredential(projectDir, site, rotation, credentialCreatedAt) {
|
|
3161
|
+
if (rotation.siteId !== site.siteId || rotation.apiBaseUrl !== site.apiBaseUrl) {
|
|
3162
|
+
throw new Error(
|
|
3163
|
+
"Credential rotation state belongs to a different site or Sakupa environment; no file was changed."
|
|
3164
|
+
);
|
|
3165
|
+
}
|
|
3166
|
+
if (!Number.isFinite(Date.parse(credentialCreatedAt))) {
|
|
3167
|
+
throw new Error(
|
|
3168
|
+
"The server returned an invalid credential creation time; no file was changed."
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
const updated = {
|
|
3172
|
+
...site,
|
|
3173
|
+
credential: rotation.candidateCredential,
|
|
3174
|
+
createdAt: credentialCreatedAt
|
|
3175
|
+
};
|
|
3176
|
+
writeSiteFile(projectDir, updated);
|
|
3177
|
+
deleteCredentialRotation(projectDir);
|
|
3178
|
+
return updated;
|
|
3179
|
+
}
|
|
3180
|
+
async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
|
|
3181
|
+
const state = loadCredentialRotation(projectDir);
|
|
3182
|
+
if (state.kind === "absent") return null;
|
|
3183
|
+
if (state.kind === "corrupted") {
|
|
3184
|
+
throw new Error(
|
|
3185
|
+
`Credential rotation state is damaged (${state.problem}). Nothing was overwritten; run help.`
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
const pending = state.file;
|
|
3189
|
+
const siteEnvironment = site.apiBaseUrl || apiBaseUrl;
|
|
3190
|
+
if (pending.siteId !== site.siteId || pending.apiBaseUrl !== apiBaseUrl || siteEnvironment !== apiBaseUrl) {
|
|
3191
|
+
throw new Error(
|
|
3192
|
+
"Credential rotation state belongs to a different site or Sakupa environment. Nothing was changed; run help."
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
try {
|
|
3196
|
+
const status = await client.getCredentialStatus(site.siteId, pending.candidateCredential);
|
|
3197
|
+
return {
|
|
3198
|
+
site: promoteRotatedCredential(
|
|
3199
|
+
projectDir,
|
|
3200
|
+
{ ...site, apiBaseUrl },
|
|
3201
|
+
pending,
|
|
3202
|
+
status.credentialCreatedAt
|
|
3203
|
+
),
|
|
3204
|
+
status,
|
|
3205
|
+
rotation: null,
|
|
3206
|
+
resumed: true
|
|
3207
|
+
};
|
|
3208
|
+
} catch (error) {
|
|
3209
|
+
if (!isSakupaError(error) || error.code !== "unauthorized") throw error;
|
|
3210
|
+
}
|
|
3211
|
+
try {
|
|
3212
|
+
await client.getCredentialStatus(site.siteId, site.credential);
|
|
3213
|
+
} catch (error) {
|
|
3214
|
+
if (!isSakupaError(error) || error.code !== "unauthorized") throw error;
|
|
3215
|
+
throw new Error(
|
|
3216
|
+
"Neither the current nor pending credential is accepted. No local file was overwritten; run help before retrying."
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
const rotation = await client.rotateCredential(site.siteId, site.credential, {
|
|
3220
|
+
newCredential: pending.candidateCredential
|
|
3221
|
+
});
|
|
3222
|
+
return {
|
|
3223
|
+
site: promoteRotatedCredential(
|
|
3224
|
+
projectDir,
|
|
3225
|
+
{ ...site, apiBaseUrl },
|
|
3226
|
+
pending,
|
|
3227
|
+
rotation.credentialCreatedAt
|
|
3228
|
+
),
|
|
3229
|
+
status: rotation,
|
|
3230
|
+
rotation,
|
|
3231
|
+
resumed: true
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3042
3235
|
// src/tools/definitions.ts
|
|
3043
|
-
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
3236
|
+
function text(resultCode, t, data = {}, outcome = "completed", nextActions = []) {
|
|
3044
3237
|
return structuredToolResult({
|
|
3045
3238
|
schemaVersion: 1,
|
|
3046
3239
|
outcome,
|
|
3047
3240
|
resultCode,
|
|
3048
3241
|
summary: t,
|
|
3049
3242
|
data,
|
|
3050
|
-
nextActions
|
|
3243
|
+
nextActions
|
|
3051
3244
|
});
|
|
3052
3245
|
}
|
|
3053
3246
|
function textJson(resultCode, header, obj, outcome = "completed") {
|
|
@@ -3115,7 +3308,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
3115
3308
|
async function buildHashedManifest(files, outputAbs) {
|
|
3116
3309
|
const manifest = [];
|
|
3117
3310
|
for (const file of files) {
|
|
3118
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
3311
|
+
const bytes = new Uint8Array(await fs2.readFile(join8(outputAbs, file.path)));
|
|
3119
3312
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
3120
3313
|
}
|
|
3121
3314
|
return manifest;
|
|
@@ -3134,7 +3327,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
3134
3327
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
3135
3328
|
);
|
|
3136
3329
|
}
|
|
3137
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
3330
|
+
const bytes = new Uint8Array(await fs2.readFile(join8(outputAbs, match.path)));
|
|
3138
3331
|
if (bytes.byteLength !== match.size) {
|
|
3139
3332
|
throw new SakupaError(
|
|
3140
3333
|
"validation_failed",
|
|
@@ -3227,14 +3420,14 @@ function outputDirectoryChain(projectRoot, outputAbs) {
|
|
|
3227
3420
|
const chain = [];
|
|
3228
3421
|
let cursor = projectRoot;
|
|
3229
3422
|
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
3230
|
-
cursor =
|
|
3423
|
+
cursor = join8(cursor, part);
|
|
3231
3424
|
chain.push(cursor);
|
|
3232
3425
|
}
|
|
3233
3426
|
return chain;
|
|
3234
3427
|
}
|
|
3235
3428
|
async function sakupaDirectoryEntries(projectDir) {
|
|
3236
3429
|
try {
|
|
3237
|
-
return await fs2.readdir(
|
|
3430
|
+
return await fs2.readdir(join8(projectDir, ".sakupa"));
|
|
3238
3431
|
} catch (error) {
|
|
3239
3432
|
const code = error.code;
|
|
3240
3433
|
if (code === "ENOENT") return [];
|
|
@@ -3345,6 +3538,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3345
3538
|
}
|
|
3346
3539
|
let existing = siteFileState.kind === "ok" ? siteFileState.file : null;
|
|
3347
3540
|
let handoff = null;
|
|
3541
|
+
let credentialSecurity = null;
|
|
3542
|
+
let credentialRotationResumed = false;
|
|
3348
3543
|
const resumedHandoffCleanup = existing ? resumeLocalSiteHandoff(ctx.projectDir, existing, Date.now()) : null;
|
|
3349
3544
|
const credentialRelocatedFrom = [];
|
|
3350
3545
|
const markerRelocatedFrom = [];
|
|
@@ -3382,8 +3577,8 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3382
3577
|
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.`,
|
|
3383
3578
|
data: {
|
|
3384
3579
|
projectRoot: ctx.projectDir,
|
|
3385
|
-
misplacedSakupaDirectory:
|
|
3386
|
-
targetSakupaDirectory:
|
|
3580
|
+
misplacedSakupaDirectory: join8(candidateDir, ".sakupa"),
|
|
3581
|
+
targetSakupaDirectory: join8(ctx.projectDir, ".sakupa"),
|
|
3387
3582
|
confirmationField: "sakupaRelocationConfirmed"
|
|
3388
3583
|
},
|
|
3389
3584
|
nextActions: [
|
|
@@ -3555,6 +3750,17 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3555
3750
|
ctx.apiBaseUrl
|
|
3556
3751
|
);
|
|
3557
3752
|
releaseHandoffLock = acquireSiteHandoffLock(handoff.site.siteId);
|
|
3753
|
+
const resumedSourceRotation = await resumeCredentialRotation(
|
|
3754
|
+
ctx.client,
|
|
3755
|
+
handoff.sourceProjectDir,
|
|
3756
|
+
handoff.site,
|
|
3757
|
+
ctx.apiBaseUrl
|
|
3758
|
+
);
|
|
3759
|
+
if (resumedSourceRotation) {
|
|
3760
|
+
handoff = { ...handoff, site: resumedSourceRotation.site };
|
|
3761
|
+
credentialSecurity = resumedSourceRotation.status;
|
|
3762
|
+
credentialRotationResumed = true;
|
|
3763
|
+
}
|
|
3558
3764
|
const cloud = await ctx.client.getSiteStatus(
|
|
3559
3765
|
handoff.site.siteId,
|
|
3560
3766
|
handoff.site.credential
|
|
@@ -3591,6 +3797,39 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
3591
3797
|
);
|
|
3592
3798
|
}
|
|
3593
3799
|
}
|
|
3800
|
+
if (existing) {
|
|
3801
|
+
try {
|
|
3802
|
+
if (!handoff) {
|
|
3803
|
+
const resumed = await resumeCredentialRotation(
|
|
3804
|
+
ctx.client,
|
|
3805
|
+
ctx.projectDir,
|
|
3806
|
+
existing,
|
|
3807
|
+
ctx.apiBaseUrl
|
|
3808
|
+
);
|
|
3809
|
+
if (resumed) {
|
|
3810
|
+
existing = resumed.site;
|
|
3811
|
+
credentialSecurity = resumed.status;
|
|
3812
|
+
credentialRotationResumed = true;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
credentialSecurity ??= await ctx.client.getCredentialStatus(
|
|
3816
|
+
existing.siteId,
|
|
3817
|
+
existing.credential
|
|
3818
|
+
);
|
|
3819
|
+
} catch (error) {
|
|
3820
|
+
if (isSakupaError(error) && error.code === "unauthorized") {
|
|
3821
|
+
return text(
|
|
3822
|
+
"credential_mismatch",
|
|
3823
|
+
`The server rejected the credential in .sakupa/site.json for site ${existing.siteId}.
|
|
3824
|
+
|
|
3825
|
+
` + siteFileRecoveryGuidance(ctx.projectDir) + "\n\nNothing was deployed and no new site was created.",
|
|
3826
|
+
{ siteId: existing.siteId },
|
|
3827
|
+
"blocked"
|
|
3828
|
+
);
|
|
3829
|
+
}
|
|
3830
|
+
throw error;
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3594
3833
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
3595
3834
|
if (!existing) {
|
|
3596
3835
|
const created = await ctx.client.createSite({
|
|
@@ -3720,11 +3959,13 @@ Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
|
3720
3959
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
3721
3960
|
` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
|
|
3722
3961
|
` : "") + (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.
|
|
3723
|
-
`) : "") + (finalized.mode === "free" ? `
|
|
3962
|
+
`) : "") + (credentialRotationResumed ? "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked.\n" : "") + (finalized.mode === "free" ? `
|
|
3724
3963
|
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
|
|
3725
3964
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
3726
3965
|
Warnings:
|
|
3727
|
-
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
3966
|
+
${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
|
|
3967
|
+
|
|
3968
|
+
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.` : ""),
|
|
3728
3969
|
{
|
|
3729
3970
|
siteId: existing.siteId,
|
|
3730
3971
|
url: finalized.url,
|
|
@@ -3735,6 +3976,13 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
3735
3976
|
filesUploaded: uploaded,
|
|
3736
3977
|
totalBytes: finalized.totalBytes,
|
|
3737
3978
|
warnings: finalized.warnings,
|
|
3979
|
+
credentialSecurity: credentialSecurity ? {
|
|
3980
|
+
credentialCreatedAt: credentialSecurity.credentialCreatedAt,
|
|
3981
|
+
ageSeconds: credentialSecurity.ageSeconds,
|
|
3982
|
+
rotationRecommended: credentialSecurity.rotationRecommended,
|
|
3983
|
+
optional: true,
|
|
3984
|
+
resumedAfterInterruption: credentialRotationResumed
|
|
3985
|
+
} : null,
|
|
3738
3986
|
...credentialRelocatedFrom.length > 0 ? { credentialRelocatedFrom } : {},
|
|
3739
3987
|
...handoff ? {
|
|
3740
3988
|
handoff: {
|
|
@@ -3747,7 +3995,15 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
|
3747
3995
|
sourceRemovalState: handoffCleanup?.sourceRemovalState
|
|
3748
3996
|
}
|
|
3749
3997
|
} : {}
|
|
3750
|
-
}
|
|
3998
|
+
},
|
|
3999
|
+
"completed",
|
|
4000
|
+
credentialSecurity?.rotationRecommended ? [
|
|
4001
|
+
{
|
|
4002
|
+
tool: "rotate",
|
|
4003
|
+
allowed: true,
|
|
4004
|
+
reasonCode: "credential_older_than_seven_days_optional_rotation"
|
|
4005
|
+
}
|
|
4006
|
+
] : []
|
|
3751
4007
|
);
|
|
3752
4008
|
} catch (e) {
|
|
3753
4009
|
return toolError(e);
|
|
@@ -3840,7 +4096,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
3840
4096
|
{
|
|
3841
4097
|
siteId: site.siteId,
|
|
3842
4098
|
plan: args.plan,
|
|
3843
|
-
idempotencyKey:
|
|
4099
|
+
idempotencyKey: randomUUID5()
|
|
3844
4100
|
},
|
|
3845
4101
|
site.credential
|
|
3846
4102
|
);
|
|
@@ -4568,7 +4824,7 @@ function registerBillingTools(server, baseCtx) {
|
|
|
4568
4824
|
}
|
|
4569
4825
|
|
|
4570
4826
|
// src/tools/help.ts
|
|
4571
|
-
import { join as
|
|
4827
|
+
import { join as join9 } from "node:path";
|
|
4572
4828
|
import { z as z4 } from "zod";
|
|
4573
4829
|
var TOOL_TOPICS = [
|
|
4574
4830
|
"init",
|
|
@@ -4576,6 +4832,7 @@ var TOOL_TOPICS = [
|
|
|
4576
4832
|
"deploy",
|
|
4577
4833
|
"refresh",
|
|
4578
4834
|
"status",
|
|
4835
|
+
"rotate",
|
|
4579
4836
|
"plans",
|
|
4580
4837
|
"subscribe",
|
|
4581
4838
|
"bind",
|
|
@@ -4637,6 +4894,14 @@ var TOOL_MANUALS = {
|
|
|
4637
4894
|
warnings: ["Billing truth comes from billing, not inferred status text."],
|
|
4638
4895
|
nextStep: "Follow only the returned real tool names."
|
|
4639
4896
|
},
|
|
4897
|
+
rotate: {
|
|
4898
|
+
purpose: "Replace the current site management credential after explicit confirmation.",
|
|
4899
|
+
sideEffects: "Confirmed rotation revokes every previous credential for this site.",
|
|
4900
|
+
preconditions: "A valid local site credential; preview is required before confirmation.",
|
|
4901
|
+
parameters: "confirmed=true only from the exact preview resume arguments.",
|
|
4902
|
+
warnings: ["Rotation is optional and never blocks deploy.", "Never expose credential values."],
|
|
4903
|
+
nextStep: "Use the preview resumeWith arguments only after the user confirms."
|
|
4904
|
+
},
|
|
4640
4905
|
plans: {
|
|
4641
4906
|
purpose: "Read the authoritative hosting plan catalog and rules.",
|
|
4642
4907
|
sideEffects: "Read-only public API request.",
|
|
@@ -4741,7 +5006,7 @@ function registerHelpTools(server, baseCtx) {
|
|
|
4741
5006
|
throw new Error("init postcondition failed: project marker missing");
|
|
4742
5007
|
const site = loadSiteFile(ctx.projectDir);
|
|
4743
5008
|
const recovery = loadRecoveryFile(ctx.projectDir);
|
|
4744
|
-
const sakupaDirectory =
|
|
5009
|
+
const sakupaDirectory = join9(ctx.projectDir, ".sakupa");
|
|
4745
5010
|
return structuredToolResult({
|
|
4746
5011
|
schemaVersion: 1,
|
|
4747
5012
|
outcome: "completed",
|
|
@@ -4815,12 +5080,26 @@ Next: ${manual.nextStep}`,
|
|
|
4815
5080
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
4816
5081
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
4817
5082
|
let recoveryState = "absent";
|
|
5083
|
+
let credentialRotationState = "absent";
|
|
5084
|
+
let credentialRotationDetails;
|
|
4818
5085
|
if (selected) {
|
|
4819
5086
|
try {
|
|
4820
5087
|
recoveryState = loadRecoveryFile(selected) === null ? "absent" : "ok";
|
|
4821
5088
|
} catch {
|
|
4822
5089
|
recoveryState = "corrupted";
|
|
4823
5090
|
}
|
|
5091
|
+
const rotation = loadCredentialRotation(selected);
|
|
5092
|
+
if (rotation.kind === "ok") {
|
|
5093
|
+
credentialRotationState = "pending";
|
|
5094
|
+
credentialRotationDetails = {
|
|
5095
|
+
siteId: rotation.file.siteId,
|
|
5096
|
+
createdAt: rotation.file.createdAt,
|
|
5097
|
+
apiBaseUrl: rotation.file.apiBaseUrl
|
|
5098
|
+
};
|
|
5099
|
+
} else if (rotation.kind === "corrupted") {
|
|
5100
|
+
credentialRotationState = "corrupted";
|
|
5101
|
+
credentialRotationDetails = { problem: rotation.problem };
|
|
5102
|
+
}
|
|
4824
5103
|
}
|
|
4825
5104
|
const opaqueFailure = args.errorCode === "internal" || args.resultCode === "error_internal";
|
|
4826
5105
|
const reportRecommended = opaqueFailure && (diagnosis.diagnosisCode === "project_bound" || diagnosis.diagnosisCode === "roots_request_failed");
|
|
@@ -4837,8 +5116,9 @@ Next: ${manual.nextStep}`,
|
|
|
4837
5116
|
allowed: true,
|
|
4838
5117
|
reasonCode: "help_confirmed_last_resort"
|
|
4839
5118
|
}
|
|
4840
|
-
] : diagnosis.diagnosisCode === "workspace_not_initialized" ? [{ tool: "init", allowed: true, reasonCode: "initialize_active_root" }] : [];
|
|
4841
|
-
const
|
|
5119
|
+
] : credentialRotationState === "pending" ? [{ tool: "rotate", allowed: true, reasonCode: "resume_confirmed_rotation" }] : diagnosis.diagnosisCode === "workspace_not_initialized" ? [{ tool: "init", allowed: true, reasonCode: "initialize_active_root" }] : [];
|
|
5120
|
+
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." : "";
|
|
5121
|
+
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.");
|
|
4842
5122
|
return structuredToolResult({
|
|
4843
5123
|
schemaVersion: 1,
|
|
4844
5124
|
outcome: diagnosis.diagnosisCode === "project_bound" ? "completed" : "blocked",
|
|
@@ -4850,6 +5130,8 @@ Next: ${manual.nextStep}`,
|
|
|
4850
5130
|
projectMarkerState: marker.kind,
|
|
4851
5131
|
siteState: site.kind,
|
|
4852
5132
|
recoveryState,
|
|
5133
|
+
credentialRotationState,
|
|
5134
|
+
...credentialRotationDetails !== void 0 ? { credentialRotationDetails } : {},
|
|
4853
5135
|
reportRecommended,
|
|
4854
5136
|
...helpAuthorization !== void 0 ? { helpAuthorization } : {},
|
|
4855
5137
|
...args.failedTool !== void 0 ? { failedTool: args.failedTool } : {},
|
|
@@ -4865,6 +5147,128 @@ Next: ${manual.nextStep}`,
|
|
|
4865
5147
|
);
|
|
4866
5148
|
}
|
|
4867
5149
|
|
|
5150
|
+
// src/tools/credential.ts
|
|
5151
|
+
import { z as z5 } from "zod";
|
|
5152
|
+
function registerCredentialTools(server, baseCtx) {
|
|
5153
|
+
server.registerTool(
|
|
5154
|
+
"rotate",
|
|
5155
|
+
{
|
|
5156
|
+
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.",
|
|
5157
|
+
inputSchema: {
|
|
5158
|
+
confirmed: z5.boolean().optional().describe(
|
|
5159
|
+
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
5160
|
+
)
|
|
5161
|
+
},
|
|
5162
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5163
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
5164
|
+
},
|
|
5165
|
+
async (args) => {
|
|
5166
|
+
let releaseLock;
|
|
5167
|
+
try {
|
|
5168
|
+
const ctx = await withProjectDir(baseCtx);
|
|
5169
|
+
let site = requireSiteFile(ctx);
|
|
5170
|
+
const pending = loadCredentialRotation(ctx.projectDir);
|
|
5171
|
+
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
5172
|
+
releaseLock = acquireSiteHandoffLock(site.siteId);
|
|
5173
|
+
}
|
|
5174
|
+
if (pending.kind !== "absent") {
|
|
5175
|
+
const resumed = await resumeCredentialRotation(
|
|
5176
|
+
ctx.client,
|
|
5177
|
+
ctx.projectDir,
|
|
5178
|
+
site,
|
|
5179
|
+
ctx.apiBaseUrl
|
|
5180
|
+
);
|
|
5181
|
+
if (!resumed) throw new Error("Credential rotation resume state disappeared.");
|
|
5182
|
+
site = resumed.site;
|
|
5183
|
+
return structuredToolResult({
|
|
5184
|
+
schemaVersion: 1,
|
|
5185
|
+
outcome: "completed",
|
|
5186
|
+
resultCode: "credential_rotation_resumed",
|
|
5187
|
+
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.`,
|
|
5188
|
+
data: {
|
|
5189
|
+
siteId: site.siteId,
|
|
5190
|
+
credentialCreatedAt: resumed.status.credentialCreatedAt,
|
|
5191
|
+
rotationRecommended: false,
|
|
5192
|
+
previousCredentialsRevoked: true,
|
|
5193
|
+
resumedAfterInterruption: true,
|
|
5194
|
+
credentialStoredLocally: true
|
|
5195
|
+
},
|
|
5196
|
+
nextActions: [{ tool: "status", allowed: true }]
|
|
5197
|
+
});
|
|
5198
|
+
}
|
|
5199
|
+
const status = await ctx.client.getCredentialStatus(site.siteId, site.credential);
|
|
5200
|
+
const confirmation = { confirmed: true };
|
|
5201
|
+
if (args.confirmed !== true) {
|
|
5202
|
+
return structuredToolResult({
|
|
5203
|
+
schemaVersion: 1,
|
|
5204
|
+
outcome: "waiting_user",
|
|
5205
|
+
resultCode: "credential_rotation_confirmation_required",
|
|
5206
|
+
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.`,
|
|
5207
|
+
data: {
|
|
5208
|
+
siteId: site.siteId,
|
|
5209
|
+
credentialCreatedAt: status.credentialCreatedAt,
|
|
5210
|
+
credentialAgeSeconds: status.ageSeconds,
|
|
5211
|
+
rotationRecommended: status.rotationRecommended,
|
|
5212
|
+
confirmation,
|
|
5213
|
+
confirmArguments: confirmation,
|
|
5214
|
+
previousCredentialsWillBeRevoked: true,
|
|
5215
|
+
optional: true
|
|
5216
|
+
},
|
|
5217
|
+
userAction: {
|
|
5218
|
+
type: "confirm_in_mcp",
|
|
5219
|
+
provider: "sakupa",
|
|
5220
|
+
expectedOutcome: "Generate one new local credential and revoke every previous credential for this site.",
|
|
5221
|
+
resumeWith: { tool: "rotate", arguments: confirmation }
|
|
5222
|
+
},
|
|
5223
|
+
nextActions: [
|
|
5224
|
+
{
|
|
5225
|
+
tool: "rotate",
|
|
5226
|
+
arguments: confirmation,
|
|
5227
|
+
allowed: true,
|
|
5228
|
+
reasonCode: "explicit_credential_rotation_confirmation"
|
|
5229
|
+
}
|
|
5230
|
+
]
|
|
5231
|
+
});
|
|
5232
|
+
}
|
|
5233
|
+
writeCredentialRotation(ctx.projectDir, {
|
|
5234
|
+
siteId: site.siteId,
|
|
5235
|
+
candidateCredential: generateCredential(),
|
|
5236
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5237
|
+
apiBaseUrl: ctx.apiBaseUrl
|
|
5238
|
+
});
|
|
5239
|
+
const completed = await resumeCredentialRotation(
|
|
5240
|
+
ctx.client,
|
|
5241
|
+
ctx.projectDir,
|
|
5242
|
+
site,
|
|
5243
|
+
ctx.apiBaseUrl
|
|
5244
|
+
);
|
|
5245
|
+
if (!completed) throw new Error("Credential rotation did not produce resumable state.");
|
|
5246
|
+
site = completed.site;
|
|
5247
|
+
return structuredToolResult({
|
|
5248
|
+
schemaVersion: 1,
|
|
5249
|
+
outcome: "completed",
|
|
5250
|
+
resultCode: "credential_rotated",
|
|
5251
|
+
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.`,
|
|
5252
|
+
data: {
|
|
5253
|
+
siteId: site.siteId,
|
|
5254
|
+
credentialCreatedAt: completed.status.credentialCreatedAt,
|
|
5255
|
+
rotationRecommended: false,
|
|
5256
|
+
previousCredentialsRevoked: true,
|
|
5257
|
+
revokedPreviousCredentials: completed.rotation?.revokedPreviousCredentials ?? null,
|
|
5258
|
+
resumedAfterInterruption: false,
|
|
5259
|
+
credentialStoredLocally: true
|
|
5260
|
+
},
|
|
5261
|
+
nextActions: [{ tool: "status", allowed: true }]
|
|
5262
|
+
});
|
|
5263
|
+
} catch (error) {
|
|
5264
|
+
return toolError(error);
|
|
5265
|
+
} finally {
|
|
5266
|
+
releaseLock?.();
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
);
|
|
5270
|
+
}
|
|
5271
|
+
|
|
4868
5272
|
// src/server.ts
|
|
4869
5273
|
import { resolve as resolve6 } from "node:path";
|
|
4870
5274
|
var instructionsFor = (hostPattern) => `Sakupa publishes AI-made static websites. AI-made pages, live in seconds.
|
|
@@ -4877,7 +5281,9 @@ Workflow:
|
|
|
4877
5281
|
site (public URL ${hostPattern}, valid 24 hours, free banner shown) and stores the
|
|
4878
5282
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
4879
5283
|
its validity; refresh extends validity without uploading; status shows the
|
|
4880
|
-
current deployment and serving state at any time.
|
|
5284
|
+
current deployment and serving state at any time. Every update checks the credential's
|
|
5285
|
+
server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
|
|
5286
|
+
optional rotate tool; never rotate without the user's explicit confirmation.
|
|
4881
5287
|
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
|
|
4882
5288
|
shows the catalog; subscribe -> Stripe-hosted checkout;
|
|
4883
5289
|
water/personal/share/business). Paying makes the
|
|
@@ -4945,6 +5351,9 @@ Safety boundaries:
|
|
|
4945
5351
|
- Never repeat, echo, or memorize the credential value from .sakupa/site.json \u2014 quoting it
|
|
4946
5352
|
into the conversation copies the site's only key outside the protected local file. Read it
|
|
4947
5353
|
only through the tools.
|
|
5354
|
+
- rotate always previews first. confirmed:true revokes EVERY prior credential, including old
|
|
5355
|
+
folders and backups. Show that consequence and obtain explicit user approval; rotation is
|
|
5356
|
+
optional and never a condition for deploy.
|
|
4948
5357
|
- Before deploying, if the entry HTML lacks a lang attribute, add one matching the content
|
|
4949
5358
|
language (infer it from the content) and then deploy; only skip when the user explicitly
|
|
4950
5359
|
wants no lang attribute.
|
|
@@ -4988,6 +5397,7 @@ function createSakupaMcpServer(opts) {
|
|
|
4988
5397
|
};
|
|
4989
5398
|
registerTools(server, ctx);
|
|
4990
5399
|
registerBillingTools(server, ctx);
|
|
5400
|
+
registerCredentialTools(server, ctx);
|
|
4991
5401
|
registerHelpTools(server, ctx);
|
|
4992
5402
|
return server;
|
|
4993
5403
|
}
|