Skip to content

Invite and promo links never reach the landing page - #37

Merged
TaprootFreak merged 34 commits into
developfrom
48a09586-landing-from-assets
Sep 9, 2026
Merged

Invite and promo links never reach the landing page#37
TaprootFreak merged 34 commits into
developfrom
48a09586-landing-from-assets

Conversation

@TaprootFreakAI

@TaprootFreakAI TaprootFreakAI commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

EN. A shared invite or promo link never reached the landing page. _routes.json hands those paths to the Function first, so the 200-rewrite in _redirects never runs and the asset lookup answers for the path as asked: 404, with the site's own 404 page. The older rewrite injected the campaign's title into that page, which is why the link looked roughly right in a browser while being the wrong document — no landing markup, no scripts, so the code lookup never ran — under a status every share crawler drops. The landing is now read from the asset binding by name, injected, and answered 200.

DE. Ein geteilter Einladungs- oder Promo-Link hat die Einladungsseite nie erreicht. Ausgeliefert wurde die Fehlerseite der Site, der nur der Titel der Kampagne aufgesetzt wurde — ohne die eigentliche Seite und ohne ihre Skripte, und mit einem Status, den Vorschau-Dienste verwerfen. Die Seite wird jetzt direkt bei der Ablage geholt, mit den Kampagnendaten versehen und mit Status 200 beantwortet.

What was measured

Three lookups side by side inside the Function, on a preview deployment, for /invite/AB12CD:

Lookup Answer
context.next() 404, 2693 bytes — the site's 404 page
context.next(request for /invite/index.html) 308
env.ASSETS.fetch('/invite/index.html') 200, 9827 bytes — the landing shell

Production before this change, on the code-bearing path:

curl -s https://realunit.app/invite/AB12CD | grep '<script'
  ->  (nothing)

The page a visitor got had no scripts, so the code lookup never ran. It was the 404 page with the campaign's title written into it.

A preview deployment of this change:

Path Status Title
/invite/AB12CD 200 RealUnit — Einladung AB12CD
/promo/EVT1 200 RealUnit — Promo-Code EVT1
/invite/AB12CD?lang=en 200 RealUnit — Invitation AB12CD
/invite/ 200 RealUnit — Einladung
/promo/ 200 RealUnit — Promo-Code
/invite/index.html 308 → /invite/ unchanged
/invite 308 → /invite/ unchanged
/js/invite-banner.js, / 200 untouched

GET and HEAD on /invite/ agree on content-type and cache policy and neither carries an ETag. The code path carries the site's Content-Security-Policy, X-Frame-Options, X-Content-Type-Options and Referrer-Policy.

The shape, and why each guard is there
  • The platform is asked first, and only 200 and 404 are touched. A redirect — /invite and /invite/index.html are canonicalised with a 308 — is handed straight back, as is a 200 that is not HTML or has no content-type at all.
  • A GET on a 200 is rewritten in place. That is /invite/ and /promo/ themselves: the file is already the right one and keeps its status.
  • A 404, and every HEAD, read the shell from the asset binding. A HEAD answer carries no body, and the body is what tells the landing shell from any other page; deciding from an empty one handed back the file's own validators beside a GET that had them stripped, which is how a client could be answered 304 against a document it never received.
  • The shell has to read as the landing shell. Two marks — id="state-loading" and aria-busy="true" — which both landings carry in one element and no other page the site ships carries together. A test walks public/ and holds that shut. A broken deploy that puts the 404 page under invite/index.html keeps saying 404.
  • The binding is asked inside a guard, status check and body read included. A Function that throws makes Pages serve the assets directly, which is precisely the answer this replaces; measured the hard way earlier on this branch.
  • The answer's headers come from the URL that was asked for, minus the length, the content coding, both validators and the integrity digests. Building them from scratch instead was tried and measured: public/_headers is applied to the asset, not to an answer this Function constructs, so it took the site's security headers off every landing.

functions/_middleware.js is under coverage.include at 100% in all four metrics. It was outside it while deciding which page every visitor and crawler sees, which is how the previous attempt reached production.

Relation to #34 and #35

#34 tried to fix the status alone, on the premise that the body was already the landing shell. It was not. #34's own marker guard therefore refused the body it was given, and with the older unconditional injection gone, visitors were shown the bare 404 page. #35 reverted it, and production went back to the 404 page with a campaign title. This change fixes the body and the status together.

Every step here was measured on a preview deployment before it was kept, which is the practice #34 lacked. Three attempts did not survive that and are in this branch's history with what they measured: asking the asset binding for redirect: 'manual' made the Function fail, so Pages served the assets directly and the code paths answered 404 again; building the answer's headers from scratch dropped the site's security headers; and taking the headers from the shell rather than from the requested URL made HEAD and GET disagree.

Carried into a follow-up

Neither reaches a request this deploy can receive today; both live on the fallback branches, which need the asset binding to be absent or to fail, and Pages always provides it.

A 304 from the platform is handed back as it came. A client can only ask for one by sending a validator it was given, and a rewritten answer carries none. It could only have got one from a raw fallback answer, which is the branch that already requires the binding to have failed.

The fallback on a HEAD hands back the file's own validators, where a GET on the same path has them stripped. Each answer is honest about what it describes, but the two diverge, and only on that same unreachable branch.

A HEAD on a second HTML file under these paths would be answered from the shell where a GET would hand the file back. No such file exists; the test that walks public/ goes red the day one appears.

Left alone deliberately

Cache-Control appears twice on the landings. public/_headers carries a rule for /invite/ and another for /invite/*, and /invite/ matches both, so Pages appends the value twice. It reads public, max-age=60, public, max-age=60 on production today, on the code that predates this change as well as on this one. The list parses and the first max-age wins; tidying the rules is its own change.

scripts/dev-server.mjs serves the landings by its own routing and does not model the 404-first shape, so the Playwright suite exercises the injection but not this decision. Pinning that would mean teaching the dev server the platform's behaviour, which is a change of its own.

TaprootFreak and others added 30 commits July 8, 2026 13:46
…edirect

An invite or promo link never reached the landing page. _routes.json hands the
request to the Function first, so the 200-rewrite in _redirects never runs, and
the asset lookup behind context.next() answers for the path as asked: 404, with
the site's own 404 page. The older rewrite injected the campaign's title into
that page, which is why a shared link looked roughly right in a browser while
being the wrong document with the wrong status — and why every share crawler
dropped it before reading a tag.

Measured on a preview deploy, three lookups side by side on /invite/AB12CD:

  context.next()                              404, 2693 bytes (the 404 page)
  context.next(request for index.html)        308
  env.ASSETS.fetch(index.html)                200, 9827 bytes (the shell)

So the shell is read from the asset binding by name, injected, and answered
200 — for HEAD as well as GET. Without the binding, or if the file under that
name is not the shell any more, the platform's own answer stands rather than an
invented one.

functions/_middleware.js is under coverage at 100%. It was outside it while it
decided which page every visitor and crawler sees, which is how the previous
attempt reached production.
…e redirect

CONTRIBUTING and README described the Function as a rewrite of bytes that were
already the landing. They were not: the 200-rewrites in _redirects never run on
these paths, so the answer behind context.next() is the site's 404 page. Both
now say that the shell is read from the asset binding by name, checked against
the two landing marks, injected and answered 200.
Reading the shell for every landing path also replaced two answers that were
already right: /invite/ and /promo/ are real files the platform serves with
200, and /invite/index.html comes back as the 308 that canonicalises it. The
first was still correct because it was rewritten anyway; the second was not —
serving the landing under the explicit file name would quietly create a second
URL for the same page.

The platform is now asked first. A 404 is the one answer that means the
200-rewrite never resolved the path, and only that case is answered from the
asset binding. Everything else stands: a redirect is handed back as it came, a
non-HTML answer likewise, and an HTML answer that really is the shell keeps its
status and is only rewritten in place.
Three statements no longer matched the code they sit next to:

- the coverage comment still said only browser logic is measured to 100%,
  while the include beside it now names the Pages Function as well;
- the quality-gate table did not mention the 100% gate this adds for
  functions/_middleware.js, which is the gate whose absence let a change that
  served the 404 page on every invite link through a full review;
- both docs said _routes.json hands /invite/* and /promo/* to the Function,
  where the file names four paths and check-site.mjs requires all four.

The paragraphs also still described the old shape, where the shell was read for
every landing path. They now say the platform is asked first and only its 404
is replaced, and that /invite/ is rewritten in place while /invite and
/invite/index.html keep their 308.

A test carries the request URL through onRequest with ?lang=en. Passing the
pathname alone would have produced a landing in the wrong language with nothing
in this file to notice.
Copying the source response's headers carried in the ones public/_headers had
already set, and Pages sets them again on the way out. Measured on the deploy,
before this change and on both the old code and the new: /invite/ answers with

  cache-control: public, max-age=60, public, max-age=60

The answer's headers are now built: a content-type, and whatever _headers adds.
That removes the duplicate and makes the list of headers to delete unnecessary,
since none of them is carried in to begin with. The case that asserted their
absence now asserts the whole header set, and its fixture sets a Cache-Control
so the duplicate it guards against is reachable.

The shell lookup asks for no redirect to be followed. A 3xx was already refused
by the ok check; saying so at the call is cheaper than relying on it.

Two assertions were weaker than they read. The invite case checked that the
string og:title appears, which both shells carry already; it now checks the
injected value. The promo case checked for the bare code, which an
invitation-shaped injection would also satisfy; it now checks the kind.
Asking the asset binding for redirect: 'manual' was a guard against a 3xx the
ok check already refuses. Measured on a preview deploy: with it, the Function
fails and Pages falls back to serving assets directly — /invite/<code> answered
404 with the site's 404 page again, and /invite/ came back unrewritten. It is
removed, and the case that suggested it stays covered by the ok check.

The commit that added it also blamed the duplicated Cache-Control on /invite/
on this pass copying headers. That was wrong: public/_headers has a rule for
/invite/ and another for /invite/*, and /invite/ matches both, so Pages appends
the value twice. It predates this change and is untouched by it. Building the
answer's headers is still right — a stale validator must not be carried into a
rewritten document — and the comment now says that instead.
…alidates

Building the answer's headers from scratch took the site's own headers off
every landing. public/_headers is applied to the asset, not to a response this
Function constructs, so the copy is what carries them. Measured on a preview
deploy: without it, /invite/<code> came back with a content-type and nothing
else — no Content-Security-Policy, no X-Frame-Options, no X-Content-Type-Options,
no Referrer-Policy, no Cache-Control.

The source's headers are carried again, minus the ones that described the bytes
before the injection: the length, the content coding, both validators and the
integrity digests. The case now asserts both halves — that the stale ones are
gone and that a security header and the cache policy survive — and its fixture
sets a Content-Security-Policy so the second half can fail.
The README said context.next() answers those paths with the site's 404 page,
where those paths was the four _routes.json hands over. Only the code-bearing
ones come back 404; /invite/ and /promo/ are real files answered 200, and
/invite and /invite/index.html come back as a 308 — which the next sentence of
the same paragraph already said. CONTRIBUTING was already narrow.
…ecting binding

The asset mock ignored the request's method and its path. Both mattered:

- If the shell were ever asked for with the client's method, a HEAD would come
  back without a body, the landing marks would not be found, and the answer
  would fall back to the platform's 404 — the bug this pass exists to remove.
  The mock now records the method and a case requires GET.
- The mock returned the same body for every URL, so the promo case proved the
  shell was asked for but not that it was used. It is path-aware now, and the
  promo case reads the promo shell's own copy.

The binding is also asked inside a try. A Function that throws makes Pages
serve the assets directly, which is precisely the wrong answer here; that was
measured the hard way earlier on this branch. A rejecting binding now leaves
the platform's answer standing, and a case holds that shut.
…g too

A HEAD answer carries no body, and the body is what tells the landing shell
from any other page. On /invite/ and /promo/ — real files the platform answers
200 — the empty body failed the shell check, so the platform's own response was
handed back with the file's ETag, Content-Length and content coding intact,
beside a GET on the same URL that had all three stripped. A client could then
be answered 304 against a document it never received, which is the hazard the
stripping exists for.

A HEAD now takes the same route as a 404: the shell is read from the binding
with a GET, and the answer is what the GET gets, without a body. The status
branch is explicit about which answers this pass may touch at all — 200 and 404
— so a redirect or any other status is handed back before anything is read, and
a 200 that is not HTML likewise.

A case holds the two methods together on the codeless landing, and the
content-type check on the code path now asserts the value rather than only that
GET and HEAD agree.
A HEAD on the codeless landing built its answer from the shell's own headers,
where the GET beside it used the platform's. public/_headers matches on the
request path, so the shell's set was matched on /invite/index.html and the
platform's on /invite/ — the same values today, but two sources for one URL,
and the case that holds the two methods together said so by failing.

The platform's headers are used wherever it had an answer of its own, and the
shell's only stand in for the 404 it could not answer.
The media-type check reads a missing header as an empty string, and no case
reached it, so the branch stood uncovered and the 100% gate said so.
…ers only can

Two statements claimed more than they hold. The file header said every crawler
snapshots the injected tags, which is a claim about other people's software;
README already says can. And the in-place branch said only the metadata goes
in, where the same injection also rewrites the Play href and the android-app
and ios-app alternates when the URL carries a code.
The status can be fine and the stream still fail. Reading the body outside the
guard would take the Function down, and a Function that throws makes Pages
serve the assets directly — which is the answer this pass exists to replace.
The status check and the read are inside the try now, and a case gives back a
response whose text() rejects.

The mock also declared exactly the content-type the assertion expected, so
dropping the line that sets it would not have shown. The fixture declares
text/html without a charset and the answer is still required to say UTF-8.
…adicted only

Five statements said something the code does not do, all of them about the same
thing: a HEAD under these paths never takes the in-place route. Its answer
carries no body, so the shell cannot be recognised in it, and it reads the shell
from the binding exactly as the 404 does. CONTRIBUTING, README and the comment
above the branch all said the real files are only rewritten in place, without
the GET that makes it true.

Two more were narrower. The comment over the header source said the shell's set
stands in when the platform had nothing to say, where the platform did answer
and its status was simply not 200. The comment over the response said a
code-bearing path is answered 200, where the same line also serves a codeless
GET keeping its own 200 and a codeless HEAD.

And this PR contradicted a rule it left alone: the ground rule said
public/js/lib is the only code with a unit-coverage gate, which the quality-gate
table stopped being true of the moment this PR added one for the middleware.

The case that holds GET and HEAD together now also asserts that they reach the
same answer by the two different routes, which is what the corrected sentences
describe.
…both routes

The shell route reads its body inside a guard because a Function that throws
makes Pages serve the assets directly — the answer this pass exists to replace.
The in-place route read the platform's body without one, for no reason other
than that it was written first. It has the same guard now, and a case gives it
a body that rejects.

The stale-header list was only asserted on the route that reads the shell. A
case now sets all eight on a real 200 file and requires the in-place rewrite to
drop them as well, while the site's own headers survive.
…plies

Three statements were wider than the code. The comment said every HEAD under
these paths takes the shell route; a redirect and a non-HTML answer leave
before that branch, whatever the method, and a case now holds both shut for a
HEAD. The in-place branch said the store hand-off goes in, where on /invite/
and /promo/ there is no code and the injection is a no-op. And the README's
testing section still named only the browser logic as measured, while this PR
added the gate for functions/_middleware.js to CONTRIBUTING — the same
one-of-two-places inconsistency that was already corrected once in this
branch.
…lready said

Three assertions were true of the fixture before any code ran. The English
case looked for en_GB anywhere, which the shell carries as its alternate
locale; it now requires the main og:locale tag to have changed and the document
language with it. The in-place case looked for the German title and the landing
marks, neither of which the injection touches; it asks in English instead, so
the locale and the language are what it reads. And the cache policy was only
compared between HEAD and GET, so losing it on both would have passed.

This is the class of test that let the original defect through a full review:
green whether the code works or not.
The branch comment said both cases answer as the GET does, where a HEAD answers
without a body — which the line two above it states and this one dropped. The
JSDoc over the answer said the campaign is written into it, where the campaign
metadata and the store hand-off go in only when the URL carries a code, and the
URL tags, the locale and the language always. And the coverage comment said the
middleware decides what every crawler sees, where it is routed to the invite
and promo paths alone.
Both sentences were mine from the previous commit and both were too precise to
be true. The locale is written on ?lang=en, not always; the campaign title also
appears without a code when English is asked for; and a bare /invite/ can carry
a code as a query parameter, which the comment said it could not.

Neither sentence needs the enumeration. They now name where each decision
lives instead of restating it at a distance, which is what kept going wrong.
@TaprootFreakAI

Copy link
Copy Markdown
Collaborator Author

EN. Ready. The double review ran to a standstill: the last round, four lanes across two vendors, reports nothing that changes what a user or a crawler gets. Every step was measured on a preview deployment before it was kept — the practice whose absence let #34 reach production and break the very links it meant to fix.

DE. Freigegeben. Die letzte Prüfrunde meldet nichts, was für Nutzer oder Vorschau-Dienste einen Unterschied macht. Jeder Schritt wurde vor der Übernahme auf einer Testauslieferung gemessen — genau das hat bei #34 gefehlt.

What the review found, and what it cost to find it

The defect this PR opens with was invisible from the code alone: an invite link had never reached the landing page. _routes.json claims those paths, so the 200-rewrite in _redirects never runs, and the asset lookup answers for the path as asked. The older rewrite dressed the site's 404 page in the campaign's title, which is why it looked roughly right in a browser while carrying no landing markup and no scripts.

Three attempts did not survive the preview measurement and are in this branch's history with what they measured:

Attempt What the deploy said
redirect: 'manual' on the asset lookup the Function failed, Pages served the assets directly, and the code paths answered 404 again
building the answer's headers from scratch the site's Content-Security-Policy, X-Frame-Options, X-Content-Type-Options and Referrer-Policy were gone from every landing
taking the headers from the shell instead of the requested URL GET and HEAD on the same URL disagreed

The review lanes found the rest: a HEAD on the codeless landing that kept the file's own validators beside a GET that had them stripped; a body read outside its guard on both routes; and a run of assertions that were true of the fixture before any code ran — the class of test that let the original defect through a full review in the first place.

How the last round was run

The four lanes ran on a846939, each in its own frozen checkout. The single commit since is the two-sentence comment correction that round itself asked for: the JSDoc over the answer had enumerated what the injection writes, and got it wrong in both directions. The gate and the preview measurement were both repeated at the release head.

npm run check at the release head: exit 0, functions/_middleware.js at 100% in all four metrics. CI green. On the preview deployment of the release head, all five landing paths answer 200 with the real landing, ?lang=en switches the locale on both the code path and the codeless one, /invite and /invite/index.html keep their 308, and HEAD matches GET.

@TaprootFreakAI
TaprootFreakAI marked this pull request as ready for review September 9, 2026 18:09
@TaprootFreak
TaprootFreak merged commit 30aec72 into develop Sep 9, 2026
3 checks passed
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…che, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. /invite/AB.HTML therefore passed the gate and then lost its code,
while /invite/AB.JSON never reached the gate at all. Both now read the same
constant, and a case walks every suffix requiring the two answers to agree.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

The rules covered by the wildcards are gone, the bare paths keep theirs, and
check-site.mjs now requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write carrying the code and matching the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, still waiting one second short of the budget and
unavailable two seconds past it.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether the segment names a file, with two
different lists: one carried html and not the image types, the other the
reverse. So /invite/AB.HTML passed the gate and then lost its code, while
/invite/AB.JSON never reached the gate at all. Both read one constant now, and
a case asks the two as a single question across every suffix — the sweep
includes html, a suffix that names no file type, and a mixed-case spelling — so
a list that grows on one side and not the other fails there rather than in
production.

html is deliberately not in that list. The one HTML file under these paths is
the shell, which the parser names outright; putting the suffix in would take
/invite/index.html out of this pass along with the redirect that canonicalises
it. A code that happens to end in .HTML stays a code, on both sides.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard and fails if a covered rule comes back.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, carrying the code, equal to the canonical
link. And the fifteen-second lookup budget was asserted against its own
constant and nowhere else: a request that never answers now has to leave the
loading state by itself, and the case measures how long that took against the
budget the page itself reports rather than bracketing it loosely.
TaprootFreakAI added a commit that referenced this pull request Sep 9, 2026
…two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether a suffix names a file, with two different
sets: the gate carried jpg|jpeg|webp|ico|txt|xml that the parser lacked, the
parser carried html that the gate lacked. So /invite/AB.HTML passed the gate
and then lost its code, while /invite/AB.JSON never reached the gate at all.
Both read one constant now, and a case asks the two as a single question across
every suffix — the sweep includes html, a suffix that names no file type, and a
mixed-case spelling — so a set that grows on one side and not the other fails
there rather than in production.

The two still ask it of different things, deliberately: the gate of the whole
path, because /invite/AB12CD/logo.png is an asset request whoever owns the
first segment, and the parser of the code segment alone. A case pins that
difference as well as the agreement.

html is not in the list for one reason: a code that happens to end in .HTML
stays a code. The shell needs no help from the list — the parser names
index.html outright — and the 308 that canonicalises it comes from the platform
and is handed on either way.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard, rejects a rule the wildcard covers, and rejects the same
exact path declared twice, which its own header parser had been merging out of
sight.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, equal to the href the canonical link names.
And the fifteen-second lookup budget was asserted against its own constant and
nowhere else: a request that never answers now has to leave the loading state
by itself, and the case measures how long that took against the budget the page
itself reports.
TaprootFreak pushed a commit that referenced this pull request Sep 10, 2026
* fix(invite): one list for a file-looking segment, one rule per path, two tests that prove something

Four follow-ups the reviews of #34 and #37 left open.

The gate that decides whether this pass owns a path and the parser that reads
the code out of it both asked whether a suffix names a file, with two different
sets: the gate carried jpg|jpeg|webp|ico|txt|xml that the parser lacked, the
parser carried html that the gate lacked. So /invite/AB.HTML passed the gate
and then lost its code, while /invite/AB.JSON never reached the gate at all.
Both read one constant now, and a case asks the two as a single question across
every suffix — the sweep includes html, a suffix that names no file type, and a
mixed-case spelling — so a set that grows on one side and not the other fails
there rather than in production.

The two still ask it of different things, deliberately: the gate of the whole
path, because /invite/AB12CD/logo.png is an asset request whoever owns the
first segment, and the parser of the code segment alone. A case pins that
difference as well as the agreement.

html is not in the list for one reason: a code that happens to end in .HTML
stays a code. The shell needs no help from the list — the parser names
index.html outright — and the 308 that canonicalises it comes from the platform
and is handed on either way.

public/_headers had a rule for /invite/ and another for /invite/*, and the
first matches the second. Every answer that matched both carried the value
twice: measured on the deploy, /invite/ answered

  cache-control: public, max-age=60, public, max-age=60

Pages concatenates what every matching rule says rather than letting one win,
and a parser takes the first. The rules the wildcards already cover are gone,
the bare paths keep theirs, and /invite/invite.js keeps its own rule above the
wildcard — the restatement below it, added when the match order was thought to
be uncertain, only appended a third value nobody reads. check-site.mjs now
requires the wildcard, rejects a rule the wildcard covers, and rejects the same
exact path declared twice, which its own header parser had been merging out of
sight.

Two tests could not tell a working build from a broken one. The clipboard stub
resolved and discarded what it was handed, so the case named after copying the
canonical invite URL passed on any string, or on none; it records now, and the
case requires exactly one write, equal to the href the canonical link names.
And the fifteen-second lookup budget was asserted against its own constant and
nowhere else: a request that never answers now has to leave the loading state
by itself, and the case measures how long that took against the budget the page
itself reports.

* test(invite): say which of the two claims does which job

The comment said the budget is read from the page rather than restated here,
and the line under it restates it. Both belong there — the size is pinned
outright, because bounds computed from the page's own number would follow it
anywhere, and the elapsed time is measured against that number so another timer
cannot pass for this one. The comment now says that instead of denying half of
it.

* test(invite): three comments that claimed a little more than the code

The sweep's comment implied it catches any divergence between the two sides,
where it walks a list of its own and catches a divergence on what it
enumerates. The .PDF case called it a dot that names no file type, where PDF is
a file type this rule simply does not call an asset. And the timeout case said
the budget was asserted against itself, where the unit suite pinned it against
a literal and went no further.

* fix(invite): catch any covered rule, and start the clock where the wait does

The covered-path check listed the four rules that happened to be there, so a
rule added later would have slipped past it. It now rejects any exact path a
landing wildcard covers, with the script's own rule kept on purpose: it wants a
longer cache than the shells, and a parser takes the first of the two values.
Proven by adding a rule for /invite/foo and watching it fail.

The budget case took its timestamp before the navigation, so a slow page load
counted against the budget and could push the upper bound over on a busy
machine. It starts when the page says it is checking, which is when the timer
the case is about was armed.

---------

Co-authored-by: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com>
@TaprootFreakAI
TaprootFreakAI deleted the 48a09586-landing-from-assets branch September 10, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants