@zalkera/client 0.21.3 → 0.21.4
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/bin/validate-storefront.mjs +77 -9
- package/package.json +1 -1
|
@@ -170,6 +170,9 @@ function resolveSourceRoot() {
|
|
|
170
170
|
*/
|
|
171
171
|
const unmeasured = [];
|
|
172
172
|
|
|
173
|
+
/** 한 파일을 읽어들이는 상한. 근거는 `readSafe` 주석. */
|
|
174
|
+
const MAX_READ_BYTES = 8 * 1024 * 1024;
|
|
175
|
+
|
|
173
176
|
/** 못 잰 것을 적는다. 사유(errno)와 무엇을 못 했는지를 함께 남긴다 — "안 잰 것을 잰 척" 하지 않기 위해. */
|
|
174
177
|
function markUnmeasured(path, code, what) {
|
|
175
178
|
unmeasured.push({path: relative(process.cwd(), path), code: code ?? "UNKNOWN", what});
|
|
@@ -184,6 +187,19 @@ function markUnmeasured(path, code, what) {
|
|
|
184
187
|
*/
|
|
185
188
|
function readSafe(file, what) {
|
|
186
189
|
try {
|
|
190
|
+
// ⚠ 크기 상한. 이 검사기는 **신뢰 밖 소스**에도 도는데(서빙 샌드박스), 상한이 없으면 import
|
|
191
|
+
// 한 줄로 임의 크기 파일을 읽혀 빌드 박스 메모리를 밀어낼 수 있다 — 실측으로 레포 밖 400MB
|
|
192
|
+
// 파일 하나에 RSS 47MB → 812MB 였다. 넘으면 **못 잰 것으로 적는다**(조용히 건너뛰면 통과로
|
|
193
|
+
// 읽힌다). 소스 한 파일이 이 크기를 넘는 것은 그 자체로 사람이 볼 일이다.
|
|
194
|
+
const {size} = statSync(file);
|
|
195
|
+
if (size > MAX_READ_BYTES) {
|
|
196
|
+
markUnmeasured(
|
|
197
|
+
file,
|
|
198
|
+
"EFBIG",
|
|
199
|
+
`${what} — ${Math.round(size / 1048576)}MB(상한 ${MAX_READ_BYTES / 1048576}MB)`,
|
|
200
|
+
);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
187
203
|
return readFileSync(file, "utf8");
|
|
188
204
|
} catch (e) {
|
|
189
205
|
if (e.code === "ENOENT") return null; // 없는 파일은 "못 잰 것"이 아니다 — 규칙이 판단할 사실이다
|
|
@@ -1461,7 +1477,9 @@ function checkDocCoordinates() {
|
|
|
1461
1477
|
if (fileExists(agents)) {
|
|
1462
1478
|
const agentsText = readSafe(agents, "D1 문서 좌표 검사(AGENTS.md)");
|
|
1463
1479
|
for (const tok of docPathTokens(agentsText ?? "")) {
|
|
1464
|
-
|
|
1480
|
+
const target = containedPath(repoRoot, tok);
|
|
1481
|
+
if (target === null) continue; // 레포 밖 좌표 — 남의 파일시스템을 탐침하지 않는다
|
|
1482
|
+
if (!fileExists(target)) {
|
|
1465
1483
|
sink.push(
|
|
1466
1484
|
`[D1] AGENTS.md 가 없는 파일을 가리킵니다: \`${tok}\` — codegen 이 이 문서를 가장 먼저 ` +
|
|
1467
1485
|
`읽습니다. 실물 경로로 고치거나, 이제 없는 파일이면 경로 표기를 지우세요.`,
|
|
@@ -1485,7 +1503,9 @@ function checkDocCoordinates() {
|
|
|
1485
1503
|
if (!fileExists(llms)) return; // 미설치 — 없는 것을 센 척하지 않는다.
|
|
1486
1504
|
const llmsText = readSafe(llms, "D2 문서 좌표 검사(llms.txt)");
|
|
1487
1505
|
for (const tok of docPathTokens(llmsText ?? "", /^src\/(app|components|lib)\//)) {
|
|
1488
|
-
|
|
1506
|
+
const target = containedPath(repoRoot, tok);
|
|
1507
|
+
if (target === null) continue; // D1 과 같은 봉쇄
|
|
1508
|
+
if (!fileExists(target)) {
|
|
1489
1509
|
sink.push(
|
|
1490
1510
|
`[D2] llms.txt 가 본보기로 지목한 \`${tok}\` 이 이 레포에 없습니다 — 레시피가 실물을 ` +
|
|
1491
1511
|
`앞질렀거나(공표 먼저) 본보기가 그 파일을 지웠습니다. 어느 쪽인지 확인해 한쪽을 맞추세요.`,
|
|
@@ -1679,6 +1699,53 @@ function within(p, base) {
|
|
|
1679
1699
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1680
1700
|
}
|
|
1681
1701
|
|
|
1702
|
+
/**
|
|
1703
|
+
* 레포 안으로 **봉쇄된** 경로 해석. 소스 내용(문서 좌표·import 지정자)에서 나온 경로 문자열을
|
|
1704
|
+
* 파일시스템에 대기 **전에** 반드시 통과시킨다.
|
|
1705
|
+
*
|
|
1706
|
+
* 왜 필요한가 — 이 검사기는 고객 자기 레포에서만 도는 물건이 아니다. `build.sh` 가 서빙 샌드박스에서
|
|
1707
|
+
* **테넌트가 올린 소스**에 `--gate` 로 돌리고, 그 출력은 `/_gate-status` logTail 로 테넌트에게 돌아간다.
|
|
1708
|
+
* 즉 입력·탐침·회신이 다 갖춰져 있어, 경계가 없으면 우리 빌드 박스에 대한 **존재 오라클**(D1/D2)과
|
|
1709
|
+
* **임의 파일 읽기**(import 그래프)가 된다. 셋 다 심의가 통제군을 세워 실증했다.
|
|
1710
|
+
*
|
|
1711
|
+
* 접두 문자열로 막으려던 판이 있었다 — `tok.startsWith("..")` 는 선행 `..` 만 막고 `a/../../x` 에
|
|
1712
|
+
* 뚫린다. 경로 경계는 **해석 후 봉쇄**로만 선다. `walkTree` 가 쓰는 규칙과 같은 것이다.
|
|
1713
|
+
*
|
|
1714
|
+
* 경계는 **소스 루트가 아니라 레포 루트**다. `src/app/page.tsx` 가 `../../content` 를 부르는 것은
|
|
1715
|
+
* 정당한 간선이고(본보기 4벌 실물), 소스 루트로 좁히면 그 간선을 끊는다.
|
|
1716
|
+
*
|
|
1717
|
+
* 반환: 봉쇄된 절대경로 · 레포 밖으로 나가면 `null`. `null` 은 "없다"가 아니라 **"우리가 판정할
|
|
1718
|
+
* 자리가 아니다"** 다 — 탐침하지도, 부재를 단정하지도 않는다.
|
|
1719
|
+
*/
|
|
1720
|
+
function containedPath(from, spec) {
|
|
1721
|
+
const target = resolve(from, spec);
|
|
1722
|
+
if (!within(target, repoRootDir)) return null; // ⓐ 문자열 해석 단계의 이탈(`..`·절대경로)
|
|
1723
|
+
// ⓑ 심링크 이탈 — `resolve` 는 심링크를 풀지 않으므로 `link/x.ts`(link → /etc)는 ⓐ 를 통과한다.
|
|
1724
|
+
// 성분마다 확인한다. 없는 성분을 만나면 그 경로는 애초에 존재하지 않으니 이탈도 아니다.
|
|
1725
|
+
let cur = repoRootDir;
|
|
1726
|
+
for (const part of relative(repoRootDir, target).split(sep)) {
|
|
1727
|
+
if (part === "") continue;
|
|
1728
|
+
cur = join(cur, part);
|
|
1729
|
+
let st;
|
|
1730
|
+
try {
|
|
1731
|
+
st = lstatSync(cur);
|
|
1732
|
+
} catch {
|
|
1733
|
+
return target; // 없음 — 죽은 좌표인지는 부르는 쪽 규칙이 판단한다
|
|
1734
|
+
}
|
|
1735
|
+
if (st.isSymbolicLink()) {
|
|
1736
|
+
let real;
|
|
1737
|
+
try {
|
|
1738
|
+
real = realpathSync(cur);
|
|
1739
|
+
} catch {
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
if (!within(real, repoRootDir)) return null;
|
|
1743
|
+
cur = real;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return cur;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1682
1749
|
/** 심링크인가. `lstatSync` 는 따라가지 않는다. 못 물으면 아니라고 답한다(뒤의 `statSync` 가 잡는다). */
|
|
1683
1750
|
function isSymlink(p) {
|
|
1684
1751
|
try {
|
|
@@ -1840,9 +1907,12 @@ function resolveImport(spec, fromFile) {
|
|
|
1840
1907
|
else if (spec.startsWith(".")) base = resolve(dirname(fromFile), spec);
|
|
1841
1908
|
else return null;
|
|
1842
1909
|
|
|
1910
|
+
// ⚠ 후보 **마다** 봉쇄한다 — `base` 만 검사하면 마지막 성분이 심링크인 `src/lib/x.ts` 가 샌다.
|
|
1843
1911
|
for (const cand of [base, ...[".ts", ".tsx", ".js", ".jsx"].flatMap((e) => [base + e, join(base, "index" + e)])]) {
|
|
1912
|
+
const safe = containedPath(repoRootDir, cand);
|
|
1913
|
+
if (safe === null) continue; // 레포 밖 — 따라가지 않는다
|
|
1844
1914
|
try {
|
|
1845
|
-
if (statSync(
|
|
1915
|
+
if (statSync(safe).isFile()) return safe;
|
|
1846
1916
|
} catch {
|
|
1847
1917
|
/* 다음 후보 */
|
|
1848
1918
|
}
|
|
@@ -1904,12 +1974,10 @@ function dynamicApiReachableFrom(entry) {
|
|
|
1904
1974
|
if (seen.has(file)) continue;
|
|
1905
1975
|
seen.add(file);
|
|
1906
1976
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
1977
|
+
// `readSafe` 를 태운다 — 조용한 `catch {}` 는 못 읽은 것을 통과로 만든다(이 파일의 처분).
|
|
1978
|
+
// 크기 상한도 여기서 듣는다(아래 MAX_READ_BYTES).
|
|
1979
|
+
const raw = readSafe(file, `C1 서버 그래프 판독(${relative(repoRootDir, file)})`);
|
|
1980
|
+
if (raw === null) continue;
|
|
1913
1981
|
if (isClientBoundary(raw)) continue; // 클라이언트 경계 — 여기서 끊는다.
|
|
1914
1982
|
|
|
1915
1983
|
const text = stripComments(raw); // 주석만 제거(문자열 값·import 지정자 보존)
|