# Audit report

> IMD Ember World: post-remediation independent Audit of the sixth-round results (package R7).
>
> SUBJECT: unofficial TypeScript Cloudflare Worker/React World/Auth/Member M1. NO Solidity; inspect TypeScript/SQL/lifecycle, not invented contracts. If unsupported, report unsupported/unknown. M1 persists public profiles; World is not wholly read-only.
>
> PIN: https://github.com/tungweb3/imd-ember-world-review/tree/c4f451b015abdaced6c35a717b29f5bb1cb351c0 ; parent445747d6a4d4fb6fa4eaa9c67b74e3e2bd9c1703. Review immutable commit. Auth ff6bed81 and deployed e48a94f have identical supplied scope; exact IDs in R7. Private history, models/3D/textures/media/full scene/WorldApp and unrelated avatar/selfie UI/tests withheld; do not request or publish them.
>
> READ: README; R7/PRIOR_REVIEWS.md, AUTH_REMEDIATION.md, TEST_RESULTS.md, PUBLIC_SOURCE_VALIDATION.json, BUILD_EVIDENCE.md, PRODUCTION_DEPLOYMENT.json, PUBLIC_CONTENT.md; manifests/r7-published-source.json, SHA256SUMS; source/docs/security/AUTH_STATE_MACHINE.md. R5/R6 and earlier source/docs/security remediation records remain historical controls, not new verdicts.
>
> ORIGINALS: previous sixth Audit 09062f1d-1a0b-49cb-af81-e55978798576 and Report 816968c0-8ff9-40a9-8e08-0e0bc4f2aef1 reviewed parent445747d. Acquire official originals via R7/PRIOR_REVIEWS.md and verify listed SHA256. Audit: https://github.com/Identity-md/research/blob/main/jobs/09062f1d-1a0b-49cb-af81-e55978798576/files/AUDIT.md . Report: https://github.com/Identity-md/research/blob/main/jobs/816968c0-8ff9-40a9-8e08-0e0bc4f2aef1/files/artifacts/report.md . Four Low + cache Info require new independent closure. Report R6-I1 duplicates Audit #2: count once. Audit #6 is a verdict matrix, not a sixth defect.
>
> PUBLIC:111 source files:95 exact,16 redacted/57 lines. Actual386/386, zero failures/skips,11 files/no scene stubs; command/UTC in PUBLIC_SOURCE_VALIDATION.json. Real Worker/SQL over node:sqlite, synthetic EOA/SIWE/provider/cookies/clocks. Public tsc exit2/16 missing-dependency diagnostics; full frontend withheld/unbuilt. First Worker compile denied; retry exit0, raw314508B/SHA256 afd82f506aceb57ca85ce44ff6c7e65146546d383ae2e2b2b7feca72bd23e92a. Exact hash depends on recorded same-depth junction/lock; empty ASSETS fixture is not frontend. Preserve failures, no bundle normalization or geometry stubs.
>
> TEAM (not reviewer/public execution): private Auth1357/1357, deployed1392/1392/TSC/Vite/pipeline pass. Same-evaluator lifecycle33/55->55/55, server/cache26/40->40/40, model79/79. Record20261004T031821Z-e48a94f; Worker6c505065-798d-4890-b7c7-0c6063d1ae9b,100% checked, UTC2026-10-04T03:20:14.463Z. Five anonymous GETs/four static hashes/24 headers/three anonymous views passed; session200/signedIn:false/no-store/no Set-Cookie. No new migration/config/header change. Deployment match partial, not Auth/wallet/D1/full-feature proof.
>
> METHOD: offline pinned source/local synthetic tests; auth-r7-fixtures.mjs, independent controller AND real AuthClient. Optional <=2 anonymous GETs >=5s apart, only https://imdember.com/ and /api/auth/session; no cookies, record UTC/status/headers/hash, stop on denial. No live login/signature/writes/scan/fuzz/transactions/D1 changes/deploy or omitted asset downloads. Local synthetic POST/PUT allowed.
>
> RETEST ORIGINAL COUNTEREXAMPLES + BEFORE/AFTER + CONTROLS:
> 1 Audit#1 Low (original46-68, priorLOW-2/R4-02/AUD4-06): committed malformed verify -> trusted PRESENT -> held home -> stop -> late home. Terminal RELEASED BEFORE home wait: accepted row/cookie survive, no abandoned logout/post-stop UI/channel/timer write. Test valid-body control, lifetimes/late callbacks; separate expectedAddress context cleanup from revival of verify owner.
> 2 Audit#2 Low (70-93), Report R6-I1 Info (113-121): dead token+own nonce refuses, no row/challenge/cookie changes; held reply after newerB/A2+pending. Require live token+nonce, revoked_at IS NULL, expires_at>now. Test missing/empty/forged/malformed/mismatch, retained/pruned challenge, live controls. ANY token forbids pending-only fallback; no-token requires original flow cookie+exact pending/unexpired nonce. Both assertions refuse; user /logout {} stays current-cookie. Separate already-live-authorized late Set-Cookie race remains.
> 3 Audit#3 Low (95-117, priorLOW-2): pre-commit ABSENT generated during verify -> headers/body stalled -> stale read delivered -> stop. Retain owner, no stale knowledge overwrite/lost cleanup. Fence=latest sessionReadSeq at RESPONSE/TRANSPORT OBSERVATION, not VERIFY_START; only causally later trusted PRESENT/ABSENT releases. Old PRESENT/ABSENT after new PRESENT ignored; malformed/network/429/503 stay UNKNOWN. One owner/in-flight attempt; EARLY-dispatched refusal delivered after fence retains owner and drains one later attempt. Test late body/204, stop/restart/old listeners; terminal RELEASED/CONSUMED never revives.
> 4 Audit#4 Low (119-139, priorN-2/R3-R1/ADV): held preflight clickA -> accountB -> late read. Account/provider/gen/lifetime click lease prevents B challenge/prompt/verify. Test provider/lock/stop/restart, challenge/signature stage changes, same-account and event-free initial-connect controls. Fresh explicit click recovers; UNKNOWN never signs.
> 5 Audit#5 Info (141-155): publicName AND lookupName negative age expires. Test backward/repeated jumps, zero/positive/exact TTL, pending/debounced/close-before-delay/failure/fresh controls. Names never authorize; distinct from monotonic rename cooldown.
>
> REGRESSIONS/MATRICES: independent original sixth#1-5 verdict plus prior fifth four-Low closure; retain R5-01..09 (not nine defects):01 account switch,02 provider/session/challenge,03 teardown,04 malformed UNKNOWN,05 invalid positive schema,06 GET-before-sign,07 no duplicate prompt/session,08 backward clock,09 server cooldown reconcile. Positive session requires valid address+positive safe-integer expiresAt; absence signedIn===false with optional boolean expired. Member cooldown uses serverTime+performance.now; failure remains cooling/positive60s retry; stale timer/read cannot unlock new context. R4-01 displayA/cookieB logout-all409 before revocation; stored SIWE equality, M1 old GET/new PUT version, atomic5-attempt quota, no-op/idempotency, bounded retention/probe cleanup, uncertain-save retry/expiry. House authority=session address+Ethereum-mainnet ownerOf/eligibility, never name/roster/publicMemberId.
>
> LIMITS: R4-03 smart-wallet write policy partly; R4-09 availability partly; AUD3-05 partly; AUD3-09 review-limit. No added bounded auth-fetch deadline. Cleanup requires running JS/delivery, best effort after offline/termination. Lost A token cannot revoke A after B replaces it; expectedAddress cannot distinguish same-wallet renewal. A live-authorized Set-Cookie clear may arrive late and remove newer browser cookie without revoking its row. Real browser/provider/OS, D1 races/cron, WAF/limiter/upstream and withheld frontend remain unknown. Only eth_accounts/eth_requestAccounts/exact SIWE personal_sign; no Solidity/Genesis Mint/CoinE1/0007/rewards/token transaction/approval/Permit/typed-data/batch/delegation.
>
> OUTPUT: fixed locally/partly/open/unknown per finding, fifth-four-Low and R5 matrices, and any new regressions. Each: severity/blocking/prior IDs, immutable file:line, preconditions/impact, reproducible command/argument, event/time order, prompt/cookie counts, created/live/revoked sessions, pending/used/invalidated challenges, UI/channel/timer/knowledge/cleanup ownership (N/A explained). Separate reviewer measurements, team claims, inference, unavailable checks; list failed/skipped/shimmed tests. Seek any-severity defects. Completed/accepted/test counts/Low labels are not certification, approval, zero vulnerabilities or fund-safety proof.

| | |
|---|---|
| Repository | https://github.com/tungweb3/imd-ember-world-review.git |
| Commit | `c4f451b015abdaced6c35a717b29f5bb1cb351c0` |
| Job | `2abde7c7-c84a-4a64-a693-f83754bccd91` |
| Judged | 2026-10-04 10:05 UTC |
| Findings | 6 low · 3 info |

Four agents audited the code as it is at `c4f451b`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Low: R7 regression: account/provider switch after a trusted PRESENT read, while the click is still live, sends no cleanup; the switched-away session and cookie stay live

`source/src/world/auth.ts:189`

```
    if(click?.owner){this.revokeAbandoned(click.owner);return;}
```

Merged from three specialist reports (same root cause, same line). Prior IDs: retained control R5-01/R5-02 (account/provider switch), over-correction of sixth Audit #1 (LOW-2/R4-02/AUD4-06). New in R7; non-blocking (no authority gained: house authority stays session address + mainnet ownerOf). automaticCleanup() returns as soon as the cancelled click has ANY owner record. Since R7 a trusted PRESENT read terminally RELEASES the owner (authLifecycle.ts:77-80) before the house read is awaited, while the click is still the live intent (reconcileVerify awaiting restore() at auth.ts:414, or signIn awaiting the verify body at auth.ts:394). accountChanged(B) (auth.ts:470-472) and providerChanged() (auth.ts:255) then call automaticCleanup with that click: revokeAbandoned() is a no-op on a terminal owner (abandon() refuses non-RETAINED, authLifecycle.ts:85) and the early return skips the `held ? {expectedAddress}` decision on line 190. Nothing is sent, nothing broadcast, no read scheduled. This contradicts source/docs/security/AUTH_STATE_MACHINE.md ('A new wallet switch may separately clean a displayed session by expectedAddress; that is a new context-consistency decision, not revival of a released verify owner') and the accountChanged contract at auth.ts:457-460. Impact: A's server row stays live and its HttpOnly cookie stays installed (up to the 7-day expiry) while the page shows wallet B as merely 'connected' with knowledge UNKNOWN; the browser still acts as A on every cookie-authenticated route (M1 profile PUT, logout-all) until a later click/channel/visibility read. Ownership: verify owner RELEASED before and after (correct, never revived); expectedAddress context cleanup owed and not sent; UI account B/session null/sessionKnown false (account case) or session A shown as mismatch (provider case); channel 0 messages; A's expiry timer cleared by session:null; no cleanup owner (retainedCount 0). Fix (keeps Audit #1 closed): take the early return only for an actionable owner, e.g. `if(click?.owner?.status==='RETAINED'){this.revokeAbandoned(click.owner);return;}`, so a terminal owner falls through to the expectedAddress branch (never the nonce branch; stop still sends nothing). Add the orderings below to auth-r7-lifecycle.test.mjs; R7-A covers only stop/restart in this window.

**Reproduction**

Setup (reviewer measurement, offline): copy source/ at c4f451b to a scratch dir as cand/, `git archive 445747d source` beside it as baseline/, `npm ci --ignore-scripts` in cand (node_modules copied to baseline), Node v24.21.0. Probes use tests/auth-r7-fixtures.mjs (real AuthClient + real Worker/SQL over node:sqlite; synthetic EOA/SIWE/provider/cookies/clock). Run `node --test tests/zz-probe.test.mjs` (candidate) and `AUTH_R7_SOURCE=../baseline node --test tests/zz-probe.test.mjs` (parent). Variant 1 (malformed body): tab intercept returns `new Response('{',{status:r.status})` for /api/auth/verify and holds the first /api/me/home; `await ready(q); flow=q.signIn(); await until(()=>held); p.switchTo(B)` (provider variant: getProvider returns a provider holding B, then q.notifyProvider()); release home; await flow. Variant 2 (valid stalled body): intercept returns stallBody(r).response for verify; after headers `q.channels.at(-1).message()`; wait for session PRESENT; `p.switchTo(B)`; `body.finish(true)`. Event order: START(ABSENT) -> SIGN_CLICK A -> GET session ABSENT -> POST challenge 200 -> personal_sign #1 -> POST verify 200 commit + Set-Cookie -> post-fence GET session 200 PRESENT(A), owner RELEASED, phase still 'verifying' -> accountsChanged([B]) -> (no request). Expected (and measured on parent 445747d, all variants): POST /api/auth/logout {expectedAddress} -> 204 with 1 Set-Cookie; sessions created 1 / live 0 / revoked 1; cookie cleared; GET /api/auth/session signedIn:false; channel ['signed-out'] (account case). Actual on c4f451b (all three variants): 0 logout requests; sessions created 1 / live 1 / revoked 0; challenges 1 used / 0 pending / 0 invalidated; prompts 1; session cookie present; GET /api/auth/session signedIn:true; client account B, session null, sessionKnown false, status 'connected' (provider variant: status 'mismatch'); owner RELEASED, retainedCount 0; channel []; session reads stay at 2. Control on c4f451b (same switch after the click finished): one logout {expectedAddress} 204, live 0 / revoked 1, cookie cleared, channel ['signed-in','signed-out'].

### 2. Low: R7 regression (R5-07 no duplicate prompt/session): a sign-in accepted by a post-fence session read while the valid verify body is still in transit is never broadcast; a sibling tab with stale ABSENT p

`source/src/world/auth.ts:396`

```
      if(owner.status!=='RETAINED'){this.set({phase:'idle'});return;}
```

Prior IDs: retained controls R5-07 (no duplicate prompt/session) and R5-06 (GET-before-sign); new in R7, non-blocking. signIn() ends signed in on three paths; the valid-body path broadcasts (auth.ts:402) and reconcileVerify broadcasts (auth.ts:421). The third is new: after lifecycle.observe() (auth.ts:387) a session read begun after the response fence is trusted, readSession() (auth.ts:272) RELEASES the owner and installs the session while signIn() still awaits sessionIn(v) (auth.ts:394). When the body arrives, line 396 sees a non-RETAINED owner, sets phase idle and returns without broadcast('signed-in'); readSession never broadcasts. Preconditions: verify 200 headers delivered, body slow; a session read begins in that window (channel message or visible()); a second tab of the same browser holds validated ABSENT. No attacker. Impact: the second tab's preflight re-reads only when knowledge is UNKNOWN, so its click goes straight to challenge + personal_sign: one extra wallet prompt and a second session row; the second verify overwrites the shared cookie and row #1 stays live with no browser holding its token until expiry (the documented lost-token limit, reached without any account switch). No authority gained. Ownership: owner RELEASED, retainedCount 0, 0 logouts (correct); UI tab 1 idle/PRESENT; channel: tab 1 posts 0 (expected 1 'signed-in'); timers: expiry timer for the accepted session only; knowledge tab 1 PRESENT, tab 2 stale ABSENT. Fix: at line 396, when the owner was released and `this.s.sessionKnown&&this.s.session`, call `this.broadcast('signed-in')` before returning (as line 421 does); add a test where a post-fence read wins against a VALID stalled body.

**Reproduction**

Setup (reviewer measurement, offline): copy source/ at c4f451b to a scratch dir as cand/, `git archive 445747d source` beside it as baseline/, `npm ci --ignore-scripts` in cand (node_modules copied to baseline), Node v24.21.0. Probes use tests/auth-r7-fixtures.mjs (real AuthClient + real Worker/SQL over node:sqlite; synthetic EOA/SIWE/provider/cookies/clock). Run `node --test tests/zz-probe.test.mjs` (candidate) and `AUTH_R7_SOURCE=../baseline node --test tests/zz-probe.test.mjs` (parent). Probe: two tabs q,q2 on one browser, both wallet A; q intercepts /api/auth/verify with `body=await stallBody(r); return body.response`. `await ready(q); await ready(q2); flow=q.signIn(); await until(()=>body); q.channels.at(-1).message(); await until(()=>q.c.state.session&&q.c.state.sessionKnown); body.finish(true); await flow; posted=q.channels.at(-1).messages; for each posted message deliver q2.channels.at(-1).message(); await q2.signIn()`. Event order: T1 START(ABSENT); T2 START(ABSENT); T1 SIGN_CLICK -> challenge 200 -> personal_sign #1 -> verify 200 headers + Set-Cookie, body stalled -> T1 channel message -> GET session (seq > fence) PRESENT(A), owner RELEASED -> body completes (valid) -> auth.ts:396 returns -> T2 SIGN_CLICK. Expected (measured on parent 445747d): T1 posted ['signed-in']; T2 prompts 0; sessions created 1 / live 1 / revoked 0; challenges 1 used. Actual on c4f451b: T1 posted []; T2 prompts 1 (total personal_sign 2); sessions created 2 / live 2 / revoked 0; challenges 2, used 2, pending 0, invalidated 0; logouts 0; both tabs show signedInNoHouse; cookie names session #2, row #1 live and unreachable.

### 3. Low: Wallet switch while the click holds a pending nonce and the page already displays a session: cleanup asserts expectedNonce only, server answers 409, displayed session is not ended

`source/src/world/auth.ts:190`

```
    const context=click?.nonce?{expectedNonce:click.nonce}:held?{expectedAddress:held.address}:null;
```

Prior IDs: R5-01/R5-02 control; related to sixth Audit #2 hardening (any token forbids the pending-only fallback). Non-blocking. automaticCleanup() picks exactly one assertion and the click's nonce wins over the displayed session. The post-challenge exit at auth.ts:377 (`if(this.s.session?.address===account){this.set({phase:'idle'});await this.refreshHome(true);return;}`) leaves a live click that has click.nonce (set at auth.ts:374) while the page displays A's session, installed by another tab's sign-in, during the forced home read. A switch A->B in that window sends {expectedNonce} with the other tab's live session token; the server correctly refuses (409 ACCOUNT_CONTEXT_CHANGED, no Set-Cookie). No expectedAddress request follows, so the displayed session A is never ended, contrary to the accountChanged contract (auth.ts:457-460: 'A's session ended; B starts as merely connected'). The server rule is right; the client chooses an assertion that cannot succeed and drops the one that would. Impact: after the switch the page re-reads and shows A's live session against wallet B (mismatch view); no extra prompt, no cross-account authority; an explicit sign-out or a new click recovers. Ownership: no verify owner (retainedCount 0); UI account B, session A, sessionKnown true; channel 0 messages; timers: A's expiry timer re-armed by the re-read. The same 409 outcome is measured on parent 445747d for this event order, so this is a retained gap, not an R7 regression. Fix: when `held` is displayed at cancel time, assert expectedAddress for it (and cancel the pending nonce separately).

**Reproduction**

Setup (reviewer measurement, offline): copy source/ at c4f451b to a scratch dir as cand/, `git archive 445747d source` beside it as baseline/, `npm ci --ignore-scripts` in cand (node_modules copied to baseline), Node v24.21.0. Probes use tests/auth-r7-fixtures.mjs (real AuthClient + real Worker/SQL over node:sqlite; synthetic EOA/SIWE/provider/cookies/clock). Run `node --test tests/zz-probe.test.mjs` (candidate) and `AUTH_R7_SOURCE=../baseline node --test tests/zz-probe.test.mjs` (parent). Probe: tab q (wallet A) holds its POST /api/auth/challenge response and every /api/me/home; `flow=q.signIn(); await until(()=>chHeld)`; second tab q2 of the same browser completes `q2.signIn()` for A; `q.channels.at(-1).message()`; wait for q session PRESENT; release the challenge; `p.switchTo(B)`; release home. Event order: SIGN_CLICK(q) -> GET session ABSENT -> POST challenge (held) -> q2 challenge+personal_sign+verify (session #1 live; q's older challenge invalidated by the newer one) -> CHANNEL_MESSAGE -> q GET session PRESENT(A) -> challenge body released: click CHALLENGE_READY with nonce, phase idle, forced home held -> accountsChanged([B]). Expected: POST /api/auth/logout {expectedAddress:A} -> 204, sessions live 0 / revoked 1, cookie cleared. Actual on c4f451b: exactly one logout, expectedNonce only -> 409, 0 Set-Cookie; sessions created 1 / live 1 / revoked 0; challenges 2: used 1, pending 0, invalidated 1; q prompts 0; cookie present; GET /api/auth/session signedIn:true; client account B, session A, sessionKnown true, status 'mismatch'; channel [].

### 4. Low: Stop/restart while an owner cleanup is in flight: the new lifetime keeps displaying a session the cleanup then revokes, with no reconciliation read

`source/src/world/auth.ts:482`

```
      if(owner.status==='RETAINED'||life!==this.life)return;
```

Prior IDs: R5-03 teardown / fifth LOW-2 (R4-02/AUD4-06), AUD3-05 stale display. Retained, not a regression (identical result on parent 445747d). Non-blocking: the cookie is cleared and the row revoked, so the stale UI cannot act on the server. The teardown returned by start() (auth.ts:231-233) abandons a retained owner and dispatches its nonce-bound cleanup; start() in a new lifetime restores at once. The new GET /api/auth/session can reach the server before the cleanup POST, so the new lifetime accepts PRESENT(A). When the cleanup completes (204), the callback on line 482 sees life!==this.life and returns; unlike the same-lifetime branches on lines 483-485 no canonical re-read is scheduled for the running client. This fails the 'STOPPED | restart | canonical restore' row of AUTH_STATE_MACHINE.md in this ordering. Impact: signed-in display (status signedInNoHouse, expiry timer armed for A, member profile loaded) persists until a later visibility event, channel message or click; signedInNoHouse has no periodic re-check. Ownership: owner RETAINED -> abandoned -> CONSUMED by one nonce logout; UI session A/sessionKnown true (stale); channel 0; knowledge PRESENT (stale); timers: expiry timer for a revoked session. Fix: when life!==this.life but the client is started again and idle, schedule a restore()/reconcileCleanup() in the current lifetime (a read of the shared cookie, not a UI write by the old lifetime), or have start() wait for in-flight owner cleanups before its first read.

**Reproduction**

Setup (reviewer measurement, offline): copy source/ at c4f451b to a scratch dir as cand/, `git archive 445747d source` beside it as baseline/, `npm ci --ignore-scripts` in cand (node_modules copied to baseline), Node v24.21.0. Probes use tests/auth-r7-fixtures.mjs (real AuthClient + real Worker/SQL over node:sqlite; synthetic EOA/SIWE/provider/cookies/clock). Run `node --test tests/zz-probe.test.mjs` (candidate) and `AUTH_R7_SOURCE=../baseline node --test tests/zz-probe.test.mjs` (parent). Probe: tab with beforeSend holding POST /api/auth/logout before it reaches the Worker; intercept returns '{' for /api/auth/verify and 429 for /api/auth/session while `corrupt`. `await ready(q); await q.signIn()` (verify 200 committed, body malformed, reconcile read 429: client UNKNOWN, owner RETAINED, retainedCount 1, rows 1/1/0, prompts 1); `corrupt=false; q.stop(); q.restart(); await until(sessionKnown&&session&&!checking)` (new-life GET session 200 PRESENT(A), status signedInNoHouse); release the held logout and wait. Expected: after the cleanup completes the running client re-reads and shows signed out. Actual on c4f451b and on 445747d: logout {expectedNonce} 204 with 1 Set-Cookie; sessions created 1 / live 0 / revoked 1; challenges 1 used / 0 pending / 0 invalidated; session cookie absent; GET /api/auth/session signedIn:false; but client session A, sessionKnown true, status 'signedInNoHouse', owner CONSUMED, retainedCount 0; 0 further session reads and 0 state notifications after the cleanup.

### 5. Low: ?fresh=1 with a refused chain:index budget defeats the 30 s ownerOf proof cache: every request re-asks the budget and re-sends the Multicall eth_call

`source/server/ownership.ts:261`

```
    },p=>young(p.indexedAt)&&!(again&&p.refused));
```

New, no prior ID; adjacent to R4-09 availability (not a duplicate). Non-blocking; no authority impact (ownerOf still proves every seat). proof() keeps a stored proof only while young(p.indexedAt), and for fresh reads young(at)=at+OWNERSHIP_TTL_MS>req.now (ownership.ts:225). When the budget refuses the index read, the rebuilt proof is dated indexedAt=indexed?.at??0 (ownership.ts:256): 0 with nothing kept, or the old index time. That is never young, so the proof just built (checkedAt=now) is rejected by the next fresh read and rebuilt. The stated bound (ownerOf proven at most once per 30 s per address and isolate) fails exactly at the refused edge: each /api/me/home?fresh=1 asks chain:index again and, for an address with any candidate, sends one keyed Alchemy eth_call. The remaining bound is the per-session home limiter; sessions per address are not capped. Preconditions: a live session (any EOA can sign in), chain:index refusing, fresh=1 (the Check again button or a direct GET). Impact: keyed-RPC cost/availability amplification under exactly the load condition the budget exists for. Sessions/challenges/cookies/prompts/UI/channel/timer/cleanup ownership: N/A (read-only route, no rows written). tests/ownership.test.mjs advances the clock 31 s between refused fresh reads, so the within-TTL case is untested. Fix: date a limited proof's freshness by its own checkedAt, e.g. keep = p.limited ? p.checkedAt+OWNERSHIP_TTL_MS>req.now : young(p.indexedAt), still excluding again&&p.refused. Real limiter behaviour for denied calls is unknown.

**Reproduction**

Reviewer measurement, offline, scratch copy of source/ at c4f451b, Node v24.21.0, `node x1.mjs`: construct `new Ownership(gateway,[])` with a roster where owners[7]=A (one candidate), a chain fetch stub that counts requests to ALCHEMY_RPC_URL and answers a Multicall3 aggregate3 result naming A as owner, and call `o.home(A,{chain,now,budget:async()=>{asks++;return admit;}},fresh)` 20 times with now advancing 1000 ms per call (20 s, inside one 30 s window). Expected in every configuration: at most 1 eth_call and 1 budget ask per 30 s. Actual: fresh=false, budget refused (control): ethCalls 1, asks 1, recheck 'limited', 1 seat. fresh=true, budget admitted (control): ethCalls 1, indexReads 1, asks 1. fresh=true, budget refused: ethCalls 20, indexReads 0, asks 20, recheck 'limited', 1 seat.

### 6. Low: Negative age passes the house-read gap: after a backward wall-clock step the owner re-check sends nothing and owner mode outlives a sold seat or revoked session

`source/src/world/auth.ts:291`

```
    if(!force&&this.now()-this.homeAt<HOME_MIN_GAP_MS&&this.s.home)return;
```

Same failure class as sixth Audit #5 (negative cache age stays fresh), in the owner re-check that R7 did not touch; new, no prior ID (controls: CORR-05 owner staleness, INT-1 60 s re-check, W-1; R5-08 backward clock covers the member cooldown only). Non-blocking: the server stays authoritative for every write. homeAt is a wall-clock stamp, so after the clock steps back by J the age is negative and the guard on line 291 stays true for J+15 s: every watchOwner tick (auth.ts:527) returns without a request. visible() has the same shape (auth.ts:174, `now-sessionAt>=HOME_MIN_GAP_MS` is false for a negative age) and the CORR-05 bound (auth.ts:309, `now-homeOkAt<=OWNER_STALE_MS`) is true for any negative age (code reading; not separately measured). Result: statusOf() stays 'owner' and eligible stays >0 on a page whose seat was sold or whose session was revoked elsewhere, until the wall clock passes the old stamp. Preconditions: signed-in owner tab and a backward correction larger than 15 s (manual change, NTP step, VM resume). Impact: stale owner-mode UI only. Prompts 0; cookies unchanged; no session/challenge rows touched (read path); timer ownership: watchOwner's timer keeps firing and the read is dropped inside refreshHome; knowledge stays PRESENT; no cleanup owner. Fix as R7 did for names: skip only when 0<=now-homeAt<HOME_MIN_GAP_MS (same for sessionAt and homeOkAt), or stamp these with a monotonic clock as member.ts does.

**Reproduction**

Reviewer measurement, offline, `node x2.mjs`: real AuthClient + watchOwner with synthetic fetch/clock/timers. /api/auth/session answers signedIn:true for A; /api/me/home answers eligible:1 (owner). `c.start()`, flush, `watchOwner(c,env)`; then `now-=jumpMs`, switch the home answer to sold (eligible 0) / revoked (401 AUTH_REQUIRED) / 429, and fire ten 60 s re-check timers (10 min of real time, wall clock advancing with them). Expected: at least one home GET and the new server answer shown. Actual, jumpMs=0 (control): sold -> 10 GETs, status 'signedInNoHouse'; revoked -> 1 GET, 'visitor'; 429 -> 10 GETs, 'ownershipUnavailable'. Actual, jumpMs=3,600,000: 0 home GETs, status 'owner', eligible 1 in all three modes.

### 7. Info: Behaviour change outside the R7 transition table: a wallet lock (accountsChanged []) with no click in progress now revokes a committed session held by a retained owner

`source/src/world/auth.ts:467`

```
    const wasFlow=!!click||this.lifecycle.retainedOwners.length>0;
```

New observation, non-blocking, fail-closed. On parent 445747d a lock with no click returned after set({account:null}); the comment at auth.ts:459-460 still says 'A locked wallet (no account) leaves a valid session alone'. In R7 any retained owner makes wasFlow true on line 467 and the lock has no early return, so the owner is abandoned and its nonce-bound logout sent with the live cookie. The state is reachable with nothing wrong at the server: verify committed, body unreadable, the single reconcile read answered 429/503/transport (client UNKNOWN, owner RETAINED). Wallets auto-lock on a timer, so a good session is revoked without switch, stop or sign-out; cost is one more signature later, no authority gained. AUTH_STATE_MACHINE.md lists 'retained owner | switch/stop', not lock, and the lock tests cover only a click in progress. Either add lock to the transition table and correct the comment, or keep a lock from abandoning an owner when no click is live. Ownership: owner RETAINED -> abandoned -> CONSUMED by one nonce logout; UI account null, session null, sessionKnown true (ABSENT); channel 0; timers none.

**Reproduction**

Setup (reviewer measurement, offline): copy source/ at c4f451b to a scratch dir as cand/, `git archive 445747d source` beside it as baseline/, `npm ci --ignore-scripts` in cand (node_modules copied to baseline), Node v24.21.0. Probes use tests/auth-r7-fixtures.mjs (real AuthClient + real Worker/SQL over node:sqlite; synthetic EOA/SIWE/provider/cookies/clock). Run `node --test tests/zz-probe.test.mjs` (candidate) and `AUTH_R7_SOURCE=../baseline node --test tests/zz-probe.test.mjs` (parent). Probe: intercept returns '{' for /api/auth/verify and 429 for the first /api/auth/session after it. `await ready(q); await q.signIn()` (owner RETAINED, retainedCount 1, rows 1/1/0); `p.switchTo(null)`. Expected per auth.ts:459-460 and measured on parent 445747d: 0 logouts; sessions created 1 / live 1 / revoked 0; cookie kept. Actual on c4f451b: 1 POST /api/auth/logout {expectedNonce} -> 204 with 1 Set-Cookie; sessions created 1 / live 0 / revoked 1; challenges 1 used / 0 pending / 0 invalidated; cookie cleared; client account null, sessionKnown true, status 'visitor', owner CONSUMED; prompts 1.

### 8. Info: floorUsd from priceUsd/priceNative is not finite-checked: a denormal priceNative yields Infinity on the client while the Worker figure is finite-checked

`source/src/world/market.ts:68`

```
  const rate=ethUsd(quote);return rate===null?floor:{...floor,floorUsd:floor.floorEth*rate};
```

Display-only, non-blocking; no prior ID. ethUsd() (market.ts:65) divides priceUsd by any positive priceNative and withUsd() multiplies with no finiteness bound. floorView() (market.ts:87) returns `mine` whenever mine.floorUsd!==undefined, although the fallback on the same line requires Number.isFinite for the Worker's figure, and JSON.stringify turns the Worker's Infinity into null, so wire and client disagree. Precondition: the public upstream quote returns an absurd but syntactically valid priceNative; no site user can set it and nothing here authorizes or moves value. What the withheld component draws for Infinity is unknown. Sessions/challenges/cookies/prompts/timers/cleanup: N/A (pure function). Fix: omit floorUsd unless the product is finite.

**Reproduction**

Reviewer measurement, offline, `node x3.mjs`: `q=selectMarket([{chainId:'ethereum',baseToken:{address:IMD_TOKEN},priceUsd:'8',priceNative:'1e-320',liquidity:{usd:1}}])`; `server=withUsd({floorEth:2.5,marketplace:'OpenSea',fetchedAt:now},q)`; build a sample whose data and extras.floor are the JSON round-trip of q and server; `marketView(sample,now).floor`. Expected: no floorUsd, or a finite one. Actual: withUsd -> floorUsd Infinity; JSON wire `"floorUsd":null`; client marketView(...).floor = {floorEth:2.5,...,floorUsd:Infinity}.

### 9. Info: Seventh-review verdict matrix (not a defect): sixth Audit #1-#5 / Report R6-I1 closure, fifth four-Low and R5-01..09 status, limits

`R7/AUTH_REMEDIATION.md:3`

```
The sixth jobs targeted public snapshot `445747d` / source `1cc61b68`. Audit supplied four new Low counterexamples plus one cache Info and a verdict-matrix Info. The latter is explicitly not a defect. Report's R6-I1 is the same dead-token authority gap as Audit #2; it is one defect, not two. Findings below are **FIXED LOCALLY**, pending a new independent review.
```

Verdict record the task requires; not a defect and not certification, approval, or proof of zero vulnerabilities or fund safety. Subject is TypeScript/SQL; no Solidity exists, so the Solidity checklists were used only as generic failure-mode prompts and no Foundry proof applies. REVIEWER MEASUREMENTS: SHA256SUMS 190/190 OK. Public 11-file suite on c4f451b: 386/386, 0 failed, 0 skipped. The project's own R7 closure tests (auth-r7-authority + auth-r7-lifecycle, 87 tests) pass 87/87 on c4f451b and 65/87 on parent 445747d with the same evaluator (22 before/after failures), which is the before/after evidence I rely on for the five originals; I did not write separate independent counterexamples for #1-#5 beyond the probes in the findings above. SIXTH ORIGINALS: Audit #1 Low: fixed locally for stop/restart (terminal RELEASED before the home wait; observed in my probes as owner RELEASED with 0 logouts), but its fix introduced the auth.ts:189 regression for switches. Audit #2 Low = Report R6-I1 Info (counted once): fixed locally (my P2 probe independently shows a live token + other nonce is refused 409 with 0 Set-Cookie and no row change). Audit #3 Low: fixed locally (fence at response observation; project tests), with the auth.ts:396 broadcast regression as a side effect of trusting post-fence reads. Audit #4 Low: fixed locally (project click-lease tests). Audit #5 Info: fixed locally for publicName/lookupName (member-r7-cache tests); the same negative-age class remains in auth.ts:291. Audit #6: matrix, not a defect. FIFTH FOUR-LOW: LOW-1 fixed locally; LOW-2 partly (auth.ts:189 reopens 'cancelled flow leaves a usable session'; auth.ts:482 retained); LOW-3 fixed locally (malformed/429 reads stay UNKNOWN in my probes, no prompt); LOW-4 fixed locally per member-r5 tests. R5 MATRIX: 01 account switch FAIL in the auth.ts:189 and :190 orderings, pass elsewhere; 02 provider/session/challenge FAIL in the auth.ts:189 provider variant; 03 teardown pass, with the :482 restart display gap; 04 malformed UNKNOWN pass; 05 invalid positive schema pass; 06 GET-before-sign pass; 07 no duplicate prompt/session FAIL in the auth.ts:396 ordering; 08 backward clock pass for the member cooldown and names, open for the owner re-check (auth.ts:291); 09 server cooldown reconcile pass. TEAM CLAIMS (not re-executed): private 1357/1357, deployed 1392/1392, tsc/Vite/pipeline, production record, Worker bundle hash. UNAVAILABLE/NOT RUN: public tsc and Worker compile, the optional anonymous GETs, the official originals' SHA256 (not fetched in this pass), real browser/provider/OS, D1 races/cron, WAF/limiter, withheld frontend. No tests were skipped or shimmed in the runs above. Stated limits (R4-03, R4-09, AUD3-05, AUD3-09, no auth-fetch deadline, best-effort cleanup, lost A token, late Set-Cookie clear) remain as documented.

**Reproduction**

In a scratch copy of source/ (repo unchanged): `sha256sum -c SHA256SUMS` in the repo root (190 OK, 0 not OK); `npm ci --ignore-scripts`; `node --test --test-concurrency=3 tests/auth.test.mjs tests/aud4-auth.test.mjs tests/auth-r5.test.mjs tests/auth-r5-authority.test.mjs tests/member-client.test.mjs tests/member-r4.test.mjs tests/member-r5.test.mjs tests/auth-lifecycle-model.test.mjs tests/auth-r7-authority.test.mjs tests/auth-r7-lifecycle.test.mjs tests/member-r7-cache.test.mjs` -> tests 386, pass 386, fail 0, skipped 0; with `git archive 445747d source` extracted as ../baseline: `AUTH_R7_SOURCE=../baseline node --test tests/auth-r7-authority.test.mjs tests/auth-r7-lifecycle.test.mjs` -> tests 87, pass 65, fail 22. Expected vs actual: closure tests pass on the candidate and fail on the parent, as the remediation claims; the six defects above are outside what those tests cover.

---

Judge's submission `f0e9d4684854101c5f3681c553bb99738056393248d486877ffb343804541182`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
