Skip to content

Break-it walkthrough (#193) - alpha Phase 2 "Prove" โ€‹

FieldValue
Issue#193 (P0 alpha row), Phase 2 "Prove"
Date2026-08-14 (four sessions)
MethodAdversarial reading of the seams, plus targeted emulator probes where a hypothesis was concrete
ScopeMulti-actor seams: permission edges, stale identity, out-of-order actions, ownership, block enforcement
Result9 findings, 13 held (all graded), 6 fixed here, 1 awaiting an operator decision (#887). Load and abuse deliberately not started.

Why this exists โ€‹

Phase 1 "Build" is nearly complete; Phase 2 "Prove" had nothing started. The gap between here and the December alpha is not features, it is that nothing had seriously tried to break what we built. The individual flows are tested. What had never been tested is the flows interacting, and the seams between them.

Read this part if you read nothing else โ€‹

Nothing broke inside a flow's own logic. Every single defect was at a seam.

That is the headline for a launch decision. The geofence, the expiry re-check, merchant ownership, per-offer authorization, block enforcement in rules: all held under direct attack, because they have tests and the tests were doing their job. What failed was always the place where one correct thing meets another correct thing. The units are sound. The composition had never been tested.

Three properties predicted where every finding was. They are the real output of this phase, because they tell the next person where to look without repeating the work:

  1. The path is an emergency control, not a routine one. Banning an admin revoked nothing, while demotion, the path admins actually use, worked correctly the whole time. Coverage follows usage, and an emergency is not usage. The tool you most need to work is the one least likely to have been tried.
  2. A guard is trusted from a pre-check read instead of re-validated inside the transaction. Four findings turn on this one sentence. Sequentially every one of them is correct, which is why review and unit tests pass.
  3. There is no test file for the route at all. True of admin sign-in and of lightLantern. In both cases a full green suite said nothing whatever about the code in question.

Property 3 is the one to run first, because it is the cheapest. Properties 1 and 2 tell you where to attack, which costs a probe each. Property 3 tells you where nobody has looked, and it is enumerable straight from the filesystem before you write a single probe: list the exported handlers, grep the test directories for their names, and read the gaps. Every route that turns up empty is a finding waiting to happen, and you know that before spending an hour on any of them.

The third deserves its own statement, because it is the most repeated lesson here and the most persuasive wrong answer available:

A suite that does not touch a path reports success for it.

lightLantern was refactored, 107 lantern tests passed, and that was nearly banked as confirmation. None of them call it. This is the same shape as a rules test passing because of a catch-all deny, or a blocklist passing on a field invented later, except that here it was hiding inside a coverage number, which is the most trusted signal available. It is available on every refactor.

And the older framing still holds for the permission findings:

A check that exists, is called, returns the correct answer, and is not positioned where it can stop anything.

The invite gate validates correctly and is bypassable. The traversal was blocked, but by a projection added for an unrelated reason. Admin authorization consults two sources and accepts either.

Findings and non-findings carry equal weight below. A walkthrough that lists only failures reads as a bug list; the "what held" section is the evidence for the launch decision, which is what this phase is for. Each entry says what was attacked, what happened, and how it was verified. Every probe was verified to bite: the guard it tests was removed, the probe was watched to fail, the guard was restored. A probe that cannot fail for the reason it names is decoration.

Still open for the operator โ€‹

  • #887, the invite gate, is NOT fixed. It is diagnosed, reproduced, and awaiting a decision on whether it is fixed now or queued. During an invite-only pilot it is the control that makes the pilot invite-only, so it should not be read as closed by this document.
  • Load, abuse volume, rate limits and App Check bypass are untouched, by choice. See the last section.

FINDINGS โ€‹

F1. The invite gate is advisory, not enforced (#887) โ€‹

NOT FIXED. Awaiting an operator decision on fix-now vs queue. Every other finding in this document is either fixed on this branch or filed with its mechanism. This one is deliberately neither, because the invite requirement is the control that defines the pilot and closing it changes who can create an account.

Severity: high for an invite-only alpha. Not "wide open".

AGENTS.md says an invite is "validated server-side, consumed on account creation". It is not. createUserHandler in phoneCreateUser.js contains zero references to invites. The real sequence is /validate (a read-only lookup that writes nothing), then account creation (no invite check), then /consume (bookkeeping afterwards). Skipping steps 1 and 3 yields an account with no invite.

Signup does still require App Check, a phone-verification HMAC the server only mints after a real OTP round trip, ban checks and IP rate limiting. So an attacker needs a working phone and a valid App Check token. What is missing is specifically the invite requirement, which is the control that makes the pilot invite-only.

Secondary, same flow: /consume was a read-then-write with no transaction, so two concurrent redemptions of one token both succeeded. That half was fixed on this branch (see F5); the gate itself was not. Both are now closed: #887 enforced the gate at creation and #902 removed /consume entirely, folding consumption into the creation transaction.

Verified by reading every account-creation path, not inferred from one file, because it contradicts our own documentation.

F2. Admin authorization has two sources of truth, and a partial demotion fails open (#886) โ€‹

Severity: low likelihood, high impact, fails open.

requireAdmin grants admin if either the Auth custom claim or users/{uid}.role says so. Demotion writes both, sequentially, with no transaction. If the claim write succeeds and the Firestore write fails, the account keeps admin via the fallback, and the audit log records a successful demotion.

F3. Path traversal in the new admin read endpoints (fixed on this branch) โ€‹

Severity: admin-authenticated, no leak, defence-in-depth.

GET /auth/admin/users/alice%2Fprivate%2Fauth reached collection('users').doc('alice/private/auth'), a valid four-segment document path, and returned 200. Firestore's .doc() takes a path relative to the collection and accepts slashes; Express will not match a literal / in a :param, but a percent-encoded one decodes into the param after routing.

Nothing leaked, and the reason is the important part: see H1. Fixed with a uid shape guard plus two probes that fail without it.

F4. Dashboard profile-completion counts are structurally always zero (#881) โ€‹

Pre-existing, surfaced while porting the calculation server-side. The counts read plaintext interests/mood, which are deleted and replaced with encrypted fields on every profile save. The dashboard reports 0 complete and every user incomplete, on a number the operator reads as truth.

F6. Banning an admin does not revoke admin access (#888, fixed on this branch) โ€‹

Severity: the control an operator reaches for when an admin goes rogue does not work.

users/{uid} = { role: 'admin', banned: true } still returns 200 on every admin route. Two independent gaps, either one sufficient: requireAdmin grants on the custom claim and returns before any Firestore read, so banned is never consulted; and admin portal login mints a role: admin token after verifying only the password, because ban enforcement lives on the app's phone login and admin auth is a deliberately separate system.

Ban is not the normal offboarding path, demotion is, and demotion works. Ban is the emergency tool, which is the one nobody rehearses. The action reports success and revokes nothing.

Fixed at both ends: login refuses to mint the token (and audits the refusal), the middleware refuses an already-issued one. requireMerchant had the identical shape and got the identical fix.

F7. Two simultaneous requests could light two lanterns for one account (#889, fixed on this branch) โ€‹

lightLantern checked for an existing active lantern, then created the new one in a separate batch. Two requests arriving together both saw "no active lantern" and both created one. The sequential path refuses the second correctly the whole time.

Single-lantern enforcement is load-bearing, not cosmetic. wave.service.js reads "the caller's single active lantern" on the stated guarantee that at most one exists; fren.service.js keeps the first per fren for the same reason; and two simultaneous lanternPins entries put one anonymous user visibly at two venues at once, which is the correlation the pin/lantern split exists to prevent. activeLanternCount is denormalized and server-owned, so a double increment is permanent drift.

This one does not need an adversary. A client retrying a slow request, or a double tap on a laggy connection, produces the same interleaving from one phone. That is ordinary mobile behaviour, not an attack, which is what makes it alpha-critical rather than theoretical. It is the privacy architecture failing at its own stated job, in the core mechanic, with no attacker present.

Fixed with a transaction, the same shape as the wave-accept and offer-redeem paths that held under identical probes.

lightLantern had zero test coverage before this. The 107 existing lantern tests passing after the rewrite said nothing about whether it survived, because none of them called it. That is the "a check that cannot evaluate the thing reports success" shape appearing in the coverage numbers themselves.

F9. The portal five-strike lockout allowed one unbounded burst (#891, fixed on this branch) โ€‹

Found because PM's warning after F8 was to check for duplication FIRST. It was duplicated, and that is how the defect stayed.

Both portal logins did read-count, verify-password, increment, with no transaction. Ten requests arriving together all read the same count, so all ten reached password verification. Measured, not argued: a burst of ten produced zero lockouts.

The counter is atomic (FieldValue.increment), so it ends up CORRECT after the burst, and this is the property that makes F9 the most serious finding of the day. The control fails open AND leaves no trace of having failed. The account locks one burst late and every record afterwards looks normal, so there is no forensic path back to it. A control that fails loudly is recoverable; one whose evidence self-heals is not. A five-strike policy that permits one unbounded burst is not a five-strike policy.

The same defect was already fixed, in this repo, in the adjacent file, with a comment explaining it. The PIN path's M-COUNTERS comment describes this exact failure and the exact remedy. It was never carried across.

That changes what this finding is evidence FOR. Not that we did not know. Knowing is not a mechanism either. A solved, documented problem one file away did not propagate, for the same reason "Mirrors adminAuth.js" did not: prose asserting an invariant guarantees nothing. Only shared code does.

adminAuth.js and merchantAuth.js had also already drifted: admin awaited its counter write, merchant fired it and forgot, so a dropped write there silently gave an attacker a free guess. "Mirrors adminAuth.js" was a comment, not a mechanism.

Fixed with one shared loginLockout.service.js, parameterised by collection. A strike is claimed inside the transaction before the password is verified, so Firestore serialises the contenders. A burst of ten now yields exactly five refusals.

Password verification deliberately stays OUTSIDE the transaction, and that is a decision rather than an oversight. The obvious simplification is to hold the transaction across the whole sign-in. Do not: a password hash is slow on purpose, and a transaction held across it would queue every login in the system behind whoever is currently being attacked, converting an authentication weakness into an availability one. The comment in the service says so, because this is exactly the kind of thing a later cleanup removes.

Verified to bite from BOTH boundaries: removing the claim failed the same two probes in the admin suite and the merchant suite identically, which is the point of there being one implementation.

Neither route had a test file before this.

WHAT HELD (evidence for the launch decision) โ€‹

Every entry below is graded, because proximity transfers credibility. An inference and a proof read identically when they sit under the same heading on the same page, and the reader has no way to tell them apart. That is not hypothetical: H3 sat here as settled for four sessions on the strength of a careful read, next to entries that had been attacked and proven.

  • PROVEN means a probe was run AND verified to bite: the guard was removed, the probe was watched to fail, the guard was restored.
  • READ means the code was read carefully and the reasoning is sound. It is not evidence, and it should not be spent as though it were.

H1. An allowlist stopped an attack nobody had thought of โ€‹

PROVEN. The traversal was executed and returned no field of the resolved document; two probes fail without the guard.

The traversal in F3 resolved a document it should never have reached, and no field of it came back, because the response is projected through an explicit ADMIN_USER_FIELDS allowlist. The allowlist was added to stop sealed-identity material reaching admin browsers; it also blocked path traversal, which nobody anticipated.

This is the case for allowlists over blocklists, and it is no longer theoretical. A blocklist defends against attacks you have named. An allowlist defends against attacks you have not thought of, because it does not need to know their names.

H2. Demotion takes effect on the next request, not an hour later โ€‹

READ when written, now PROVEN. Superseded by H12, which minted a token before the demotion and presented it after.

requireAdmin performs a live getAuth().getUser(uid) rather than trusting the role claim inside the decoded ID token. The common vulnerability here is the opposite, and this is not it. Worth protecting: reading decoded.role instead would be an easy "optimisation" that silently opens a stale-privilege window.

H3. A merchant cannot act on another merchant's venue or offers โ€‹

Was READ, now PROVEN, test:merchant-access:emulator. It was the weakest claim in this section, so it was the first thing attacked after the grading. See the note below on what proving it turned up.

requireMerchantAccess compares the caller's own merchantId, read server-side from their user document, against the merchantId in the route. The per-offer checks then confirm the offer belongs to that merchant. The route parameter alone proves nothing, which is the correct design.

Eleven probes through the real /merchants/:merchantId mount, because the route parameter is half of what is under test and a hand-made req would prove nothing about how it arrives. A merchant cannot reach another merchant; a merchant role in the custom claim cannot substitute for owning the merchant, so a stale or over-broad claim is not a skeleton key; a merchant-role user with no merchantId reaches nothing (including the literal path segment undefined); an ordinary user and a user with no document reach nothing. Admin reaching any merchant is deliberate and is asserted so, rather than left ambiguous.

Verified to bite: removing the ownership comparison failed three probes with expected 200 to be 403.

F8. Proving H3 found #888 again, in a service the fix had not reached โ€‹

This one is on me, and it is the finding I would least have predicted.

The merchants service carries its own copy of requireAdmin and requireMerchantAccess. Fixing the ban gap in the auth service's middleware earlier the same day did not touch them, so a banned merchant still had full portal access, and a banned admin still reached every merchant. The probes for it failed on the first run.

That is the fix-the-case-not-the-invariant mistake, made by the person who had just written the section of this document warning about it. The fix now goes through one loadUserGate helper in that file, with a comment saying why it exists, so the next guard added there cannot quietly skip the check.

The transferable part is about test design, not about ban. The probe was written to prove a claim about merchant ownership. It found an unrelated defect because it asserted the whole boundary rather than the one property under discussion. A test scoped tightly to its stated purpose would have passed and told nobody.

H13. The PIN lockout counts every concurrent failure exactly once โ€‹

PROVEN, test:pin-lockout:emulator.

The most load-bearing thing in the app that had no test. The secret is a 6-digit PIN: the entire difference between that and a real password is that an attacker gets five guesses instead of a million. If the counter under-counts, the PIN is worth close to nothing.

Fourteen probes across both axes (the userId-keyed lockout and the phoneHash-keyed mirror for sealed accounts). Five concurrent failures count five; twenty count twenty; four do not lock and the fifth does; an expired lockout stops blocking; a successful login clears both the counter and the lockout, so a legitimate user is not locked out by their next typo; the two axes are independent. The pinLockouts doc is asserted to carry no userId, since co-locating it with a phoneHash is the exact re-identification sealed identity exists to prevent.

Verified to bite, and this is the number worth remembering: reverting the transaction to the plain read-modify-write it replaced produced expected 1 to be 20. Twenty simultaneous wrong PINs would have counted as one. That is unlimited guessing against a six-digit secret, and the fix predates this session; what did not exist was anything proving it still worked.

H4. Expired lanterns cannot authorize an offer claim โ€‹

READ when written, now PROVEN. Superseded by H10.

hasActiveLanternAtVenue does not trust status: 'active'. It re-checks expiresAt against the current time, with a comment naming the exact hazard (the status flag lags the TTL by up to ~46h). It also fails closed when expiresAt is missing or unparseable.

H5. Blocked users cannot wave or message โ€‹

READ when written, now PROVEN. The rules half by H11, the wave half by H9.

Waves check isBlockedEitherDirection on both send and accept. Messages are written client-side, so firestore.rules is the only gate there, and it refuses a severed (blockClosed) connection, a block in either direction, and any message whose senderId is not the caller.

H6. Destructive admin routes rejected malformed ids, but ACCIDENTALLY (now made deliberate) โ€‹

PROVEN, for the deliberate guard that replaced the accident. The accident itself was never a guarantee, which is the point of the entry.

Delete-admin, delete-merchant, patch-merchant, attach-merchant and resend-setup all call auth.getUser(userId) first, so the traversal shape in F3 could not reach them: a slashed uid does not exist in Firebase Auth. That was an accidental defence, not a designed one. It was load-bearing security nobody chose, and a refactor reordering or removing the Auth round trip would have opened all five silently, with no test failing.

All five now validate the uid explicitly, so the accident is redundant rather than load-bearing. Recorded here because the history matters even once the mechanism no longer does: the general form is that a test is the right answer for an intentional guarantee, not for a coincidence. Testing a coincidence protects today's behaviour and preserves tomorrow's fragility.

Session 2: collision probes (concurrency and out-of-order) โ€‹

Reading found the SHAPE of the invite race; only a probe found the FACT. That is the argument for this section existing at all: every case below is two individually-correct behaviours colliding, and the sequential path is right in every one of them.

Each probe was verified to bite: the guard it tests was temporarily neutered, the probe was watched to fail, then the guard was restored. A probe that cannot fail for the reason it names is decoration.

The emulator's limit, stated because a concurrency claim rests on it. The Firestore EMULATOR serialises transactions. That is not how production behaves, and it can HIDE the exact bug a concurrency probe is written to catch. #889 is the proof: the first single-lantern fix used a transactional query returning empty, the emulator serialised the two lights so the test passed, and in production an empty query takes no lock, so two lights at different venues would both have committed. A passing test under a serialising emulator is strictly worse than an absent one: absent leaves you uncertain, passing leaves you confident and wrong.

So the collision claims below (H7, H8, H9, H10, H13) are proven against a harness that serialises. What makes them trustworthy despite that is that each rests on a deterministic doc id or a shared written document (a per-(user,offer) claim id, a single wave doc, the per-user PIN counter), so two contenders collide on a real write/write conflict that production DOES detect. The lantern fix failed precisely because it did NOT: an auto-id insert plus an empty query share no document. The mechanism that makes a collision claim survive production is a shared write target, not the emulator passing.

F5. A single-use invite could be consumed by TWO users (fixed on this branch) โ€‹

SUPERSEDED 2026-08-17 by #902. POST /auth/user/invite/consume no longer exists. The invite is now read, validated and consumed inside the SAME transaction that writes the user doc, in createUserHandler, so consumption cannot happen without an account being created in the same commit. That is strictly stronger than the transaction described below, which still allowed a token to be consumed by a caller who never created an account.

The route was removed rather than kept, because it was unauthenticated and took the uid from the request body: anyone holding a forwarded invite link could burn someone else's invite and choose the usedBy value recorded against it.

The finding below is still correct and its walkthrough is still worth reading. What changed is where the guarantee lives. concurrency.integration.test.js went with the route; the property it proved is asserted in phoneCreateUser.invite.integration.test.js as "two CONCURRENT creations on one token, DIFFERENT phones, yield one account", which additionally checks the losing racer was refused BY THE INVITE rather than by phone uniqueness.

Two concurrent /consume calls for one token with different uids returned 200 and 200. The handler was a read-then-write with no transaction, so both callers observed usedAt: null before either wrote. A sequential control in the same suite returns 200 then 409 correctly, which is exactly why review could not see it.

Fixed with a transaction. Idempotent re-consume by the same user still returns 200, because a retried signup must not be told its own invite is used.

Sibling of F1: even once the gate is enforced at creation, single-use is only single-use if the consume is atomic.

H7. The per-user redemption limit holds under real contention โ€‹

PROVEN, test:claim-race:emulator.

Two concurrent redeemOffer calls against per_user_limit: 1 produce exactly one success and one LIMIT_REACHED, with redemptionCount ending at 1. The check sits INSIDE the transaction, so Firestore's retry serialises them. "One per customer" is the merchant's actual promise and it survives a double tap.

H8. Concurrent claims are idempotent โ€‹

PROVEN, test:claim-race:emulator.

Two simultaneous claimOffer calls produce ONE claim row, not two, because the claim id is deterministic per (user, offer). A double tap is a no-op rather than a duplicate.

H9. Wave accepts do not fan out or cross a block โ€‹

PROVEN, test:wave-race:emulator.

Seven probes, all held: two concurrent accepts produce ONE connection; a block landing BETWEEN send and accept prevents the connection in either direction (a mutual close, not one-way); a non-recipient gets NOT_RECIPIENT; an expired wave and a withdrawn wave are both refused; a missing wave gives WAVE_NOT_FOUND.

The design reason this holds: acceptWaveById re-validates recipient, status, expiry and the already-accepted case inside the transaction rather than trusting its pre-check read.

H10. An expired lantern cannot claim, proven rather than read โ€‹

PROVEN, test:claim-race:emulator.

Session 1 recorded this from reading (H4). Now demonstrated: a lantern whose status still says active but whose expiresAt is in the past is refused with NO_LANTERN. Also refused: claiming a non-active offer, and redeeming before claiming.

H11. A block landing MID-CONVERSATION stops the thread immediately โ€‹

PROVEN, test:rules.

Eight rules probes, all held. This surface matters more than the others: firestore.rules is the SOLE gate here. Chat bodies are E2EE and written straight from the client, so there is no server handler behind these rules to fail closed. Everywhere else in this walkthrough, a rule failing open would still meet server logic; here it would not.

Held: a severed (blockClosed) connection refuses sends from both parties; a block by the recipient stops the blocked party; a block by the sender also stops them, so a blocker cannot keep a private channel to someone they blocked; a severed thread cannot be read either; a non-participant cannot send; a participant cannot forge another user as senderId; and a client cannot clear blockClosed to reopen a severed thread.

There is also a positive control: before any block, a participant CAN send. Without it the suite could pass by denying everything.

Verified to bite: removing the two block checks from the message-create rule produced seven failures, three of them in pre-existing tests written by someone else for the original block work. Restored, 226/226.

H12. A demoted admin loses access on the very next request, proven rather than read โ€‹

PROVEN, test:privilege-window:emulator.

Session 1 recorded this from reading (H2). Reading cannot prove it: the proof needs a token minted BEFORE the demotion and presented AFTER it. Now probed against real Auth and Firestore emulators, the first suite in the repo to use the Auth emulator, because minting a stale ID token is not otherwise possible.

An ID token minted while the account was an admin, then presented after a full demotion, is refused immediately. There is no up-to-an-hour stale-privilege window. requireAdmin performs a live getUser() rather than trusting the decoded token.

Verified to bite, and this one is worth the detail: making requireAdmin read the role from the decoded token instead of the live lookup produced exactly expected 200 to be 403. That is the industry-standard version of this bug, reproduced on demand in our own codebase, and it is a two-line "optimisation" away. The comment in requireAdmin now says so.

The same suite demonstrates F2 (#886) rather than asserting it: clearing the claim but not Firestore leaves the account admin, and clearing Firestore but not the claim does too. Both directions of a half-completed demotion fail open.

The pattern behind four of these findings โ€‹

Worth naming once rather than four times implicitly, because it is the thing a future contributor has to preserve:

Re-validate state INSIDE the transaction. A pre-check read is a courtesy, not a guarantee.

  • acceptWaveById re-reads recipient, status, expiry and the already-accepted case inside its transaction (H9).
  • redeemOffer checks the per-user limit inside its transaction (H7).
  • claimOffer uses a deterministic claim id inside a transaction, so a double tap is idempotent (H8).
  • The invite consume did not, and that is precisely the one that broke (F5).

The failure is invisible sequentially: every one of these is correct when the calls arrive one at a time, which is why review and unit tests pass. The pre-check is still worth keeping, because it gives a fast, clear error in the common case, but it must never be the only check.

The model, checked against the findings โ€‹

Each finding mapped back to the properties at the top, so the model can be judged rather than taken on trust.

FindingEmergency pathPre-check readNo test file
F1 invite gate not enforced (#887)yes
F2 two sources, partial demotion (#886)yesyes
F3 admin path traversalyes
F5 invite consumed twiceyes
F6 banned admin keeps access (#888)yesyes
F7 two lanterns for one account (#889)yesyes
F4 dashboard counts always zero (#881)yes
F8 ban gap in a second service (#888)yesyes
F9 portal lockout allowed a burst (#891)yesyesyes

F4 is worth a note, because it is the one finding the model did not lead to. It surfaced while porting the calculation server-side, not while hunting. But it fits property 3 anyway, and that was checked rather than assumed: no test in apps/admin references fetchDashboardStats, profileCompletion, or the fields it reads. So the model would have found it; this session simply arrived from another direction first.

The distinction matters for the next person. The model tells you where bugs are likely; it does not claim to be the only route to one. F4 arrived by a different road and the model still accounts for it, which is the outcome you want from a model rather than a rule.

Property 3, run rather than asserted โ€‹

The claim above is that property 3 is enumerable from the filesystem, so it was enumerated. Across the six Cloud Run API services, 87 exported functions are named by no test file anywhere in their own service.

The count is deliberately generous and therefore a floor, not an estimate: a name appearing anywhere in any test file counts as covered, so it cannot see a test that imports a function and never exercises the branch that matters. lightLantern correctly dropped off the list once this session's probes were added, which is the calibration check.

The alpha-critical subset, in the order worth attacking. requireMerchantAccess was at the top of this list and has since been done, which is what produced F8:

FunctionServiceWhy it matters
extinguishLantern, getActiveLanternslanternsThe other half of the core mechanic. Its sibling had a real bug (F7).
cleanupExpiredWaves, cleanupExpiredConnectionslanternsRetention and purge. Silent failure means data outliving its policy.
sanitizeDisplayNamelanternsRuns on every public pin. This session leaned on it without testing it.
findNearbyVenues, listVenuesvenuesDiscovery, the first thing a user touches.
scrubAnthropicRequestassistantStrips data before it leaves for a third party.

This is what forced the grading on the held section. H3 rests on reading requireMerchantAccess, and that function has no test. It had been sitting for four sessions next to entries that were attacked and proven, reading identically to them. Every held entry now carries PROVEN or READ, because an inference and a proof are indistinguishable on the page and the reader has no way to tell.

The general form, worth more than the instance: evidence and inference must be visibly distinguishable in the same document, because proximity transfers credibility.

Not yet attacked (next session) โ€‹

  • Load, abuse volume, rate-limit and App Check bypass. Deliberately NOT started: it is a different discipline wanting tooling this session does not have, and a shallow pass would read as coverage. #193's own title asks for it, so it remains genuinely open.
  • Rate-limit and App Check bypass surfaces.
  • Load and abuse volume, which #193's title mentions and this session did not touch at all.

Built with VitePress