@sakupa/mcp 0.7.21 → 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 +958 -58
- package/dist/index.js +953 -53
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -124,7 +124,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
124
124
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/version.js
|
|
127
|
-
var SAKUPA_MCP_VERSION = "0.7.
|
|
127
|
+
var SAKUPA_MCP_VERSION = "0.7.23";
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/errors.js
|
|
130
130
|
var HTTP_STATUS = {
|
|
@@ -155,8 +155,8 @@ var SakupaError = class extends Error {
|
|
|
155
155
|
this.details = details;
|
|
156
156
|
}
|
|
157
157
|
};
|
|
158
|
-
function isSakupaError(
|
|
159
|
-
return
|
|
158
|
+
function isSakupaError(err2) {
|
|
159
|
+
return err2 instanceof SakupaError;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// ../core/dist/domain/tiers.js
|
|
@@ -431,7 +431,12 @@ function safeDecode(bytes) {
|
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
// ../core/dist/domain/credentials.js
|
|
434
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
435
|
+
var CREDENTIAL_PREFIX = "sk_";
|
|
434
436
|
var CREDENTIAL_PATTERN = /^sk_[A-Za-z0-9_-]{43}$/;
|
|
437
|
+
function generateCredential(random = randomBytes) {
|
|
438
|
+
return CREDENTIAL_PREFIX + random(32).toString("base64url");
|
|
439
|
+
}
|
|
435
440
|
|
|
436
441
|
// ../core/dist/domain/hashing.js
|
|
437
442
|
async function sha256Hex(bytes) {
|
|
@@ -556,6 +561,29 @@ var FetchTransport = class {
|
|
|
556
561
|
);
|
|
557
562
|
}
|
|
558
563
|
}
|
|
564
|
+
async download(url) {
|
|
565
|
+
let target;
|
|
566
|
+
try {
|
|
567
|
+
target = new URL(url);
|
|
568
|
+
} catch {
|
|
569
|
+
throw new SakupaError("invalid_request", "Archive download URL is invalid");
|
|
570
|
+
}
|
|
571
|
+
if (target.origin !== new URL(this.baseUrl).origin) {
|
|
572
|
+
throw new SakupaError("forbidden", "Archive download URL is outside the Sakupa API origin");
|
|
573
|
+
}
|
|
574
|
+
const res = await fetch(target, {
|
|
575
|
+
method: "GET",
|
|
576
|
+
headers: this.testAccessHeadersFor(target.toString())
|
|
577
|
+
});
|
|
578
|
+
if (!res.ok) {
|
|
579
|
+
const detail = await res.text().catch(() => "");
|
|
580
|
+
throw new SakupaError(
|
|
581
|
+
res.status === 429 || res.status >= 500 ? "internal" : "validation_failed",
|
|
582
|
+
`Archive download failed with HTTP ${res.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
586
|
+
}
|
|
559
587
|
};
|
|
560
588
|
|
|
561
589
|
// src/api-client.ts
|
|
@@ -650,6 +678,16 @@ var HttpApiClient = class {
|
|
|
650
678
|
credential
|
|
651
679
|
});
|
|
652
680
|
}
|
|
681
|
+
async getSiteArchive(siteId, credential) {
|
|
682
|
+
return this.call(
|
|
683
|
+
"GET",
|
|
684
|
+
`/v1/sites/${encodeURIComponent(siteId)}/archive`,
|
|
685
|
+
{ credential }
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
async downloadArchive(url) {
|
|
689
|
+
return this.transport.download(url);
|
|
690
|
+
}
|
|
653
691
|
async deleteSite(siteId, credential, req) {
|
|
654
692
|
return this.call("POST", `/v1/sites/${encodeURIComponent(siteId)}/delete`, {
|
|
655
693
|
credential,
|
|
@@ -747,19 +785,23 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
|
|
|
747
785
|
import { dirname, join } from "node:path";
|
|
748
786
|
var SITE_DIR = ".sakupa";
|
|
749
787
|
var SITE_FILE = "site.json";
|
|
788
|
+
var RECOVERY_FILE = "recovery.json";
|
|
750
789
|
function siteFilePath(projectDir) {
|
|
751
790
|
return join(projectDir, SITE_DIR, SITE_FILE);
|
|
752
791
|
}
|
|
792
|
+
function recoveryFilePath(projectDir) {
|
|
793
|
+
return join(projectDir, SITE_DIR, RECOVERY_FILE);
|
|
794
|
+
}
|
|
753
795
|
function loadSiteFile(projectDir) {
|
|
754
796
|
const path = siteFilePath(projectDir);
|
|
755
797
|
if (!existsSync(path)) return { kind: "absent" };
|
|
756
798
|
let raw;
|
|
757
799
|
try {
|
|
758
800
|
raw = readFileSync(path, "utf8");
|
|
759
|
-
} catch (
|
|
801
|
+
} catch (err2) {
|
|
760
802
|
return {
|
|
761
803
|
kind: "corrupted",
|
|
762
|
-
problem: `the file exists but could not be read (${
|
|
804
|
+
problem: `the file exists but could not be read (${err2 instanceof Error ? err2.message : String(err2)})`
|
|
763
805
|
};
|
|
764
806
|
}
|
|
765
807
|
let parsed;
|
|
@@ -796,6 +838,40 @@ function loadSiteFile(projectDir) {
|
|
|
796
838
|
}
|
|
797
839
|
};
|
|
798
840
|
}
|
|
841
|
+
function loadRecoveryFile(projectDir) {
|
|
842
|
+
const path = recoveryFilePath(projectDir);
|
|
843
|
+
if (!existsSync(path)) return null;
|
|
844
|
+
try {
|
|
845
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
846
|
+
if (typeof parsed.verificationId !== "string" || parsed.verificationId.length === 0 || typeof parsed.credential !== "string" || !CREDENTIAL_PATTERN.test(parsed.credential)) {
|
|
847
|
+
throw new Error("required recovery fields are missing or invalid");
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
verificationId: parsed.verificationId,
|
|
851
|
+
credential: parsed.credential,
|
|
852
|
+
createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : ""
|
|
853
|
+
};
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`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.`
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
function writeRecoveryFile(projectDir, file) {
|
|
861
|
+
const dir = join(projectDir, SITE_DIR);
|
|
862
|
+
mkdirSync(dir, { recursive: true });
|
|
863
|
+
const path = join(dir, RECOVERY_FILE);
|
|
864
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}
|
|
865
|
+
`, "utf8");
|
|
866
|
+
try {
|
|
867
|
+
chmodSync(path, 384);
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function deleteRecoveryFile(projectDir) {
|
|
872
|
+
const path = recoveryFilePath(projectDir);
|
|
873
|
+
if (existsSync(path)) rmSync(path, { force: true });
|
|
874
|
+
}
|
|
799
875
|
function siteFileRecoveryGuidance(projectDir) {
|
|
800
876
|
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.`;
|
|
801
877
|
}
|
|
@@ -885,8 +961,8 @@ async function isFile(path) {
|
|
|
885
961
|
}
|
|
886
962
|
async function readTextIfExists(path, maxBytes = CONTENT_READ_MAX_BYTES) {
|
|
887
963
|
try {
|
|
888
|
-
const
|
|
889
|
-
if (!
|
|
964
|
+
const stat2 = await fs.stat(path);
|
|
965
|
+
if (!stat2.isFile() || stat2.size > maxBytes) return null;
|
|
890
966
|
return await fs.readFile(path, "utf8");
|
|
891
967
|
} catch {
|
|
892
968
|
return null;
|
|
@@ -926,8 +1002,8 @@ async function walkFiles(dir, opts) {
|
|
|
926
1002
|
await recurse(join2(current, entry.name), rel);
|
|
927
1003
|
} else if (entry.isFile()) {
|
|
928
1004
|
try {
|
|
929
|
-
const
|
|
930
|
-
out.push({ path: rel, size:
|
|
1005
|
+
const stat2 = await fs.stat(join2(current, entry.name));
|
|
1006
|
+
out.push({ path: rel, size: stat2.size });
|
|
931
1007
|
} catch {
|
|
932
1008
|
}
|
|
933
1009
|
}
|
|
@@ -1342,8 +1418,8 @@ function withProjectDir(ctx, projectDirArg) {
|
|
|
1342
1418
|
`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.`
|
|
1343
1419
|
);
|
|
1344
1420
|
}
|
|
1345
|
-
const
|
|
1346
|
-
if (!
|
|
1421
|
+
const stat2 = statSync(dir, { throwIfNoEntry: false });
|
|
1422
|
+
if (!stat2?.isDirectory()) {
|
|
1347
1423
|
throw new LocalGuidanceError(
|
|
1348
1424
|
"invalid_request",
|
|
1349
1425
|
`projectDir "${dir}" does not exist or is not a directory. Pass the absolute path of the directory the user is currently working in.`
|
|
@@ -1408,17 +1484,646 @@ function toolError(e) {
|
|
|
1408
1484
|
// src/tools/definitions.ts
|
|
1409
1485
|
import { randomUUID } from "node:crypto";
|
|
1410
1486
|
import { existsSync as existsSync3, promises as fs2 } from "node:fs";
|
|
1411
|
-
import { join as
|
|
1487
|
+
import { join as join5, resolve as resolve4 } from "node:path";
|
|
1412
1488
|
import { z as z3 } from "zod";
|
|
1413
1489
|
|
|
1490
|
+
// src/recovery-archive.ts
|
|
1491
|
+
import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
1492
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve3, sep as sep2 } from "node:path";
|
|
1493
|
+
|
|
1494
|
+
// ../../node_modules/fflate/esm/index.mjs
|
|
1495
|
+
import { createRequire } from "module";
|
|
1496
|
+
var require2 = createRequire("/");
|
|
1497
|
+
var _a;
|
|
1498
|
+
var Worker;
|
|
1499
|
+
var isMarkedAsUntransferable;
|
|
1500
|
+
try {
|
|
1501
|
+
_a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable;
|
|
1502
|
+
} catch (e) {
|
|
1503
|
+
}
|
|
1504
|
+
var u8 = Uint8Array;
|
|
1505
|
+
var u16 = Uint16Array;
|
|
1506
|
+
var i32 = Int32Array;
|
|
1507
|
+
var fleb = new u8([
|
|
1508
|
+
0,
|
|
1509
|
+
0,
|
|
1510
|
+
0,
|
|
1511
|
+
0,
|
|
1512
|
+
0,
|
|
1513
|
+
0,
|
|
1514
|
+
0,
|
|
1515
|
+
0,
|
|
1516
|
+
1,
|
|
1517
|
+
1,
|
|
1518
|
+
1,
|
|
1519
|
+
1,
|
|
1520
|
+
2,
|
|
1521
|
+
2,
|
|
1522
|
+
2,
|
|
1523
|
+
2,
|
|
1524
|
+
3,
|
|
1525
|
+
3,
|
|
1526
|
+
3,
|
|
1527
|
+
3,
|
|
1528
|
+
4,
|
|
1529
|
+
4,
|
|
1530
|
+
4,
|
|
1531
|
+
4,
|
|
1532
|
+
5,
|
|
1533
|
+
5,
|
|
1534
|
+
5,
|
|
1535
|
+
5,
|
|
1536
|
+
0,
|
|
1537
|
+
/* unused */
|
|
1538
|
+
0,
|
|
1539
|
+
0,
|
|
1540
|
+
/* impossible */
|
|
1541
|
+
0
|
|
1542
|
+
]);
|
|
1543
|
+
var fdeb = new u8([
|
|
1544
|
+
0,
|
|
1545
|
+
0,
|
|
1546
|
+
0,
|
|
1547
|
+
0,
|
|
1548
|
+
1,
|
|
1549
|
+
1,
|
|
1550
|
+
2,
|
|
1551
|
+
2,
|
|
1552
|
+
3,
|
|
1553
|
+
3,
|
|
1554
|
+
4,
|
|
1555
|
+
4,
|
|
1556
|
+
5,
|
|
1557
|
+
5,
|
|
1558
|
+
6,
|
|
1559
|
+
6,
|
|
1560
|
+
7,
|
|
1561
|
+
7,
|
|
1562
|
+
8,
|
|
1563
|
+
8,
|
|
1564
|
+
9,
|
|
1565
|
+
9,
|
|
1566
|
+
10,
|
|
1567
|
+
10,
|
|
1568
|
+
11,
|
|
1569
|
+
11,
|
|
1570
|
+
12,
|
|
1571
|
+
12,
|
|
1572
|
+
13,
|
|
1573
|
+
13,
|
|
1574
|
+
/* unused */
|
|
1575
|
+
0,
|
|
1576
|
+
0
|
|
1577
|
+
]);
|
|
1578
|
+
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
1579
|
+
var freb = function(eb, start) {
|
|
1580
|
+
var b = new u16(31);
|
|
1581
|
+
for (var i = 0; i < 31; ++i) {
|
|
1582
|
+
b[i] = start += 1 << eb[i - 1];
|
|
1583
|
+
}
|
|
1584
|
+
var r = new i32(b[30]);
|
|
1585
|
+
for (var i = 1; i < 30; ++i) {
|
|
1586
|
+
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
1587
|
+
r[j] = j - b[i] << 5 | i;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return { b, r };
|
|
1591
|
+
};
|
|
1592
|
+
var _a = freb(fleb, 2);
|
|
1593
|
+
var fl = _a.b;
|
|
1594
|
+
var revfl = _a.r;
|
|
1595
|
+
fl[28] = 258, revfl[258] = 28;
|
|
1596
|
+
var _b = freb(fdeb, 0);
|
|
1597
|
+
var fd = _b.b;
|
|
1598
|
+
var revfd = _b.r;
|
|
1599
|
+
var rev = new u16(32768);
|
|
1600
|
+
for (i = 0; i < 32768; ++i) {
|
|
1601
|
+
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
1602
|
+
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
1603
|
+
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
1604
|
+
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
1605
|
+
}
|
|
1606
|
+
var x;
|
|
1607
|
+
var i;
|
|
1608
|
+
var hMap = function(cd, mb, r) {
|
|
1609
|
+
var s = cd.length;
|
|
1610
|
+
var i = 0;
|
|
1611
|
+
var l = new u16(mb);
|
|
1612
|
+
for (; i < s; ++i) {
|
|
1613
|
+
if (cd[i])
|
|
1614
|
+
++l[cd[i] - 1];
|
|
1615
|
+
}
|
|
1616
|
+
var le = new u16(mb);
|
|
1617
|
+
for (i = 1; i < mb; ++i) {
|
|
1618
|
+
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
1619
|
+
}
|
|
1620
|
+
var co;
|
|
1621
|
+
if (r) {
|
|
1622
|
+
co = new u16(1 << mb);
|
|
1623
|
+
var rvb = 15 - mb;
|
|
1624
|
+
for (i = 0; i < s; ++i) {
|
|
1625
|
+
if (cd[i]) {
|
|
1626
|
+
var sv = i << 4 | cd[i];
|
|
1627
|
+
var r_1 = mb - cd[i];
|
|
1628
|
+
var v = le[cd[i] - 1]++ << r_1;
|
|
1629
|
+
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
1630
|
+
co[rev[v] >> rvb] = sv;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
} else {
|
|
1635
|
+
co = new u16(s);
|
|
1636
|
+
for (i = 0; i < s; ++i) {
|
|
1637
|
+
if (cd[i]) {
|
|
1638
|
+
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return co;
|
|
1643
|
+
};
|
|
1644
|
+
var flt = new u8(288);
|
|
1645
|
+
for (i = 0; i < 144; ++i)
|
|
1646
|
+
flt[i] = 8;
|
|
1647
|
+
var i;
|
|
1648
|
+
for (i = 144; i < 256; ++i)
|
|
1649
|
+
flt[i] = 9;
|
|
1650
|
+
var i;
|
|
1651
|
+
for (i = 256; i < 280; ++i)
|
|
1652
|
+
flt[i] = 7;
|
|
1653
|
+
var i;
|
|
1654
|
+
for (i = 280; i < 288; ++i)
|
|
1655
|
+
flt[i] = 8;
|
|
1656
|
+
var i;
|
|
1657
|
+
var fdt = new u8(32);
|
|
1658
|
+
for (i = 0; i < 32; ++i)
|
|
1659
|
+
fdt[i] = 5;
|
|
1660
|
+
var i;
|
|
1661
|
+
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
1662
|
+
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
1663
|
+
var max = function(a) {
|
|
1664
|
+
var m = a[0];
|
|
1665
|
+
for (var i = 1; i < a.length; ++i) {
|
|
1666
|
+
if (a[i] > m)
|
|
1667
|
+
m = a[i];
|
|
1668
|
+
}
|
|
1669
|
+
return m;
|
|
1670
|
+
};
|
|
1671
|
+
var bits = function(d, p, m) {
|
|
1672
|
+
var o = p / 8 | 0;
|
|
1673
|
+
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
1674
|
+
};
|
|
1675
|
+
var bits16 = function(d, p) {
|
|
1676
|
+
var o = p / 8 | 0;
|
|
1677
|
+
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
1678
|
+
};
|
|
1679
|
+
var shft = function(p) {
|
|
1680
|
+
return (p + 7) / 8 | 0;
|
|
1681
|
+
};
|
|
1682
|
+
var slc = function(v, s, e) {
|
|
1683
|
+
if (s == null || s < 0)
|
|
1684
|
+
s = 0;
|
|
1685
|
+
if (e == null || e > v.length)
|
|
1686
|
+
e = v.length;
|
|
1687
|
+
return new u8(v.subarray(s, e));
|
|
1688
|
+
};
|
|
1689
|
+
var ec = [
|
|
1690
|
+
"unexpected EOF",
|
|
1691
|
+
"invalid block type",
|
|
1692
|
+
"invalid length/literal",
|
|
1693
|
+
"invalid distance",
|
|
1694
|
+
"stream finished",
|
|
1695
|
+
"no stream handler",
|
|
1696
|
+
,
|
|
1697
|
+
// determined by compression function
|
|
1698
|
+
"no callback",
|
|
1699
|
+
"invalid UTF-8 data",
|
|
1700
|
+
"extra field too long",
|
|
1701
|
+
"date not in range 1980-2099",
|
|
1702
|
+
"filename too long",
|
|
1703
|
+
"stream finishing",
|
|
1704
|
+
"invalid zip data"
|
|
1705
|
+
// determined by unknown compression method
|
|
1706
|
+
];
|
|
1707
|
+
var err = function(ind, msg, nt) {
|
|
1708
|
+
var e = new Error(msg || ec[ind]);
|
|
1709
|
+
e.code = ind;
|
|
1710
|
+
if (Error.captureStackTrace)
|
|
1711
|
+
Error.captureStackTrace(e, err);
|
|
1712
|
+
if (!nt)
|
|
1713
|
+
throw e;
|
|
1714
|
+
return e;
|
|
1715
|
+
};
|
|
1716
|
+
var inflt = function(dat, st, buf, dict) {
|
|
1717
|
+
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
1718
|
+
if (!sl || st.f && !st.l)
|
|
1719
|
+
return buf || new u8(0);
|
|
1720
|
+
var noBuf = !buf;
|
|
1721
|
+
var resize = noBuf || st.i != 2;
|
|
1722
|
+
var noSt = st.i;
|
|
1723
|
+
if (noBuf)
|
|
1724
|
+
buf = new u8(sl * 3);
|
|
1725
|
+
var cbuf = function(l2) {
|
|
1726
|
+
var bl = buf.length;
|
|
1727
|
+
if (l2 > bl) {
|
|
1728
|
+
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
1729
|
+
nbuf.set(buf);
|
|
1730
|
+
buf = nbuf;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
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;
|
|
1734
|
+
var tbts = sl * 8;
|
|
1735
|
+
do {
|
|
1736
|
+
if (!lm) {
|
|
1737
|
+
final = bits(dat, pos, 1);
|
|
1738
|
+
var type = bits(dat, pos + 1, 3);
|
|
1739
|
+
pos += 3;
|
|
1740
|
+
if (!type) {
|
|
1741
|
+
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
1742
|
+
if (t > sl) {
|
|
1743
|
+
if (noSt)
|
|
1744
|
+
err(0);
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
if (resize)
|
|
1748
|
+
cbuf(bt + l);
|
|
1749
|
+
buf.set(dat.subarray(s, t), bt);
|
|
1750
|
+
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
1751
|
+
continue;
|
|
1752
|
+
} else if (type == 1)
|
|
1753
|
+
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
1754
|
+
else if (type == 2) {
|
|
1755
|
+
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
1756
|
+
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
1757
|
+
pos += 14;
|
|
1758
|
+
var ldt = new u8(tl);
|
|
1759
|
+
var clt = new u8(19);
|
|
1760
|
+
for (var i = 0; i < hcLen; ++i) {
|
|
1761
|
+
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
1762
|
+
}
|
|
1763
|
+
pos += hcLen * 3;
|
|
1764
|
+
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
1765
|
+
var clm = hMap(clt, clb, 1);
|
|
1766
|
+
for (var i = 0; i < tl; ) {
|
|
1767
|
+
var r = clm[bits(dat, pos, clbmsk)];
|
|
1768
|
+
pos += r & 15;
|
|
1769
|
+
var s = r >> 4;
|
|
1770
|
+
if (s < 16) {
|
|
1771
|
+
ldt[i++] = s;
|
|
1772
|
+
} else {
|
|
1773
|
+
var c = 0, n = 0;
|
|
1774
|
+
if (s == 16)
|
|
1775
|
+
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
1776
|
+
else if (s == 17)
|
|
1777
|
+
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
1778
|
+
else if (s == 18)
|
|
1779
|
+
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
1780
|
+
while (n--)
|
|
1781
|
+
ldt[i++] = c;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
1785
|
+
lbt = max(lt);
|
|
1786
|
+
dbt = max(dt);
|
|
1787
|
+
lm = hMap(lt, lbt, 1);
|
|
1788
|
+
dm = hMap(dt, dbt, 1);
|
|
1789
|
+
} else
|
|
1790
|
+
err(1);
|
|
1791
|
+
if (pos > tbts) {
|
|
1792
|
+
if (noSt)
|
|
1793
|
+
err(0);
|
|
1794
|
+
break;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (resize)
|
|
1798
|
+
cbuf(bt + 131072);
|
|
1799
|
+
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
1800
|
+
var lpos = pos;
|
|
1801
|
+
for (; ; lpos = pos) {
|
|
1802
|
+
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
1803
|
+
pos += c & 15;
|
|
1804
|
+
if (pos > tbts) {
|
|
1805
|
+
if (noSt)
|
|
1806
|
+
err(0);
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
if (!c)
|
|
1810
|
+
err(2);
|
|
1811
|
+
if (sym < 256)
|
|
1812
|
+
buf[bt++] = sym;
|
|
1813
|
+
else if (sym == 256) {
|
|
1814
|
+
lpos = pos, lm = null;
|
|
1815
|
+
break;
|
|
1816
|
+
} else {
|
|
1817
|
+
var add = sym - 254;
|
|
1818
|
+
if (sym > 264) {
|
|
1819
|
+
var i = sym - 257, b = fleb[i];
|
|
1820
|
+
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
1821
|
+
pos += b;
|
|
1822
|
+
}
|
|
1823
|
+
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
1824
|
+
if (!d)
|
|
1825
|
+
err(3);
|
|
1826
|
+
pos += d & 15;
|
|
1827
|
+
var dt = fd[dsym];
|
|
1828
|
+
if (dsym > 3) {
|
|
1829
|
+
var b = fdeb[dsym];
|
|
1830
|
+
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
1831
|
+
}
|
|
1832
|
+
if (pos > tbts) {
|
|
1833
|
+
if (noSt)
|
|
1834
|
+
err(0);
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
if (resize)
|
|
1838
|
+
cbuf(bt + 131072);
|
|
1839
|
+
var end = bt + add;
|
|
1840
|
+
if (bt < dt) {
|
|
1841
|
+
var shift = dl - dt, dend = Math.min(dt, end);
|
|
1842
|
+
if (shift + bt < 0)
|
|
1843
|
+
err(3);
|
|
1844
|
+
for (; bt < dend; ++bt)
|
|
1845
|
+
buf[bt] = dict[shift + bt];
|
|
1846
|
+
}
|
|
1847
|
+
for (; bt < end; ++bt)
|
|
1848
|
+
buf[bt] = buf[bt - dt];
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
1852
|
+
if (lm)
|
|
1853
|
+
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
1854
|
+
} while (!final);
|
|
1855
|
+
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
1856
|
+
};
|
|
1857
|
+
var et = /* @__PURE__ */ new u8(0);
|
|
1858
|
+
var b2 = function(d, b) {
|
|
1859
|
+
return d[b] | d[b + 1] << 8;
|
|
1860
|
+
};
|
|
1861
|
+
var b4 = function(d, b) {
|
|
1862
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
1863
|
+
};
|
|
1864
|
+
var b8 = function(d, b) {
|
|
1865
|
+
return b4(d, b) + b4(d, b + 4) * 4294967296;
|
|
1866
|
+
};
|
|
1867
|
+
function inflateSync(data, opts) {
|
|
1868
|
+
return inflt(data, { i: 2 }, opts && opts.out, opts && opts.dictionary);
|
|
1869
|
+
}
|
|
1870
|
+
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
1871
|
+
var tds = 0;
|
|
1872
|
+
try {
|
|
1873
|
+
td.decode(et, { stream: true });
|
|
1874
|
+
tds = 1;
|
|
1875
|
+
} catch (e) {
|
|
1876
|
+
}
|
|
1877
|
+
var dutf8 = function(d) {
|
|
1878
|
+
for (var r = "", i = 0; ; ) {
|
|
1879
|
+
var c = d[i++];
|
|
1880
|
+
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
1881
|
+
if (i + eb > d.length)
|
|
1882
|
+
return { s: r, r: slc(d, i - 1) };
|
|
1883
|
+
if (!eb)
|
|
1884
|
+
r += String.fromCharCode(c);
|
|
1885
|
+
else if (eb == 3) {
|
|
1886
|
+
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);
|
|
1887
|
+
} else if (eb & 1)
|
|
1888
|
+
r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
|
|
1889
|
+
else
|
|
1890
|
+
r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
function strFromU8(dat, latin1) {
|
|
1894
|
+
if (latin1) {
|
|
1895
|
+
var r = "";
|
|
1896
|
+
for (var i = 0; i < dat.length; i += 16384)
|
|
1897
|
+
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
|
|
1898
|
+
return r;
|
|
1899
|
+
} else if (td) {
|
|
1900
|
+
return td.decode(dat);
|
|
1901
|
+
} else {
|
|
1902
|
+
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
1903
|
+
if (r.length)
|
|
1904
|
+
err(8);
|
|
1905
|
+
return s;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
var slzh = function(d, b) {
|
|
1909
|
+
return b + 30 + b2(d, b + 26) + b2(d, b + 28);
|
|
1910
|
+
};
|
|
1911
|
+
var zh = function(d, b, z6) {
|
|
1912
|
+
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;
|
|
1913
|
+
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];
|
|
1914
|
+
return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
|
|
1915
|
+
};
|
|
1916
|
+
var z64hs = function(d, b, l, z6, sc, su, off) {
|
|
1917
|
+
var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
|
|
1918
|
+
var nf = nsc + nsu + noff;
|
|
1919
|
+
if (z6 && nf) {
|
|
1920
|
+
for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
|
|
1921
|
+
if (b2(d, b) == 1) {
|
|
1922
|
+
return [
|
|
1923
|
+
nsc ? b8(d, b + 4 + 8 * nsu) : sc,
|
|
1924
|
+
nsu ? b8(d, b + 4) : su,
|
|
1925
|
+
noff ? b8(d, b + 4 + 8 * (nsu + nsc)) : off,
|
|
1926
|
+
1
|
|
1927
|
+
];
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (z6 < 2)
|
|
1931
|
+
err(13);
|
|
1932
|
+
}
|
|
1933
|
+
return [sc, su, off, 0];
|
|
1934
|
+
};
|
|
1935
|
+
function unzipSync(data, opts) {
|
|
1936
|
+
var files = {};
|
|
1937
|
+
var e = data.length - 22;
|
|
1938
|
+
for (; b4(data, e) != 101010256; --e) {
|
|
1939
|
+
if (!e || data.length - e > 65558)
|
|
1940
|
+
err(13);
|
|
1941
|
+
}
|
|
1942
|
+
;
|
|
1943
|
+
var c = b2(data, e + 8);
|
|
1944
|
+
if (!c)
|
|
1945
|
+
return {};
|
|
1946
|
+
var o = b4(data, e + 16);
|
|
1947
|
+
var z6 = b4(data, e - 20) == 117853008;
|
|
1948
|
+
if (z6) {
|
|
1949
|
+
var ze = b4(data, e - 12);
|
|
1950
|
+
z6 = b4(data, ze) == 101075792;
|
|
1951
|
+
if (z6) {
|
|
1952
|
+
c = b4(data, ze + 32);
|
|
1953
|
+
o = b4(data, ze + 48);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
var fltr = opts && opts.filter;
|
|
1957
|
+
for (var i = 0; i < c; ++i) {
|
|
1958
|
+
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);
|
|
1959
|
+
o = no;
|
|
1960
|
+
if (!fltr || fltr({
|
|
1961
|
+
name: fn,
|
|
1962
|
+
size: sc,
|
|
1963
|
+
originalSize: su,
|
|
1964
|
+
compression: c_2
|
|
1965
|
+
})) {
|
|
1966
|
+
if (!c_2)
|
|
1967
|
+
files[fn] = slc(data, b, b + sc);
|
|
1968
|
+
else if (c_2 == 8)
|
|
1969
|
+
files[fn] = inflateSync(data.subarray(b, b + sc), { out: new u8(su) });
|
|
1970
|
+
else
|
|
1971
|
+
err(14, "unknown compression type " + c_2);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return files;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/recovery-archive.ts
|
|
1978
|
+
function safeOutputPath(projectDir, outputDir) {
|
|
1979
|
+
if (outputDir.length === 0 || isAbsolute2(outputDir)) {
|
|
1980
|
+
throw new SakupaError("invalid_request", "Recovery outputDir must be a relative directory");
|
|
1981
|
+
}
|
|
1982
|
+
const root = resolve3(projectDir);
|
|
1983
|
+
const target = resolve3(root, outputDir);
|
|
1984
|
+
const rel = relative(root, target);
|
|
1985
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute2(rel)) {
|
|
1986
|
+
throw new SakupaError("invalid_request", "Recovery outputDir must stay inside projectDir");
|
|
1987
|
+
}
|
|
1988
|
+
if (rel === ".sakupa" || rel.startsWith(`.sakupa${sep2}`)) {
|
|
1989
|
+
throw new SakupaError("invalid_request", "Recovery content cannot be written inside .sakupa");
|
|
1990
|
+
}
|
|
1991
|
+
return target;
|
|
1992
|
+
}
|
|
1993
|
+
function safeEntryName(name) {
|
|
1994
|
+
if (name.length === 0 || name.startsWith("/") || name.includes("\\") || /^[A-Za-z]:/.test(name)) {
|
|
1995
|
+
throw new SakupaError("validation_failed", `Unsafe recovery archive path: ${name}`);
|
|
1996
|
+
}
|
|
1997
|
+
const segments = name.split("/");
|
|
1998
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
1999
|
+
throw new SakupaError("validation_failed", `Unsafe recovery archive path: ${name}`);
|
|
2000
|
+
}
|
|
2001
|
+
return segments.join("/");
|
|
2002
|
+
}
|
|
2003
|
+
async function pathExists(path) {
|
|
2004
|
+
try {
|
|
2005
|
+
await stat(path);
|
|
2006
|
+
return true;
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
if (error.code === "ENOENT") return false;
|
|
2009
|
+
throw error;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
async function listExistingFiles(root, current = root) {
|
|
2013
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
2014
|
+
const files = [];
|
|
2015
|
+
for (const entry of entries) {
|
|
2016
|
+
if (entry.isSymbolicLink()) {
|
|
2017
|
+
throw new SakupaError("state_conflict", `Recovery output contains a symlink: ${entry.name}`);
|
|
2018
|
+
}
|
|
2019
|
+
const absolute = join3(current, entry.name);
|
|
2020
|
+
if (entry.isDirectory()) files.push(...await listExistingFiles(root, absolute));
|
|
2021
|
+
else if (entry.isFile()) files.push(relative(root, absolute).split(sep2).join("/"));
|
|
2022
|
+
else
|
|
2023
|
+
throw new SakupaError(
|
|
2024
|
+
"state_conflict",
|
|
2025
|
+
`Recovery output contains a special file: ${entry.name}`
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return files.sort();
|
|
2029
|
+
}
|
|
2030
|
+
async function existingOutputMatches(outputDir, files) {
|
|
2031
|
+
const existing = await listExistingFiles(outputDir);
|
|
2032
|
+
const expected = Object.keys(files).sort();
|
|
2033
|
+
if (existing.length !== expected.length || existing.some((name, i) => name !== expected[i])) {
|
|
2034
|
+
return false;
|
|
2035
|
+
}
|
|
2036
|
+
for (const name of expected) {
|
|
2037
|
+
const actual = await readFile(join3(outputDir, ...name.split("/")));
|
|
2038
|
+
const wanted = files[name];
|
|
2039
|
+
if (!wanted || actual.byteLength !== wanted.byteLength || !actual.equals(wanted)) return false;
|
|
2040
|
+
}
|
|
2041
|
+
return true;
|
|
2042
|
+
}
|
|
2043
|
+
async function extractRecoveryArchive(input) {
|
|
2044
|
+
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) {
|
|
2045
|
+
throw new SakupaError("validation_failed", "Recovery archive metadata exceeds product limits");
|
|
2046
|
+
}
|
|
2047
|
+
const outputDir = safeOutputPath(input.projectDir, input.outputDir ?? "html");
|
|
2048
|
+
const maxArchiveBytes = input.expectedBytes + input.expectedFiles * 4096 + 65536;
|
|
2049
|
+
if (input.archive.byteLength > maxArchiveBytes) {
|
|
2050
|
+
throw new SakupaError("validation_failed", "Recovery archive is larger than its site metadata");
|
|
2051
|
+
}
|
|
2052
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2053
|
+
let declaredBytes = 0;
|
|
2054
|
+
let declaredFiles = 0;
|
|
2055
|
+
const files = unzipSync(input.archive, {
|
|
2056
|
+
filter(file) {
|
|
2057
|
+
const name = safeEntryName(file.name);
|
|
2058
|
+
if (seen.has(name)) {
|
|
2059
|
+
throw new SakupaError("validation_failed", `Duplicate recovery archive path: ${name}`);
|
|
2060
|
+
}
|
|
2061
|
+
seen.add(name);
|
|
2062
|
+
declaredFiles += 1;
|
|
2063
|
+
declaredBytes += file.originalSize;
|
|
2064
|
+
if (declaredFiles > input.expectedFiles || declaredFiles > MAX_FILE_COUNT || declaredBytes > input.expectedBytes || declaredBytes > PAID_SITE_MAX_TOTAL_BYTES) {
|
|
2065
|
+
throw new SakupaError("validation_failed", "Recovery archive exceeds declared site limits");
|
|
2066
|
+
}
|
|
2067
|
+
return true;
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
if (declaredFiles !== input.expectedFiles || declaredBytes !== input.expectedBytes) {
|
|
2071
|
+
throw new SakupaError("validation_failed", "Recovery archive does not match site metadata");
|
|
2072
|
+
}
|
|
2073
|
+
if (await pathExists(outputDir)) {
|
|
2074
|
+
const info = await stat(outputDir);
|
|
2075
|
+
if (info.isDirectory() && await existingOutputMatches(outputDir, files)) {
|
|
2076
|
+
return {
|
|
2077
|
+
outputDir,
|
|
2078
|
+
totalBytes: declaredBytes,
|
|
2079
|
+
fileCount: declaredFiles,
|
|
2080
|
+
alreadyPresent: true
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
throw new SakupaError(
|
|
2084
|
+
"state_conflict",
|
|
2085
|
+
`Recovery output already exists with different content: ${outputDir}. Move it aside or choose another outputDir.`
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
const tempDir = await mkdtemp(join3(resolve3(input.projectDir), ".sakupa-restore-"));
|
|
2089
|
+
try {
|
|
2090
|
+
let writtenBytes = 0;
|
|
2091
|
+
const entries = Object.entries(files).sort(([a], [b]) => a.localeCompare(b));
|
|
2092
|
+
for (const [rawName, data] of entries) {
|
|
2093
|
+
const name = safeEntryName(rawName);
|
|
2094
|
+
const destination = join3(tempDir, ...name.split("/"));
|
|
2095
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2096
|
+
await writeFile(destination, data, { flag: "wx" });
|
|
2097
|
+
writtenBytes += data.byteLength;
|
|
2098
|
+
}
|
|
2099
|
+
if (entries.length !== input.expectedFiles || writtenBytes !== input.expectedBytes) {
|
|
2100
|
+
throw new SakupaError(
|
|
2101
|
+
"validation_failed",
|
|
2102
|
+
"Extracted recovery data does not match site metadata"
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
await mkdir(dirname2(outputDir), { recursive: true });
|
|
2106
|
+
await rename(tempDir, outputDir);
|
|
2107
|
+
return {
|
|
2108
|
+
outputDir,
|
|
2109
|
+
totalBytes: writtenBytes,
|
|
2110
|
+
fileCount: entries.length,
|
|
2111
|
+
alreadyPresent: false
|
|
2112
|
+
};
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
2115
|
+
throw error;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
1414
2119
|
// src/creation-registry.ts
|
|
1415
2120
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1416
2121
|
import { homedir as homedir2 } from "node:os";
|
|
1417
|
-
import { dirname as
|
|
2122
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1418
2123
|
var RECENT_WINDOW_MS = FREE_SITE_TTL_HOURS * 60 * 60 * 1e3;
|
|
1419
2124
|
function creationRegistryPath() {
|
|
1420
2125
|
const base = process.env["SAKUPA_STATE_DIR"] ?? homedir2();
|
|
1421
|
-
return
|
|
2126
|
+
return join4(base, ".sakupa", "created-sites.json");
|
|
1422
2127
|
}
|
|
1423
2128
|
function readAll() {
|
|
1424
2129
|
const path = creationRegistryPath();
|
|
@@ -1435,7 +2140,7 @@ function readAll() {
|
|
|
1435
2140
|
}
|
|
1436
2141
|
function writeAll(records) {
|
|
1437
2142
|
const path = creationRegistryPath();
|
|
1438
|
-
mkdirSync2(
|
|
2143
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1439
2144
|
writeFileSync2(path, `${JSON.stringify(records, null, 2)}
|
|
1440
2145
|
`, "utf-8");
|
|
1441
2146
|
}
|
|
@@ -1664,7 +2369,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1664
2369
|
async function buildHashedManifest(files, outputAbs) {
|
|
1665
2370
|
const manifest = [];
|
|
1666
2371
|
for (const file of files) {
|
|
1667
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
2372
|
+
const bytes = new Uint8Array(await fs2.readFile(join5(outputAbs, file.path)));
|
|
1668
2373
|
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1669
2374
|
}
|
|
1670
2375
|
return manifest;
|
|
@@ -1683,7 +2388,7 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1683
2388
|
`No local file matches upload target "${target.path}"; aborting upload.`
|
|
1684
2389
|
);
|
|
1685
2390
|
}
|
|
1686
|
-
const bytes = new Uint8Array(await fs2.readFile(
|
|
2391
|
+
const bytes = new Uint8Array(await fs2.readFile(join5(outputAbs, match.path)));
|
|
1687
2392
|
if (bytes.byteLength !== match.size) {
|
|
1688
2393
|
throw new SakupaError(
|
|
1689
2394
|
"validation_failed",
|
|
@@ -1727,15 +2432,15 @@ ${block}`, checklist: toDnsChecklist(diag.checks) };
|
|
|
1727
2432
|
}
|
|
1728
2433
|
}
|
|
1729
2434
|
function projectRootAbove(projectDir) {
|
|
1730
|
-
if (existsSync3(
|
|
1731
|
-
return findAncestor(projectDir, (dir) => existsSync3(
|
|
2435
|
+
if (existsSync3(join5(projectDir, "package.json"))) return null;
|
|
2436
|
+
return findAncestor(projectDir, (dir) => existsSync3(join5(dir, "package.json")), 4);
|
|
1732
2437
|
}
|
|
1733
2438
|
function findNeighborBinding(projectDir, outputRel) {
|
|
1734
2439
|
const bound = (dir) => loadSiteFile(dir).kind !== "absent";
|
|
1735
2440
|
const above = findAncestor(projectDir, bound, 3);
|
|
1736
2441
|
if (above) return above;
|
|
1737
2442
|
if (outputRel && outputRel !== ".") {
|
|
1738
|
-
const outputAbs =
|
|
2443
|
+
const outputAbs = resolve4(projectDir, outputRel);
|
|
1739
2444
|
if (bound(outputAbs)) return outputAbs;
|
|
1740
2445
|
}
|
|
1741
2446
|
return null;
|
|
@@ -1818,7 +2523,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1818
2523
|
return notDeployableResult(analysis);
|
|
1819
2524
|
}
|
|
1820
2525
|
const files = analysis.files;
|
|
1821
|
-
const outputAbs =
|
|
2526
|
+
const outputAbs = resolve4(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1822
2527
|
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1823
2528
|
const siteFileState = loadSiteFile(ctx.projectDir);
|
|
1824
2529
|
if (siteFileState.kind === "corrupted") {
|
|
@@ -2295,33 +3000,138 @@ Full status:`, res);
|
|
|
2295
3000
|
server.registerTool(
|
|
2296
3001
|
"recover",
|
|
2297
3002
|
{
|
|
2298
|
-
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.
|
|
3003
|
+
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.",
|
|
2299
3004
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2300
3005
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
2301
3006
|
inputSchema: {
|
|
2302
3007
|
projectDir: projectDirInput,
|
|
2303
|
-
action: z3.enum(["start", "status", "complete"]),
|
|
3008
|
+
action: z3.enum(["start", "status", "complete", "download"]),
|
|
2304
3009
|
hostname: z3.string().optional().describe("Required for start."),
|
|
2305
|
-
verificationId: z3.string().optional().describe("
|
|
3010
|
+
verificationId: z3.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
3011
|
+
outputDir: z3.string().optional().describe("Relative extraction directory for complete/download (default: html)."),
|
|
2306
3012
|
preserveExistingCredentials: z3.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
2307
3013
|
}
|
|
2308
3014
|
},
|
|
2309
3015
|
async (args) => {
|
|
2310
3016
|
try {
|
|
2311
3017
|
const ctx = withProjectDir(baseCtx, args.projectDir);
|
|
3018
|
+
const localSite = loadSiteFile(ctx.projectDir);
|
|
3019
|
+
const localCredentialIsActive = async () => {
|
|
3020
|
+
if (localSite.kind !== "ok") return false;
|
|
3021
|
+
try {
|
|
3022
|
+
await ctx.client.getSiteStatus(localSite.file.siteId, localSite.file.credential);
|
|
3023
|
+
return true;
|
|
3024
|
+
} catch (error) {
|
|
3025
|
+
if (isSakupaError(error) && error.code === "unauthorized") return false;
|
|
3026
|
+
throw error;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
const download = async () => {
|
|
3030
|
+
const site = requireSiteFile(ctx);
|
|
3031
|
+
const archive = await ctx.client.getSiteArchive(site.siteId, site.credential);
|
|
3032
|
+
const bytes = await ctx.client.downloadArchive(archive.archiveUrl);
|
|
3033
|
+
const extracted = await extractRecoveryArchive({
|
|
3034
|
+
projectDir: ctx.projectDir,
|
|
3035
|
+
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {},
|
|
3036
|
+
archive: bytes,
|
|
3037
|
+
expectedBytes: archive.totalBytes,
|
|
3038
|
+
expectedFiles: archive.fileCount
|
|
3039
|
+
});
|
|
3040
|
+
deleteRecoveryFile(ctx.projectDir);
|
|
3041
|
+
return { archive, extracted };
|
|
3042
|
+
};
|
|
3043
|
+
if (args.action === "download") {
|
|
3044
|
+
const { archive, extracted } = await download();
|
|
3045
|
+
return text(
|
|
3046
|
+
"recovery_content_downloaded",
|
|
3047
|
+
`Recovered site content with the credential in .sakupa/site.json.
|
|
3048
|
+
Output directory: ${extracted.outputDir}
|
|
3049
|
+
Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
3050
|
+
No DNS verification was started or repeated.`,
|
|
3051
|
+
{
|
|
3052
|
+
siteId: archive.siteId,
|
|
3053
|
+
outputDir: extracted.outputDir,
|
|
3054
|
+
fileCount: extracted.fileCount,
|
|
3055
|
+
totalBytes: extracted.totalBytes,
|
|
3056
|
+
alreadyPresent: extracted.alreadyPresent,
|
|
3057
|
+
credentialUsedFromLocalFile: true,
|
|
3058
|
+
dnsVerificationRepeated: false
|
|
3059
|
+
}
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
2312
3062
|
if (args.action === "start") {
|
|
3063
|
+
if (localSite.kind === "ok" && await localCredentialIsActive()) {
|
|
3064
|
+
return structuredToolResult({
|
|
3065
|
+
schemaVersion: 1,
|
|
3066
|
+
outcome: "completed",
|
|
3067
|
+
resultCode: "recovery_credential_already_present",
|
|
3068
|
+
summary: "A valid .sakupa/site.json already exists. Management authority is restored; do not start another TXT recovery. Continue with recover download.",
|
|
3069
|
+
data: {
|
|
3070
|
+
siteId: localSite.file.siteId,
|
|
3071
|
+
credentialStoredLocally: true,
|
|
3072
|
+
dnsVerificationRepeated: false
|
|
3073
|
+
},
|
|
3074
|
+
nextActions: [
|
|
3075
|
+
{
|
|
3076
|
+
tool: "recover",
|
|
3077
|
+
arguments: {
|
|
3078
|
+
projectDir: ctx.projectDir,
|
|
3079
|
+
action: "download",
|
|
3080
|
+
outputDir: args.outputDir ?? "html"
|
|
3081
|
+
},
|
|
3082
|
+
allowed: true
|
|
3083
|
+
}
|
|
3084
|
+
]
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
if (localSite.kind === "corrupted") {
|
|
3088
|
+
throw new SakupaError(
|
|
3089
|
+
"state_conflict",
|
|
3090
|
+
`Cannot start recovery while .sakupa/site.json is damaged: ${localSite.problem}`
|
|
3091
|
+
);
|
|
3092
|
+
}
|
|
3093
|
+
const pending2 = loadRecoveryFile(ctx.projectDir);
|
|
3094
|
+
if (pending2) {
|
|
3095
|
+
return structuredToolResult({
|
|
3096
|
+
schemaVersion: 1,
|
|
3097
|
+
outcome: "waiting_user",
|
|
3098
|
+
resultCode: "domain_recovery_local_state_found",
|
|
3099
|
+
summary: "A resumable DNS recovery already exists in this project. Do not create another TXT challenge; continue the stored verification.",
|
|
3100
|
+
data: { verificationId: pending2.verificationId, resumable: true },
|
|
3101
|
+
nextActions: [
|
|
3102
|
+
{
|
|
3103
|
+
tool: "recover",
|
|
3104
|
+
arguments: {
|
|
3105
|
+
projectDir: ctx.projectDir,
|
|
3106
|
+
action: "status",
|
|
3107
|
+
verificationId: pending2.verificationId
|
|
3108
|
+
},
|
|
3109
|
+
allowed: true
|
|
3110
|
+
}
|
|
3111
|
+
]
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
2313
3114
|
if (!args.hostname) {
|
|
2314
3115
|
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
2315
3116
|
}
|
|
2316
3117
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
3118
|
+
writeRecoveryFile(ctx.projectDir, {
|
|
3119
|
+
verificationId: res2.verificationId,
|
|
3120
|
+
credential: generateCredential(),
|
|
3121
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3122
|
+
});
|
|
3123
|
+
const txtShort = shortHostFor(res2.verificationRecord.name, res2.apexDomain);
|
|
2317
3124
|
return text(
|
|
2318
3125
|
"domain_recovery_started",
|
|
2319
3126
|
`Recovery started for ${args.hostname} (apex domain: ${res2.apexDomain}).
|
|
2320
3127
|
|
|
2321
3128
|
Create this DNS record to prove apex-domain control:
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
value:
|
|
3129
|
+
type: ${res2.verificationRecord.type}
|
|
3130
|
+
host/label: ${txtShort}
|
|
3131
|
+
value: ${res2.verificationRecord.value}
|
|
3132
|
+
full name: ${res2.verificationRecord.name}
|
|
3133
|
+
|
|
3134
|
+
Enter ONLY "${txtShort}" in DNS panels whose host/name field automatically appends ".${res2.apexDomain}". The saved full record must be exactly "${res2.verificationRecord.name}" and must never contain the apex twice.
|
|
2325
3135
|
|
|
2326
3136
|
${res2.message}
|
|
2327
3137
|
|
|
@@ -2331,20 +3141,60 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2331
3141
|
{
|
|
2332
3142
|
verificationId: res2.verificationId,
|
|
2333
3143
|
apexDomain: res2.apexDomain,
|
|
2334
|
-
verificationRecord:
|
|
2335
|
-
|
|
3144
|
+
verificationRecord: {
|
|
3145
|
+
...res2.verificationRecord,
|
|
3146
|
+
shortHost: txtShort
|
|
3147
|
+
},
|
|
3148
|
+
requiredRecords: [
|
|
3149
|
+
{
|
|
3150
|
+
type: res2.verificationRecord.type,
|
|
3151
|
+
shortHost: txtShort,
|
|
3152
|
+
name: res2.verificationRecord.name,
|
|
3153
|
+
value: res2.verificationRecord.value
|
|
3154
|
+
}
|
|
3155
|
+
],
|
|
3156
|
+
revokesPreviousCredentialsByDefault: true,
|
|
3157
|
+
localRecoveryStateStored: true
|
|
2336
3158
|
},
|
|
2337
3159
|
"waiting_user"
|
|
2338
3160
|
);
|
|
2339
3161
|
}
|
|
2340
|
-
if (
|
|
3162
|
+
if (localSite.kind === "ok" && args.action === "status" && await localCredentialIsActive()) {
|
|
3163
|
+
return structuredToolResult({
|
|
3164
|
+
schemaVersion: 1,
|
|
3165
|
+
outcome: "completed",
|
|
3166
|
+
resultCode: "recovery_credential_already_present",
|
|
3167
|
+
summary: "The recovered credential is already stored in .sakupa/site.json. Resume content download with the local key; DNS recovery must not be repeated.",
|
|
3168
|
+
data: { siteId: localSite.file.siteId, credentialStoredLocally: true },
|
|
3169
|
+
nextActions: [
|
|
3170
|
+
{
|
|
3171
|
+
tool: "recover",
|
|
3172
|
+
arguments: {
|
|
3173
|
+
projectDir: ctx.projectDir,
|
|
3174
|
+
action: "download",
|
|
3175
|
+
outputDir: args.outputDir ?? "html"
|
|
3176
|
+
},
|
|
3177
|
+
allowed: true
|
|
3178
|
+
}
|
|
3179
|
+
]
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
const pending = loadRecoveryFile(ctx.projectDir);
|
|
3183
|
+
const verificationId = args.verificationId ?? pending?.verificationId;
|
|
3184
|
+
if (!verificationId) {
|
|
2341
3185
|
throw new SakupaError(
|
|
2342
3186
|
"invalid_request",
|
|
2343
|
-
"verificationId is required
|
|
3187
|
+
"verificationId is required because no local resumable recovery state exists"
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
if (pending && pending.verificationId !== verificationId) {
|
|
3191
|
+
throw new SakupaError(
|
|
3192
|
+
"state_conflict",
|
|
3193
|
+
"The supplied verificationId differs from this project\u2019s stored recovery state."
|
|
2344
3194
|
);
|
|
2345
3195
|
}
|
|
2346
3196
|
if (args.action === "status") {
|
|
2347
|
-
const res2 = await ctx.client.getRecoveryStatus(
|
|
3197
|
+
const res2 = await ctx.client.getRecoveryStatus(verificationId);
|
|
2348
3198
|
return structuredToolResult({
|
|
2349
3199
|
schemaVersion: 1,
|
|
2350
3200
|
outcome: res2.status === "expired" ? "expired" : res2.readyToComplete ? "completed" : "pending_provider",
|
|
@@ -2354,14 +3204,29 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2354
3204
|
nextActions: [
|
|
2355
3205
|
{
|
|
2356
3206
|
tool: "recover",
|
|
2357
|
-
arguments: {
|
|
3207
|
+
arguments: {
|
|
3208
|
+
projectDir: ctx.projectDir,
|
|
3209
|
+
action: "complete",
|
|
3210
|
+
verificationId,
|
|
3211
|
+
outputDir: args.outputDir ?? "html"
|
|
3212
|
+
},
|
|
2358
3213
|
allowed: res2.readyToComplete,
|
|
2359
3214
|
...res2.readyToComplete ? {} : { reasonCode: res2.status }
|
|
2360
3215
|
}
|
|
2361
3216
|
]
|
|
2362
3217
|
});
|
|
2363
3218
|
}
|
|
2364
|
-
const
|
|
3219
|
+
const recoveryState = pending ?? (() => {
|
|
3220
|
+
const created = {
|
|
3221
|
+
verificationId,
|
|
3222
|
+
credential: generateCredential(),
|
|
3223
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3224
|
+
};
|
|
3225
|
+
writeRecoveryFile(ctx.projectDir, created);
|
|
3226
|
+
return created;
|
|
3227
|
+
})();
|
|
3228
|
+
const res = await ctx.client.completeRecovery(verificationId, {
|
|
3229
|
+
credential: recoveryState.credential,
|
|
2365
3230
|
...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
|
|
2366
3231
|
});
|
|
2367
3232
|
writeSiteFile(
|
|
@@ -2369,31 +3234,63 @@ After the DNS record resolves, re-run recover with verificationId: "${res2.verif
|
|
|
2369
3234
|
{
|
|
2370
3235
|
siteId: res.siteId,
|
|
2371
3236
|
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
2372
|
-
credential:
|
|
3237
|
+
credential: recoveryState.credential,
|
|
2373
3238
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2374
3239
|
apiBaseUrl: ctx.apiBaseUrl
|
|
2375
3240
|
},
|
|
2376
3241
|
{ allowReplace: true }
|
|
2377
3242
|
);
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
3243
|
+
deleteRecoveryFile(ctx.projectDir);
|
|
3244
|
+
try {
|
|
3245
|
+
const { archive, extracted } = await download();
|
|
3246
|
+
return text(
|
|
3247
|
+
"domain_recovery_completed",
|
|
3248
|
+
`Recovery complete.
|
|
2381
3249
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
2382
3250
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
2383
3251
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
3252
|
+
The NEW management credential was written to .sakupa/site.json before content download. The site archive was then requested with that key and extracted.
|
|
3253
|
+
Output directory: ${extracted.outputDir}
|
|
3254
|
+
Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
3255
|
+
` + credentialGitReminder(ctx.projectDir),
|
|
3256
|
+
{
|
|
3257
|
+
siteId: res.siteId,
|
|
3258
|
+
boundHostnames: res.boundHostnames,
|
|
3259
|
+
revokedPreviousCredentials: res.revokedPreviousCredentials,
|
|
3260
|
+
archiveExpiresAt: archive.archiveExpiresAt,
|
|
3261
|
+
outputDir: extracted.outputDir,
|
|
3262
|
+
fileCount: extracted.fileCount,
|
|
3263
|
+
totalBytes: extracted.totalBytes,
|
|
3264
|
+
alreadyPresent: extracted.alreadyPresent,
|
|
3265
|
+
credentialStoredLocally: true,
|
|
3266
|
+
archiveRequestedWithRecoveredCredential: true
|
|
3267
|
+
}
|
|
3268
|
+
);
|
|
3269
|
+
} catch (downloadError) {
|
|
3270
|
+
return structuredToolResult({
|
|
3271
|
+
schemaVersion: 1,
|
|
3272
|
+
outcome: "waiting_user",
|
|
3273
|
+
resultCode: "recovery_credential_restored_download_pending",
|
|
3274
|
+
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.`,
|
|
3275
|
+
data: {
|
|
3276
|
+
siteId: res.siteId,
|
|
3277
|
+
credentialStoredLocally: true,
|
|
3278
|
+
contentDownloadCompleted: false,
|
|
3279
|
+
dnsVerificationRepeated: false
|
|
3280
|
+
},
|
|
3281
|
+
nextActions: [
|
|
3282
|
+
{
|
|
3283
|
+
tool: "recover",
|
|
3284
|
+
arguments: {
|
|
3285
|
+
projectDir: ctx.projectDir,
|
|
3286
|
+
action: "download",
|
|
3287
|
+
outputDir: args.outputDir ?? "html"
|
|
3288
|
+
},
|
|
3289
|
+
allowed: true
|
|
3290
|
+
}
|
|
3291
|
+
]
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
2397
3294
|
} catch (e) {
|
|
2398
3295
|
return toolError(e);
|
|
2399
3296
|
}
|
|
@@ -2710,6 +3607,8 @@ Workflow:
|
|
|
2710
3607
|
Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2711
3608
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2712
3609
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
3610
|
+
Recovery writes the new local credential before downloading content. If a session stops after
|
|
3611
|
+
.sakupa/site.json exists, NEVER repeat DNS recovery: resume with recover action "download".
|
|
2713
3612
|
5. support (subscribed sites) opens a support ticket; report sends a
|
|
2714
3613
|
sanitized diagnostic report after the user explicitly confirms it.
|
|
2715
3614
|
|
|
@@ -2728,8 +3627,9 @@ frustrated, proactively offer report: it files the problem into Sakupa's ticket
|
|
|
2728
3627
|
alert stream, and you should attach your own factual account via agentContext.
|
|
2729
3628
|
|
|
2730
3629
|
Present every step as Sakupa's own: never attribute DNS, certificates or hosting to
|
|
2731
|
-
underlying infrastructure vendors in front of the user. Relay DNS record
|
|
2732
|
-
|
|
3630
|
+
underlying infrastructure vendors in front of the user. Relay DNS record values and full
|
|
3631
|
+
names verbatim, but use the tool's shortHost value for a DNS panel host/name field that
|
|
3632
|
+
automatically appends the apex domain. The saved FQDN must never contain the apex twice.
|
|
2733
3633
|
|
|
2734
3634
|
Safety boundaries:
|
|
2735
3635
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|