Commit Graph

12 Commits

Author SHA1 Message Date
Kazuki Yamada 54c6a3d238 fix(website): Address claude third-pass review on siteverify metric
Six items from claude's incremental review (`12:48:43Z`):

- monitoring/dashboard.json: Group the outcomes widget by both
  `metric.label.outcome` and `metric.label.reason`. Previously all
  failures collapsed into a single `turnstile_failed` series, which
  contradicted the README claim that the `reason` label drives the
  breakdown.
- monitoring/metrics/*.yaml: Narrow the metric filter to
  `jsonPayload.event=("turnstile_siteverify" OR "pack_completed")`.
  Without this anchor, any future code path attaching
  `siteverifyDurationMs` to an unrelated log silently joins the
  distribution and creates new metric label values.
- usePackRequest.ts: Mirror `progressMessage.value = null` alongside
  the `progressStage.value = null` clear on token-acquisition aborted /
  error branches. Prevents a future edit setting a verifying message
  from leaking prior-run state.
- turnstile.test.ts: Add a focused `describe` block with five tests
  asserting `siteverifyDurationMs` is attached to every post-siteverify
  log (one success path + four reject branches). The metric YAML
  filters on field presence, so a refactor that drops the field on any
  branch would silently break the metric without other tests failing.
  Uses the existing `vi.spyOn(logger, ...)` pattern; no clock injection
  needed.
- monitoring/README.md: Note that the metric filter pins
  `service_name="repomix-server-us"`, so future regions (`-eu`,
  `-asia`) silently drop out until the filter is broadened or
  per-region counterparts applied.
- monitoring/README.md: Add a `gcloud logging metrics describe` snippet
  for verifying a YAML edit was actually applied (gcloud update is
  silent on no-op vs effective change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:59:53 +09:00
autofix-ci[bot] ac91b99397 [autofix.ci] apply automated fixes 2026-05-03 16:17:10 +00:00
Kazuki Yamada b58644154e fix(website): Address claude iteration 2 review
intent(maintainability): claude のフォローアップレビューで残った Improve / test gap / nit を対応。production 影響はないが将来の保守性を上げる。

fix(server/log-helper): turnstile.ts で 7 回繰り返されていた `{ event, outcome, reason, requestId, source, ...cf }` の log payload 構築を `rejectAndLog(reason, message, level, extra)` ヘルパーにまとめた。約 40 行短縮、新しい reason を追加するときに envelope を取りこぼすリスクが消える。
fix(server/extract-siteverify): 同ファイル内に `runSiteverify` を抽出して try/catch を内包。中身が無関係な fail-closed 経路 (network error, JSON parse error) を Error sentinel として返し、middleware 本体はポリシー(reject vs pass)だけに集中させる。
fix(client/cli-fallback): script-blocked エラー時のメッセージに `npx repomix --remote owner/repo` の CLI 案内を追加。privacy extension で詰まったユーザーへの逃げ道を提示。
fix(client/file-size): useTurnstile.ts が 286 行 → 250 行ガイドライン超過していたため、script load 部分を `useTurnstileScript.ts` に分離。useTurnstile 側は widget lifecycle / token request / abort に集中。

test(turnstile): 18 → 20 cases。
- siteverify が non-JSON (HTML 5xx ページ等) を返した時に runSiteverify が Error sentinel に変換し、middleware が 403 fail-closed する経路をカバー
- cross-stack contract: server の EXPECTED_TURNSTILE_ACTION (export) と client の useTurnstile.ts に埋め込まれた `action: 'pack'` 文字列が一致する、を regex で検証。片側 rename で他方が silent break する drift を防ぐ

skipped (with reason):
- Log sampling (claude #1): 現状スケール (21K req/day = 0.24 req/sec) で Cloud Logging quota 影響なし、metric 移行のときに併せて検討
- secretMissingLogged tautology (claude #2): 既に `0b09cf9d` で logger spy に置き換え済み
- hostname check (claude #3): 既に `0b09cf9d` で ALLOWED_HOSTNAMES として実装済み
- siteverify timeout fake-timer test: codex も round 2 で skip 推奨、setup が複雑で得る保証が小さい
- Client useTurnstile.ts component test: Vue test 環境が未整備
- External /api/pack consumer release note: code change ではないので別途
- secret_missing alert: out of scope の follow-up issue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 01:15:51 +09:00
autofix-ci[bot] 74312781f4 [autofix.ci] apply automated fixes 2026-05-03 14:29:56 +00:00
Kazuki Yamada 0b09cf9dfa fix(website): Address codex re-review (round 2)
intent(robustness): codex の再レビューで残った blocker と should-fix を全て対応。

fix(client/stale-callback): submitRequest の onSuccess/onError/onAbort/onProgress と getToken catch ブロック内の state mutation 全てを `isCurrent()` (= `requestController === controller`) でガード。連打で新 request が始まった後、旧 request の callback が新 request の loading/result/error を上書きする race を解消。
fix(client/abort-during-script-load): getToken に signal が渡された場合、ensureWidget() を AbortSignal-aware な Promise.race に変更。script load 中の cancel/timeout が即座に拾える。listener 登録直前にも signal.aborted を再チェック。
fix(client/site-key-throw): production で VITE_TURNSTILE_SITE_KEY 未設定なら useTurnstile() で throw。前は console.error だけだったので smoke test で見落とし得たが、throw にすればフォーム初期化が即失敗して misconfig が unmissable になる。
fix(server/hostname-claim): siteverify レスポンスの hostname を ALLOWED_HOSTNAMES (`['repomix.com']`) と照合。leaked sitekey が攻撃者ドメインで使われた場合に弾く。test sitekey は hostname を返さないので undefined は backward-compat で許可(action 検証と同じパターン)。
fix(client/window-callback-cleanup): READY_CALLBACK が success path でも `delete` されるように。stale closure 残存を完全に排除。

test(turnstile): 14 → 18 cases に拡張。
- secretMissingLogged を logger spy で「dev/test では 1 回、production では毎回」を直接 assert に変更(前は expect(true) のプレースホルダだった)
- hostname allowed / mismatch / omitted (backward-compat) を追加

skipped (codex 認定): middleware reorder。codex も「siteverify 外部呼び出しを増やすリスクがあるので見送り妥当」と再確認。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:19:00 +09:00
Kazuki Yamada e96835a0ad fix(website): Address claude review feedback on Turnstile
intent(robustness): claude のレビューで追加指摘が複数あったので、cancel UX と script-load 失敗時の冗長性を改善する。

fix(client/abort-signal): getToken に AbortSignal を追加。usePackRequest が controller.signal を渡すことで、cancel/timeout 中の Turnstile challenge が即座に中断される。前は getToken が hung している間(最大 15s)cancel が反映されない UX gap があった。
fix(client/onload-cleanup): resetForRetry で window[READY_CALLBACK] も delete する belt-and-suspenders。late-arriving script load が stale closure を解決する事故を排除。
fix(client/error-message): production で getToken 失敗時のエラーメッセージを 2 種類に分岐。"script/load/missing" を含むエラーは「ad blocker / privacy extension を無効化してリロード」、それ以外は generic verification failure。recovery path を提示してサポート負荷を減らす。
fix(client/cancel-during-getToken): getToken が abort で reject された場合、cancel reason に応じて「Request was cancelled」「Request timed out」を表示。短絡で handlePackRequest をスキップしてもメッセージは一致させる。

test(turnstile): remoteip omission/inclusion・secretMissingLogged 連続呼出・error-codes 配列で middleware が壊れない、を追加。10 → 14 cases。

skipped (low ROI):
- verifyResult の zod validation: Cloudflare API は安定、過剰防衛
- hostname の cross-check: Site Key が Cloudflare 側で hostname-pinned 済み
- siteverify timeout の fake-timer test: 設定が複雑な割に得られる保証が小さい

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:35 +09:00
autofix-ci[bot] 33260d42ea [autofix.ci] apply automated fixes 2026-05-03 13:59:00 +00:00
Kazuki Yamada 3c07e5a424 fix(website): Harden Turnstile integration per codex review
intent(robustness): codex の追加レビューで複数の race condition / セキュリティ穴 / config bug が指摘されたため、production 適用前に潰す。

fix(client/timer-leak): useTurnstile の Promise.race timeout が clearTimeout されておらず、stale timer が次の getToken の pendingResolve/pendingReject を消す race を解消。generation counter (currentGen) を導入し、callback / timeout / supersede 全てを gen 検査でガード。
fix(client/finally-race): submitRequest の finally が後続リクエスト中の共有状態 (loading, requestController) を上書きする問題を解消。`if (requestController === controller)` で local controller が現役のときだけ reset。
fix(server/prod-fail-closed): production で TURNSTILE_SECRET_KEY 未設定のときは fail-open ではなく 403 fail-closed (`reason: secret_missing`)。dev/test は従来通り skip。`isProduction` を deps として注入することでテスト容易性を確保。
fix(server/action-claim): siteverify の `action` claim を `pack` と一致するか verify。token を将来の別エンドポイントから replay されないようにする。client widget 側に `data-action='pack'` を付与済み。Cloudflare のテストキーは action を返さないため undefined は backward-compat で許可。
fix(server/token-validation): token を trim、空白 only / 2048 文字超を siteverify 前に 403。Cloudflare の上限を超える token は guaranteed-invalid なので siteverify 呼ばずに弾く。
fix(client/prod-fallback-warn): VITE_TURNSTILE_SITE_KEY 未設定の production ビルドで console.error。test sitekey が本番に混入したまま deploy される事故を smoke test で気づけるようにする。
fix(client/prod-fail-fast): production で getToken 失敗時は /api/pack を呼ばずユーザー向けエラー表示。サーバ 403 確定なので origin hit を節約。dev/preview では従来通り token なしで続行。

rejected(middleware-reorder): codex は Turnstile を rate limit *前* に置けば token 無し bot が Upstash daily limit を消費しない、と指摘していたが、middleware の登録順を変える影響範囲が大きく、token max length pre-check で大半の cheap reject は実現できているため見送り。

test(turnstile): production fail-closed / whitespace token / oversized token / action mismatch / action absent (backward-compat) を追加。10 cases pass。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:57:47 +09:00
Kazuki Yamada 523fb07111 feat(website): Add Cloudflare Turnstile verification to /api/pack
intent(pack-defense): Repomix を「コード抽出 API」として大量に叩く匿名クローラ対策。GA + Cloud Run logs の調査で 2026-04 末に 21K+ unique GitHub repos を 7 日で系統列挙されたインシデントが確認された。IP ベースの rate limit (3/min, 30/day) は機能していたが residential proxy + Tencent Cloud SG で 2,256+ unique IPs に分散され実質無力化されていた。
decision(turnstile-vs-asn): Turnstile (JS challenge) を採用。ASN ブロックは residential proxy で無力、daily limit 強化は IP 数で水増し可能。invisible JS challenge は residential proxy 経由でも通せないので一番 cost asymmetric。
constraint(scope): /api/pack のみ。docs / health / ホームページ閲覧は無関係 — Googlebot / GPTBot / ClaudeBot 等は GET HTML しか叩かないので SEO/LLMO に影響なし。
decision(fail-policy): TURNSTILE_SECRET_KEY 未設定なら fail-open(dev/preview を壊さない、警告ログは出す)。設定済みでトークン欠落・siteverify 失敗・ネットワーク失敗は全て fail-closed の 403。
decision(token-transport): X-Turnstile-Token ヘッダで送信。FormData フィールドにすると packRequestSchema (valibot) を汚染するため、cross-cutting concern として layer を分離。
decision(client-widget): invisible 不可視ウィジェットを TryIt.vue マウント時にレンダ、submit 直前に turnstile.execute() で 1-shot トークン取得。トークンは 5 分有効・1 回限りなので毎 pack で reset → execute。
rejected(form-field-token): cfTurnstileToken を FormData に入れる案 — packRequestSchema が strict object のため新フィールド追加が必要、ビジネスロジックと認証が混ざる。
rejected(asn-block): Tencent Cloud SG (AS132203) WAF ブロック — バルク部分には効くが residential proxy 部分(家庭 ISP・モバイル・大学ネット)が世界中に散らばっており ASN 単位で弾けない、正規ユーザを巻き込むリスク。
rejected(daily-limit-tightening): 30 → 10/day per IP — IP 数で水増しできる相手には無意味、人間ユーザの体験のみ悪化。
constraint(observability): outcome="turnstile_failed" として既存の pack_completed イベントに乗せる。新 metric 不要、既存ダッシュボードに自動で reject reason として現れる。
learned(rate-limit-effectiveness): スパイク期間中の SG pack 成功率は 0.15% (32/21,501)。app-level rate limit は処理は止めていたが入口の負荷(TCP/TLS/Upstash check)は受けていた。Turnstile は CDN 層に近く、より早く弾ける利点がある。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:41:04 +09:00
Kazuki Yamada 65b6f115eb test(server): Anchor leading-prefix check on the exact defect shape
decision(assertion-precision): switch the "no stray leading \`: : \`" guard from \`not.toContain(': : ')\` to an anchored regex \`/^Invalid request: : /\` — both catch the defect today, but the anchored form documents exactly which prefix shape we're guarding against and rules out any legitimate \`: : \` that might appear later in the message

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:31:15 +09:00
Kazuki Yamada 61e853e318 test(server): Guard classifier against valibot PathItem drift
intent(drift-guard): claude's follow-up review flagged that the hand-rolled schemaErrorWith fixture can't catch a change in valibot's internal PathItem shape — fixture-based tests would stay green while production fails, so add one real-valibot-issue case
decision(fixture-vs-real): keep the existing fixture-based tests as the bulk of coverage (cheap, focused on classifier logic) and layer a single v.safeParse-driven test on top — the fixtures stay fast, the new test catches shape drift

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:16:46 +09:00
Kazuki Yamada b25f4446d8 test(server): Relocate server tests into website/server
intent(test-ownership): move tests into website/server/tests/ so they collocate with the code under test and stop reaching up through three parents; reviewer follow-up wanted dedicated coverage and the root vs. website/server package boundary makes collocation the right long-term layout
decision(vitest-config): give website/server its own vitest.config.ts + `test` script; root's existing tests/**/*.test.ts include no longer catches server tests since they moved outside that tree, so the two test runs stay independent
decision(tsconfig-test): add tsconfig.test.json extending the build config and lift lint-tsc to `-p tsconfig.test.json` — the build tsconfig's rootDir: "./src" excludes tests/, so a single lint command wouldn't have type-checked them
learned(valibot-instanceof): with tests now resolving valibot from the same website/server/node_modules as validateRequest, the cause-check can go back to `instanceof v.ValiError` — the duck-type workaround was only needed when the root harness and server pulled different valibot copies
constraint(ci-website): added a `test-website-server` job that links the local repomix build the same way lint-website-server does; tests don't actually import repomix today, but colocation means they easily could later and the link step keeps parity
2026-04-19 21:47:29 +09:00