@zalkera/client 0.21.3 → 0.21.5
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 +108 -11
- 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});
|
|
@@ -183,7 +186,33 @@ function markUnmeasured(path, code, what) {
|
|
|
183
186
|
* 한 자리로 모은다.
|
|
184
187
|
*/
|
|
185
188
|
function readSafe(file, what) {
|
|
189
|
+
// ⚠ **심링크를 따라가지 않는다.** 0.21.4 는 파생 경로(`containedPath`)만 봉쇄했는데, 검사기가
|
|
190
|
+
// **이름으로 여는 파일** — `AGENTS.md`·`globals.css`·`content/index.ts` 등 — 에는 봉쇄가 없었다.
|
|
191
|
+
// 테넌트가 그 이름을 임의 절대경로로 심링크하면 서빙 샌드박스에서 존재·가독성·정확한 MB·
|
|
192
|
+
// 백틱 안 경로형 문자열을 회신으로 되받는다(심의 실측). 여는 자리에도 같은 규칙을 건다.
|
|
193
|
+
//
|
|
194
|
+
// 레포 **안**을 가리키는 심링크는 그대로 읽는다(모노레포의 공유 설정 등) — 경계는 이탈이지
|
|
195
|
+
// 심링크 자체가 아니다.
|
|
196
|
+
const safe = containedPath(repoRootDir, file);
|
|
197
|
+
if (safe === null) {
|
|
198
|
+
markUnmeasured(file, "EXDEV", `${what} — 레포 밖을 가리켜 읽지 않았습니다`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
file = safe;
|
|
186
202
|
try {
|
|
203
|
+
// ⚠ 크기 상한. 이 검사기는 **신뢰 밖 소스**에도 도는데(서빙 샌드박스), 상한이 없으면 import
|
|
204
|
+
// 한 줄로 임의 크기 파일을 읽혀 빌드 박스 메모리를 밀어낼 수 있다 — 실측으로 레포 밖 400MB
|
|
205
|
+
// 파일 하나에 RSS 47MB → 812MB 였다. 넘으면 **못 잰 것으로 적는다**(조용히 건너뛰면 통과로
|
|
206
|
+
// 읽힌다). 소스 한 파일이 이 크기를 넘는 것은 그 자체로 사람이 볼 일이다.
|
|
207
|
+
const {size} = statSync(file);
|
|
208
|
+
if (size > MAX_READ_BYTES) {
|
|
209
|
+
markUnmeasured(
|
|
210
|
+
file,
|
|
211
|
+
"EFBIG",
|
|
212
|
+
`${what} — ${Math.round(size / 1048576)}MB(상한 ${MAX_READ_BYTES / 1048576}MB)`,
|
|
213
|
+
);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
187
216
|
return readFileSync(file, "utf8");
|
|
188
217
|
} catch (e) {
|
|
189
218
|
if (e.code === "ENOENT") return null; // 없는 파일은 "못 잰 것"이 아니다 — 규칙이 판단할 사실이다
|
|
@@ -1438,6 +1467,22 @@ function fileExists(path) {
|
|
|
1438
1467
|
}
|
|
1439
1468
|
}
|
|
1440
1469
|
|
|
1470
|
+
/**
|
|
1471
|
+
* **이름이 있는가** — 심링크를 따라가지 않는다.
|
|
1472
|
+
*
|
|
1473
|
+
* 고정 이름 파일(`AGENTS.md`·`llms.txt`)의 존재를 `statSync` 로 물으면 **심링크 표적**을 따라가고,
|
|
1474
|
+
* 그 표적이 있느냐 없느냐로 회신이 갈린다 — 표적이 신뢰 밖 소스의 통제 아래라 그 차이가 오라클이다.
|
|
1475
|
+
* 이름의 존재만 묻고, 읽을 수 있는지는 `readSafe` 의 봉쇄가 판단하게 둔다(표적과 무관하게 한 줄).
|
|
1476
|
+
*/
|
|
1477
|
+
function nameExists(path) {
|
|
1478
|
+
try {
|
|
1479
|
+
lstatSync(path);
|
|
1480
|
+
return true;
|
|
1481
|
+
} catch {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1441
1486
|
/**
|
|
1442
1487
|
* D 규칙의 심각도 — 좌표의 실재는 **사실 판정**이라 어느 레포에서도 참이지만, 남의 레포 문서에
|
|
1443
1488
|
* 에러를 던지지는 않는다(전제 1 "강제할 수 없다"). 우리 계약을 하나라도 선언한 레포에서만 error 다.
|
|
@@ -1458,10 +1503,12 @@ function checkDocCoordinates() {
|
|
|
1458
1503
|
const sink = docsSink();
|
|
1459
1504
|
|
|
1460
1505
|
const agents = join(repoRoot, "AGENTS.md");
|
|
1461
|
-
if (
|
|
1506
|
+
if (nameExists(agents)) {
|
|
1462
1507
|
const agentsText = readSafe(agents, "D1 문서 좌표 검사(AGENTS.md)");
|
|
1463
1508
|
for (const tok of docPathTokens(agentsText ?? "")) {
|
|
1464
|
-
|
|
1509
|
+
const target = containedPath(repoRoot, tok);
|
|
1510
|
+
if (target === null) continue; // 레포 밖 좌표 — 남의 파일시스템을 탐침하지 않는다
|
|
1511
|
+
if (!fileExists(target)) {
|
|
1465
1512
|
sink.push(
|
|
1466
1513
|
`[D1] AGENTS.md 가 없는 파일을 가리킵니다: \`${tok}\` — codegen 이 이 문서를 가장 먼저 ` +
|
|
1467
1514
|
`읽습니다. 실물 경로로 고치거나, 이제 없는 파일이면 경로 표기를 지우세요.`,
|
|
@@ -1482,10 +1529,12 @@ function checkDocCoordinates() {
|
|
|
1482
1529
|
if (!isExemplar) return;
|
|
1483
1530
|
|
|
1484
1531
|
const llms = join(repoRoot, "node_modules", "@zalkera", "client", "llms.txt");
|
|
1485
|
-
if (!
|
|
1532
|
+
if (!nameExists(llms)) return; // 미설치 — 없는 것을 센 척하지 않는다.
|
|
1486
1533
|
const llmsText = readSafe(llms, "D2 문서 좌표 검사(llms.txt)");
|
|
1487
1534
|
for (const tok of docPathTokens(llmsText ?? "", /^src\/(app|components|lib)\//)) {
|
|
1488
|
-
|
|
1535
|
+
const target = containedPath(repoRoot, tok);
|
|
1536
|
+
if (target === null) continue; // D1 과 같은 봉쇄
|
|
1537
|
+
if (!fileExists(target)) {
|
|
1489
1538
|
sink.push(
|
|
1490
1539
|
`[D2] llms.txt 가 본보기로 지목한 \`${tok}\` 이 이 레포에 없습니다 — 레시피가 실물을 ` +
|
|
1491
1540
|
`앞질렀거나(공표 먼저) 본보기가 그 파일을 지웠습니다. 어느 쪽인지 확인해 한쪽을 맞추세요.`,
|
|
@@ -1679,6 +1728,53 @@ function within(p, base) {
|
|
|
1679
1728
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1680
1729
|
}
|
|
1681
1730
|
|
|
1731
|
+
/**
|
|
1732
|
+
* 레포 안으로 **봉쇄된** 경로 해석. 소스 내용(문서 좌표·import 지정자)에서 나온 경로 문자열을
|
|
1733
|
+
* 파일시스템에 대기 **전에** 반드시 통과시킨다.
|
|
1734
|
+
*
|
|
1735
|
+
* 왜 필요한가 — 이 검사기는 고객 자기 레포에서만 도는 물건이 아니다. `build.sh` 가 서빙 샌드박스에서
|
|
1736
|
+
* **테넌트가 올린 소스**에 `--gate` 로 돌리고, 그 출력은 `/_gate-status` logTail 로 테넌트에게 돌아간다.
|
|
1737
|
+
* 즉 입력·탐침·회신이 다 갖춰져 있어, 경계가 없으면 우리 빌드 박스에 대한 **존재 오라클**(D1/D2)과
|
|
1738
|
+
* **임의 파일 읽기**(import 그래프)가 된다. 셋 다 심의가 통제군을 세워 실증했다.
|
|
1739
|
+
*
|
|
1740
|
+
* 접두 문자열로 막으려던 판이 있었다 — `tok.startsWith("..")` 는 선행 `..` 만 막고 `a/../../x` 에
|
|
1741
|
+
* 뚫린다. 경로 경계는 **해석 후 봉쇄**로만 선다. `walkTree` 가 쓰는 규칙과 같은 것이다.
|
|
1742
|
+
*
|
|
1743
|
+
* 경계는 **소스 루트가 아니라 레포 루트**다. `src/app/page.tsx` 가 `../../content` 를 부르는 것은
|
|
1744
|
+
* 정당한 간선이고(본보기 4벌 실물), 소스 루트로 좁히면 그 간선을 끊는다.
|
|
1745
|
+
*
|
|
1746
|
+
* 반환: 봉쇄된 절대경로 · 레포 밖으로 나가면 `null`. `null` 은 "없다"가 아니라 **"우리가 판정할
|
|
1747
|
+
* 자리가 아니다"** 다 — 탐침하지도, 부재를 단정하지도 않는다.
|
|
1748
|
+
*/
|
|
1749
|
+
function containedPath(from, spec) {
|
|
1750
|
+
const target = resolve(from, spec);
|
|
1751
|
+
if (!within(target, repoRootDir)) return null; // ⓐ 문자열 해석 단계의 이탈(`..`·절대경로)
|
|
1752
|
+
// ⓑ 심링크 이탈 — `resolve` 는 심링크를 풀지 않으므로 `link/x.ts`(link → /etc)는 ⓐ 를 통과한다.
|
|
1753
|
+
// 성분마다 확인한다. 없는 성분을 만나면 그 경로는 애초에 존재하지 않으니 이탈도 아니다.
|
|
1754
|
+
let cur = repoRootDir;
|
|
1755
|
+
for (const part of relative(repoRootDir, target).split(sep)) {
|
|
1756
|
+
if (part === "") continue;
|
|
1757
|
+
cur = join(cur, part);
|
|
1758
|
+
let st;
|
|
1759
|
+
try {
|
|
1760
|
+
st = lstatSync(cur);
|
|
1761
|
+
} catch {
|
|
1762
|
+
return target; // 없음 — 죽은 좌표인지는 부르는 쪽 규칙이 판단한다
|
|
1763
|
+
}
|
|
1764
|
+
if (st.isSymbolicLink()) {
|
|
1765
|
+
let real;
|
|
1766
|
+
try {
|
|
1767
|
+
real = realpathSync(cur);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
if (!within(real, repoRootDir)) return null;
|
|
1772
|
+
cur = real;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return cur;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1682
1778
|
/** 심링크인가. `lstatSync` 는 따라가지 않는다. 못 물으면 아니라고 답한다(뒤의 `statSync` 가 잡는다). */
|
|
1683
1779
|
function isSymlink(p) {
|
|
1684
1780
|
try {
|
|
@@ -1840,9 +1936,12 @@ function resolveImport(spec, fromFile) {
|
|
|
1840
1936
|
else if (spec.startsWith(".")) base = resolve(dirname(fromFile), spec);
|
|
1841
1937
|
else return null;
|
|
1842
1938
|
|
|
1939
|
+
// ⚠ 후보 **마다** 봉쇄한다 — `base` 만 검사하면 마지막 성분이 심링크인 `src/lib/x.ts` 가 샌다.
|
|
1843
1940
|
for (const cand of [base, ...[".ts", ".tsx", ".js", ".jsx"].flatMap((e) => [base + e, join(base, "index" + e)])]) {
|
|
1941
|
+
const safe = containedPath(repoRootDir, cand);
|
|
1942
|
+
if (safe === null) continue; // 레포 밖 — 따라가지 않는다
|
|
1844
1943
|
try {
|
|
1845
|
-
if (statSync(
|
|
1944
|
+
if (statSync(safe).isFile()) return safe;
|
|
1846
1945
|
} catch {
|
|
1847
1946
|
/* 다음 후보 */
|
|
1848
1947
|
}
|
|
@@ -1904,12 +2003,10 @@ function dynamicApiReachableFrom(entry) {
|
|
|
1904
2003
|
if (seen.has(file)) continue;
|
|
1905
2004
|
seen.add(file);
|
|
1906
2005
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
2006
|
+
// `readSafe` 를 태운다 — 조용한 `catch {}` 는 못 읽은 것을 통과로 만든다(이 파일의 처분).
|
|
2007
|
+
// 크기 상한도 여기서 듣는다(아래 MAX_READ_BYTES).
|
|
2008
|
+
const raw = readSafe(file, `C1 서버 그래프 판독(${relative(repoRootDir, file)})`);
|
|
2009
|
+
if (raw === null) continue;
|
|
1913
2010
|
if (isClientBoundary(raw)) continue; // 클라이언트 경계 — 여기서 끊는다.
|
|
1914
2011
|
|
|
1915
2012
|
const text = stripComments(raw); // 주석만 제거(문자열 값·import 지정자 보존)
|