fix: reject unknown built-in demo references locally - #3
Merged
Merged
Conversation
`demo` is an internal namespace, not a GitHub account, but only `demo/learning-platform` was treated as special. Every other `demo/*` reference was syntactically valid, so it fell through to the GitHub adapter and spent a request from the anonymous allowance to be told what was already known: the repository does not exist. The rule now lives in the two places a reference crosses a boundary, sharing one predicate: - `readRepoRef` in the route helpers, which all six routes already call, so no endpoint can be given the check separately and no direct API request escapes it; - `servedFromBuiltin` in the service layer, which is the single question each service function asks before deciding between the built-in fixture and GitHub. An unknown reference gets the ordinary 404 and names what the namespace holds. It is never silently swapped for the demo that does exist, and it cannot reach the test fixtures even with fixture mode on. Owners that merely resemble the reserved one -- `demos`, `my-demo` -- are real accounts and keep going down the ordinary path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README still said unknown `demo/*` references go down the ordinary GitHub path, which was the behaviour this change removes. It now states that the namespace is reserved, where the rule is enforced, and why a locally decided failure reports no quota. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause
demowas treated as an ordinary GitHub owner. Onlydemo/learning-platformwasspecial-cased; every other
demo/*reference parsed as a syntactically validowner/repo, missed the built-in check, and fell through to the GitHub adapter:So the 404 was correct but expensive: it spent a request from the 60/hour
anonymous allowance to be told what the application already knew. Reproduced
against production before the fix — the response carried
{"remaining":59}withrateLimitAgeMs: 0, which is a request that had justhappened.
There was a second, quieter problem in the same response. The rate-limit store is
module-level and holds whatever the last real GitHub response reported, so a
locally decided failure inherited a snapshot from an unrelated earlier request —
implying quota had been spent and dating the reading to now.
Where the rule lives
demois now a reserved internal namespace holding exactly one reference. Onepredicate,
isUnknownBuiltinRef, is enforced at the two boundaries a referencecrosses:
readRepoRef—src/lib/api/route-helpers.tsservedFromBuiltin—src/lib/github/service.tsservedFromBuiltinreplaced the six separateisBuiltinRefgates, so the"is this built-in?" question and the "then it cannot exist" answer are the same
call. Neither branch can be skipped for one endpoint.
Behaviour
demo/learning-platform— unchanged, still served from the built-in fixture.demo/*— local404 not-found, on all six routes, naming what thenamespace holds. Never silently swapped for the demo that does exist.
Authorizationheader, no quota consumed.rateLimit: nullandrateLimitAgeMs: null.RTM_FIXTURE_MODE=1.demos/…andmy-demo/…are real accounts and keep going down the ordinaryGitHub path.
400 invalid-inputbehaviour.Locally decided failures in general now report no quota — that includes the
400s from parameter validation, which had the same leak. Genuine GitHubfailures still report theirs, including the rate-limited case, which carries the
snapshot from its own response.
Proof
Route-handler tests drive the real
GETexports withfetchstubbed to throw onany call, so a single GitHub request fails the test:
demo/not-a-real-demowithrateLimitandrateLimitAgeMsnull, and zero adapter calls;demo/*shapes refused, including in fixture mode.The exact regression sequence asked for:
x-ratelimit-*headers →200, andthe response reports
{limit: 60, remaining: 42};demo/not-a-real-demo;404;rateLimit: null;rateLimitAgeMs: null;fetchcall count unchanged from step 1.Two counter-tests keep the fix honest: a genuine GitHub 404 still reports
{remaining: 17}, and a live repository still produces exactlyhttps://github.com/ghapi/repos/octocat/hello-world.E2E adds the same checks against the production build, plus one that a shared
?repo=demo/not-a-real-demoURL shows the not-found state, makes no GitHubrequest, offers the demo as a choice, and does not show the built-in badge.
Verification
tsc --noEmiteslint .vitest runnext buildplaywright testPreserved
0 GitHub requests, in both the header and the landingcard.
0 API requestswas not reintroduced.0files changed under.github/).src/lib/github/client.tsis untouched, so the server-only token architectureis exactly as it was. A test asserts the browser still receives only
tokenConfigured.dataSource: builtin,htmlUrl: null.🤖 Generated with Claude Code