@sakupa/mcp 0.7.22 → 0.7.23
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 +936 -52
- package/dist/index.js +931 -47
- package/package.json +3 -2
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.23";
|
|
133
133
|
|
|
134
134
|
// ../core/dist/domain/errors.js
|
|
135
135
|
var HTTP_STATUS = {
|
|
@@ -160,8 +160,8 @@ var SakupaError = class extends Error {
|
|
|
160
160
|
this.details = details;
|
|
161
161
|
}
|
|
162
162
|
};
|
|
163
|
-
function isSakupaError(
|
|
164
|
-
return
|
|
163
|
+
function isSakupaError(err2) {
|
|
164
|
+
return err2 instanceof SakupaError;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// ../core/dist/domain/tiers.js
|
|
@@ -436,7 +436,12 @@ function safeDecode(bytes) {
|
|
|
436
436
|
}
|
|
437
437
|
|
|
438
438
|
// ../core/dist/domain/credentials.js
|
|
439
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
440
|
+
var CREDENTIAL_PREFIX = "sk_";
|
|
439
441
|
var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
442
|
+
function generateCredential(random = randomBytes) {
|
|
443
|
+
return CREDENTIAL_PREFIX + random(32).toString("base64url");
|
|
444
|
+
}
|
|
440
445
|
|
|
441
446
|
// ../core/dist/domain/hashing.js
|
|
442
447
|
async function sha256Hex(bytes) {
|
|
@@ -580,6 +585,16 @@ var HttpApiClient = class {
|
|
|
580
585
|
credential
|
|
581
586
|
});
|
|
582
587
|
}
|
|
588
|
+
async getSiteArchive(siteId, credential) {
|
|
589
|
+
return this.call(
|
|
590
|
+
"GET",
|
|
591
|
+
`/v1/sites/${encodeURIComponent(siteId)}/archive`,
|
|
592
|
+
{ credential }
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
async downloadArchive(url) {
|
|
596
|
+
return this.transport.download(url);
|
|
597
|
+
}
|
|
583
598
|
async deleteSite(siteId, credential, req) {
|
|
584
599
|
return this.call("POST", `/v1/sites/${encodeURIComponent(siteId)}/delete`, {
|
|
585
600
|
credential,
|
|
@@ -675,7 +690,7 @@ var HttpApiClient = class {
|
|
|
675
690
|
// src/tools/definitions.ts
|
|
676
691
|
import { randomUUID } from "node:crypto";
|
|
677
692
|
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
678
|
-
import { join as
|
|
693
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
679
694
|
import { z as z3 } from "zod";
|
|
680
695
|
|
|
681
696
|
// src/analyze/analyzer.ts
|
|
@@ -716,8 +731,8 @@ async function isFile(path) {
|
|
|
716
731
|
}
|
|
717
732
|
async function readTextIfExists(path, maxBytes = CONTENT_READ_MAX_BYTES) {
|
|
718
733
|
try {
|
|
719
|
-
const
|
|
720
|
-
if (!
|
|
734
|
+
const stat2 = await fs.stat(path);
|
|
735
|
+
if (!stat2.isFile() || stat2.size > maxBytes) return null;
|
|
721
736
|
return await fs.readFile(path, "utf8");
|
|
722
737
|
} catch {
|
|
723
738
|
return null;
|
|
@@ -757,8 +772,8 @@ async function walkFiles(dir, opts) {
|
|
|
757
772
|
await recurse(join(current, entry.name), rel);
|
|
758
773
|
} else if (entry.isFile()) {
|
|
759
774
|
try {
|
|
760
|
-
const
|
|
761
|
-
out.push({ path: rel, size:
|
|
775
|
+
const stat2 = await fs.stat(join(current, entry.name));
|
|
776
|
+
out.push({ path: rel, size: stat2.size });
|
|
762
777
|
} catch {
|
|
763
778
|
}
|
|
764
779
|
}
|
|
@@ -1102,19 +1117,23 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
|
|
|
1102
1117
|
import { dirname, join as join2 } from "node:path";
|
|
1103
1118
|
var SITE_DIR = ".sakupa";
|
|
1104
1119
|
var SITE_FILE = "site.json";
|
|
1120
|
+
var RECOVERY_FILE = "recovery.json";
|
|
1105
1121
|
function siteFilePath(projectDir) {
|
|
1106
1122
|
return join2(projectDir, SITE_DIR, SITE_FILE);
|
|
1107
1123
|
}
|
|
1124
|
+
function recoveryFilePath(projectDir) {
|
|
1125
|
+
return join2(projectDir, SITE_DIR, RECOVERY_FILE);
|
|
1126
|
+
}
|
|
1108
1127
|
function loadSiteFile(projectDir) {
|
|
1109
1128
|
const path = siteFilePath(projectDir);
|
|
1110
1129
|
if (!existsSync(path)) return { kind: "absent" };
|
|
1111
1130
|
let raw;
|
|
1112
1131
|
try {
|
|
1113
1132
|
raw = readFileSync(path, "utf8");
|
|
1114
|
-
} catch (
|
|
1133
|
+
} catch (err2) {
|
|
1115
1134
|
return {
|
|
1116
1135
|
kind: "corrupted",
|
|
1117
|
-
problem: `the file exists but could not be read (${
|
|
1136
|
+
problem: `the file exists but could not be read (${err2 instanceof Error ? err2.message : String(err2)})`
|
|
1118
1137
|
};
|
|
1119
1138
|
}
|
|
1120
1139
|
let parsed;
|
|
@@ -1151,6 +1170,40 @@ function loadSiteFile(projectDir) {
|
|
|
1151
1170
|
}
|
|
1152
1171
|
};
|
|
1153
1172
|
}
|
|
1173
|
+
function loadRecoveryFile(projectDir) {
|
|
1174
|
+
const path = recoveryFilePath(projectDir);
|
|
1175
|
+
if (!existsSync(path)) return null;
|
|
1176
|
+
try {
|
|
1177
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1178
|
+
if (typeof parsed.verificationId !== "string" || parsed.verificationId.length === 0 || typeof parsed.credential !== "string" || !CREDENTIAL_PATTERN.test(parsed.credential)) {
|
|
1179
|
+
throw new Error("required recovery fields are missing or invalid");
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
verificationId: parsed.verificationId,
|
|
1183
|
+
credential: parsed.credential,
|
|
1184
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : ""
|
|
1185
|
+
};
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Recovery state ${path} is damaged (${error instanceof Error ? error.message : String(error)}). Do not start another DNS recovery until this file is repaired or deliberately removed.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function writeRecoveryFile(projectDir, file) {
|
|
1193
|
+
const dir = join2(projectDir, SITE_DIR);
|
|
1194
|
+
mkdirSync(dir, { recursive: true });
|
|
1195
|
+
const path = join2(dir, RECOVERY_FILE);
|
|
1196
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}
|
|
1197
|
+
`, "utf8");
|
|
1198
|
+
try {
|
|
1199
|
+
chmodSync(path, 384);
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
function deleteRecoveryFile(projectDir) {
|
|
1204
|
+
const path = recoveryFilePath(projectDir);
|
|
1205
|
+
if (existsSync(path)) rmSync(path, { force: true });
|
|
1206
|
+
}
|
|
1154
1207
|
function siteFileRecoveryGuidance(projectDir) {
|
|
1155
1208
|
return `The site itself is intact on the server; only the local binding file (${siteFilePath(projectDir)}) is the problem. Restore the file (from a backup or by undoing the local edit). Do NOT delete it to work around the error: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site. Publishing this project as a brand-NEW site requires the user to manually delete the .sakupa directory first \u2014 the tool will never overwrite it.`;
|
|
1156
1209
|
}
|
|
@@ -1202,14 +1255,643 @@ function credentialGitReminder(projectDir) {
|
|
|
1202
1255
|
return '\nNOTE: this project is inside a git repository. The management credential in .sakupa/site.json is the key to this site \u2014 do NOT commit it to a PUBLIC repository (add ".sakupa/" to .gitignore yourself if you want to keep it out of version control).';
|
|
1203
1256
|
}
|
|
1204
1257
|
|
|
1258
|
+
// src/recovery-archive.ts
|
|
1259
|
+
import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1260
|
+
import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2, sep as sep2 } from "node:path";
|
|
1261
|
+
|
|
1262
|
+
// ../../node_modules/fflate/esm/index.mjs
|
|
1263
|
+
import { createRequire } from "module";
|
|
1264
|
+
var require2 = createRequire("/");
|
|
1265
|
+
var _a;
|
|
1266
|
+
var Worker;
|
|
1267
|
+
var isMarkedAsUntransferable;
|
|
1268
|
+
try {
|
|
1269
|
+
_a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
}
|
|
1272
|
+
var u8 = Uint8Array;
|
|
1273
|
+
var u16 = Uint16Array;
|
|
1274
|
+
var i32 = Int32Array;
|
|
1275
|
+
var fleb = new u8([
|
|
1276
|
+
0,
|
|
1277
|
+
0,
|
|
1278
|
+
0,
|
|
1279
|
+
0,
|
|
1280
|
+
0,
|
|
1281
|
+
0,
|
|
1282
|
+
0,
|
|
1283
|
+
0,
|
|
1284
|
+
1,
|
|
1285
|
+
1,
|
|
1286
|
+
1,
|
|
1287
|
+
1,
|
|
1288
|
+
2,
|
|
1289
|
+
2,
|
|
1290
|
+
2,
|
|
1291
|
+
2,
|
|
1292
|
+
3,
|
|
1293
|
+
3,
|
|
1294
|
+
3,
|
|
1295
|
+
3,
|
|
1296
|
+
4,
|
|
1297
|
+
4,
|
|
1298
|
+
4,
|
|
1299
|
+
4,
|
|
1300
|
+
5,
|
|
1301
|
+
5,
|
|
1302
|
+
5,
|
|
1303
|
+
5,
|
|
1304
|
+
0,
|
|
1305
|
+
/* unused */
|
|
1306
|
+
0,
|
|
1307
|
+
0,
|
|
1308
|
+
/* impossible */
|
|
1309
|
+
0
|
|
1310
|
+
]);
|
|
1311
|
+
var fdeb = new u8([
|
|
1312
|
+
0,
|
|
1313
|
+
0,
|
|
1314
|
+
0,
|
|
1315
|
+
0,
|
|
1316
|
+
1,
|
|
1317
|
+
1,
|
|
1318
|
+
2,
|
|
1319
|
+
2,
|
|
1320
|
+
3,
|
|
1321
|
+
3,
|
|
1322
|
+
4,
|
|
1323
|
+
4,
|
|
1324
|
+
5,
|
|
1325
|
+
5,
|
|
1326
|
+
6,
|
|
1327
|
+
6,
|
|
1328
|
+
7,
|
|
1329
|
+
7,
|
|
1330
|
+
8,
|
|
1331
|
+
8,
|
|
1332
|
+
9,
|
|
1333
|
+
9,
|
|
1334
|
+
10,
|
|
1335
|
+
10,
|
|
1336
|
+
11,
|
|
1337
|
+
11,
|
|
1338
|
+
12,
|
|
1339
|
+
12,
|
|
1340
|
+
13,
|
|
1341
|
+
13,
|
|
1342
|
+
/* unused */
|
|
1343
|
+
0,
|
|
1344
|
+
0
|
|
1345
|
+
]);
|
|
1346
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
1347
|
+
var freb = function(eb, start) {
|
|
1348
|
+
var b = new u16(31);
|
|
1349
|
+
for (var i = 0; i < 31; ++i) {
|
|
1350
|
+
b[i] = start += 1 << eb[i - 1];
|
|
1351
|
+
}
|
|
1352
|
+
var r = new i32(b[30]);
|
|
1353
|
+
for (var i = 1; i < 30; ++i) {
|
|
1354
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
1355
|
+
r[j] = j - b[i] << 5 | i;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return { b, r };
|
|
1359
|
+
};
|
|
1360
|
+
var _a = freb(fleb, 2);
|
|
1361
|
+
var fl = _a.b;
|
|
1362
|
+
var revfl = _a.r;
|
|
1363
|
+
fl[28] = 258, revfl[258] = 28;
|
|
1364
|
+
var _b = freb(fdeb, 0);
|
|
1365
|
+
var fd = _b.b;
|
|
1366
|
+
var revfd = _b.r;
|
|
1367
|
+
var rev = new u16(32768);
|
|
1368
|
+
for (i = 0; i < 32768; ++i) {
|
|
1369
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
1370
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
1371
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
1372
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
1373
|
+
}
|
|
1374
|
+
var x;
|
|
1375
|
+
var i;
|
|
1376
|
+
var hMap = function(cd, mb, r) {
|
|
1377
|
+
var s = cd.length;
|
|
1378
|
+
var i = 0;
|
|
1379
|
+
var l = new u16(mb);
|
|
1380
|
+
for (; i < s; ++i) {
|
|
1381
|
+
if (cd[i])
|
|
1382
|
+
++l[cd[i] - 1];
|
|
1383
|
+
}
|
|
1384
|
+
var le = new u16(mb);
|
|
1385
|
+
for (i = 1; i < mb; ++i) {
|
|
1386
|
+
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
1387
|
+
}
|
|
1388
|
+
var co;
|
|
1389
|
+
if (r) {
|
|
1390
|
+
co = new u16(1 << mb);
|
|
1391
|
+
var rvb = 15 - mb;
|
|
1392
|
+
for (i = 0; i < s; ++i) {
|
|
1393
|
+
if (cd[i]) {
|
|
1394
|
+
var sv = i << 4 | cd[i];
|
|
1395
|
+
var r_1 = mb - cd[i];
|
|
1396
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
1397
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
1398
|
+
co[rev[v] >> rvb] = sv;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
} else {
|
|
1403
|
+
co = new u16(s);
|
|
1404
|
+
for (i = 0; i < s; ++i) {
|
|
1405
|
+
if (cd[i]) {
|
|
1406
|
+
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
return co;
|
|
1411
|
+
};
|
|
1412
|
+
var flt = new u8(288);
|
|
1413
|
+
for (i = 0; i < 144; ++i)
|
|
1414
|
+
flt[i] = 8;
|
|
1415
|
+
var i;
|
|
1416
|
+
for (i = 144; i < 256; ++i)
|
|
1417
|
+
flt[i] = 9;
|
|
1418
|
+
var i;
|
|
1419
|
+
for (i = 256; i < 280; ++i)
|
|
1420
|
+
flt[i] = 7;
|
|
1421
|
+
var i;
|
|
1422
|
+
for (i = 280; i < 288; ++i)
|
|
1423
|
+
flt[i] = 8;
|
|
1424
|
+
var i;
|
|
1425
|
+
var fdt = new u8(32);
|
|
1426
|
+
for (i = 0; i < 32; ++i)
|
|
1427
|
+
fdt[i] = 5;
|
|
1428
|
+
var i;
|
|
1429
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
1430
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
1431
|
+
var max = function(a) {
|
|
1432
|
+
var m = a[0];
|
|
1433
|
+
for (var i = 1; i < a.length; ++i) {
|
|
1434
|
+
if (a[i] > m)
|
|
1435
|
+
m = a[i];
|
|
1436
|
+
}
|
|
1437
|
+
return m;
|
|
1438
|
+
};
|
|
1439
|
+
var bits = function(d, p, m) {
|
|
1440
|
+
var o = p / 8 | 0;
|
|
1441
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
1442
|
+
};
|
|
1443
|
+
var bits16 = function(d, p) {
|
|
1444
|
+
var o = p / 8 | 0;
|
|
1445
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
1446
|
+
};
|
|
1447
|
+
var shft = function(p) {
|
|
1448
|
+
return (p + 7) / 8 | 0;
|
|
1449
|
+
};
|
|
1450
|
+
var slc = function(v, s, e) {
|
|
1451
|
+
if (s == null || s < 0)
|
|
1452
|
+
s = 0;
|
|
1453
|
+
if (e == null || e > v.length)
|
|
1454
|
+
e = v.length;
|
|
1455
|
+
return new u8(v.subarray(s, e));
|
|
1456
|
+
};
|
|
1457
|
+
var ec = [
|
|
1458
|
+
"unexpected EOF",
|
|
1459
|
+
"invalid block type",
|
|
1460
|
+
"invalid length/literal",
|
|
1461
|
+
"invalid distance",
|
|
1462
|
+
"stream finished",
|
|
1463
|
+
"no stream handler",
|
|
1464
|
+
,
|
|
1465
|
+
// determined by compression function
|
|
1466
|
+
"no callback",
|
|
1467
|
+
"invalid UTF-8 data",
|
|
1468
|
+
"extra field too long",
|
|
1469
|
+
"date not in range 1980-2099",
|
|
1470
|
+
"filename too long",
|
|
1471
|
+
"stream finishing",
|
|
1472
|
+
"invalid zip data"
|
|
1473
|
+
// determined by unknown compression method
|
|
1474
|
+
];
|
|
1475
|
+
var err = function(ind, msg, nt) {
|
|
1476
|
+
var e = new Error(msg || ec[ind]);
|
|
1477
|
+
e.code = ind;
|
|
1478
|
+
if (Error.captureStackTrace)
|
|
1479
|
+
Error.captureStackTrace(e, err);
|
|
1480
|
+
if (!nt)
|
|
1481
|
+
throw e;
|
|
1482
|
+
return e;
|
|
1483
|
+
};
|
|
1484
|
+
var inflt = function(dat, st, buf, dict) {
|
|
1485
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
1486
|
+
if (!sl || st.f && !st.l)
|
|
1487
|
+
return buf || new u8(0);
|
|
1488
|
+
var noBuf = !buf;
|
|
1489
|
+
var resize = noBuf || st.i != 2;
|
|
1490
|
+
var noSt = st.i;
|
|
1491
|
+
if (noBuf)
|
|
1492
|
+
buf = new u8(sl * 3);
|
|
1493
|
+
var cbuf = function(l2) {
|
|
1494
|
+
var bl = buf.length;
|
|
1495
|
+
if (l2 > bl) {
|
|
1496
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
1497
|
+
nbuf.set(buf);
|
|
1498
|
+
buf = nbuf;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
1502
|
+
var tbts = sl * 8;
|
|
1503
|
+
do {
|
|
1504
|
+
if (!lm) {
|
|
1505
|
+
final = bits(dat, pos, 1);
|
|
1506
|
+
var type = bits(dat, pos + 1, 3);
|
|
1507
|
+
pos += 3;
|
|
1508
|
+
if (!type) {
|
|
1509
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
1510
|
+
if (t > sl) {
|
|
1511
|
+
if (noSt)
|
|
1512
|
+
err(0);
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
if (resize)
|
|
1516
|
+
cbuf(bt + l);
|
|
1517
|
+
buf.set(dat.subarray(s, t), bt);
|
|
1518
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
1519
|
+
continue;
|
|
1520
|
+
} else if (type == 1)
|
|
1521
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
1522
|
+
else if (type == 2) {
|
|
1523
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
1524
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
1525
|
+
pos += 14;
|
|
1526
|
+
var ldt = new u8(tl);
|
|
1527
|
+
var clt = new u8(19);
|
|
1528
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
1529
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
1530
|
+
}
|
|
1531
|
+
pos += hcLen * 3;
|
|
1532
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
1533
|
+
var clm = hMap(clt, clb, 1);
|
|
1534
|
+
for (var i = 0; i < tl; ) {
|
|
1535
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
1536
|
+
pos += r & 15;
|
|
1537
|
+
var s = r >> 4;
|
|
1538
|
+
if (s < 16) {
|
|
1539
|
+
ldt[i++] = s;
|
|
1540
|
+
} else {
|
|
1541
|
+
var c = 0, n = 0;
|
|
1542
|
+
if (s == 16)
|
|
1543
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
1544
|
+
else if (s == 17)
|
|
1545
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
1546
|
+
else if (s == 18)
|
|
1547
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
1548
|
+
while (n--)
|
|
1549
|
+
ldt[i++] = c;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
1553
|
+
lbt = max(lt);
|
|
1554
|
+
dbt = max(dt);
|
|
1555
|
+
lm = hMap(lt, lbt, 1);
|
|
1556
|
+
dm = hMap(dt, dbt, 1);
|
|
1557
|
+
} else
|
|
1558
|
+
err(1);
|
|
1559
|
+
if (pos > tbts) {
|
|
1560
|
+
if (noSt)
|
|
1561
|
+
err(0);
|
|
1562
|
+
break;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (resize)
|
|
1566
|
+
cbuf(bt + 131072);
|
|
1567
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
1568
|
+
var lpos = pos;
|
|
1569
|
+
for (; ; lpos = pos) {
|
|
1570
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
1571
|
+
pos += c & 15;
|
|
1572
|
+
if (pos > tbts) {
|
|
1573
|
+
if (noSt)
|
|
1574
|
+
err(0);
|
|
1575
|
+
break;
|
|
1576
|
+
}
|
|
1577
|
+
if (!c)
|
|
1578
|
+
err(2);
|
|
1579
|
+
if (sym < 256)
|
|
1580
|
+
buf[bt++] = sym;
|
|
1581
|
+
else if (sym == 256) {
|
|
1582
|
+
lpos = pos, lm = null;
|
|
1583
|
+
break;
|
|
1584
|
+
} else {
|
|
1585
|
+
var add = sym - 254;
|
|
1586
|
+
if (sym > 264) {
|
|
1587
|
+
var i = sym - 257, b = fleb[i];
|
|
1588
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
1589
|
+
pos += b;
|
|
1590
|
+
}
|
|
1591
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
1592
|
+
if (!d)
|
|
1593
|
+
err(3);
|
|
1594
|
+
pos += d & 15;
|
|
1595
|
+
var dt = fd[dsym];
|
|
1596
|
+
if (dsym > 3) {
|
|
1597
|
+
var b = fdeb[dsym];
|
|
1598
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
1599
|
+
}
|
|
1600
|
+
if (pos > tbts) {
|
|
1601
|
+
if (noSt)
|
|
1602
|
+
err(0);
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
if (resize)
|
|
1606
|
+
cbuf(bt + 131072);
|
|
1607
|
+
var end = bt + add;
|
|
1608
|
+
if (bt < dt) {
|
|
1609
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
1610
|
+
if (shift + bt < 0)
|
|
1611
|
+
err(3);
|
|
1612
|
+
for (; bt < dend; ++bt)
|
|
1613
|
+
buf[bt] = dict[shift + bt];
|
|
1614
|
+
}
|
|
1615
|
+
for (; bt < end; ++bt)
|
|
1616
|
+
buf[bt] = buf[bt - dt];
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
1620
|
+
if (lm)
|
|
1621
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
1622
|
+
} while (!final);
|
|
1623
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
1624
|
+
};
|
|
1625
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
1626
|
+
var b2 = function(d, b) {
|
|
1627
|
+
return d[b] | d[b + 1] << 8;
|
|
1628
|
+
};
|
|
1629
|
+
var b4 = function(d, b) {
|
|
1630
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
1631
|
+
};
|
|
1632
|
+
var b8 = function(d, b) {
|
|
1633
|
+
return b4(d, b) + b4(d, b + 4) * 4294967296;
|
|
1634
|
+
};
|
|
1635
|
+
function inflateSync(data, opts) {
|
|
1636
|
+
return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
|
|
1637
|
+
}
|
|
1638
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
1639
|
+
var tds = 0;
|
|
1640
|
+
try {
|
|
1641
|
+
td.decode(et, { stream: true });
|
|
1642
|
+
tds = 1;
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
}
|
|
1645
|
+
var dutf8 = function(d) {
|
|
1646
|
+
for (var r = "", i = 0; ; ) {
|
|
1647
|
+
var c = d[i++];
|
|
1648
|
+
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
1649
|
+
if (i + eb > d.length)
|
|
1650
|
+
return { s: r, r: slc(d, i - 1) };
|
|
1651
|
+
if (!eb)
|
|
1652
|
+
r += String.fromCharCode(c);
|
|
1653
|
+
else if (eb == 3) {
|
|
1654
|
+
c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
|
1655
|
+
} else if (eb & 1)
|
|
1656
|
+
r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
|
|
1657
|
+
else
|
|
1658
|
+
r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
function strFromU8(dat, latin1) {
|
|
1662
|
+
if (latin1) {
|
|
1663
|
+
var r = "";
|
|
1664
|
+
for (var i = 0; i < dat.length; i += 16384)
|
|
1665
|
+
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
|
|
1666
|
+
return r;
|
|
1667
|
+
} else if (td) {
|
|
1668
|
+
return td.decode(dat);
|
|
1669
|
+
} else {
|
|
1670
|
+
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
1671
|
+
if (r.length)
|
|
1672
|
+
err(8);
|
|
1673
|
+
return s;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
var slzh = function(d, b) {
|
|
1677
|
+
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1678
|
+
};
|
|
1679
|
+
var zh = function(d, b, z6) {
|
|
1680
|
+
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;
|
|
1681
|
+
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];
|
|
1682
|
+
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
1683
|
+
};
|
|
1684
|
+
var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
1685
|
+
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
1686
|
+
var nf = nsc + nsu + noff;
|
|
1687
|
+
if (z6 && nf) {
|
|
1688
|
+
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
1689
|
+
if (b2(d, b) == 1) {
|
|
1690
|
+
return [
|
|
1691
|
+
nsc ? b8(d, b + 4 + 8 * nsu) : sc,
|
|
1692
|
+
nsu ? b8(d, b + 4) : su,
|
|
1693
|
+
noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off,
|
|
1694
|
+
1
|
|
1695
|
+
];
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (z6 < 2)
|
|
1699
|
+
err(13);
|
|
1700
|
+
}
|
|
1701
|
+
return [sc, su, off, 0];
|
|
1702
|
+
};
|
|
1703
|
+
function unzipSync(data, opts) {
|
|
1704
|
+
var files = {};
|
|
1705
|
+
var e = data.length - 22;
|
|
1706
|
+
for (; b4(data, e) != 101010256; --e) {
|
|
1707
|
+
if (!e || data.length - e > 65558)
|
|
1708
|
+
err(13);
|
|
1709
|
+
}
|
|
1710
|
+
;
|
|
1711
|
+
var c = b2(data, e + 8);
|
|
1712
|
+
if (!c)
|
|
1713
|
+
return {};
|
|
1714
|
+
var o = b4(data, e + 16);
|
|
1715
|
+
var z6 = b4(data, e - 20) == 117853008;
|
|
1716
|
+
if (z6) {
|
|
1717
|
+
var ze = b4(data, e - 12);
|
|
1718
|
+
z6 = b4(data, ze) == 101075792;
|
|
1719
|
+
if (z6) {
|
|
1720
|
+
c = b4(data, ze + 32);
|
|
1721
|
+
o = b4(data, ze + 48);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
var fltr = opts && opts.filter;
|
|
1725
|
+
for (var i = 0; i < c; ++i) {
|
|
1726
|
+
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);
|
|
1727
|
+
o = no;
|
|
1728
|
+
if (!fltr || fltr({
|
|
1729
|
+
name: fn,
|
|
1730
|
+
size: sc,
|
|
1731
|
+
originalSize: su,
|
|
1732
|
+
compression: c_2
|
|
1733
|
+
})) {
|
|
1734
|
+
if (!c_2)
|
|
1735
|
+
files[fn] = slc(data, b, b + sc);
|
|
1736
|
+
else if (c_2 == 8)
|
|
1737
|
+
files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
|
|
1738
|
+
else
|
|
1739
|
+
err(14, "unknown compression type " + c_2);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return files;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/recovery-archive.ts
|
|
1746
|
+
function safeOutputPath(projectDir, outputDir) {
|
|
1747
|
+
if (outputDir.length === 0 || isAbsolute(outputDir)) {
|
|
1748
|
+
throw new SakupaError("invalid_request", "Recovery outputDir must be a relative directory");
|
|
1749
|
+
}
|
|
1750
|
+
const root = resolve2(projectDir);
|
|
1751
|
+
const target = resolve2(root, outputDir);
|
|
1752
|
+
const rel = relative(root, target);
|
|
1753
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel)) {
|
|
1754
|
+
throw new SakupaError("invalid_request", "Recovery outputDir must stay inside projectDir");
|
|
1755
|
+
}
|
|
1756
|
+
if (rel === ".sakupa" || rel.startsWith(`.sakupa${sep2}`)) {
|
|
1757
|
+
throw new SakupaError("invalid_request", "Recovery content cannot be written inside .sakupa");
|
|
1758
|
+
}
|
|
1759
|
+
return target;
|
|
1760
|
+
}
|
|
1761
|
+
function safeEntryName(name) {
|
|
1762
|
+
if (name.length === 0 || name.startsWith("/") || name.includes("\\") || /^[A-Za-z]:/.test(name)) {
|
|
1763
|
+
throw new SakupaError("validation_failed", `Unsafe recovery archive path: ${name}`);
|
|
1764
|
+
}
|
|
1765
|
+
const segments = name.split("/");
|
|
1766
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1767
|
+
throw new SakupaError("validation_failed", `Unsafe recovery archive path: ${name}`);
|
|
1768
|
+
}
|
|
1769
|
+
return segments.join("/");
|
|
1770
|
+
}
|
|
1771
|
+
async function pathExists(path) {
|
|
1772
|
+
try {
|
|
1773
|
+
await stat(path);
|
|
1774
|
+
return true;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
if (error.code === "ENOENT") return false;
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
async function listExistingFiles(root, current = root) {
|
|
1781
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
1782
|
+
const files = [];
|
|
1783
|
+
for (const entry of entries) {
|
|
1784
|
+
if (entry.isSymbolicLink()) {
|
|
1785
|
+
throw new SakupaError("state_conflict", `Recovery output contains a symlink: ${entry.name}`);
|
|
1786
|
+
}
|
|
1787
|
+
const absolute = join3(current, entry.name);
|
|
1788
|
+
if (entry.isDirectory()) files.push(...await listExistingFiles(root, absolute));
|
|
1789
|
+
else if (entry.isFile()) files.push(relative(root, absolute).split(sep2).join("/"));
|
|
1790
|
+
else
|
|
1791
|
+
throw new SakupaError(
|
|
1792
|
+
"state_conflict",
|
|
1793
|
+
`Recovery output contains a special file: ${entry.name}`
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
return files.sort();
|
|
1797
|
+
}
|
|
1798
|
+
async function existingOutputMatches(outputDir, files) {
|
|
1799
|
+
const existing = await listExistingFiles(outputDir);
|
|
1800
|
+
const expected = Object.keys(files).sort();
|
|
1801
|
+
if (existing.length !== expected.length || existing.some((name, i) => name !== expected[i])) {
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
for (const name of expected) {
|
|
1805
|
+
const actual = await readFile(join3(outputDir, ...name.split("/")));
|
|
1806
|
+
const wanted = files[name];
|
|
1807
|
+
if (!wanted || actual.byteLength !== wanted.byteLength || !actual.equals(wanted)) return false;
|
|
1808
|
+
}
|
|
1809
|
+
return true;
|
|
1810
|
+
}
|
|
1811
|
+
async function extractRecoveryArchive(input) {
|
|
1812
|
+
if (!Number.isSafeInteger(input.expectedBytes) || input.expectedBytes < 0 || input.expectedBytes > PAID_SITE_MAX_TOTAL_BYTES || !Number.isSafeInteger(input.expectedFiles) || input.expectedFiles < 0 || input.expectedFiles > MAX_FILE_COUNT) {
|
|
1813
|
+
throw new SakupaError("validation_failed", "Recovery archive metadata exceeds product limits");
|
|
1814
|
+
}
|
|
1815
|
+
const outputDir = safeOutputPath(input.projectDir, input.outputDir ?? "html");
|
|
1816
|
+
const maxArchiveBytes = input.expectedBytes + input.expectedFiles * 4096 + 65536;
|
|
1817
|
+
if (input.archive.byteLength > maxArchiveBytes) {
|
|
1818
|
+
throw new SakupaError("validation_failed", "Recovery archive is larger than its site metadata");
|
|
1819
|
+
}
|
|
1820
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1821
|
+
let declaredBytes = 0;
|
|
1822
|
+
let declaredFiles = 0;
|
|
1823
|
+
const files = unzipSync(input.archive, {
|
|
1824
|
+
filter(file) {
|
|
1825
|
+
const name = safeEntryName(file.name);
|
|
1826
|
+
if (seen.has(name)) {
|
|
1827
|
+
throw new SakupaError("validation_failed", `Duplicate recovery archive path: ${name}`);
|
|
1828
|
+
}
|
|
1829
|
+
seen.add(name);
|
|
1830
|
+
declaredFiles += 1;
|
|
1831
|
+
declaredBytes += file.originalSize;
|
|
1832
|
+
if (declaredFiles > input.expectedFiles || declaredFiles > MAX_FILE_COUNT || declaredBytes > input.expectedBytes || declaredBytes > PAID_SITE_MAX_TOTAL_BYTES) {
|
|
1833
|
+
throw new SakupaError("validation_failed", "Recovery archive exceeds declared site limits");
|
|
1834
|
+
}
|
|
1835
|
+
return true;
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
if (declaredFiles !== input.expectedFiles || declaredBytes !== input.expectedBytes) {
|
|
1839
|
+
throw new SakupaError("validation_failed", "Recovery archive does not match site metadata");
|
|
1840
|
+
}
|
|
1841
|
+
if (await pathExists(outputDir)) {
|
|
1842
|
+
const info = await stat(outputDir);
|
|
1843
|
+
if (info.isDirectory() && await existingOutputMatches(outputDir, files)) {
|
|
1844
|
+
return {
|
|
1845
|
+
outputDir,
|
|
1846
|
+
totalBytes: declaredBytes,
|
|
1847
|
+
fileCount: declaredFiles,
|
|
1848
|
+
alreadyPresent: true
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
throw new SakupaError(
|
|
1852
|
+
"state_conflict",
|
|
1853
|
+
`Recovery output already exists with different content: ${outputDir}. Move it aside or choose another outputDir.`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
const tempDir = await mkdtemp(join3(resolve2(input.projectDir), ".sakupa-restore-"));
|
|
1857
|
+
try {
|
|
1858
|
+
let writtenBytes = 0;
|
|
1859
|
+
const entries = Object.entries(files).sort(([a], [b]) => a.localeCompare(b));
|
|
1860
|
+
for (const [rawName, data] of entries) {
|
|
1861
|
+
const name = safeEntryName(rawName);
|
|
1862
|
+
const destination = join3(tempDir, ...name.split("/"));
|
|
1863
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
1864
|
+
await writeFile(destination, data, { flag: "wx" });
|
|
1865
|
+
writtenBytes += data.byteLength;
|
|
1866
|
+
}
|
|
1867
|
+
if (entries.length !== input.expectedFiles || writtenBytes !== input.expectedBytes) {
|
|
1868
|
+
throw new SakupaError(
|
|
1869
|
+
"validation_failed",
|
|
1870
|
+
"Extracted recovery data does not match site metadata"
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
await mkdir(dirname2(outputDir), { recursive: true });
|
|
1874
|
+
await rename(tempDir, outputDir);
|
|
1875
|
+
return {
|
|
1876
|
+
outputDir,
|
|
1877
|
+
totalBytes: writtenBytes,
|
|
1878
|
+
fileCount: entries.length,
|
|
1879
|
+
alreadyPresent: false
|
|
1880
|
+
};
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
1883
|
+
throw error;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1205
1887
|
// src/creation-registry.ts
|
|
1206
1888
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1207
1889
|
import { homedir } from "node:os";
|
|
1208
|
-
import { dirname as
|
|
1890
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1209
1891
|
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1210
1892
|
function creationRegistryPath() {
|
|
1211
1893
|
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir();
|
|
1212
|
-
return
|
|
1894
|
+
return join4(base, ".sakupa", "created-sites.json");
|
|
1213
1895
|
}
|
|
1214
1896
|
function readAll() {
|
|
1215
1897
|
const path = creationRegistryPath();
|
|
@@ -1226,7 +1908,7 @@ function readAll() {
|
|
|
1226
1908
|
}
|
|
1227
1909
|
function writeAll(records) {
|
|
1228
1910
|
const path = creationRegistryPath();
|
|
1229
|
-
mkdirSync2(
|
|
1911
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1230
1912
|
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1231
1913
|
`, "utf-8");
|
|
1232
1914
|
}
|
|
@@ -1387,7 +2069,7 @@ var CLIENT_TYPE = "sakupa-mcp";
|
|
|
1387
2069
|
import { z as z2 } from "zod";
|
|
1388
2070
|
import { statSync } from "node:fs";
|
|
1389
2071
|
import { homedir as homedir2 } from "node:os";
|
|
1390
|
-
import { isAbsolute, parse, resolve as
|
|
2072
|
+
import { isAbsolute as isAbsolute2, parse, resolve as resolve3 } from "node:path";
|
|
1391
2073
|
|
|
1392
2074
|
// src/tools/result.ts
|
|
1393
2075
|
import { z } from "zod";
|
|
@@ -1446,21 +2128,21 @@ function withProjectDir(ctx, projectDirArg) {
|
|
|
1446
2128
|
"projectDir is REQUIRED on every call: pass the absolute path of the directory the user is CURRENTLY working in. The server never guesses a directory \u2014 a wrong guess once published one project's files over a different project's PAID site."
|
|
1447
2129
|
);
|
|
1448
2130
|
}
|
|
1449
|
-
if (!
|
|
2131
|
+
if (!isAbsolute2(projectDirArg)) {
|
|
1450
2132
|
throw new LocalGuidanceError(
|
|
1451
2133
|
"invalid_request",
|
|
1452
2134
|
`projectDir must be an ABSOLUTE path (got "${projectDirArg}"). Pass the full path of the directory the user is currently working in.`
|
|
1453
2135
|
);
|
|
1454
2136
|
}
|
|
1455
|
-
const dir =
|
|
2137
|
+
const dir = resolve3(projectDirArg);
|
|
1456
2138
|
if (parse(dir).root === dir || dir === homedir2()) {
|
|
1457
2139
|
throw new LocalGuidanceError(
|
|
1458
2140
|
"invalid_request",
|
|
1459
2141
|
`projectDir "${dir}" is a filesystem root or the home directory. Pass the specific project folder that holds the site's files, not a top-level directory.`
|
|
1460
2142
|
);
|
|
1461
2143
|
}
|
|
1462
|
-
const
|
|
1463
|
-
if (!
|
|
2144
|
+
const stat2 = statSync(dir, { throwIfNoEntry: false });
|
|
2145
|
+
if (!stat2?.isDirectory()) {
|
|
1464
2146
|
throw new LocalGuidanceError(
|
|
1465
2147
|
"invalid_request",
|
|
1466
2148
|
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
@@ -1598,7 +2280,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1598
2280
|
async function buildHashedManifest(files, outputAbs) {
|
|
1599
2281
|
const manifest = [];
|
|
1600
2282
|
for (const file of files) {
|
|
1601
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
2283
|
+
const bytes = new Uint8Array(await fs2.readFile(join5(outputAbs, file.path)));
|
|
1602
2284
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1603
2285
|
}
|
|
1604
2286
|
return manifest;
|
|
@@ -1617,7 +2299,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1617
2299
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1618
2300
|
);
|
|
1619
2301
|
}
|
|
1620
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
2302
|
+
const bytes = new Uint8Array(await fs2.readFile(join5(outputAbs, match.path)));
|
|
1621
2303
|
if (bytes.byteLength !== match.size) {
|
|
1622
2304
|
throw new SakupaError(
|
|
1623
2305
|
"validation_failed",
|
|
@@ -1661,15 +2343,15 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
1661
2343
|
}
|
|
1662
2344
|
}
|
|
1663
2345
|
function projectRootAbove(projectDir) {
|
|
1664
|
-
if (existsSync3(
|
|
1665
|
-
return findAncestor(projectDir, (dir) => existsSync3(
|
|
2346
|
+
if (existsSync3(join5(projectDir, "package.json"))) return null;
|
|
2347
|
+
return findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
|
|
1666
2348
|
}
|
|
1667
2349
|
function findNeighborBinding(projectDir, outputRel) {
|
|
1668
2350
|
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1669
2351
|
const above = findAncestor(projectDir, bound, 3);
|
|
1670
2352
|
if (above) return above;
|
|
1671
2353
|
if (outputRel && outputRel !== ".") {
|
|
1672
|
-
const outputAbs =
|
|
2354
|
+
const outputAbs = resolve4(projectDir, outputRel);
|
|
1673
2355
|
if (bound(outputAbs)) return outputAbs;
|
|
1674
2356
|
}
|
|
1675
2357
|
return null;
|
|
@@ -1752,7 +2434,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1752
2434
|
return notDeployableResult(analysis);
|
|
1753
2435
|
}
|
|
1754
2436
|
const files = analysis.files;
|
|
1755
|
-
const outputAbs =
|
|
2437
|
+
const outputAbs = resolve4(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1756
2438
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1757
2439
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1758
2440
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -2229,25 +2911,126 @@ Full status:`, res);
|
|
|
2229
2911
|
server.registerTool(
|
|
2230
2912
|
"recover",
|
|
2231
2913
|
{
|
|
2232
|
-
description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials.
|
|
2914
|
+
description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Recovery is resumable: start stores local pending state; complete installs and writes the new .sakupa/site.json credential BEFORE requesting content; download uses that credential to reissue an archive and safely extract it into html without repeating DNS.",
|
|
2233
2915
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2234
2916
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2235
2917
|
inputSchema: {
|
|
2236
2918
|
projectDir: projectDirInput,
|
|
2237
|
-
action: z3.enum(["start", "status", "complete"]),
|
|
2919
|
+
action: z3.enum(["start", "status", "complete", "download"]),
|
|
2238
2920
|
hostname: z3.string().optional().describe("Required for start."),
|
|
2239
|
-
verificationId: z3.string().optional().describe("
|
|
2921
|
+
verificationId: z3.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
2922
|
+
outputDir: z3.string().optional().describe("Relative extraction directory for complete/download (default: html)."),
|
|
2240
2923
|
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
2241
2924
|
}
|
|
2242
2925
|
},
|
|
2243
2926
|
async (args) => {
|
|
2244
2927
|
try {
|
|
2245
2928
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
2929
|
+
const localSite = loadSiteFile(ctx.projectDir);
|
|
2930
|
+
const localCredentialIsActive = async () => {
|
|
2931
|
+
if (localSite.kind !== "ok") return false;
|
|
2932
|
+
try {
|
|
2933
|
+
await ctx.client.getSiteStatus(localSite.file.siteId, localSite.file.credential);
|
|
2934
|
+
return true;
|
|
2935
|
+
} catch (error) {
|
|
2936
|
+
if (isSakupaError(error) && error.code === "unauthorized") return false;
|
|
2937
|
+
throw error;
|
|
2938
|
+
}
|
|
2939
|
+
};
|
|
2940
|
+
const download = async () => {
|
|
2941
|
+
const site = requireSiteFile(ctx);
|
|
2942
|
+
const archive = await ctx.client.getSiteArchive(site.siteId, site.credential);
|
|
2943
|
+
const bytes = await ctx.client.downloadArchive(archive.archiveUrl);
|
|
2944
|
+
const extracted = await extractRecoveryArchive({
|
|
2945
|
+
projectDir: ctx.projectDir,
|
|
2946
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
|
|
2947
|
+
archive: bytes,
|
|
2948
|
+
expectedBytes: archive.totalBytes,
|
|
2949
|
+
expectedFiles: archive.fileCount
|
|
2950
|
+
});
|
|
2951
|
+
deleteRecoveryFile(ctx.projectDir);
|
|
2952
|
+
return { archive, extracted };
|
|
2953
|
+
};
|
|
2954
|
+
if (args.action === "download") {
|
|
2955
|
+
const { archive, extracted } = await download();
|
|
2956
|
+
return text(
|
|
2957
|
+
"recovery_content_downloaded",
|
|
2958
|
+
`Recovered site content with the credential in .sakupa/site.json.
|
|
2959
|
+
Output directory: ${extracted.outputDir}
|
|
2960
|
+
Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
2961
|
+
No DNS verification was started or repeated.`,
|
|
2962
|
+
{
|
|
2963
|
+
siteId: archive.siteId,
|
|
2964
|
+
outputDir: extracted.outputDir,
|
|
2965
|
+
fileCount: extracted.fileCount,
|
|
2966
|
+
totalBytes: extracted.totalBytes,
|
|
2967
|
+
alreadyPresent: extracted.alreadyPresent,
|
|
2968
|
+
credentialUsedFromLocalFile: true,
|
|
2969
|
+
dnsVerificationRepeated: false
|
|
2970
|
+
}
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2246
2973
|
if (args.action === "start") {
|
|
2974
|
+
if (localSite.kind === "ok" && await localCredentialIsActive()) {
|
|
2975
|
+
return structuredToolResult({
|
|
2976
|
+
schemaVersion: 1,
|
|
2977
|
+
outcome: "completed",
|
|
2978
|
+
resultCode: "recovery_credential_already_present",
|
|
2979
|
+
summary: "A valid .sakupa/site.json already exists. Management authority is restored; do not start another TXT recovery. Continue with recover download.",
|
|
2980
|
+
data: {
|
|
2981
|
+
siteId: localSite.file.siteId,
|
|
2982
|
+
credentialStoredLocally: true,
|
|
2983
|
+
dnsVerificationRepeated: false
|
|
2984
|
+
},
|
|
2985
|
+
nextActions: [
|
|
2986
|
+
{
|
|
2987
|
+
tool: "recover",
|
|
2988
|
+
arguments: {
|
|
2989
|
+
projectDir: ctx.projectDir,
|
|
2990
|
+
action: "download",
|
|
2991
|
+
outputDir: args.outputDir ?? "html"
|
|
2992
|
+
},
|
|
2993
|
+
allowed: true
|
|
2994
|
+
}
|
|
2995
|
+
]
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
if (localSite.kind === "corrupted") {
|
|
2999
|
+
throw new SakupaError(
|
|
3000
|
+
"state_conflict",
|
|
3001
|
+
`Cannot start recovery while .sakupa/site.json is damaged: ${localSite.problem}`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
3004
|
+
const pending2 = loadRecoveryFile(ctx.projectDir);
|
|
3005
|
+
if (pending2) {
|
|
3006
|
+
return structuredToolResult({
|
|
3007
|
+
schemaVersion: 1,
|
|
3008
|
+
outcome: "waiting_user",
|
|
3009
|
+
resultCode: "domain_recovery_local_state_found",
|
|
3010
|
+
summary: "A resumable DNS recovery already exists in this project. Do not create another TXT challenge; continue the stored verification.",
|
|
3011
|
+
data: { verificationId: pending2.verificationId, resumable: true },
|
|
3012
|
+
nextActions: [
|
|
3013
|
+
{
|
|
3014
|
+
tool: "recover",
|
|
3015
|
+
arguments: {
|
|
3016
|
+
projectDir: ctx.projectDir,
|
|
3017
|
+
action: "status",
|
|
3018
|
+
verificationId: pending2.verificationId
|
|
3019
|
+
},
|
|
3020
|
+
allowed: true
|
|
3021
|
+
}
|
|
3022
|
+
]
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
2247
3025
|
if (!args.hostname) {
|
|
2248
3026
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
2249
3027
|
}
|
|
2250
3028
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
3029
|
+
writeRecoveryFile(ctx.projectDir, {
|
|
3030
|
+
verificationId: res2.verificationId,
|
|
3031
|
+
credential: generateCredential(),
|
|
3032
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3033
|
+
});
|
|
2251
3034
|
const txtShort = shortHostFor(res2.verificationRecord.name, res2.apexDomain);
|
|
2252
3035
|
return text(
|
|
2253
3036
|
"domain_recovery_started",
|
|
@@ -2281,19 +3064,48 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2281
3064
|
value: res2.verificationRecord.value
|
|
2282
3065
|
}
|
|
2283
3066
|
],
|
|
2284
|
-
revokesPreviousCredentialsByDefault: true
|
|
3067
|
+
revokesPreviousCredentialsByDefault: true,
|
|
3068
|
+
localRecoveryStateStored: true
|
|
2285
3069
|
},
|
|
2286
3070
|
"waiting_user"
|
|
2287
3071
|
);
|
|
2288
3072
|
}
|
|
2289
|
-
if (
|
|
3073
|
+
if (localSite.kind === "ok" && args.action === "status" && await localCredentialIsActive()) {
|
|
3074
|
+
return structuredToolResult({
|
|
3075
|
+
schemaVersion: 1,
|
|
3076
|
+
outcome: "completed",
|
|
3077
|
+
resultCode: "recovery_credential_already_present",
|
|
3078
|
+
summary: "The recovered credential is already stored in .sakupa/site.json. Resume content download with the local key; DNS recovery must not be repeated.",
|
|
3079
|
+
data: { siteId: localSite.file.siteId, credentialStoredLocally: true },
|
|
3080
|
+
nextActions: [
|
|
3081
|
+
{
|
|
3082
|
+
tool: "recover",
|
|
3083
|
+
arguments: {
|
|
3084
|
+
projectDir: ctx.projectDir,
|
|
3085
|
+
action: "download",
|
|
3086
|
+
outputDir: args.outputDir ?? "html"
|
|
3087
|
+
},
|
|
3088
|
+
allowed: true
|
|
3089
|
+
}
|
|
3090
|
+
]
|
|
3091
|
+
});
|
|
3092
|
+
}
|
|
3093
|
+
const pending = loadRecoveryFile(ctx.projectDir);
|
|
3094
|
+
const verificationId = args.verificationId ?? pending?.verificationId;
|
|
3095
|
+
if (!verificationId) {
|
|
2290
3096
|
throw new SakupaError(
|
|
2291
3097
|
"invalid_request",
|
|
2292
|
-
"verificationId is required
|
|
3098
|
+
"verificationId is required because no local resumable recovery state exists"
|
|
3099
|
+
);
|
|
3100
|
+
}
|
|
3101
|
+
if (pending && pending.verificationId !== verificationId) {
|
|
3102
|
+
throw new SakupaError(
|
|
3103
|
+
"state_conflict",
|
|
3104
|
+
"The supplied verificationId differs from this project\u2019s stored recovery state."
|
|
2293
3105
|
);
|
|
2294
3106
|
}
|
|
2295
3107
|
if (args.action === "status") {
|
|
2296
|
-
const res2 = await ctx.client.getRecoveryStatus(
|
|
3108
|
+
const res2 = await ctx.client.getRecoveryStatus(verificationId);
|
|
2297
3109
|
return structuredToolResult({
|
|
2298
3110
|
schemaVersion: 1,
|
|
2299
3111
|
outcome: res2.status === "expired" ? "expired" : res2.readyToComplete ? "completed" : "pending_provider",
|
|
@@ -2303,14 +3115,29 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2303
3115
|
nextActions: [
|
|
2304
3116
|
{
|
|
2305
3117
|
tool: "recover",
|
|
2306
|
-
arguments: {
|
|
3118
|
+
arguments: {
|
|
3119
|
+
projectDir: ctx.projectDir,
|
|
3120
|
+
action: "complete",
|
|
3121
|
+
verificationId,
|
|
3122
|
+
outputDir: args.outputDir ?? "html"
|
|
3123
|
+
},
|
|
2307
3124
|
allowed: res2.readyToComplete,
|
|
2308
3125
|
...res2.readyToComplete ? {} : { reasonCode: res2.status }
|
|
2309
3126
|
}
|
|
2310
3127
|
]
|
|
2311
3128
|
});
|
|
2312
3129
|
}
|
|
2313
|
-
const
|
|
3130
|
+
const recoveryState = pending ?? (() => {
|
|
3131
|
+
const created = {
|
|
3132
|
+
verificationId,
|
|
3133
|
+
credential: generateCredential(),
|
|
3134
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3135
|
+
};
|
|
3136
|
+
writeRecoveryFile(ctx.projectDir, created);
|
|
3137
|
+
return created;
|
|
3138
|
+
})();
|
|
3139
|
+
const res = await ctx.client.completeRecovery(verificationId, {
|
|
3140
|
+
credential: recoveryState.credential,
|
|
2314
3141
|
...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
|
|
2315
3142
|
});
|
|
2316
3143
|
writeSiteFile(
|
|
@@ -2318,31 +3145,63 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2318
3145
|
{
|
|
2319
3146
|
siteId: res.siteId,
|
|
2320
3147
|
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
2321
|
-
credential:
|
|
3148
|
+
credential: recoveryState.credential,
|
|
2322
3149
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2323
3150
|
apiBaseUrl: ctx.apiBaseUrl
|
|
2324
3151
|
},
|
|
2325
3152
|
{ allowReplace: true }
|
|
2326
3153
|
);
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
3154
|
+
deleteRecoveryFile(ctx.projectDir);
|
|
3155
|
+
try {
|
|
3156
|
+
const { archive, extracted } = await download();
|
|
3157
|
+
return text(
|
|
3158
|
+
"domain_recovery_completed",
|
|
3159
|
+
`Recovery complete.
|
|
2330
3160
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
2331
3161
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
2332
3162
|
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
3163
|
+
The NEW management credential was written to .sakupa/site.json before content download. The site archive was then requested with that key and extracted.
|
|
3164
|
+
Output directory: ${extracted.outputDir}
|
|
3165
|
+
Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
3166
|
+
` + credentialGitReminder(ctx.projectDir),
|
|
3167
|
+
{
|
|
3168
|
+
siteId: res.siteId,
|
|
3169
|
+
boundHostnames: res.boundHostnames,
|
|
3170
|
+
revokedPreviousCredentials: res.revokedPreviousCredentials,
|
|
3171
|
+
archiveExpiresAt: archive.archiveExpiresAt,
|
|
3172
|
+
outputDir: extracted.outputDir,
|
|
3173
|
+
fileCount: extracted.fileCount,
|
|
3174
|
+
totalBytes: extracted.totalBytes,
|
|
3175
|
+
alreadyPresent: extracted.alreadyPresent,
|
|
3176
|
+
credentialStoredLocally: true,
|
|
3177
|
+
archiveRequestedWithRecoveredCredential: true
|
|
3178
|
+
}
|
|
3179
|
+
);
|
|
3180
|
+
} catch (downloadError) {
|
|
3181
|
+
return structuredToolResult({
|
|
3182
|
+
schemaVersion: 1,
|
|
3183
|
+
outcome: "waiting_user",
|
|
3184
|
+
resultCode: "recovery_credential_restored_download_pending",
|
|
3185
|
+
summary: `Management authority was restored and .sakupa/site.json is safely written, but content download did not finish (${downloadError instanceof Error ? downloadError.message : String(downloadError)}). Do not repeat DNS recovery; run recover download in this project.`,
|
|
3186
|
+
data: {
|
|
3187
|
+
siteId: res.siteId,
|
|
3188
|
+
credentialStoredLocally: true,
|
|
3189
|
+
contentDownloadCompleted: false,
|
|
3190
|
+
dnsVerificationRepeated: false
|
|
3191
|
+
},
|
|
3192
|
+
nextActions: [
|
|
3193
|
+
{
|
|
3194
|
+
tool: "recover",
|
|
3195
|
+
arguments: {
|
|
3196
|
+
projectDir: ctx.projectDir,
|
|
3197
|
+
action: "download",
|
|
3198
|
+
outputDir: args.outputDir ?? "html"
|
|
3199
|
+
},
|
|
3200
|
+
allowed: true
|
|
3201
|
+
}
|
|
3202
|
+
]
|
|
3203
|
+
});
|
|
3204
|
+
}
|
|
2346
3205
|
} catch (e) {
|
|
2347
3206
|
return toolError(e);
|
|
2348
3207
|
}
|
|
@@ -2700,6 +3559,29 @@ var FetchTransport = class {
|
|
|
2700
3559
|
);
|
|
2701
3560
|
}
|
|
2702
3561
|
}
|
|
3562
|
+
async download(url) {
|
|
3563
|
+
let target;
|
|
3564
|
+
try {
|
|
3565
|
+
target = new URL(url);
|
|
3566
|
+
} catch {
|
|
3567
|
+
throw new SakupaError("invalid_request", "Archive download URL is invalid");
|
|
3568
|
+
}
|
|
3569
|
+
if (target.origin !== new URL(this.baseUrl).origin) {
|
|
3570
|
+
throw new SakupaError("forbidden", "Archive download URL is outside the Sakupa API origin");
|
|
3571
|
+
}
|
|
3572
|
+
const res = await fetch(target, {
|
|
3573
|
+
method: "GET",
|
|
3574
|
+
headers: this.testAccessHeadersFor(target.toString())
|
|
3575
|
+
});
|
|
3576
|
+
if (!res.ok) {
|
|
3577
|
+
const detail = await res.text().catch(() => "");
|
|
3578
|
+
throw new SakupaError(
|
|
3579
|
+
res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
|
|
3580
|
+
`Archive download failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
3584
|
+
}
|
|
2703
3585
|
};
|
|
2704
3586
|
|
|
2705
3587
|
// src/server.ts
|
|
@@ -2730,6 +3612,8 @@ Workflow:
|
|
|
2730
3612
|
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2731
3613
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2732
3614
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
3615
|
+
Recovery writes the new local credential before downloading content. If a session stops after
|
|
3616
|
+
.sakupa/site.json exists, NEVER repeat DNS recovery: resume with recover action "download".
|
|
2733
3617
|
5. support (subscribed sites) opens a support ticket; report sends a
|
|
2734
3618
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2735
3619
|
|
|
@@ -2796,7 +3680,7 @@ async function main() {
|
|
|
2796
3680
|
`[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; every tool call requires projectDir)`
|
|
2797
3681
|
);
|
|
2798
3682
|
}
|
|
2799
|
-
main().catch((
|
|
2800
|
-
console.error("[sakupa-mcp] fatal:",
|
|
3683
|
+
main().catch((err2) => {
|
|
3684
|
+
console.error("[sakupa-mcp] fatal:", err2 instanceof Error ? err2.message : err2);
|
|
2801
3685
|
process.exit(1);
|
|
2802
3686
|
});
|