@zalkera/client 0.21.7 → 0.21.9

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.
@@ -1418,14 +1418,16 @@ const CONTENT_MODE = detectContentMode(root);
1418
1418
  *
1419
1419
  * 무엇을 검사 대상으로 삼는가 — **오탐이 나면 이 검사는 무력화된다**(µ주석이 붙어 조용히 꺼진다).
1420
1420
  * 그래서 "명백히 이 레포의 파일 경로"인 백틱 토큰만 본다:
1421
- * ⑴ 백틱 안 · `/` 포함 · 확장자(ts/tsx/js/jsx/mjs/cjs/json/css/md)로 끝남
1421
+ * ⑴ 백틱 안 · `/` 포함 · 확장자(ts/tsx/js/jsx/mjs/cjs/json/css/md)로 끝남. 이름 부분은 유니코드
1422
+ * 글자·숫자를 받는다 — 한국 SMB 대상 제품에서 `src/app/회사연혁/page.tsx` 는 기본값이고,
1423
+ * ASCII 로 잠그면 그런 좌표는 죽은 링크여도 **조용히 통과한다**.
1422
1424
  * ⑵ 자리표시자·글로브 문자 없음(`<slug>`·`**`·`{id}` 는 경로가 아니라 패턴이다 — 문자 집합으로 배제)
1423
1425
  * ⑶ 다른 레포·설치물·생성물이 아님(`doc/` = 백엔드 정본 · `node_modules/` · `.zalkera/` = 팩 산출물 ·
1424
1426
  * 절대경로 · `@scope/` 패키지 경로)
1425
1427
  * 남는 것은 전부 레포 루트 기준 상대 경로여야 한다. **디렉터리는 안 센다** — `public/` 처럼 팩이
1426
1428
  * 만들어 주는 자리가 있어 존재 판정이 참이 아니다.
1427
1429
  */
1428
- const DOC_PATH_TOKEN = /^[A-Za-z0-9_.\-/[\]]+\.(?:tsx?|jsx?|mjs|cjs|json|css|md)$/;
1430
+ const DOC_PATH_TOKEN = /^[\p{L}\p{N}_.\-/[\]]+\.(?:tsx?|jsx?|mjs|cjs|json|css|md)$/u;
1429
1431
  const DOC_PATH_SKIP_PREFIX = ["doc/", "node_modules/", ".zalkera/", "@", "/", "http"];
1430
1432
 
1431
1433
  /** 문서 본문에서 이 레포의 파일을 가리키는 것으로 판정되는 백틱 토큰. */
@@ -2053,7 +2055,7 @@ function checkLayoutBlastRadius() {
2053
2055
  warnings.push(`[C1b] ${detail} — 예외 허용(zalkera-allow-dynamic: ${allow[1].trim()}).`);
2054
2056
  } else {
2055
2057
  servingSink().push(
2056
- `[C1b] ${detail}(memo31 §0-1). ${remedyFor(why)}. 꼭 필요하면 ` +
2058
+ `[C1b] ${detail}. ${remedyFor(why)}. 꼭 필요하면 ` +
2057
2059
  `${relative(process.cwd(), file)} 에 \`// zalkera-allow-dynamic: <이유>\` 마커로 정당화하라 ` +
2058
2060
  `(마커는 layout 이 아니라 **이 파일**에 붙어야 듣는다).`,
2059
2061
  );
@@ -2249,11 +2251,13 @@ function checkContentContract() {
2249
2251
  }
2250
2252
 
2251
2253
  // N3 — 매니페스트 ↔ 파일. 파일만 있으면 그 페이지는 라우트도 sitemap 도 모르는 유령이 된다.
2254
+ // slug 는 **파일 이름 그대로**다(`[^"'/]`). ASCII 로 잠그면 한글 페이지가 정확히 배선돼 있어도
2255
+ // 반려되고, 면제 마커가 없는 축이라 고객은 slug 를 영문으로 바꾸는 것 말고 길이 없다.
2252
2256
  // **주석은 지운다** — 매니페스트의 사용법 예시가 주석 안에 import 문을 담고 있고, 그걸 실제
2253
2257
  // import 로 읽으면 없는 파일을 찾는 오탐이 난다(실제로 이 검사를 넣자마자 그렇게 죽었다).
2254
2258
  // 이 레포가 C1b·S8 에서 이미 밟은 함정과 같은 것이라 같은 처방을 쓴다.
2255
2259
  const declaredSlugs = manifest
2256
- ? new Set([...stripComments(manifest).matchAll(/from\s+["']\.\/pages\/([\w-]+)\.json["']/g)].map((m) => m[1]))
2260
+ ? new Set([...stripComments(manifest).matchAll(/from\s+["']\.\/pages\/([^"'/]+)\.json["']/g)].map((m) => m[1]))
2257
2261
  : null;
2258
2262
  if (declaredSlugs) {
2259
2263
  for (const file of files) {
@@ -2450,7 +2454,7 @@ function check(file) {
2450
2454
  servingSink().push(
2451
2455
  `[C1] ${detail}. SEO 페이지는 ISR(export const revalidate = N) 또는 static 이어야 한다. ` +
2452
2456
  `실시간·개인화 값은 클라이언트 컴포넌트(아일랜드)로, 상태 변경은 BFF route handler 로 옮겨라. ` +
2453
- `동적 SSR 이 꼭 필요하면 \`// zalkera-allow-dynamic: <이유>\` 마커로 정당화하라(memo31 §0-12).`,
2457
+ `동적 SSR 이 꼭 필요하면 \`// zalkera-allow-dynamic: <이유>\` 마커로 정당화하라.`,
2454
2458
  );
2455
2459
  }
2456
2460
  }
@@ -2469,7 +2473,7 @@ function check(file) {
2469
2473
  if (alien.length > 0) {
2470
2474
  s6.push(
2471
2475
  `[S6] ${rel}: 남의 토큰 어휘 ${alien.join("·")} — 우리 @theme 에 없는 이름이라 색이 빠진다. ` +
2472
- `재작성 표로 옮겨라(memo102 §4.1: bg-card→bg-surface, text-muted-foreground→text-muted 등).`,
2476
+ `우리 어휘로 옮겨라(bg-card→bg-surface, text-muted-foreground→text-muted 등).`,
2473
2477
  );
2474
2478
  }
2475
2479
  }
@@ -2863,13 +2867,13 @@ function judgeGuardPlacement(rawBody) {
2863
2867
  // 가드가 어디에도 없다 vs 첫 구문이 아니다 — 사유를 갈라 준다(고치는 방법이 다르다).
2864
2868
  if (!new RegExp(CALL).test(body)) {
2865
2869
  return (
2866
- `가 변이 메서드인데 assertSameOrigin 호출이 없습니다 — 교차사이트 위조가 열립니다(memo118).` +
2870
+ `가 변이 메서드인데 assertSameOrigin 호출이 없습니다 — 교차사이트 위조가 열립니다.` +
2867
2871
  ` 정당한 예외면 파일 상단에 \`// zalkera-allow-cross-origin: 이유\` 를 다세요.`
2868
2872
  );
2869
2873
  }
2870
2874
  return (
2871
2875
  `의 assertSameOrigin 이 **본문의 첫 구문이 아닙니다** — 앞선 쿠키 쓰기는 403 응답에 Set-Cookie 를` +
2872
- ` 실어 memo118 §3 불변식을 깹니다(실측 재현됨). 가드를 감싸거나(헬퍼·중첩 함수) 뒤로 미루지` +
2876
+ ` 실어 "가드에 막힌 요청은 아무 상태도 남긴다"를 깹니다. 가드를 감싸거나(헬퍼·중첩 함수) 뒤로 미루지` +
2873
2877
  ` 말고 본문 맨 앞에 두세요.`
2874
2878
  );
2875
2879
  }
@@ -2943,7 +2947,7 @@ function checkCrossOriginGuards() {
2943
2947
  crossOriginSink().push(
2944
2948
  `${where} 가 본문을 따라갈 수 없는 형태로 export 됩니다(재export·간접 참조·중괄호 없는` +
2945
2949
  ` 화살표) — 가드 위치를 기계로 확인할 수 없습니다. 핸들러를 이 파일에 중괄호 본문으로` +
2946
- ` 직접 선언하세요(memo118 §4).`,
2950
+ ` 직접 선언하세요.`,
2947
2951
  );
2948
2952
  continue;
2949
2953
  }
@@ -2963,7 +2967,7 @@ function checkCrossOriginGuards() {
2963
2967
  if (/Access-Control-Allow-Origin/i.test(code)) {
2964
2968
  crossOriginSink().push(
2965
2969
  `[X2] ${relative(root, file)} 가 CORS 헤더를 답니다 — 읽기 GET 을 가드에서 빼는 근거가` +
2966
- ` "교차 오리진 JS 가 응답을 못 읽는다"인데, 그 전제가 무너집니다(memo118 §7-2).`,
2970
+ ` "교차 오리진 JS 가 응답을 못 읽는다"인데, 그 전제가 무너집니다.`,
2967
2971
  );
2968
2972
  }
2969
2973
  }
@@ -3024,7 +3028,7 @@ function checkCrossOriginGuards() {
3024
3028
  crossOriginSink().push(
3025
3029
  `[X3] ${relative(root, consumeDef.file)} 의 consumeOAuthState 가 state 쿠키를 소각하지` +
3026
3030
  ` 않습니다 — 대조 통과 여부와 무관하게 지워야 1회용이 됩니다. 남기면 유효기간 동안` +
3027
- ` 리플레이가 가능합니다(memo118 §2).`,
3031
+ ` 리플레이가 가능합니다.`,
3028
3032
  );
3029
3033
  }
3030
3034
  // ⚠ **원문(raw)에 건다.** `stripLiterals` 를 거친 소스에서는 `sameSite: "strict"` 가
@@ -3126,7 +3130,7 @@ function checkPagesRouter() {
3126
3130
  if (!/(?:\w+\s*\.\s*)?assertSameOrigin\s*\(/.test(code)) {
3127
3131
  crossOriginSink().push(
3128
3132
  `[X1p] ${rel}: pages/api 핸들러에 assertSameOrigin 호출이 없습니다 — 교차사이트 위조가 ` +
3129
- `열립니다(memo118). 이 형태(default export + req.method 분기)에서는 검사기가 **가드의 ` +
3133
+ `열립니다. 이 형태(default export + req.method 분기)에서는 검사기가 **가드의 ` +
3130
3134
  `위치를 못 잽니다** — 호출 유무만 봅니다. 읽기 전용이면 비-GET 을 405 로 되돌리거나 ` +
3131
3135
  `파일 상단에 \`// zalkera-allow-cross-origin: 이유\` 를 다세요.`,
3132
3136
  );
@@ -3134,7 +3138,7 @@ function checkPagesRouter() {
3134
3138
  // X2 — 읽기 GET 면제의 전제(교차 오리진 JS 가 응답을 못 읽는다)가 CORS 헤더로 무너진다.
3135
3139
  if (/Access-Control-Allow-Origin/i.test(raw)) {
3136
3140
  crossOriginSink().push(
3137
- `[X2] ${rel} 가 CORS 헤더를 답니다 — 읽기 GET 을 가드에서 빼는 근거가 무너집니다(memo118 §7-2).`,
3141
+ `[X2] ${rel} 가 CORS 헤더를 답니다 — 읽기 GET 을 가드에서 빼는 근거가 무너집니다.`,
3138
3142
  );
3139
3143
  }
3140
3144
  continue;
@@ -3170,7 +3174,7 @@ function checkPagesRouter() {
3170
3174
  if (allow) warnings.push(`[C1p] ${detail} — 예외 허용(zalkera-allow-dynamic: ${allow[1].trim()}).`);
3171
3175
  else
3172
3176
  servingSink().push(
3173
- `[C1p] ${detail}(memo31 §0-12). getStaticProps + revalidate(ISR)로 바꾸고, 실시간·개인화 값은 ` +
3177
+ `[C1p] ${detail}. getStaticProps + revalidate(ISR)로 바꾸고, 실시간·개인화 값은 ` +
3174
3178
  `클라이언트에서 가져오세요. 꼭 필요하면 \`// zalkera-allow-dynamic: <이유>\` 마커로 정당화하세요.`,
3175
3179
  );
3176
3180
  }
package/dist/index.cjs CHANGED
@@ -339,6 +339,7 @@ function internalPath(raw) {
339
339
  } catch {
340
340
  return null;
341
341
  }
342
+ if (out.startsWith("//")) return null;
342
343
  try {
343
344
  if (new URL(out, DUMMY_ORIGIN).origin !== DUMMY_ORIGIN) return null;
344
345
  } catch {