Commit Graph

842 Commits

Author SHA1 Message Date
dependabot[bot] 8623e4e80a chore(deps): Bump the npm_and_yarn group across 3 directories with 3 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [fast-xml-builder](https://github.com/NaturalIntelligence/fast-xml-builder) and [fast-uri](https://github.com/fastify/fast-uri).
Bumps the npm_and_yarn group with 2 updates in the /website/client directory: [fast-uri](https://github.com/fastify/fast-uri) and [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs).
Bumps the npm_and_yarn group with 2 updates in the /website/server directory: [fast-xml-builder](https://github.com/NaturalIntelligence/fast-xml-builder) and [fast-uri](https://github.com/fastify/fast-uri).


Updates `fast-xml-builder` from 1.1.4 to 1.1.7
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-builder/blob/main/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-builder/compare/v1.1.4...V1.1.7)

Updates `fast-uri` from 3.1.0 to 3.1.2
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

Updates `fast-uri` from 3.0.6 to 3.1.2
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

Updates `@babel/plugin-transform-modules-systemjs` from 7.25.9 to 7.29.4
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs)

Updates `fast-xml-builder` from 1.1.4 to 1.2.0
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-builder/blob/main/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-builder/compare/v1.1.4...V1.1.7)

Updates `fast-uri` from 3.1.0 to 3.1.2
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-xml-builder
  dependency-version: 1.1.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: "@babel/plugin-transform-modules-systemjs"
  dependency-version: 7.29.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-xml-builder
  dependency-version: 1.2.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-10 05:49:37 +00:00
dependabot[bot] 4492598a84 chore(deps): Bump the npm_and_yarn group across 3 directories with 3 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [hono](https://github.com/honojs/hono) and [ip-address](https://github.com/beaugunderson/ip-address).
Bumps the npm_and_yarn group with 1 update in the /website/client directory: [serialize-javascript](https://github.com/yahoo/serialize-javascript).
Bumps the npm_and_yarn group with 2 updates in the /website/server directory: [hono](https://github.com/honojs/hono) and [ip-address](https://github.com/beaugunderson/ip-address).


Updates `hono` from 4.12.14 to 4.12.18
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.14...v4.12.18)

Updates `ip-address` from 10.1.0 to 10.2.0
- [Commits](https://github.com/beaugunderson/ip-address/commits)

Updates `hono` from 4.12.14 to 4.12.18
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.14...v4.12.18)

Updates `ip-address` from 10.1.0 to 10.2.0
- [Commits](https://github.com/beaugunderson/ip-address/commits)

Updates `serialize-javascript` from 6.0.2 to 7.0.5
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.2...v7.0.5)

Updates `hono` from 4.12.16 to 4.12.18
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.14...v4.12.18)

Updates `ip-address` from 10.1.0 to 10.2.0
- [Commits](https://github.com/beaugunderson/ip-address/commits)

Updates `hono` from 4.12.16 to 4.12.18
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.12.14...v4.12.18)

Updates `ip-address` from 10.1.0 to 10.2.0
- [Commits](https://github.com/beaugunderson/ip-address/commits)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.18
  dependency-type: indirect
- dependency-name: hono
  dependency-version: 4.12.18
  dependency-type: direct:production
- dependency-name: ip-address
  dependency-version: 10.2.0
  dependency-type: indirect
- dependency-name: ip-address
  dependency-version: 10.2.0
  dependency-type: indirect
- dependency-name: serialize-javascript
  dependency-version: 7.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-09 16:04:53 +00:00
Kazuki Yamada a27cf29a4e Merge pull request #1550 from yamadashy/renovate/npm-hono-vulnerability
chore(deps): update dependency hono to v4.12.16 [security]
2026-05-09 21:17:37 +09:00
Kazuki Yamada 4caea59b2a Merge pull request #1556 from yamadashy/feat/drop-node-20-add-26
chore(deps): Drop Node.js 20, add Node.js 26 support
2026-05-09 21:16:27 +09:00
Kazuki Yamada b9388665d2 fix(website): Key isBot() cache on UA string for shared-process safety
Previous memoization stored a single boolean at module scope. In any
Node context where the same module instance might be reused across
requests (VitePress SSG, dev server, preview server with `navigator`
polyfilled per request), the first request's UA would silently leak
into subsequent calls.

In production this code is browser-only — Cloud Run's Hono server
doesn't import `botDetect.ts`, and Cloudflare Pages serves the bundle
as static files with one fresh module instance per browser tab — so
the bug was theoretical. But the UA-keyed memo costs nothing extra
and removes the foot-gun: a long-lived process now invalidates the
cache automatically when a different UA shows up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:12:15 +09:00
Kazuki Yamada e4a635c2f2 fix(website): Address PR review feedback on isBot pre-mint guard
Four items from gemini and claude reviews:

- botDetect.ts: Memoize isBot() result. navigator.userAgent is immutable
  for the page lifetime, so re-running the isbot regex on every Turnstile
  pre-mint debounce and post-submit re-mint check is wasted work. SSR
  fallback is intentionally not cached so a module instance reused across
  SSR/CSR still reaches the real UA check on first CSR call.
- usePackRequest.ts: Disambiguate the "submit-path NOT gated" comment —
  it was confusing because the new post-submit re-mint also lives inside
  submitRequest's finally. Reworded to "click-path acquireTurnstileToken"
  to make clear which call site is intentionally skipped.
- usePackRequest.ts: Update the userTouched comment to reflect autofill
  reality — modern Chromium/Firefox DO fire input events on autofill, so
  the rationale ("autofill doesn't trigger") was already stale. The new
  isBot() guard covers the gap for well-behaved crawler UAs.
- usePackRequest.ts: Add English glosses for the Japanese CF dashboard
  labels (提示チャレンジ / 未解決) so non-Japanese-reading maintainers can
  follow the comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:07:08 +09:00
Kazuki Yamada d94475ef01 perf(website): Skip Turnstile pre-mint for bot-shaped user agents
The CF Turnstile dashboard shows ~17k unsolved challenges over the past
7 days vs ~10k solved — a 2:1 unsolved:solved ratio that's larger than
the post-submit auto re-mint can explain. Most of those are JS-executing
crawlers (Slackbot, Discord card validator, Twitter card validator, X,
Apple link preview, Googlebot, etc.) that render the page, somehow trigger
a DOM input event on the URL field (autofill / accessibility tools /
focus tricks), and pay a CF challenge they can't solve.

Add an `isBot()` short-circuit at the two pre-mint call sites in
`usePackRequest`:
  - the debounced `onTrigger` after `markUserTouched` flips
  - the post-submit auto re-mint in `submitRequest`'s finally block

The actual security gate is the server-side `siteverify` in
`turnstileMiddleware` — that stays the only authoritative check, so a
crawler that spoofs UA past `isBot()` still gets blocked there. The
submit-path `takeToken()` is intentionally NOT gated to avoid
false-positive lockouts of legitimate users with unusual UAs (e.g. older
clients, accessibility tools).

Net effect:
  - "提示チャレンジ" / "未解決" CF dashboard counters drop sharply
    (well-behaved crawlers stop minting unsolvable tokens)
  - `pack_completed` server logs unaffected (legit users don't change
    paths; bots couldn't reach `/api/pack` either way)
  - server-side spend on siteverify unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 19:56:19 +09:00
Kazuki Yamada ef190425c1 docs(website): Bump pinned action versions in GitHub Actions guide examples
The hi/vi/id translations of the GitHub Actions guide still showed
`actions/checkout@v3`, `actions/setup-node@v3`,
`actions/upload-artifact@v3`, and `softprops/action-gh-release@v1`,
which are stale relative to both the English doc (already on @v4) and
the project's own CI workflows.

Bump these illustrative examples to @v4 (and @v2 for action-gh-release)
to match the English source and avoid pointing readers at deprecated
action majors.
2026-05-09 19:55:06 +09:00
Kazuki Yamada 5a1423e118 chore: Replace npm run with node --run inside package.json scripts
Follow up to commit 042750c which only converted workflow-level
invocations. With Node.js 22 as the floor, the chained scripts inside
each package.json can also use `node --run` directly, dropping the
intermediate npm process when these scripts run.

- root `prepare`
- browser `build-all`, `lint`
- website/client `lint`
- website/server `lint`
2026-05-09 19:55:06 +09:00
Kazuki Yamada df881a7c57 fix(ci): Address PR review feedback
- Migrate the build-and-run job in ci.yml to `node --run build --
  --sourceMap --declaration` so the inline command matches the
  `node --run` style of the test job (claude review round 2 #1)
- Update the hi github-actions.md matrix example from `[22, 24]` to
  `[22, 24, 26]` so the doc mirrors the project's actual CI matrix
  (gemini-code-assist / coderabbitai inline comment)
- Bump the stale "(Node 20+)" baseline in reviewer-performance.md to
  "(Node 22+)" to track the new engines floor (claude review round 2
  minor)
2026-05-09 19:48:15 +09:00
Kazuki Yamada f61ea347f2 docs(website): Update Node.js version mentions from 20 to 22
Sweep the multilingual website docs for remaining Node.js 20 references
that survived the initial English-only update:

- installation.md: System Requirements `≥ 20.0.0` → `≥ 22.0.0` across
  all 12 languages that include this section (incl. en, which was
  missed earlier)
- development/index.md: Prerequisites `≥ 20.0.0` / `v20` / `versi 20`
  / `phiên bản 20` → 22 across all 14 languages
- faq.md: programming-language Q&A narrative "Node.js 20 or later" → 22
  across all 14 languages

The version digit substitutions are mechanical and identical across
locales, so updating them in this PR keeps the docs consistent with the
new minimum without requiring the usual translation handoff.
2026-05-09 19:08:19 +09:00
Kazuki Yamada 042750cb4e chore(ci): Replace npm run with node --run in workflows
Now that the minimum supported Node.js version is 22, `node --run` is
available everywhere. It avoids the npm process-spawn overhead and
matches the style already used in package.json scripts.

Affects all GitHub Actions workflows that invoke npm scripts and the
website/server Dockerfile bundle step. `npm ci` is left as-is since it
is npm-specific.
2026-05-09 19:08:10 +09:00
Kazuki Yamada 9caf541368 chore(deps): Drop Node.js 20, add Node.js 26 support
Node.js 20 reaches end-of-life on 2026-04-30, so raise the minimum
supported version to 22 (the next active LTS) and add Node.js 26 to the
CI matrix as the current release line.

- Bump engines.node to >=22.0.0 in package.json and scripts/memory
- Update CI matrix to [22.x, 24.x, 26.x] (drop 20.x and 25.x; 25.x EOL 2026-06)
- Update test-action.yml matrix to [22, 24, 26]
- Drop the obsolete `node --run` workaround comment in ci.yml since
  `node --run` is supported on all matrix versions
- Update Node.js version mentions in English docs, llms-install.md,
  configShard, bug report template, and code samples in hi/vi
  github-actions guides

Dockerfile (node:22-slim) is intentionally left at the minimum supported
version so the published image confirms Repomix runs on the floor.
2026-05-09 18:49:37 +09:00
renovate[bot] 1f080c1163 chore(deps): update dependency hono to v4.12.16 [security] 2026-05-08 12:18:55 +00:00
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
Kazuki Yamada fa06e5059c fix(website): Address PR review feedback on siteverify metric
Six items from gemini, claude initial review, and claude follow-up:

- turnstile.ts: Update misleading comment that claimed the metric filters
  on `event=turnstile_siteverify` and `outcome=success`. The actual
  Cloud Monitoring metrics in `monitoring/metrics/` filter on
  `siteverifyDurationMs` field presence, which uniformly captures both
  the parallel success log (event=turnstile_siteverify) and the four
  rejectAndLog failure paths (event=pack_completed). The comment
  contradicted README and YAML and would mislead future readers.
- turnstile.ts: Wrap rejectAndLog in a local `rejectWithDuration` helper
  so every post-siteverify branch automatically carries
  `siteverifyDurationMs`. Prevents drift if a fifth reject reason gets
  added later.
- client.ts: Split the wire-protocol `PackProgressStage` (server-emitted
  SSE values) from the display-only `DisplayProgressStage` superset that
  adds `verifying`. Keeping the synthetic stage out of the wire type
  prevents silent divergence with the server's `PackProgressStage`.
- usePackRequest.ts, TryItLoading.vue, TryItResult.vue: Switch the
  display-side type to `DisplayProgressStage`. `onProgress` callbacks
  still take the wire `PackProgressStage`.
- usePackRequest.ts: Clear `progressStage` on token-acquisition failure
  branches (aborted / error). Functionally invisible since loading=false
  hides the loading UI, but prevents the next submit's verifying flash
  from briefly showing the previous run's stale state.
- monitoring/metrics/turnstile_siteverify_duration.yaml: Retune the
  exponential bucket layout for the 100ms-1s SLO band where decisions
  get made. Doubling buckets only placed ~3 boundaries between 100ms
  and 1s; growthFactor=1.5 with scale=10 places ~8 boundaries there.
  18 finite buckets cover 10ms to ~9.85s, comfortably above the 5s
  siteverify timeout so timeouts don't land in overflow.
- monitoring/README.md: Document that pre-network rejections
  (secret_missing, missing_token, token_too_long) intentionally don't
  carry siteverifyDurationMs, so they're excluded from both metrics
  but still appear in the existing pack_requests metric.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:42:21 +09:00
Kazuki Yamada 76522fc13e chore(monitoring): Check in Turnstile siteverify metric definitions
Move the metric setup from prose-only README instructions to checked-in
YAML files under `monitoring/metrics/` so the dashboard, the metrics it
depends on, and the apply commands all live next to each other.

- `turnstile_siteverify_duration.yaml`: distribution metric on
  `jsonPayload.siteverifyDurationMs`, exponential buckets 1ms-32s.
- `turnstile_siteverify_outcomes.yaml`: counter metric with `outcome`
  and `reason` labels for the success-vs-failure breakdown widget.

README updated with the gcloud commands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:24:48 +09:00
Kazuki Yamada aea2401bfc chore(monitoring): Add Turnstile siteverify dashboard widgets
Wire up two new tiles to surface the `siteverifyDurationMs` field added
in the previous commit:

- "Turnstile siteverify latency P50 / P95 / P99" — line chart with a
  1s threshold marker so a steady regression jumps off the chart.
- "Turnstile siteverify outcomes (by outcome)" — stacked area
  breaking down success vs turnstile_failed counts over time.

Both depend on log-based metrics `turnstile_siteverify_duration`
(distribution) and `turnstile_siteverify_outcomes` (counter) that need
to be created once in the GCP Console — README documents the filter,
field, and label extractors so the setup is reproducible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:21:47 +09:00
Kazuki Yamada 35c56abd02 perf(website): Show verifying step + emit siteverify duration metric
Two changes targeting the visible "..." gap between Pack click and the
first SSE progress event observed after PR #1544 landed:

- Client: add a synthetic `verifying` PackProgressStage so the loading
  UI displays "Verifying request..." while the server runs Turnstile
  siteverify (typically 100-1000ms before the first 'cache-check' SSE
  event arrives). The first onProgress callback from handlePackRequest
  overwrites it with the real server-reported stage.

- Server: time the siteverify round-trip in `turnstileMiddleware` and
  emit `siteverifyDurationMs` on every outcome (success / network
  failure / rejected / action mismatch / hostname mismatch). Success
  path adds a structured log with `event: turnstile_siteverify` so
  Cloud Monitoring can build a log-based distribution metric for
  p50/p95/p99 latency and alert on regressions during Cloudflare
  incidents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 19:19:01 +09:00
Kazuki Yamada 6fe66f172a revert(website): Drop ?repo= -> userTouched flip to avoid amplification
A valid `?repo=` permalink was treated as an intent signal so the
visitor's click path used a pre-minted token. claude's follow-up review
flagged that this re-creates the dashboard counter inflation this PR is
meant to fix: any third-party page driving traffic to
`https://repomix.com/?repo=<owner/repo>` (Slack / Discord / Twitter card
validators that execute JS) would mint a token per visit, regardless of
whether the visitor ever submits.

Permalink visitors now pay the cold mint on their first click; the user's
first real form interaction (typing, mode click, option tweak, file
upload) is what gates the pre-mint.

Also document the idle-tab cliff in `expired-callback`: re-arming
pre-mint there would burn a challenge every 240s for the lifetime of
an open tab, which is worse than the cold-mint-on-return cost it would
save.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:37:59 +09:00
claude[bot] 23494e5c8d fix(website): Remove leftover useTurnstile.error references
Commit 2a0922d dropped the `error` ref declaration from `useTurnstile`
but left two references behind — `error.value = null` at the top of
`mintToken()` and `error` in the returned object. The first throws
ReferenceError at runtime; the second is a tsc error. No external
caller reads `useTurnstile().error` (TryIt.vue only consumes
`usePackRequest.error`), so the export was already vestigial.

Co-Authored-By: Kazuki Yamada <yamadashy@users.noreply.github.com>
2026-05-05 13:34:07 +00:00
Kazuki Yamada 2a0922d114 refactor(website): Address claude follow-up review on Turnstile pre-mint
- Drop the unused `error` ref from `useTurnstile`. The widget-level
  error-callback writes had no observer (only `usePackRequest.error`
  feeds the UI), so the export and writes were vestigial.
- Drop `getResponse` from `TurnstileGlobal`. Never called anywhere in
  the codebase; clearer to leave it off the typed surface.
- Don't `console.warn` on normal cancel/timeout flows in
  `acquireTurnstileToken`. Move the warn after the `signal.aborted`
  check so the dev console only logs genuine challenge / script-load
  failures.
- Hoist the consecutive `if (widgetId.value)` guards in `mintToken` by
  capturing the rendered widget id into a local const after the throw.
- Drop the redundant `userTouched.value` check in the post-pack
  pre-mint guard. `userTouched` is necessarily true at this point —
  it was a precondition for `isSubmitValid` being true when the submit
  started.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:28:33 +09:00
Kazuki Yamada 2ee93b3214 refactor(website): Address remaining PR feedback on Turnstile pre-mint
- Mark `userTouched=true` when arriving via a valid `?repo=` permalink so
  the visitor's click path uses a pre-minted token. Browser autofill and
  malformed `?repo=` values still don't burn a challenge.
- After a non-aborted submit completes, schedule a fresh `preMintToken()`
  in the finally block. Warms the cache for the typical "view result →
  tweak options → repack" flow and for `repackWithSelectedFiles`.
- Reduce the pre-mint debounce from 500ms to 300ms. Tightens the window
  where a paste-and-click cadence misses the cache.
- Split composables to fit the 250-line file-size guideline:
  * Extract token cache (cache state + single-flight mint + atomic
    one-shot consumption) into `useTurnstileTokenCache.ts`. Shrinks
    `useTurnstile.ts` from 358 → 241 lines and lets the widget file
    focus on render lifecycle / supersede logic.
  * Extract pre-mint debounce trigger into `usePreMintDebounce.ts`.
  * Extract Turnstile token acquisition + user-facing failure copy into
    `turnstileSubmit.ts`. Drops `usePackRequest.ts` from 345 → 331
    lines; `submitRequest` is a single cohesive request lifecycle that
    resists further splitting.
- Drop the unused `consumed` flag on `CachedToken` (claude review). The
  cache nulls the entry on consumption instead, which is what the
  takeToken atomic-claim loop already relies on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:01:25 +09:00
Kazuki Yamada b9a9e719f8 fix(website): Address PR review feedback on Turnstile pre-mint
- Drop the unused `invalidateCache` export from useTurnstile. Both call
  paths (takeToken cache claim, expired-callback) already null
  cachedToken inline, so the helper had no callers.
- Update stale `turnstile.getToken()` references in usePackRequest and
  useTurnstileScript comments to match the renamed `takeToken()` /
  `preMintToken()` API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:41:48 +09:00
Kazuki Yamada 01a1d237b9 fix(website): Address codex review on Turnstile pre-mint flow
- useTurnstile: Make takeToken() one-shot under concurrency. Two callers
  awaiting the same shared mintPromise both received the same token,
  which siteverify rejects as `timeout-or-duplicate`. Claim the cache
  atomically post-await and loop into a fresh mint if another caller won.
- usePackRequest: Drop pending pre-mint debounce timer at submitRequest
  start and on unmount, and skip scheduling while loading is true. Stops
  a debounce-firing-during-submit from minting an extra Turnstile
  challenge alongside the click path's mint.
- TryItPackOptions: Emit userInput from option handlers and wire to
  markUserTouched in TryIt. Without this, users hydrating via `?repo=`
  who only tweak format/include patterns/checkboxes never tripped the
  pre-mint gate, so their click path always cold-minted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:54:28 +09:00
Kazuki Yamada bef4c4a805 fix(website): Share mint promise between pre-mint and click paths
intent(fast-click-race): When the user clicked Pack within the 500 ms pre-mint debounce window, takeToken() cold-pathed into mintToken() *and* the debounce timer later fired preMintToken() which started a second mintToken(). The generation-counter supersede logic in mintToken() rejected the first call as "Superseded by new Turnstile request", so the user's own click surfaced as a verification failure even though Turnstile would have happily issued a token.

fix(unified-startMint): Extract a single `startMint()` that both takeToken (cold path) and preMintToken share. Concurrent calls return the same in-flight promise, so only one `turnstile.execute()` ever runs and the supersede branch only triggers when there is genuinely a stale request.

fix(takeToken-abort-race): The signal threading is now via a `waitWithAbort` helper that races the awaiter against the abort signal but lets the underlying mint keep going. If the user cancels mid-mint, the underlying challenge still runs to completion and the token lands in the cache for whoever submits next, instead of being thrown away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:32:09 +09:00
Kazuki Yamada 7c9e44616b perf(website): Pre-mint Turnstile token on user intent, not on mount
intent(latency-and-counter): The Cloudflare Turnstile dashboard's "challenges issued" counter sits at roughly 1.25× of GA page_view, which means simply loading `api.js` on every visitor (the PR #1541 pre-warm) is already inflating the counter — render() and execute() are not the only trigger. At the same time, click→token latency is the main remaining UX cost. This PR reshapes pre-warm so script load + challenge happen only when the user has shown real intent (filled a valid URL or chose a file *and* interacted with the form), achieving both a lower counter and a near-zero click→token latency.

fix(useTurnstile-api): Replace the single `getToken()` entry point with a `preMintToken()` / `takeToken()` pair backed by an in-memory `{ token, mintedAt, consumed }` cache. `preMintToken()` runs the challenge in the background and stashes the resulting token; `takeToken()` consumes the cache synchronously (instant submit) or awaits the in-flight mint, falling back to a cold mint with the supplied AbortSignal. `invalidateCache()` lets the caller drop the cache without minting a new token. TTL is bounded at 240 s — Cloudflare hard-caps tokens at 300 s, the margin absorbs clock skew and network round-trips so a token that's "almost expired" is never sent to siteverify.

fix(useTurnstile-no-mount-prewarm): Stop calling `loadTurnstileScript()` from `setContainer()`. The mount-time script load was the source of the page-view-shaped counter inflation. Pre-warm now only runs from the intent-gated trigger in `usePackRequest`, so visitors who never interact with the form never appear on the dashboard.

fix(usePackRequest-intent-gate): Add a `userTouched` ref that flips on real user interaction (input event, file upload, mode switch) and never goes back. A debounced (500 ms) `watch(isSubmitValid && userTouched)` calls `preMintToken()`, so URL-parameter hydration (`?repo=`), browser form restoration, and autofill never pre-mint. `submitRequest()` switches from `getToken()` to `takeToken()` so the cached token is consumed on the first click, with the cold mint path as a transparent fallback.

fix(TryItUrlInput-user-input-event): Emit a new `userInput` event from the URL field's actual `@input` handler. `TryIt.vue` wires it to `markUserTouched()`. Watching `inputUrl` directly would have re-fired during onMounted hydration and defeated the gate.

learned(cloudflare-counter-includes-script-load): Even with `execution: 'execute'`, the Cloudflare Turnstile dashboard counts api.js loads toward "challenges issued" (verified by comparing GA page_view ≈ 106 with CF issued ≈ 132 in the same 30 min window after the PR #1541 deploy). Treat any new place that loads api.js as a billable analytics side effect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:16:54 +09:00
Kazuki Yamada 0f3a6e69a1 fix(website): Address review feedback on PR #1541
intent(comment-drift): Claude's review surfaced three stale comments that no longer match the code after pre-warm was narrowed to the script-load step. No behaviour change — the comments were lying about what the code actually does now.

fix(useTurnstileScript-jsdoc): The JSDoc on `TurnstileRenderOptions.execution` claimed `'execute'` was chosen so the pre-warm path could render() without firing a challenge. PR #1541 proved that's not what `'execute'` does in practice — render() itself counts toward the dashboard's challenge counters, regardless of `execution`. Rewrote the comment to explain why we still pass `'execute'` (token-mint guardrail) and why we no longer pre-warm by rendering.

fix(useTurnstile-render-comment): The inline comment at the render() call site said this option is "what makes the pre-warm in setContainer() free of side-effects". setContainer() no longer calls render(), so the rationale is obsolete. Updated to describe the current role: a guardrail against an accidental render() minting a token before getToken() is ready.

fix(useTurnstile-race-comment): The single-flight cache comment said "pre-warm and getToken() can both race past the widgetId.value null check". Pre-warm doesn't enter ensureWidget anymore, so only back-to-back getToken() submits can race. Updated to reflect the narrower scope.

perf(preconnect-crossorigin): Add a `crossorigin` companion to the existing `<link rel="preconnect">`. Turnstile's `api.js` is fetched anonymously, but the Turnstile iframe issues CORS sub-requests on a separate browser connection pool. Without both hints, the iframe's first handshake still happens on click. Cheap defensive addition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:37:59 +09:00
Kazuki Yamada c5fd291399 fix(website): Defer Turnstile render() until pack click
intent(token-waste): Production telemetry (12-hour Cloudflare dashboard sample) showed 2,620 challenges issued and 986 solved against only ~150-200 actual pack clicks tracked in GA. Despite the widget being configured with `execution: 'execute'`, calling `turnstile.render()` at form-mount time was inflating the dashboard's challenge counters by every visitor — humans, crawlers, ad-blocked browsers, abandoned tabs. Cloudflare's docs say render() should be side-effect free in execute mode, but the analytics disagree.

fix(prewarm-scope): Drop the widget render from `setContainer()`. Pre-warm now only calls `loadTurnstileScript()` so the script is cached before the user clicks; the actual `turnstile.render()` happens on the first `getToken()` call. This restores the documented 1:1 relationship between solved challenges and actual pack submissions.

perf(preconnect): Add `preconnect` and `dns-prefetch` hints to challenges.cloudflare.com so the DNS / TLS / HTTP/2 handshake is warm before the click. Compensates for losing the render() pre-warm — the script load and the challenge round-trip both reuse the warmed connection.

learned(don't-trust-docs-when-telemetry-disagrees): When the dashboard says one thing and the docs say another, the dashboard wins. The previous PR added pre-warm believing render() was inert in execute mode; the analytics showed it wasn't. Going forward, treat any new render() call site as a billable side effect until proved otherwise on the dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:38:41 +09:00
Kazuki Yamada 7fdda0e897 fix(website): Single-flight ensureWidget to prevent double render race
intent(race-fix): gemini レビューで指摘 — pre-warm 導入により ensureWidget が `setContainer` (pre-warm) と `getToken` (submit) の両方から並行に呼ばれる経路ができた。両者が `await loadTurnstileScript()` で待機 → 解決後に両方が `if (!widgetId.value)` を通過し `turnstile.render()` を 2 回実行 → 最初の widgetId が上書きされ leak (onBeforeUnmount の remove() は 2 個目しか掃除できない)。

fix(single-flight): module-level ではなく composable-instance スコープの `ensureWidgetPromise` で in-flight render を 1 件に絞る。後続呼び出しは同じ promise を返すので render() は最大 1 回。失敗時は promise を null にして retry 経路を維持 (useTurnstileScript の resetForRetry 由来)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:23:12 +09:00
Kazuki Yamada cbf41ba9a7 fix(website): Clear Turnstile containerEl on unmount
intent(race-safety): codex re-review で指摘 — pre-warm 中に component が unmount された場合、ensureWidget の script load Promise が後から戻ってきて detached DOM element に widget を render してしまう余地がある。既存の `containerEl.value !== el` ガードはあるが、それを成立させるには unmount 時に containerEl を解放する必要がある。
fix(unmount-cleanup): onBeforeUnmount 冒頭で `containerEl.value = null` を明示。これで in-flight pre-warm が後から戻っても `null !== el` で render を skip し、widget が detached node にバインドされて remove() がない状態で leak することを防ぐ。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:14:11 +09:00
Kazuki Yamada 0e0f572964 fix(website): Defer Turnstile challenge with execution='execute'
intent(token-waste): デプロイ後の Turnstile ダッシュボードで「解決チャレンジ 90 / siteverify 22」というギャップ (約 68 個の token が無駄に発行されて捨てられている) と「未解決チャレンジ 127 件 / ボット可能性 58.53%」が観測された。

fix(root-cause): Cloudflare Turnstile の invisible widget はデフォルトで `execution: 'render'` モードであり、`render()` 直後に automatic challenge を実行して token を発行する。我々の pre-warm ロジック (commit `68090aa2`) は render() で widget を立ち上げると同時に challenge も走らせていた。pack ボタンを押すと execute() で 2 度目の challenge が走り、最初の token は捨てられる。さらに正規クローラ (Googlebot 等) がホームページを訪れた際、JS 実行で widget が render → automatic challenge → クローラは解けず「未解決」としてカウント、これがボット率を inflate していた。

fix(execution-execute): render オプションに `execution: 'execute'` を追加。これで pre-warm は script load + widget shell のみ実行し、challenge は明示的な turnstile.execute() (= ユーザが pack ボタン押下時) まで遅延する。pre-warm の latency 削減効果はそのまま、token 浪費とクローラ false-positive は消える。

constraint(type-update): TurnstileRenderOptions interface に `execution?: 'render' | 'execute'` を追加して TypeScript で表現可能に。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:09:42 +09:00
Kazuki Yamada 68090aa2d2 perf(website): Pre-warm Turnstile widget on container mount
intent(latency): Turnstile 導入後の "Processing repository..." 直後の体感ラグ (~1s) を削減。デプロイ後の動作確認で、pack ボタン押下から実 API 呼び出しまでに体感 1 秒のスピナ点滅が観測された。原因は click 時にゼロから「script load → widget init → execute → token」を直列で走らせていたこと。
fix(prewarm): setContainer で element が登録された時点で ensureWidget を fire-and-forget で発火。script load + widget init をページの idle 時間に前倒し、click 時は execute() のみで済むようにする。invisible widget は token を勝手に発行しないので prewarm しても token を浪費しない。
constraint(error-swallow): pre-warm 失敗は意図的に握り潰す。ページレンダリングをブロックさせない方が良く、同じ loadTurnstileScript / ensureWidget の経路は getToken() 呼び出し時に再実行される(リトライ + フルエラー伝搬)ので、ユーザーには submit 時に正しく見える。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:07:03 +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
Kazuki Yamada c5406507a4 fix(website): Remove runtime throw scoped too broadly
intent(deployment-safety): devin が指摘 — `useTurnstile.ts:139` の `import.meta.env.PROD` runtime throw が広すぎて、CF Pages preview deploy / 手元の docs:build / CI build を crash させる。これらは `.vitepress/config.ts` のコメントで「test sitekey で fall through する」と明記された環境。
fix(scope): runtime throw を削除。Production-only 保護は build-time の `.vitepress/config.ts` (CF_PAGES === '1' && CF_PAGES_BRANCH === 'main') に集約。defense-in-depth として server-side middleware の fail-closed (action/hostname mismatch で 403) が残るので、万が一 production に test sitekey が混入しても全 pack が落ちて検知できる。

learned(env-flags): import.meta.env.PROD は「すべての vitepress build 出力」で true。CF production の判定は CF_PAGES + CF_PAGES_BRANCH を見る必要がある。runtime と build-time で同じ条件式を使い分ける時は、両方で同じ env signal を参照するか、片方に集約する。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:54:39 +09:00
autofix-ci[bot] 2d9341c05a [autofix.ci] apply automated fixes 2026-05-03 15:15:37 +00:00
Kazuki Yamada e10d098198 fix(website): Scope Turnstile build-time guard to CF Pages production
intent(deployment-safety): codex round 5 で、前回追加した `.vitepress/config.ts` のビルド時 guard が広すぎて、Cloudflare Pages の **preview deploy** や **CI** や **local docs:build** まで env 必須にしてしまい、PR preview を一律落とすことが判明。

fix(scope): guard を Cloudflare Pages の production branch deploy (`CF_PAGES === '1' && CF_PAGES_BRANCH === 'main'`) に限定。preview / local / CI は test sitekey で従来通り build できる。useTurnstile() 内の runtime throw が second line of defence として残るので、preview deploy でも実際にページを開けば form mount が失敗して misconfig は見える。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:14:29 +09:00
autofix-ci[bot] 74312781f4 [autofix.ci] apply automated fixes 2026-05-03 14:29:56 +00:00
Kazuki Yamada 365732bb89 fix(website): Address codex re-review (round 3)
intent(robustness): codex round 3 で残った should-fix と nice-to-have を全て対応。

fix(client/cancel-warning): cancelRequest が requestController を null にすることで、isCurrent() ガードが現在の request の onAbort callback も stale 扱いにしていた。結果として手動 cancel のメッセージが表示されない regression が発生。cancelRequest 側で error.value を直接セットする方針に修正(isCurrent ガードは superseded request のみを止める意図通りの動作)。
fix(client/ensure-widget-detached): ensureWidget で script load 完了後に containerEl.value !== el の場合は throw。component が unmount された後に detached element に対して render() してリーク、を防ぐ。
fix(client/build-time-validation): vitepress build は SSR throw を catch して exit 0 で抜けるため、useTurnstile の throw だけでは misconfig をビルド失敗させられなかった。.vitepress/config.ts のトップレベルで production && !VITE_TURNSTILE_SITE_KEY を検出して throw、Cloudflare Pages のビルドステップが即落ちるようにする。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:26:16 +09: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 9d430f7923 fix(website): Address PR review feedback on Turnstile integration
intent(robustness): PR #1538 のレビューで複数の resilience / race condition の指摘を受けたため、Turnstile 統合の堅牢性を上げる。
fix(client/race): submitRequest 内で requestController を local const にキャプチャ。await turnstile.getToken() 中に cancelRequest() が走ると requestController=null になり、後続の requestController.signal アクセスで TypeError が出ていた (devin 指摘)。
fix(client/script-retry): scriptPromise を rejection 時に reset し、dead <script> タグを DOM から除去。CDN blip で 1 回失敗するとセッション中ずっと getToken が永久に rejection を返す問題を解消 (coderabbit 指摘)。
fix(client/hang): getToken に 15s の Promise.race timeout 追加。Cloudflare の timeout-callback は invisible widget では発火しないため、CDN stall 時にローディングスピナが永久に止まらない問題を解消 (coderabbit 指摘)。
fix(client/unmount): onBeforeUnmount で pending getToken を reject。コンポーネント破棄後も await 側にハングしていた問題を解消 (gemini 指摘)。
fix(server/remoteip): clientInfo.ip が '0.0.0.0' (=IP 取得失敗時の sentinel) の場合は siteverify の remoteip フィールドをオミット。Cloudflare の risk scoring を混乱させない (gemini 指摘)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:27:35 +09:00
Kazuki Yamada 599b8df6cb fix(website): Allow X-Turnstile-Token in CORS preflight
intent(cors): X-Turnstile-Token ヘッダを `/api/pack` で受け入れるため、CORS の allowHeaders に追加。
constraint(preflight): Custom request header はブラウザが自動的に preflight (OPTIONS) を発行する。allowHeaders に列挙されていないと preflight が失敗し、本リクエスト (POST) が一切送信されない — Turnstile を入れても発火しない死に体になる。
learned(deployment-checklist): 新しいヘッダを使うミドルウェアを追加するときは、CORS 設定にもセットで追加するのを忘れない。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:03:44 +09:00
Kazuki Yamada 184fb4b0b8 chore(website): Wire TURNSTILE_SECRET_KEY into Cloud Run deploy
intent(deployment): /api/pack の Turnstile 検証を本番で有効化するため、新しいシークレット参照を Cloud Build の deploy ステップに追加。
constraint(secret-manager): GCP Secret Manager 上に `turnstile-secret-key` を事前作成、Cloud Run のサービスアカウント (`{PROJECT_NUMBER}-compute@developer.gserviceaccount.com`) に `roles/secretmanager.secretAccessor` を付与済み。
decision(secret-version): `:latest` で参照することで、Secret 値のローテーション(version 追加 → 旧版 destroy)を deploy なしで反映できるようにする。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:58:34 +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 657350e93e docs(website): Address PR review feedback
intent(review): Address CodeRabbit and Gemini feedback on SEO metadata and FAQ wording.

decision(metadata): Remove static per-page OG and Twitter fields so transformHead remains the only source of canonical and page-specific social metadata.

decision(i18n): Correct English Tree-sitter hyphenation and French FAQ wording without changing the FAQ structure.
2026-05-03 20:17:48 +09:00
Kazuki Yamada 3f15a97748 docs(website): Use Hermes Agent product name
intent(naming): user clarified that Hermes Agent should be written with its formal product name.

decision(naming): updated README and localized Repomix Explorer Skill docs to use Hermes Agent in prose while keeping the hermes CLI command and ~/.hermes path unchanged.
2026-05-03 19:50:34 +09:00