{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"2abde7c7-c84a-4a64-a693-f83754bccd91","kind":"audit","nodes":[{"acceptedSubmissionHash":"9fa9133898f76e149df286c4aa373177e86e04fbbadc59a8270c859c940b527d","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"2137fbb3a1378331f3f3032735555d29b7617bcf3121df2e1fa4a717d415cf11","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"f0e9d4684854101c5f3681c553bb99738056393248d486877ffb343804541182","dependsOn":["audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"dfd88b1eb021a04128bd55ad10f941978b8791fdd8a5cacc43603e518fee67d6","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"3c5abe43df78d055a28e804947b55800192cf8b17b4d1aeed976e3bd02cb9c87","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"}],"objective":"IMD Ember World: post-remediation independent Audit of the sixth-round results (package R7).\n\nSUBJECT: 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.\n\nPIN: 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.\n\nREAD: 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.\n\nORIGINALS: 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.\n\nPUBLIC: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.\n\nTEAM (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.\n\nMETHOD: 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.\n\nRETEST ORIGINAL COUNTEREXAMPLES + BEFORE/AFTER + CONTROLS:\n1 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.\n2 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.\n3 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.\n4 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.\n5 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.\n\nREGRESSIONS/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.\n\nLIMITS: 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.\n\nOUTPUT: 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.","parentJobId":null,"planHash":"f4dde2a6a636e0bb5a7c09c5beda9cc78121f0aa278de878c0bec32b8c749dcf","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"2abde7c7-c84a-4a64-a693-f83754bccd91","publication":{"commit":null,"deliveredAt":null,"repoUrl":null},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51018","feedbackHash":"0a0276ee9ca450180ff46994c3d3150384d8a7355ad17dd776b2fb65bcd68709","nodeKey":"audit_economics","submissionHash":"9fa9133898f76e149df286c4aa373177e86e04fbbadc59a8270c859c940b527d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51023","feedbackHash":"73282036ddd8599ba118ecb3236334c21a35045bf377f502d24534ee3221fd27","nodeKey":"audit_flow","submissionHash":"2137fbb3a1378331f3f3032735555d29b7617bcf3121df2e1fa4a717d415cf11","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51481","feedbackHash":"ce52be91447a158a7ed15d547ba6ac4b2ed098674b2b152ecf451a0c9e1ae4b3","nodeKey":"audit_judge","submissionHash":"f0e9d4684854101c5f3681c553bb99738056393248d486877ffb343804541182","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51504","feedbackHash":"c71ab4f12d2ba9f494d487867c9c50e41c6ae5c42f47063fa5dfbff043221193","nodeKey":"audit_math","submissionHash":"dfd88b1eb021a04128bd55ad10f941978b8791fdd8a5cacc43603e518fee67d6","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50939","feedbackHash":"76a1a44c395f6ba589e2f4eedfc36de122c830cf91a791042531ad462f8fd30e","nodeKey":"audit_permissions","submissionHash":"3c5abe43df78d055a28e804947b55800192cf8b17b4d1aeed976e3bd02cb9c87","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"bb09b81479aa41c231511fd775bd99f50a4b9280aede5254df4fa06954991bb3","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca080fd306399669","findings":[{"citation":"resolved","description":"NEW in R7 (not one of sixth Audit #1-#5; regression against retained control R5-07 'no duplicate prompt/session' and R5-06 'GET-before-sign'). Blocking: no (Low). Reviewer measurement, real AuthClient + real Worker/SQL over node:sqlite via tests/auth-r7-fixtures.mjs.\n\nRoot cause: signIn() has three places where the click ends signed in. Two tell the other tabs: the valid-body path (auth.ts:402 `this.broadcast('signed-in')`) and reconcileVerify (auth.ts:421). The third is new in R7: after `lifecycle.observe(owner,...)` at auth.ts:387 a session read begun after the response fence is now trusted, and readSession() (auth.ts:272) terminally RELEASES the owner and installs the session while signIn() is still awaiting `sessionIn(v)` at auth.ts:394. When the body then arrives, auth.ts:396 sees `owner.status!=='RETAINED'`, sets phase idle and returns. No `broadcast('signed-in')` is sent on this path, and readSession itself never broadcasts. In the parent 445747d the same ordering fell through to the valid-body branch and broadcast (measured below).\n\nPreconditions: verify 200 headers delivered, body not yet readable (slow/stalled body); any session read begins in that window in the same lifetime/generation (another tab's channel message, or visible()); a second tab of the same browser already holds validated knowledge ABSENT. No attacker needed; a hostile page cannot trigger it cross-origin.\n\nImpact: the second tab keeps IDLE_ABSENT. Its preflight only re-reads when knowledge is UNKNOWN (auth.ts:337), so its click goes straight to challenge + personal_sign: one extra wallet prompt and a second session row for the same wallet. The second verify overwrites the shared cookie; the first row stays live until expiry with no browser holding its token (the documented 'lost token cannot be revoked' limit, reached here without any account switch). No authority is gained: house authority stays session address + mainnet ownerOf.\n\nOwnership ledger: cleanup owner RELEASED, retainedCount 0, no logout sent (correct). UI: tab 1 phase idle/session PRESENT. Channel: 0 messages posted by tab 1 (expected 1 'signed-in'). Timers: expiry timer armed for the accepted session only. Knowledge: tab 1 PRESENT, tab 2 stale ABSENT.\n\nFix direction (preserves design): at auth.ts:396, when the owner was released and `this.s.sessionKnown&&this.s.session`, call `this.broadcast('signed-in')` before returning (as reconcileVerify does at :421); and add this ordering to auth-r7-lifecycle.test.mjs, which has no test where a post-fence read wins against a VALID stalled body.","line":396,"path":"source/src/world/auth.ts","reproduction":"Setup: copy source/ outside the repo, `npm ci --ignore-scripts`, save the script below as tests/probe-a.test.mjs, run `node --test tests/probe-a.test.mjs` (candidate) and, with the parent tree extracted next to it as ../baseline, `AUTH_R7_SOURCE=../baseline node --test tests/probe-a.test.mjs`.\n\nimport test from 'node:test';import assert from 'node:assert/strict';\nimport {setup,newAccount,provider,tab,ready,until,flush,rows,prompts,routeEvents,stallBody} from './auth-r7-fixtures.mjs';\ntest('probe A',async()=>{\n  const w=setup(),A=newAccount(),b=w.browser(),p=provider(A),p2=provider(A);let body;\n  const q=tab(w,b,p,{intercept:async(path,r)=>{if(path==='/api/auth/verify'){body=await stallBody(r);return body.response;}return r;}});\n  const q2=tab(w,b,p2);await ready(q);await ready(q2);\n  const flow=q.signIn();await until(()=>body);\n  q.channels.at(-1).message();\n  await until(()=>q.c.state.session&&q.c.state.sessionKnown);await flush();\n  body.finish(true);await flow;await flush();\n  const posted=q.channels.at(-1).messages;\n  for(const _ of posted)q2.channels.at(-1).message();await flush(20);\n  await q2.signIn();await flush(20);\n  assert.ok(posted.includes('signed-in'));assert.equal(prompts([p2]),0);assert.equal(rows(w).counts.created,1);\n});\n\nEvent order: T1 START (ABSENT) ; T2 START (ABSENT) ; T1 SIGN_CLICK -> challenge 200 -> personal_sign #1 -> POST /verify commits, 200 headers + Set-Cookie delivered, body stalled (fence recorded) ; T1 channel message -> GET /session (seq > fence) -> PRESENT(A): owner RELEASED, session shown ; verify body completes (valid JSON) -> auth.ts:396 returns ; T2 SIGN_CLICK.\n\nExpected (and measured on parent 445747d): T1 posts ['signed-in']; T2 re-reads, sees PRESENT(A); T2 prompts 0, challenges 0; sessions created 1 / live 1 / revoked 0; challenges used 1 / pending 0 / invalidated 0.\nActual on c4f451b: T1 posts [] ; T2 prompts 1 (total personal_sign 2), T2 challenges 1; sessions created 2 / live 2 / revoked 0; challenges 2 used 2 / pending 0 / invalidated 0; logouts 0; cookie now names session #2, row #1 live and unreachable. Assertion 'accepted sign-in is broadcast' fails on candidate, passes on baseline.","severity":"low","snippet":"      if(owner.status!=='RETAINED'){this.set({phase:'idle'});return;}","title":"R7 regression (R5-07 duplicate prompt/session): a sign-in accepted through a post-fence session read while the verify body is still in transit is never broadcast, so another tab with stale ABSENT asks"},{"citation":"resolved","description":"NEW in R7; touches sixth Audit #1 (the task's 'separate expectedAddress context cleanup from revival of verify owner') and retained control R5-01. Blocking: no (Low). Reviewer measurement on real AuthClient + real Worker/SQL (node:sqlite).\n\nRoot cause: automaticCleanup() returns as soon as the cancelled click carries ANY owner record, whatever its status. After a malformed verify body, reconcileVerify() (auth.ts:418) is still awaiting restore(), whose readSession() has already accepted PRESENT(A), terminally RELEASED the owner (auth.ts:272) and is awaiting refreshHome (auth.ts:275). The click is therefore still the lifecycle intent and click.owner is the RELEASED record. accountChanged(B) (auth.ts:470-472) drops A locally (session:null, sessionKnown:false, hint cleared) and calls automaticCleanup(g,life,click,ended=A,other=true); line 189 calls revokeAbandoned (a correct no-op on a terminal owner) and returns, so the separate context decision `{expectedAddress: held.address}` on line 190 is never reached, nothing is broadcast, and no session read is scheduled. providerChanged() (auth.ts:255) reaches the same early return. AUTH_STATE_MACHINE.md says a wallet switch 'may separately clean a displayed session by expectedAddress; that is a new context-consistency decision, not revival of a released verify owner'; the accountChanged contract (auth.ts:457-460) says A's session is ended and, failing that, the page shows it 'as a mismatch (never as signed out)'. Both are violated only in this window; once the click has finished (control) the same switch does send the expectedAddress logout.\n\nPreconditions: verify committed with an unreadable 2xx body (or any path into reconcileVerify), trusted post-fence PRESENT(A), house read still in flight, then accountsChanged(B) or a provider change in the same lifetime. No attacker; ordinary slow /api/me/home plus a wallet switch.\n\nImpact: server session A stays live and the HttpOnly cookie stays installed while the UI shows status 'connected' for B with no session, knowledge UNKNOWN, and no pending read: the user is told nothing is signed in on a browser that still acts as A for every cookie-authenticated route (M1 profile writes, logout-all) until a later click, channel message or visibility read. Other tabs get no 'signed-out'. No authority is gained by B (house authority = session address + mainnet ownerOf), and the next explicit click re-reads and then logs A out, so this is a stale-context/cleanup-ownership defect, not an escalation.\n\nOwnership ledger: verify owner RELEASED before and after (correct, never revived, 0 nonce logouts). expectedAddress context cleanup: owed by the switch, not sent (0 logouts). UI: account B, session null, sessionKnown false. Channel: 0 messages. Timers: expiry timer for A cleared by session:null. Knowledge: UNKNOWN with no read in flight.\n\nFix direction (keeps Audit #1 closed): on line 189 return early only when the owner is still actionable (`click.owner.status==='RETAINED'`); for a terminal owner fall through to the `held ? {expectedAddress}` branch (never the nonce branch), so stop still sends nothing and a switch still ends the displayed session. Add the ordering 'reconcile PRESENT / held home / account and provider switch' to auth-r7-lifecycle.test.mjs; R7-A only covers stop and stop-restart there.","line":189,"path":"source/src/world/auth.ts","reproduction":"Setup as for the other finding (source copied outside the repo, `npm ci --ignore-scripts`; parent tree at ../baseline). Save as tests/probe-b.test.mjs and run `node --test tests/probe-b.test.mjs`, then `AUTH_R7_SOURCE=../baseline node --test tests/probe-b.test.mjs`.\n\nimport test from 'node:test';import assert from 'node:assert/strict';\nimport {setup,newAccount,provider,tab,ready,defer,until,flush,rows,logouts,SESSION_COOKIE} from './auth-r7-fixtures.mjs';\ntest('probe B',async()=>{\n  const w=setup(),A=newAccount(),B=newAccount(),b=w.browser(),p=provider(A),home=defer();let held=false;\n  const q=tab(w,b,p,{intercept:async(path,r)=>{\n    if(path==='/api/auth/verify')return new Response('{',{status:r.status});\n    if(path.startsWith('/api/me/home')&&!held){held=true;await home.promise;}return r;}});\n  await ready(q);const flow=q.signIn();await until(()=>held);\n  p.switchTo(B);await flush(20);home.resolve();await flow;await new Promise(r=>setTimeout(r,150));await flush(20);\n  assert.equal(logouts(q).filter(e=>e.addressAssertion).length,1);assert.equal(rows(w).counts.live,0);assert.equal(b.jar.has(SESSION_COOKIE),false);\n});\n\nEvent order: START (ABSENT) ; SIGN_CLICK A -> challenge 200 -> personal_sign #1 -> POST /verify 200 commits + Set-Cookie, body '{' ; reconcile GET /session (post-fence) -> PRESENT(A), owner RELEASED ; GET /api/me/home held ; accountsChanged(B) ; home released.\n\nExpected (measured on parent 445747d): one POST /api/auth/logout {expectedAddress:A} -> 204; sessions created 1 / live 0 / revoked 1; session cookie cleared; channel ['signed-out']; prompts 1.\nActual on c4f451b: 0 logout requests; sessions created 1 / live 1 / revoked 0; challenges 1 used / 0 pending / 0 invalidated; session cookie still present and GET /api/auth/session answers signedIn:true for A; client account B, session null, sessionKnown false, statusOf 'connected'; channel []; prompts 1; session reads stay at 2 (no reconciliation read). Control on c4f451b (same switch after the click finished): logout {expectedAddress} 204, live 0, revoked 1, channel ['signed-in','signed-out'].\nProvider variant (replace p.switchTo(B) with a provider change to a wallet holding B, notified through onProviderChange): c4f451b sends 0 logouts, live 1, cookie present; parent sends {expectedAddress} 204, live 0.","severity":"low","snippet":"    if(click?.owner){this.revokeAbandoned(click.owner);return;}","title":"R7 regression (R5-01 account switch / sixth Audit #1 over-correction): wallet or provider switch while the reconciling click awaits the house read sends no expectedAddress cleanup, leaving A's session"},{"citation":"resolved","description":"NEW observation, Info, non-blocking. Reviewer measurement. accountChanged(null) used to return after `set({account:null})` (parent 445747d: 'A locked wallet (no account) leaves a valid session alone', a sentence still present at auth.ts:459-460). In R7 any RETAINED owner makes wasFlow true, and the lock event has no early return, so a lock with no click in progress abandons the retained owner and sends the nonce-bound logout with the live cookie. The retained-owner state is reachable with nothing wrong on the server: verify committed (row live, cookie installed), body unreadable, and the single reconcile read answered 429/503/transport, leaving IDLE_UNKNOWN with the owner RETAINED. Wallets auto-lock on a timer, so the user's good session is then revoked without any switch, stop or sign-out. AUTH_STATE_MACHINE.md lists 'retained owner | switch/stop' and says a lock cancels a click; it does not list lock as a trigger that revokes a retained owner, and the tests' lock cases (R7-C, click lease) only cover a click in progress. Direction is fail-closed (one extra signature later, no authority gained), so this is a documentation/design-decision gap: either state lock in the transition table and correct the comment at auth.ts:459-460, or keep a lock from abandoning an owner when no click is live.\n\nOwnership ledger: owner RETAINED -> abandoned -> CONSUMED by one nonce-asserted logout (204). UI: account null, session null, sessionKnown true (ABSENT). Channel: 0 messages. Timers: none armed. Prompts: 1 (the original).","line":467,"path":"source/src/world/auth.ts","reproduction":"tests/probe-d.test.mjs beside the supplied tests (source copied outside the repo, `npm ci --ignore-scripts`), run `node --test tests/probe-d.test.mjs` and `AUTH_R7_SOURCE=../baseline node --test tests/probe-d.test.mjs`:\n\nimport test from 'node:test';import assert from 'node:assert/strict';\nimport {setup,newAccount,provider,tab,ready,flush,rows,logouts,SESSION_COOKIE} from './auth-r7-fixtures.mjs';\ntest('probe D',async()=>{\n  const w=setup(),A=newAccount(),b=w.browser(),p=provider(A);let verified=false,bad=true;\n  const q=tab(w,b,p,{intercept:async(path,r)=>{\n    if(path==='/api/auth/verify'){verified=true;return new Response('{',{status:r.status});}\n    if(path==='/api/auth/session'&&verified&&bad){bad=false;return Response.json({error:'LIMITED'},{status:429});}return r;}});\n  await ready(q);await q.signIn();await flush();\n  p.switchTo(null);await new Promise(r=>setTimeout(r,150));await flush(20);\n  assert.equal(rows(w).counts.live,1);assert.equal(b.jar.has(SESSION_COOKIE),true);assert.equal(logouts(q).length,0);\n});\n\nEvent order: SIGN_CLICK A -> challenge -> personal_sign #1 -> verify 200 commits, body '{' -> reconcile GET /session 429 -> notice session-unknown, owner RETAINED, sessions live 1 ; accountsChanged([]).\nExpected per auth.ts:459-460 and measured on parent 445747d: 0 logouts; sessions created 1 / live 1 / revoked 0; cookie kept; client UNKNOWN.\nActual on c4f451b: 1 POST /api/auth/logout {expectedNonce} -> 204; sessions created 1 / live 0 / revoked 1; challenges 1 used / 0 pending / 0 invalidated; cookie cleared; client session null, sessionKnown true.","severity":"info","snippet":"    const wasFlow=!!click||this.lifecycle.retainedOwners.length>0;","title":"Behaviour change not in the R7 transition table: a wallet lock (accountsChanged []) now revokes a committed session whose only defect is one failed reconciliation read"}],"hash":"2137fbb3a1378331f3f3032735555d29b7617bcf3121df2e1fa4a717d415cf11","nodeId":"d4d496cb-8391-4e5d-a97f-eed818a6b5f7","outcome":"completed","summary":"The audit is written to `.imd-findings.json`: two new Low regressions and one Info, all measured against the parent commit. Each of the five sixth-round items passes its own evaluator, but #1 is over-corrected and I rate it partly fixed.\n\n## New findings (reviewer measurements, real AuthClient + real Worker/SQL on node:sqlite)\n\n1. **Low — accepted sign-in is not broadcast (`source/src/world/auth.ts:396`).** This breaks the R5-07 no-duplicate-prompt control. If a session read begun after the verify headers accepts PRESENT while a valid verify body is still in transit, the click returns without `broadcast('signed-in')`. A second tab holding stale ABSENT then prompts again: 2 prompts and 2 live sessions, where the parent gives 1 and 1.\n2. **Low — wallet or provider switch during the held house read sends no cleanup (`source/src/world/auth.ts:189`).** This breaks the R5-01 account-switch control and is the over-correction of Audit #1. `automaticCleanup` returns early on a released owner, so the `expectedAddress` logout is never sent. A's session and cookie stay live while the page shows B as merely connected, with no broadcast and no re-read. The parent revokes A (live 0, revoked 1).\n3. **Info — a wallet lock now revokes an uncertain but committed session (`source/src/world/auth.ts:467`).** After a malformed verify body and one 429 reconcile read, `accountsChanged([])` sends the nonce logout and revokes the session. The parent left it alone, the comment at `auth.ts:459-460` still says so, and the transition table does not list lock for a retained owner. It fails closed.\n\n## Verdicts on the sixth-round items\n\n| Item | Verdict | Basis |\n|---|---|---|\n| Audit #1 (accepted PRESENT, held home, stop, late home) | Partly fixed | The stop counterexample is closed: 0 logouts, row and cookie survive. Finding 2 shows the fix also suppresses the switch-time `expectedAddress` cleanup. |\n| Audit #2 / Report R6-I1 (dead token + own nonce), counted once | Fixed locally | Authority evaluator 23/32 on parent, 32/32 on R7; SQL now requires live token, nonce, `revoked_at IS NULL`, `expires_at>now`; any token forbids the pending-only fallback. |\n| Audit #3 (pre-commit ABSENT releases owner) | Fixed locally | The R7-B, read-ordering and cleanup-ownership cases pass; the fence is taken at header or transport observation. Finding 1 is a side effect of trusting post-fence reads. |\n| Audit #4 (held preflight click transfers to B) | Fixed locally | R7-C and click-lease cases pass; no B challenge, prompt or verify from an old click. |\n| Audit #5 (negative cache age stays fresh) | Fixed locally | Cache evaluator 3/8 on parent, 8/8 on R7; both `publicName` and `lookupName` expire on negative age. |\n| Audit #6 | Not a defect | Verdict matrix only. |\n\n- **Fifth-round four Lows and R5-01..09:** The supplied 11-file command gave 386/386, zero fail or skip, at 2026-10-04T09:40Z. I did not re-derive each row by hand. R5-01 and R5-07 each have the narrow new counterexample above; the others I have no evidence against.\n- **Before/after, same evaluators:** lifecycle 33/55 on parent and 55/55 on R7; server plus cache 26/40 and 40/40; model 79/79. These match the team's figures. The model test imports the product `AuthLifecycle`, so it is not an independent implementation.\n\n## Not done or unknown\n\n- I did not fetch the original Audit and Report or verify their SHA-256; verdicts rely on the R7 line references.\n- I did not make the optional anonymous GETs to imdember.com, run `tsc`, or attempt the Worker dry-run.\n- The team's private counts (1357/1357, 1392/1392) and the deployment record are team claims I could not check.\n- Real browser, provider and OS behaviour, D1 races and cron, WAF and limiter, and the withheld frontend remain unknown.\n- The documented limits (no fetch deadline, best-effort cleanup, lost A token, late cookie clear) still hold and were not re-counted as defects.\n\nProbes ran in a copy under `/tmp/r7`; the repository is unchanged apart from `.imd-findin","treeHash":null,"usage":{"cachedInputTokens":2052348,"inputTokens":34,"model":"claude-fable-5-1","outputTokens":48174,"runtime":"claude","turns":19,"wallClockMs":633952}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"72b617d4b615473a","findings":[{"citation":"resolved","description":"Prior IDs: Audit 09062f1d #1 (LOW-2/R4-02/AUD4-06) remediation side effect; regresses R5-01 (account switch ends the displayed session). Non-blocking, Low. Reviewer measurement, not a team claim.\nAuthClient.automaticCleanup() returns as soon as the cancelled click carries a verify owner: `if(click?.owner){this.revokeAbandoned(click.owner);return;}`. After R7 the owner is terminally RELEASED the moment a trusted PRESENT is read (authLifecycle.ts:77-80), so revokeAbandoned() is a no-op (abandon() refuses a non-RETAINED owner, authLifecycle.ts:85) and the early return skips the `held ? {expectedAddress}` branch on the next line. In the reconcile path (auth.ts:412-422) the click is still the lifecycle intent while `await this.restore()` waits for /api/me/home (readSession awaits refreshHome at auth.ts:275), so accountChanged(B) (auth.ts:461-474) hands automaticCleanup a click whose owner is RELEASED together with ended=session A. Nothing is sent.\nThis 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 comment (\"A's session ended\"). It is asymmetric with the valid-verify-body path, which calls lifecycle.finish(click) before the home read (auth.ts:400-402) and therefore does send POST /api/auth/logout {expectedAddress:A} in the same window, and with the parent commit 445747d, which revoked A in both variants.\nImpact: after the wallet has moved to account B, A's server session row stays live and A's __Host-imd_session cookie stays in the browser (up to the 7-day absolute expiry) with no cleanup owner at all (retainedCount 0, zero logout requests). The page shows account B with session unknown; any later session read in this browser restores A's session (mismatch view), and other tabs of the browser keep acting as A (M1 profile PUT, /api/me/home) although the user switched away. No new signature, no cross-account authority, hence Low. Unknown: real provider/browser timing and the withheld frontend.\nFix sketch (keeps the R7 design): in automaticCleanup only return early when the owner is still RETAINED (or revokeAbandoned actually claimed a cleanup); for a RELEASED/CONSUMED owner fall through to the expectedAddress assertion for `held`. Add the malformed-body + switch-during-held-home case to auth-r7-lifecycle.test.mjs next to R7-A, which today only covers stop/restart, not accountsChanged.","line":189,"path":"source/src/world/auth.ts","reproduction":"State: tab with wallet account A, no session. Real AuthClient + real Worker/SQLite via tests/auth-r7-fixtures.mjs (synthetic EOA/provider/cookies/clock). Inputs: /api/auth/verify 200 whose body is replaced by the malformed text \"{\" (the Audit #1 counterexample body), first /api/me/home response held.\nOrder: SIGN_CLICK -> GET session 200 ABSENT -> POST challenge 200 -> personal_sign (1 prompt) -> POST verify 200 (row committed, Set-Cookie applied, body unreadable) -> reconcile GET session 200 PRESENT(A), owner RELEASED, click state VERIFY_RECONCILING still current -> GET /api/me/home?fresh=1 HELD -> provider emits accountsChanged([B]) -> home released.\nExpected (doc, R5-01, valid-body control, and parent 445747d): exactly one POST /api/auth/logout with expectedAddress=A answered 204; sessions created 1 / live 0 / revoked 1; session cookie cleared; GET /api/auth/session -> signedIn:false.\nActual at c4f451b (measured 2026-10-04, scratch copy of the pinned source, `node --test tests/zz-probe.test.mjs`, test \"P1 malformed\"): logouts=[] (0 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 (A); client account=B, session=null, sessionKnown=false, phase idle, lifecycle IDLE_UNKNOWN, owner RELEASED, retainedCount 0; no channel broadcast, no timer/cleanup owner.\nControls: same probe with the valid verify body -> one logout {addressAssertion:true} 204, live 0 / revoked 1, cookie cleared. Same probe with AUTH_R7_SOURCE=../baseline (parent 445747d): malformed variant -> logout {expectedAddress} 204 (+ one refused nonce logout 409), live 0 / revoked 1.\nProbe source (place as source/tests/zz-probe.test.mjs beside auth-r7-fixtures.mjs, needs viem/@noble from package-lock; test P1):\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport {setup,newAccount,provider,tab,ready,defer,until,flush,rows,prompts,routeEvents,logouts,SESSION_COOKIE} from './auth-r7-fixtures.mjs';\nfor(const body of ['valid','malformed'])\ntest(`P1 ${body} verify / PRESENT / held home / wallet switch A->B`,async T=>{\n  const w=setup(),A=newAccount(),B=newAccount(),b=w.browser(),p=provider(A),home=defer();let held=false,flow;\n  const q=tab(w,b,p,{intercept:async(path,r)=>{\n    if(path==='/api/auth/verify'&&body==='malformed')return new Response('{',{status:r.status});\n    if(path.startsWith('/api/me/home')&&!held){held=true;await home.promise;}return r;}});\n  await ready(q);flow=q.signIn();await until(()=>held,'held home');\n  assert.equal(q.c.state.session.address,A.address.toLowerCase());\n  const snap=q.c.lifecycleSnapshot??{click:null,cleanup:{}};\n  p.switchTo(B);await flush(20);await until(()=>logouts(q).every(e=>e.finished),'x');await flush(20);\n  home.resolve();await flow;await flush(20);\n  const sess=await (await b.get('/api/auth/session')).json();\n  T.diagnostic(JSON.stringify({body,clickBeforeSwitch:snap.click&&snap.click.state,owner:snap.cleanup.status,\n    logouts:logouts(q).map(e=>({nonce:e.nonceAssertion,addr:e.addressAssertion,status:e.status})),counts:rows(w).counts,\n    cookie:b.jar.has(SESSION_COOKIE),serverSession:sess.signedIn,prompts:prompts([p]),\n    client:{account:q.c.state.account===B.address.toLowerCase()?'B':q.c.state.account,session:!!q.c.state.session,known:q.c.state.sessionKnown,phase:q.c.state.phase},\n    events:q.events.filter(e=>e.kind==='route').map(e=>e.method+' '+e.path+' '+e.status)}));\n  q.stop();\n});\ntest('P2 other-tab PRESENT while challenge awaited / held home / wallet switch A->B',async T=>{\n  const w=setup(),A=newAccount(),B=newAccount(),b=w.browser(),p=provider(A),home=defer(),ch=defer();let held=false,chHeld=false,flow;\n  const q=tab(w,b,p,{intercept:async(path,r)=>{\n    if(path==='/api/auth/challenge'&&!chHeld){chHeld=true;await ch.promise;}\n    if(path.startsWith('/api/me/home')){held=true;await home.promise;}return r;}});\n  await ready(q);flow=q.signIn();await until(()=>chHeld,'challenge","severity":"low","snippet":"    if(click?.owner){this.revokeAbandoned(click.owner);return;}","title":"R7 regression (R5-01 account switch): a wallet switch A->B during the held home read of the verify-reconcile path sends no cleanup, so the accepted session of A stays live with its cookie"},{"citation":"resolved","description":"Prior IDs: R5-01 (account switch) / R5-02 (provider/session/challenge) regression control; related to Audit 09062f1d #2 hardening (\"ANY token forbids pending-only fallback\"). Non-blocking, Low. Reviewer measurement.\nautomaticCleanup() chooses exactly one assertion: `click?.nonce ? {expectedNonce} : held ? {expectedAddress} : null`. R7 added a post-challenge exit at auth.ts:377 (`if(this.s.session?.address===account){this.set({phase:'idle'});await this.refreshHome(true);return;}`): when another tab of the same browser signs in as A while this tab awaits its challenge, the click keeps click.nonce (set at auth.ts:374) and stays the lifecycle intent during the forced home read while the page displays A's session. A wallet switch A->B in that window calls automaticCleanup(click with nonce, ended=session A): the nonce wins, the request carries the live session token of the other tab's sign-in (a different nonce), and server/auth.ts:632-640 correctly refuses it with 409 ACCOUNT_CONTEXT_CHANGED because a supplied token forbids the pending-only fallback. No second request with expectedAddress is sent, so the displayed session A is never ended and the click's own pending challenge is not invalidated either (it stays pending until its 5-minute accept_until; unusable without A's signature and flow cookie).\nImpact: the server-side rule is right; the client picks an assertion that cannot succeed and drops the one that would. After the switch the page re-reads and shows A's live session against wallet B (mismatch view) instead of having ended it, contrary to the accountChanged contract (\"A's session ended; B starts as merely connected\"). No extra prompt, no cross-account authority; an explicit sign-out or a new click (which logs A out by expectedAddress first) recovers. Low.\nFix sketch: when `held` is present, assert expectedAddress for the displayed session (and, separately or afterwards without the session cookie mattering, cancel the pending nonce), or prefer expectedAddress whenever this.s.session was displayed at cancel time; add the case to the R7 lifecycle tests.","line":190,"path":"source/src/world/auth.ts","reproduction":"State: tab q (wallet A, no session) and a second tab of the same browser. Real AuthClient + real Worker/SQLite via tests/auth-r7-fixtures.mjs. Inputs: q's POST /api/auth/challenge response held; other tab completes challenge+verify for A (its session cookie is installed in q's jar); channel message delivered to q; /api/me/home responses held.\nOrder: SIGN_CLICK(q) -> GET session ABSENT -> POST challenge (held) -> other tab signs in as A (session #1 live) -> CHANNEL_MESSAGE -> q GET session 200 PRESENT(A) -> challenge body released: click state CHALLENGE_READY, nonce captured, phase idle, forced GET /api/me/home held -> accountsChanged([B]).\nExpected: the displayed session A is ended by POST /api/auth/logout {expectedAddress:A} (204; live 0, revoked 1) and q's pending challenge is cancelled.\nActual at c4f451b (measured 2026-10-04, test \"P2\" of the probe below): one logout with expectedNonce only -> 409, 0 Set-Cookie; sessions created 1 / live 1 / revoked 0; challenges 2: used 1, pending 1, invalidated 0; prompts 0; follow-up GET session 200 -> page shows session A (sessionKnown true) with wallet B; GET /api/auth/session with the jar -> signedIn:true.\nParent 445747d has no such window (it went on to prompt and verify; its switch ended the session by expectedAddress: live 1 of 2 created, the new one revoked).\nProbe: test \"P2\" in the probe source quoted in the previous finding (same file, same command `node --test tests/zz-probe.test.mjs` from source/).","severity":"low","snippet":"    const context=click?.nonce?{expectedNonce:click.nonce}:held?{expectedAddress:held.address}:null;","title":"Wallet switch while the click holds a pending nonce but the page already displays a session: cleanup asserts expectedNonce only, server answers 409, neither the displayed session nor the pending chall"}],"hash":"3c5abe43df78d055a28e804947b55800192cf8b17b4d1aeed976e3bd02cb9c87","nodeId":"05d1cddd-4949-45e1-b861-5e07efbd99f2","outcome":"completed","summary":"The review is finished: `.imd-findings.json` in the repository root holds two Low findings, both in `source/src/world/auth.ts`, and no repository file was changed. All five sixth-round items retest as fixed locally, but the fix for Audit #1 introduced a narrow regression of the account-switch control (R5-01).\n\n## Findings\n\n1. **Low, non-blocking — `source/src/world/auth.ts:189`** (prior IDs: Audit #1 remediation side effect, R5-01).\n   - **Trigger:** verify returns 200 with a malformed body, the reconcile read returns PRESENT for A, the home read is held, and the wallet switches A→B.\n   - **Actual:** no logout is sent. One session created, 1 live, 0 revoked; A's cookie is still present; the client shows account B with session unknown.\n   - **Expected:** one `logout {expectedAddress:A}` answered 204, as the state-machine document says and as the valid-body control and parent `445747d` both do (live 0, revoked 1).\n   - **Cause:** `automaticCleanup` returns early whenever the click carries a verify owner, even one already RELEASED, so the `expectedAddress` branch is skipped.\n\n2. **Low, non-blocking — `source/src/world/auth.ts:190`** (R5-01/R5-02, related to the Audit #2 hardening).\n   - **Trigger:** another tab signs in as A while this tab awaits its challenge; the click keeps its nonce and waits on the home read; the wallet switches A→B.\n   - **Actual:** one logout with `expectedNonce` only, answered 409. One session created, 1 live, 0 revoked; two challenges, 1 used and 1 still pending; 0 prompts. The page then shows session A against wallet B.\n   - **Expected:** the displayed session A is ended and the pending challenge cancelled.\n   - **Cause:** cleanup picks the nonce assertion over the address assertion, and the server correctly refuses it because a live token is supplied.\n\nBoth carry the event order, counts and the probe source in the findings file. Neither grants cross-account authority or an extra prompt.\n\n## Verdicts on the sixth-round items\n\n| Item | Verdict | Basis |\n|---|---|---|\n| Audit #1 Low | Fixed locally for the stop / late-home counterexample; partly overall | Finding 1 is the regression its fix introduced |\n| Audit #2 Low / Report R6-I1 (one defect) | Fixed locally | My own 16-case probe: every unauthorized case returned 409 (400 for both assertions) with 0 Set-Cookie and no row change; live controls returned 204 |\n| Audit #3 Low | Fixed locally | Code read of the response fence and one-owner drain, plus the public R7-B tests |\n| Audit #4 Low | Fixed locally | Code read of the click lease, plus the public R7-C tests |\n| Audit #5 Info | Fixed locally | Both `publicName` and `lookupName` require a non-negative age within the TTL |\n\nAudit #6 is a verdict matrix and is not counted as a defect.\n\n- **Fifth-round four Lows and R5-01..09:** the public regression tests pass. R5-01 is partly regressed by finding 1, and finding 2 touches R5-01/R5-02.\n- **Server controls, by code read:** logout-all compares the live cookie's address before revoking; house authority is session address plus `ownerOf`; member writes require an EOA session and the matching public member id.\n\n## What was measured\n\n- **Reviewer measurement:** the recorded 11-file test command gave 386/386 passing, 0 failed, 0 skipped, on a scratch copy of the pinned source at 2026-10-04T09:39:52Z.\n- **Probes:** my own probes ran against the candidate; the lifecycle probe (findings 1 and 2) also ran against parent `445747d`.\n- **Originals:** both prior-review originals were fetched and their SHA256 match `R7/PRIOR_REVIEWS.md`.\n- **Shims and failures:** dependencies were installed from the lockfile with dev dependencies omitted. Running `tests/*.test.mjs` unfiltered fails three files (`home-entry`, `ownership`, `wallet-client`); the error sampled was a missing withheld module, `src/world/households.ts`. I did not pursue it further.\n- **Team claims, not rerun:** the private 1357/1392 suites, the 33→55 and 26→40 before/after counts, and the deployment evidence.\n- ","treeHash":null,"usage":{"cachedInputTokens":2335309,"inputTokens":34,"model":"claude-fable-5-1","outputTokens":27753,"runtime":"claude","turns":23,"wallClockMs":350405}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"30a6c1a419ef4f9c","findings":[{"citation":"resolved","description":"Prior IDs: R5-01 account switch / R5-02 provider switch (Report lines 101-102), fifth-Audit LOW-1/LOW-2 class (R4-02/AUD4-06), sibling of sixth Audit #1. Non-blocking for funds or house authority; it reopens the stated contract that a wallet switch A -> B ends A's displayed session (auth.ts accountChanged comment, lines 457-460, and AUTH_STATE_MACHINE.md transition 'PREFLIGHT | account/provider/stop | cancel original click'). Classification: NEW in R7. The identical probe passes on parent 445747d, where the switch sent POST /api/auth/logout {expectedAddress:A} and got 204.\n\nMechanism: automaticCleanup() is called by accountChanged (line 472) and providerChanged (line 255) with the cancelled click. Line 189 returns as soon as the click has an owner, whatever that owner's status. When a causally later session read (a channel message, visible(), or any restore after the verify fence) has already accepted PRESENT(A) while the click is still live because the verify body has not settled, lifecycle.read() has terminally RELEASED that owner (authLifecycle.ts line 77). revokeAbandoned() then does nothing (abandon() refuses a non-RETAINED owner), the early return skips the expectedAddress fallback that the no-click path uses, and the retainedOwners loop at line 473/256 finds nothing. No request is sent. The browser keeps A's live session cookie, the server keeps A's live row, and the UI shows account B as 'connected' (account switch) or A's session as 'mismatch' (provider switch) with sessionKnown=false.\n\nPreconditions: verify for A committed and its headers observed (cookie installed), body still pending (slow body, or any ordering where a read lands first); one post-fence session read accepted PRESENT(A); then accountsChanged(B) or a provider change before the body settles. No attacker needed; same user, same browser.\n\nImpact: A's EOA session (M1 bootstrap/PUT authority for A) survives the switch until expiry (7 days), an explicit sign-out, or the next sign-in click (whose preflight read shows A again and only then logs it out). This is the 'cancelled flow leaves a usable session' class that LOW-2 was meant to close, now reachable through the new terminal-release rule.\n\nFix (minimal, keeps terminal release): only take the early return for an actionable owner, e.g. 'if(click?.owner?.status===\"RETAINED\"){this.revokeAbandoned(click.owner);return;}', so a released owner falls through to the held-session context ({expectedAddress:held.address}; the switch already has 'ended' = the displayed A session) exactly as the no-click path does. Keep the nonce path for RETAINED owners and add the ordering above to auth-r7-lifecycle.test.mjs (R7-C/R7-E currently never read PRESENT between fence and body before switching). Verified: with exactly that one-line change in a scratch copy, both N2 variants pass, both controls still pass, and the published 11-file public suite stays 386/386.","line":189,"path":"source/src/world/auth.ts","reproduction":"Scratch copy of the pinned source with npm ci (lockfile deps, Node 22.23 native TS stripping, node:sqlite), real Worker/routes/migrations, fixtures from tests/auth-r7-fixtures.mjs (tab, provider, stallBody). Test N2 'account' and 'provider' in a reviewer file (run: node --test tests/reviewer-r7.test.mjs). Steps: tab(w,b,providerA) with intercept returning stallBody(r).response for /api/auth/verify; await ready(q); flow=q.signIn(); await until(()=>body) [verify headers observed, cookie in jar, body open]; q.channels.at(-1).message(); await until(()=>state.session.address===A && !checking) [post-fence GET /api/auth/session 200 PRESENT(A); lifecycleSnapshot.cleanup.status==='RELEASED', retainedCount 0, state PRESENT_ACCEPTED, phase still 'verifying']; then providerA.switchTo(B) (or select providerB and fire onProviderChange); flush; body.finish(); await flow.\nEvent order measured (synthetic clock 1790596800000): GET session 200 -> POST challenge 200 -> personal_sign (1 prompt) -> POST verify 200 headers, body stalled -> CHANNEL_MESSAGE -> GET session 200 signedIn:true A -> GET /api/me/home?fresh=1 200 -> accountsChanged([B]) -> (no request) -> body settles -> flow ends.\nExpected: one conditional cleanup for A (expectedAddress or nonce) answered 204; sessions created/live/revoked 1/0/1; session cookie cleared; GET /api/auth/session signedIn:false. Actual on c4f451b: zero POST /api/auth/logout; rows stay 1/1/0; cookie present; GET /api/auth/session signedIn:true address A; UI account=B, session=null, sessionKnown=false (account case) or session=A shown as mismatch (provider case); challenges used 1 / pending 0 / invalidated 0; prompts 1; no broadcast, no timer rearm. Controls in the same file: (a) same ordering without the channel read (owner RETAINED) -> switch sends {expectedNonce} 204, rows 1/0/1, cookie cleared; (b) normal completed sign-in then switch -> {expectedAddress} 204, rows 1/0/1. Parent 445747d: the failing case passes (switch sends {expectedAddress}, 204, 1/0/1).","severity":"low","snippet":"    if(click?.owner){this.revokeAbandoned(click.owner);return;}","title":"R7 regression: account/provider switch after a post-fence PRESENT read sends no cleanup, so the switched-away session stays live"},{"citation":"resolved","description":"Prior IDs: R5-03 teardown cleanup / fifth LOW-2 (R4-02/AUD4-06), AUD3-05 (stale display window). Retained, not a regression: the same probe fails on parent 445747d. Non-blocking for funds or house authority; the cookie is gone and the row revoked, so nothing the stale UI offers can act on the server. It is a correctness gap in the 'STOPPED | restart | canonical restore' row of AUTH_STATE_MACHINE.md.\n\nMechanism: the teardown returned by start() (line 231-233) abandons a retained owner and dispatches its conditional cleanup, then start() in a new lifetime immediately restores. The new GET /api/auth/session can reach the server before the cleanup POST, so the new lifetime accepts PRESENT(A) (the uncertain verify's own, still-live session) and shows signed in. When the cleanup completes (204: row revoked, Set-Cookie clear applied), its callback at line 482 sees life!==this.life and returns without doing anything. Unlike the same-lifetime branches (reconcileCleanup/loggedOut at lines 483-485), no canonical re-read is scheduled for the running client. The UI keeps session A, sessionKnown true, status signedInNoHouse/owner, the expiry timer armed for A, and the MemberClient keeps A's profile loaded, until a later focus/visibility event, owner re-check (owner mode only) or click happens to re-read and get 401/ABSENT.\n\nPreconditions: a retained owner at stop (committed verify whose body or recovery read was unusable), then stop and restart close together (route remount, React effect re-run) with the cleanup still in flight. No attacker.\n\nImpact: stale signed-in display for an indefinite time in signedInNoHouse (no periodic re-check there); member name form usable until its PUT returns 401. Low: presentation only, self-heals on the next server contact.\n\nFix: in the revokeAbandoned callback, when life!==this.life but the client is currently started (not stopped), schedule the canonical reconciliation in the current lifetime (e.g. if(!this.busy&&this.s.phase==='idle')void this.restore(); or reconcileCleanup()) instead of returning; a restore is not a UI write by the old lifetime, it reads the shared cookie. Alternatively make start() wait for in-flight owner cleanups before its first read.","line":482,"path":"source/src/world/auth.ts","reproduction":"Test N1 in the reviewer file (node --test tests/reviewer-r7.test.mjs), same harness. Steps: tab with beforeSend holding POST /api/auth/logout before it reaches the Worker, intercept returning '{' for /api/auth/verify and 429 for /api/auth/session while 'corrupt' is set; await ready(q); await q.signIn() [verify 200 committed, body malformed, recovery read 429: knowledge UNKNOWN, lifecycleSnapshot.cleanup RETAINED, rows 1/1/0, prompts 1]; set corrupt=false; q.stop() [one POST /api/auth/logout {expectedNonce} dispatched and held]; q.restart(); await until(sessionKnown && session && !checking) [new life GET /api/auth/session 200 PRESENT(A), GET /api/me/home 200, status signedInNoHouse]; release the held logout; await its completion.\nExpected: after the cleanup completes the running client re-reads and shows signed out (session null, ended 'revoked' or ABSENT). Actual on c4f451b (and on parent 445747d): logout 204, rows 1/0/1, session cookie absent, GET /api/auth/session signedIn:false, but state.session still A, sessionKnown true, statusOf 'signedInNoHouse', lifecycleSnapshot.state IDLE_PRESENT with cleanup CONSUMED; zero further session reads and zero state notifications after the cleanup. Counts: created/live/revoked 1/0/1; challenges used 1, pending 0, invalidated 0; prompts 1; cookies: session absent, flow absent.","severity":"low","snippet":"      if(owner.status==='RETAINED'||life!==this.life)return;","title":"Restart during an in-flight owner cleanup: the new lifetime keeps displaying a session the cleanup then revokes, with no reconciliation"},{"citation":"resolved","description":"Not a defect: the review verdict the task requires, anchored to the remediation table. Reviewed commit c4f451b015abdaced6c35a717b29f5bb1cb351c0 (parent 445747d). Originals fetched from the official Identity-md/research paths and verified: AUDIT.md SHA-256 5a905a34...f394c and report.md b5af1077...9b122 both match R7/PRIOR_REVIEWS.md. SHA256SUMS: 190/190 match. Subject is TypeScript Worker/React; no Solidity exists, so the Solidity checklists were applied only as generic failure-mode prompts (entry-point guards, nonce consumption, ordering, untested edges).\n\nSIXTH ORIGINALS (reviewer measurements, own probe file, before/after on parent 445747d with the same harness):\n- Audit #1 Low (accepted PRESENT, held home, stop, late home): FIXED LOCALLY. c4f451b: 0 logouts, rows 1/1/0, cookie kept, GET session signedIn:true, lifecycle RELEASED before the home await; parent: late logout 204, rows 1/0/1. Valid-body control passes on both.\n- Audit #2 Low = Report R6-I1 (dead token + own nonce): FIXED LOCALLY. expired and revoked token + matching nonce -> 409 ACCOUNT_CONTEXT_CHANGED, no Set-Cookie, zero row change; held response applied after B signed in and got a pending challenge leaves B's cookie, row and pending challenge intact. Parent: 204 + session clear (expired case also writes revoked_at). Live token + own nonce + different current flow: 204 clears only the session cookie, B's pending challenge verifies afterwards. Project auth-r7-authority.test.mjs (empty/forged/malformed/expired/revoked/live-other-nonce tokens, pruned/retained challenge, pending-only modes, both assertions 400, explicit {} and logout-all) re-run and pass.\n- Audit #3 Low (pre-commit ABSENT after headers, stalled body, stop): FIXED LOCALLY. c4f451b: pre-commit read ignored (knowledge stays UNKNOWN, owner RETAINED, fence = read seq at headers), stop sends exactly one {expectedNonce} 204, rows 1/0/1, cookie cleared, late body adds no request (owner CONSUMED). Parent: stale read set sessionKnown true/session null, stop sent nothing, rows 1/1/0 until the body settled; the no-overlap control on parent also sent a duplicate second logout. Project R7-B/early-refusal/drain tests pass.\n- Audit #4 Low (held preflight, click A, accountsChanged(B)): FIXED LOCALLY. c4f451b: 0 challenges, 0 prompts, 0 sessions after the switch; a fresh explicit click signs B once. Parent: challenge for B, 1 prompt, B session created by the stale click. Project R7-C (account/provider/lock/stop/restart, same-account control) and click-lease tests pass.\n- Audit #5 Info (negative name-cache age): FIXED LOCALLY. publicName and lookupName refresh after a backward jump (2 GETs, 'NewName'); parent kept 'PriorName' with 1 GET. Project member-r7-cache tests pass.\n- Audit #6: matrix, not a defect.\n\nNEW DEFECTS THIS ROUND: one R7 regression (auth.ts:189, Low: switch after post-fence PRESENT read leaves A live; parent passed) and one retained gap (auth.ts:482, Low: stop/restart race leaves a revoked session displayed; parent also fails). See the two findings.\n\nFIFTH FOUR-LOW: LOW-1 FIXED LOCALLY (R6-I1/Audit #2 closed; limits: lost A token after B replaces it cannot revoke A; expectedAddress cannot tell same-wallet renewal). LOW-2 PARTLY: Audit #1/#3 orderings closed; the new auth.ts:189 ordering reopens 'cancelled flow leaves a usable session'; process/offline cleanup remains best effort. LOW-3 FIXED LOCALLY (strict schema; badReads matrix: malformed/empty/array/invalid-positive/unsafe-expiry/429/503/transport stay UNKNOWN with 0 prompts; no auth-fetch deadline remains). LOW-4 FIXED LOCALLY (member-r5 cooldown tests pass; member.ts unchanged except the cache; real OS suspension unknown).\n\nR5-01..09: 01 account switch: PASS for the tested newer-B/A2/pending cases, FAIL for the auth.ts:189 ordering. 02 provider switch: same. 03 teardown: PASS (Audit #1/#3 closed), stop/restart display gap at auth.ts:482. 04 malformed UNKNOWN: PASS. 05 invalid positive schema: PASS. 06 GET before sign: PASS (UNKNOWN never promp","line":3,"path":"R7/AUTH_REMEDIATION.md","reproduction":"Commands in a /tmp scratch copy of source/ (repo tree unchanged): npm ci --ignore-scripts; node --test --test-reporter=spec --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 (386/386); node --test tests/worker.test.mjs tests/member.test.mjs tests/aud4-member-server.test.mjs tests/aud4-discovery.test.mjs tests/presence.test.mjs tests/headers.test.mjs tests/review-record.test.mjs tests/dependencies.test.mjs (109/109); node --test tests/reviewer-r7.test.mjs on c4f451b (11/14) and on git archive 445747d with tests/auth-r7-fixtures.mjs copied in (6/14); sha256sum -c SHA256SUMS (190 OK); curl of the two permitted anonymous URLs 6 s apart.","severity":"info","snippet":"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.","title":"Seventh-review verdict matrix: sixth Audit #1-#5 / Report R6-I1 closure, fifth four-Low and R5-01..09 status, regressions, limits"}],"hash":"9fa9133898f76e149df286c4aa373177e86e04fbbadc59a8270c859c940b527d","nodeId":"95f000bd-6479-426d-8adc-2afbbb9b2a5f","outcome":"completed","summary":"Review complete. The findings file holds three entries, and nothing in the repository tree was modified.\n\n**Verdict on the sixth-round originals.** All five are fixed locally at commit c4f451b. I reproduced each original counterexample with my own probe file against the real Worker over node:sqlite and ran the same file against the parent 445747d, which fails all five, so the before/after holds. Report R6-I1 was counted once with Audit #2. Both official originals were fetched and their SHA-256 values match R7/PRIOR_REVIEWS.md.\n\n**New defects found.**\n- **Low, new R7 regression** at `source/src/world/auth.ts:189`. After a committed verify whose body is still pending, a causally later session read accepts PRESENT and terminally releases the owner. An account or provider switch then calls automaticCleanup, which returns early because the click has an owner, even though that owner is released. No logout is sent, so the switched-away session stays live in the cookie jar and on the server. The parent sent an expectedAddress logout here. A one-line fix (take the early return only for a RETAINED owner) closes both variants and keeps the 386 public tests green.\n- **Low, retained** at `source/src/world/auth.ts:482`. A stop with a retained owner dispatches its cleanup; a restart's first read can land first and show the session, and the cleanup callback then returns on the lifetime check without scheduling a re-read. The new lifetime keeps showing a session whose cookie is gone and whose row is revoked. The parent has the same gap.\n- **Info** anchored at `R7/AUTH_REMEDIATION.md:3`: the full verdict matrix, fifth four-Low status, R5-01..09 rows, regressions checked, execution record, production readback, team claims not verified, and retained limits.\n\n**Execution record.** Node 22.23 with locked dependencies. Published 11-file command: 386/386. Remaining loadable server tests: 109/109. Three files cannot load on the withheld households module, so the R7 edit to the wallet-client ADV-1 test is unverified. SHA256SUMS: 190/190. Two anonymous production GETs succeeded; the session body hash matches the team's record. No tsc, Vite, or Wrangler rebuild was run.\n\n**Limits kept open.** R4-03, R4-09, AUD3-05, AUD3-09, no bounded auth-fetch deadline, best-effort cleanup after termination, late authorized cookie clears, and everything about real browsers, production D1, WAF and the withheld frontend remain unknown. Test counts and Low labels are not certification.","treeHash":null,"usage":{"cachedInputTokens":5135187,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":74938,"runtime":"claude","turns":66,"wallClockMs":1015570}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"0238a59bba722237","findings":[{"citation":"resolved","description":"Boundary x invariant seam (new, no prior ID; adjacent to R4-09 availability, not a duplicate). proof() keeps a stored proof only while keep(p)=young(p.indexedAt) holds, and for fresh reads young(at)=at+OWNERSHIP_TTL_MS>req.now (ownership.ts:225). When the chain:index budget refuses the index read, the rebuilt proof is dated indexedAt=indexed?.at??0 (ownership.ts:256): 0 when nothing is kept, or the old index time when a kept answer exists. That value is by construction never 'young', so the freshly built proof (checkedAt=now) is rejected by the very next fresh read and rebuilt again, although it is 0-30 s old. The invariant the code and docs state (OWNERSHIP_TTL_MS: ownerOf proven at most once per 30 s per address and isolate; DESIGN_W1 INT-1 'an hour of re-checks = 60 eth_call'; auth.ts cost comment 'reads by the proof cache (30 s per address and isolate) and the home limit') therefore fails exactly at the refused edge: each /api/me/home?fresh=1 (a) calls req.budget() again, i.e. one more chain:index limiter ask per request instead of at most one per 30 s, and (b) when the address has any candidate (roster or kept index answer) sends one unbudgeted keyed Alchemy eth_call (Multicall3, up to 256 ownerOf). The only remaining bound is the per-session 'home' key (AUTH_LIMITER 20/min, wrangler.jsonc:49), and sessions per address are not capped, so the rate is 20 x sessions eth_call/min per location versus the intended 2/min per address and isolate. Preconditions: a live session (any EOA key can sign in), chain:index refusing at that location (20/min constant key; reachable under load or by throwaway sessions), fresh=1 (the client's Check again, or a direct GET). Impact: keyed-RPC cost/availability amplification and, if Cloudflare counts denied calls (documented as unknown), one session's 20 asks/min can hold chain:index closed. No authority impact: ownerOf still proves every seat; sessions/challenges/cookies/prompts unchanged (N/A: read-only route, no session or challenge rows written, no UI/channel/timer ownership involved). Existing tests always advance the clock 31 s between refused fresh reads (tests/ownership.test.mjs:145-149), so the within-TTL case is untested. Fix that keeps the design: date a limited/refused 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.","line":261,"path":"source/server/ownership.ts","reproduction":"Reviewer measurement, offline, pinned source c4f451b, Node v24.21.0, `npm ci --ignore-scripts` in a copy of source/. Save as source/x1.mjs and run `node x1.mjs`:\nimport {Ownership,ALCHEMY_RPC_URL,MULTICALL3} from './server/ownership.ts';\nimport {encodeFunctionResult,multicall3Abi,pad} from 'viem';\nconst A='0x'+'ab'.repeat(20);\nconst owners=Array(10).fill(null);owners[7]=A;\nconst gateway={source:async k=>k==='swarm'?{state:'fresh',data:{seats:{},owners},url:'u',fetchedAt:1}:{state:'fresh',data:{count:0,workers:[]},url:'u',fetchedAt:1}};\nlet ethCalls=0,indexReads=0;\nconst chain={key:'k',fetch:async(url,init)=>{\n  if(String(url).startsWith(ALCHEMY_RPC_URL)){ethCalls++;\n    const result=encodeFunctionResult({abi:multicall3Abi,functionName:'aggregate3',result:[{success:true,returnData:pad('0x64')},{success:true,returnData:pad(A)}]});\n    return new Response(JSON.stringify({jsonrpc:'2.0',id:1,result}),{status:200});}\n  indexReads++;return new Response(JSON.stringify({ownedNfts:[]}),{status:200});}};\nconst o=new Ownership(gateway,[]);\nlet now=1_000_000,budgetAsks=0;\nconst req=()=>({chain,now,budget:async()=>{budgetAsks++;return false;}});   // chain:index closed\n// control: 20 non-fresh reads inside one 30 s proof window\nfor(let i=0;i<20;i++){now+=1000;await o.home(A,req(),false);}\nconsole.log('control fresh=0: 20 reads/20 s ->',{ethCalls,indexReads,budgetAsks});\nconst o2=new Ownership(gateway,[]);ethCalls=0;budgetAsks=0;now=1_000_000;\nfor(let i=0;i<20;i++){now+=1000;const h=await o2.home(A,req(),true);if(i===19)console.log('view',h.recheck,h.seats.length);}\nconsole.log('fresh=1, budget refused: 20 reads/20 s ->',{ethCalls,indexReads,budgetAsks});\n// control: fresh=1 with budget admitted\nconst o3=new Ownership(gateway,[]);ethCalls=0;budgetAsks=0;indexReads=0;now=1_000_000;\nfor(let i=0;i<20;i++){now+=1000;await o3.home(A,{chain,now,budget:async()=>{budgetAsks++;return true;}},true);}\nconsole.log('fresh=1, budget admitted: 20 reads/20 s ->',{ethCalls,indexReads,budgetAsks});\n\nState: address A is owners[7] on the roster (one candidate), budget()=false (chain:index refused), 20 GET-equivalents 1 s apart inside one 30 s window. Expected (control, fresh=false, printed first): ethCalls 1, budgetAsks 1. Expected for fresh=true as well: at most 1 eth_call and 1 budget ask per 30 s. Actual: `fresh=1, budget refused: 20 reads/20 s -> { ethCalls: 20, indexReads: 0, budgetAsks: 20 }` (view recheck 'limited', 1 seat). Second control (fresh=true, budget admitted): ethCalls 1, indexReads 1, budgetAsks 1, so only the refused edge degenerates.\nRoute-level confirmation through the real Worker + SQL (tests/wallet-harness.mjs, synthetic EOA/SIWE/cookies/clock; 1 prompt-equivalent signature, 1 session created/live, 0 revoked, 1 challenge used, cookies unchanged by the reads). Save as source/x5.mjs, run `node x5.mjs`:\nimport {setup,newAccount,fakeImd,fakeChain} from './tests/wallet-harness.mjs';\nimport {ALCHEMY_RPC_URL,ALCHEMY_NFTS_URL} from './server/ownership.ts';\nfor(const path of ['/api/me/home','/api/me/home?fresh=1']){\n  const A=newAccount(),a=A.address.toLowerCase(),keys=[];const owners=Array(2000).fill(null);owners[361]=a;\n  const w=setup({imd:fakeImd({seats:{361:'51320'},owners,online:[361]}),chain:fakeChain({owners:{361:a}}),\n    env:{CHAIN_LIMITER:{limit:async({key})=>{keys.push(key);return {success:false};}}}});\n  const b=w.browser();await b.signIn(A);\n  const n=p=>w.chain.state.calls.filter(c=>c.url.startsWith(p)).length;let last;\n  for(let i=0;i<20;i++){w.clock.advance(1000);const r=await b.get(path);last=[r.status,(await r.json()).recheck];}\n  console.log(path,JSON.stringify({last,eth_call:n(ALCHEMY_RPC_URL),indexReads:n(ALCHEMY_NFTS_URL),chainIndexAsks:keys.filter(k=>k==='chain:index').length,laneAsks:keys.filter(k=>k==='chain:index:lane').length}));\n}\n\nMeasured: `/api/me/home {\"last\":[200,\"limited\"],\"eth_call\":1,\"indexReads\":0,\"chainIndexAsks\":1}` (control) versus `/api/me/home?fresh=1 {\"last\":[200,\"limited\"],\"eth_call\":20,\"indexReads\":0,\"","severity":"low","snippet":"    },p=>young(p.indexedAt)&&!(again&&p.refused));","title":"?fresh=1 with a refused index budget bypasses the 30 s ownerOf proof cache: every request re-sends the Multicall eth_call and re-asks chain:index"},{"citation":"resolved","description":"Same failure class as sixth Audit #5 (negative cache age stays fresh), but in the owner re-check that R7 did not touch; new, no prior ID (controls affected: CORR-05/spec D07 owner staleness, INT-1 60 s re-check, W-1). refreshHome() skips a non-forced read while now-homeAt<HOME_MIN_GAP_MS. homeAt is a Date.now() stamp, so after the wall clock is stepped back by J the age is negative and the guard stays true for J+15 s of real time: every watchOwner tick (auth.ts:527, every 60 s) returns without a request. visible() has the same shape (auth.ts:174: now-sessionAt>=HOME_MIN_GAP_MS is false for a negative age, so the tab-visible session re-read is skipped), and CORR-05's bound (auth.ts:309: now-homeOkAt<=OWNER_STALE_MS) is true for any negative age, so a forced read answered 429 keeps the old house without limit. Result: statusOf() stays 'owner', enterGate() stays 'ok' and eligible stays >0 on a page whose seat was sold or whose session was revoked elsewhere (logout-all on another device), until the wall clock passes the old stamp. The session-expiry timer is also wall-clock based, so it does not end it. Preconditions: a signed-in owner tab, a backward clock correction larger than 15 s (manual change, NTP step, VM resume, DST misconfiguration); no attacker control of the server is needed and the server stays authoritative for every write (member writes, house authority = session address + ownerOf), so the impact is stale owner-mode UI and entry to the own-home interior view only. Event order measured: session GET 200 -> home GET (owner) -> clock -1 h -> seat sold / session revoked -> ten 60 s re-check ticks -> 0 home GETs, status 'owner'. Prompts 0, cookies unchanged, no session or challenge rows created/revoked/invalidated (N/A: read path only); timer ownership: watchOwner's timer keeps firing, the read is dropped inside refreshHome; knowledge stays PRESENT; no cleanup owner involved. Fix in the style R7 used for names: require a non-negative age (skip only when 0<=now-homeAt<HOME_MIN_GAP_MS; keep a stale house only when 0<=now-homeOkAt<=OWNER_STALE_MS; same for sessionAt), or stamp these three with a monotonic clock as member.ts does for the cooldown.","line":291,"path":"source/src/world/auth.ts","reproduction":"Reviewer measurement, offline, real AuthClient + watchOwner from the pinned source with a synthetic fetch/clock/timer env (no provider, no cookies). Save as source/x2.mjs and run `node x2.mjs sold`, `node x2.mjs revoked`, `node x2.mjs 429`:\nimport {AuthClient,watchOwner,statusOf,HOME_MIN_GAP_MS,OWNER_RECHECK_MS,OWNER_STALE_MS} from './src/world/auth.ts';\nconst A='0x'+'ab'.repeat(20);\nasync function run(jumpMs){\n  let now=1_800_000_000_000,homeGets=0,mode='owner';const timers=[];\n  const env={set:(fn,ms)=>{const t={fn,at:now+ms,real:real+ms};timers.push(t);return t;},clear:t=>{const i=timers.indexOf(t);if(i>=0)timers.splice(i,1);},hidden:()=>false,now:()=>now,onVisible:()=>()=>{}};\n  var real=0; // monotonic time: timers fire on real elapsed time, not on the wall clock\n  const expiresAt=now+7*86_400_000;\n  const fetch=async(path)=>{\n    if(path==='/api/auth/session')return Response.json({signedIn:true,address:A,expiresAt});\n    if(path.startsWith('/api/me/home')){homeGets++;\n      if(mode==='owner')return Response.json({address:A,seats:[],eligible:1,size:'s',block:1,checkedAt:now,presence:'fresh'});\n      if(mode==='sold')return Response.json({address:A,seats:[],eligible:0,size:null,block:2,checkedAt:now,presence:'fresh'});\n      if(mode==='revoked')return Response.json({error:'AUTH_REQUIRED'},{status:401});\n      return Response.json({error:'RATE_LIMITED'},{status:429});}\n    throw new Error(path);};\n  const c=new AuthClient({fetch,provider:()=>null,now:()=>now,env,hint:{get:()=>null,set(){}}});\n  c.start();for(let i=0;i<20;i++)await new Promise(r=>setImmediate(r));\n  const stop=watchOwner(c,env);\n  const first=homeGets,s0=statusOf(c.state,now);\n  now-=jumpMs;                       // wall clock corrected backward (NTP / manual / VM resume)\n  mode=process.argv[2]??'sold';      // the seat is sold, or the session revoked elsewhere, right after\n  const advance=async ms=>{const end=real+ms;for(;;){const due=timers.filter(t=>t.real<=end).sort((a,b)=>a.real-b.real)[0];if(!due)break;\n      now+=due.real-real;real=due.real;timers.splice(timers.indexOf(due),1);due.fn();for(let i=0;i<20;i++)await new Promise(r=>setImmediate(r));}\n    now+=end-real;real=end;};\n  await advance(10*OWNER_RECHECK_MS);  // ten owner re-checks come due (10 min of real time)\n  console.log(JSON.stringify({jumpMs,mode,statusBefore:s0,homeGetsAtStart:first,homeGetsAfter10Rechecks:homeGets-first,statusAfter10min:statusOf(c.state,now),eligibleShown:c.state.home?.eligible??c.state.home}));\n  stop();\n}\nawait run(0);await run(1);await run(3_600_000);\n\nEach run prints three lines for a backward step of 0 ms, 1 ms and 3,600,000 ms taken right after the first owner read, followed by ten 60 s re-check ticks (10 min real time). Expected in every line: homeGetsAfter10Rechecks>=1 and the new server answer shown. Actual: jump 0 and 1 ms (controls): sold -> 10 GETs, status 'signedInNoHouse'; revoked -> 1 GET, status 'visitor'; 429 -> 10 GETs, 'ownershipUnavailable'. Jump 3,600,000 ms: `homeGetsAfter10Rechecks:0, statusAfter10min:\"owner\", eligibleShown:1` in all three modes.","severity":"low","snippet":"    if(!force&&this.now()-this.homeAt<HOME_MIN_GAP_MS&&this.s.home)return;","title":"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"},{"citation":"resolved","description":"Math precision / divide-by-edge-value. decimal() (market.ts:34) accepts any priceNative that is finite and >0, including denormals, and ethUsd() (market.ts:65) divides priceUsd by it; withUsd() multiplies floorEth by the quotient with no finiteness or magnitude bound. For priceNative='1e-320' and priceUsd='8' the rate is Infinity and floorUsd is Infinity (the same happens by overflow for a very large floorEth x rate). floorView() (market.ts:87) then returns `mine` because mine.floorUsd!==undefined, although the fallback branch on the same line demands Number.isFinite for the Worker's figure: the two branches do not enforce the same invariant ('a shown floorUsd is a finite number'). On the Worker JSON.stringify turns the Infinity into null, so the wire and the client disagree as well. Preconditions: DEX Screener (public upstream) returns an absurd but syntactically valid priceNative; no user or attacker on the site can set it, and nothing here authorizes or moves value, so this is display-only (the rendering component is withheld, so what is drawn for Infinity is unknown). Sessions/challenges/cookies/prompts/timers/cleanup: N/A (pure function). Fix: in ethUsd/withUsd return null/omit floorUsd unless the product is finite (and optionally within a sane range), matching the existing fallback check.","line":68,"path":"source/src/world/market.ts","reproduction":"Reviewer measurement, offline. Save as source/x3.mjs and run `node x3.mjs`:\nimport {selectMarket,withUsd,floorView,marketView,IMD_TOKEN} from './src/world/market.ts';\nconst q=selectMarket([{chainId:'ethereum',baseToken:{address:IMD_TOKEN},priceUsd:'8',priceNative:'1e-320',liquidity:{usd:1}}]);\nconst now=Date.now(),floor={floorEth:2.5,marketplace:'OpenSea',fetchedAt:now};\nconst server=withUsd(floor,q);console.log('worker extras',server,'wire',JSON.stringify(server));\nconst sample={state:'fresh',url:'u',fetchedAt:now,data:JSON.parse(JSON.stringify(q)),extras:{floorEnabled:true,floor:JSON.parse(JSON.stringify(server))}};\nconsole.log('client floorView',marketView(sample,now).floor);\n\nInput: one matching DEX Screener row with priceUsd '8', priceNative '1e-320'; floor 2.5 ETH. Expected: no floorUsd (or a finite one). Actual: worker extras floorUsd: Infinity, wire `\"floorUsd\":null`, client marketView(...).floor = {floorEth:2.5,...,floorUsd:Infinity}.","severity":"info","snippet":"  const rate=ethUsd(quote);return rate===null?floor:{...floor,floorUsd:floor.floorEth*rate};","title":"floorUsd computed from priceUsd/priceNative is not checked for finiteness: a denormal priceNative yields Infinity on the client while the Worker's own figure is finite-checked"}],"hash":"dfd88b1eb021a04128bd55ad10f941978b8791fdd8a5cacc43603e518fee67d6","nodeId":"02a180ee-6c30-4e7b-aebc-e7f5ff603802","outcome":"completed","summary":"The math/boundary audit is done: three new findings (two Low, one Info) are in `.imd-findings.json`, and the R7 fixes held at every exact boundary I probed. No repository files were changed; all experiments ran in a temp copy of `source/`.\n\n## New findings\n\n1. **Low — `source/server/ownership.ts:261`: `?fresh=1` with a refused index budget bypasses the 30 s proof cache.** A proof built while `chain:index` refuses is dated by its old (or zero) index time, so the next fresh read always rebuilds it. Through the real Worker route, 20 GETs one second apart sent 20 `eth_call` and 20 `chain:index` asks; the non-fresh control sent 1 and 1. This is cost/availability only: `ownerOf` still proves every seat.\n2. **Low — `source/src/world/auth.ts:291`: a negative age passes the house-read gap.** This is the same class as sixth Audit #5, in the owner re-check that R7 did not touch. After a 1 h backward wall-clock step, ten 60 s re-check ticks sent 0 home GETs and the status stayed `owner`, though the server would now answer \"seat sold\", 401 or 429. Controls at 0 ms and 1 ms re-read normally. `visible()` (line 174) and the stale-house bound (line 309) have the same shape. The effect is stale owner-mode UI only; the server stays authoritative.\n3. **Info — `source/src/world/market.ts:68`: `floorUsd` is not checked for finiteness.** A `priceNative` of `\"1e-320\"` gives `floorUsd: Infinity` on the client, while the Worker's own figure is finite-checked and goes over the wire as `null`. Display only, and it needs a garbage upstream value.\n\nEach finding carries its full reproduction script and expected-versus-actual output.\n\n## Sixth-round items within this area (reviewer measurements)\n\n| Item | Verdict | What I measured |\n|---|---|---|\n| Audit #2 / R6-I1 (dead token + own nonce) | Fixed locally | At `expiresAt-1`: 204, session revoked. At `expiresAt+0` and `+1`: 409, no Set-Cookie, no row or cookie change. A supplied empty, malformed or forged token with a pending nonce: 409, challenge still pending. No-token control: 204, one challenge invalidated. |\n| Audit #5 (name-cache negative age) | Fixed locally | Age 0 and 59,999 ms hit the cache; 60,000 ms refetches; −1 ms refetches; a repeated backward jump refetches again. |\n| Challenge expiry edge | Consistent | At `acceptUntil-1`: verify 200, pending cancel 204. At `acceptUntil+0`: verify 410, cancel 409, nothing written. |\n| Rename cooldown edge (R5-08/09 server side) | Consistent | At deadline −1: 409 `NAME_CHANGE_COOLDOWN`. At the deadline: 200 and the view shows no cooldown. |\n\nAudit #1, #3 and #4 are lifecycle-ordering items rather than numeric ones. I read the fence logic (`readSeq > responseFence`, with reconcile bumping the read counter first) and found no off-by-one, but did not write independent counterexamples for them. Their status rests on the published tests passing here, so treat them as fixed locally per the team's tests rather than independently closed by me. The same applies to the fifth-round four-Low and R5-01..09 matrices, where only the items in the table were probed independently.\n\n## Test runs\n\n- Full glob `node --test tests/*.test.mjs` on Node v24.21.0: 504 pass, 5 fail. The failures are `home-entry`, `ownership` and `wallet-client`, which cannot load because `src/world/households.ts` is withheld. I did not stub it.\n- Run individually, all pass with no skips: `auth-r7-authority` 32, `auth-r7-lifecycle` 55, `member-r7-cache` 8, `auth-lifecycle-model` 79, `auth-r5` 49, `auth-r5-authority` 36, `member-r5` 11, `member-r4` 16, `aud4-auth` 17, `aud4-member-server` 24, `member-client` 15.\n- I did not reproduce the team's exact 386/386 command, the public `tsc` exit 2, or the Worker bundle hash.\n\n## Not done\n\n- The official originals of the sixth Audit and Report were not fetched and their SHA256 not verified.\n- No live GETs were made to imdember.com; everything is offline against the pinned source.\n- Solidity and the Foundry profile do not apply: there are no contracts in the subjec","treeHash":null,"usage":{"cachedInputTokens":6404870,"inputTokens":72,"model":"claude-fable-5-1","outputTokens":35964,"runtime":"claude","turns":44,"wallClockMs":487499}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3f91b58cf7cd2d45","findings":[{"citation":"resolved","description":"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.","line":189,"path":"source/src/world/auth.ts","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'].","severity":"low","snippet":"    if(click?.owner){this.revokeAbandoned(click.owner);return;}","title":"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"},{"citation":"resolved","description":"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.","line":396,"path":"source/src/world/auth.ts","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.","severity":"low","snippet":"      if(owner.status!=='RETAINED'){this.set({phase:'idle'});return;}","title":"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"},{"citation":"resolved","description":"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).","line":190,"path":"source/src/world/auth.ts","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 [].","severity":"low","snippet":"    const context=click?.nonce?{expectedNonce:click.nonce}:held?{expectedAddress:held.address}:null;","title":"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"},{"citation":"resolved","description":"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.","line":482,"path":"source/src/world/auth.ts","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.","severity":"low","snippet":"      if(owner.status==='RETAINED'||life!==this.life)return;","title":"Stop/restart while an owner cleanup is in flight: the new lifetime keeps displaying a session the cleanup then revokes, with no reconciliation read"},{"citation":"resolved","description":"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.","line":261,"path":"source/server/ownership.ts","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.","severity":"low","snippet":"    },p=>young(p.indexedAt)&&!(again&&p.refused));","title":"?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"},{"citation":"resolved","description":"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.","line":291,"path":"source/src/world/auth.ts","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.","severity":"low","snippet":"    if(!force&&this.now()-this.homeAt<HOME_MIN_GAP_MS&&this.s.home)return;","title":"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"},{"citation":"resolved","description":"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.","line":467,"path":"source/src/world/auth.ts","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.","severity":"info","snippet":"    const wasFlow=!!click||this.lifecycle.retainedOwners.length>0;","title":"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"},{"citation":"resolved","description":"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.","line":68,"path":"source/src/world/market.ts","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}.","severity":"info","snippet":"  const rate=ethUsd(quote);return rate===null?floor:{...floor,floorUsd:floor.floorEth*rate};","title":"floorUsd from priceUsd/priceNative is not finite-checked: a denormal priceNative yields Infinity on the client while the Worker figure is finite-checked"},{"citation":"resolved","description":"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.","line":3,"path":"R7/AUTH_REMEDIATION.md","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.","severity":"info","snippet":"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.","title":"Seventh-review verdict matrix (not a defect): sixth Audit #1-#5 / Report R6-I1 closure, fifth four-Low and R5-01..09 status, limits"}],"hash":"f0e9d4684854101c5f3681c553bb99738056393248d486877ffb343804541182","nodeId":"11634c88-9e16-4b66-9da6-b6bfd2b22268","outcome":"completed","summary":"The review is finished: `.imd-findings.json` holds 9 entries — 6 Low, 2 Info, and the verdict matrix. Every specialist finding reproduced against `c4f451b`; the three reports of the `auth.ts:189` defect are merged into one. No repository files were changed.\n\n| # | Severity | Where | Defect | Parent `445747d` |\n|---|---|---|---|---|\n| 1 | Low | `source/src/world/auth.ts:189` | Account or provider switch after a trusted PRESENT read, while the click is still live, sends no cleanup. A's session row and cookie stay live (0 logouts, sessions 1 created / 1 live / 0 revoked). | Sent `{expectedAddress}`, 204, live 0 — R7 regression |\n| 2 | Low | `source/src/world/auth.ts:396` | A sign-in accepted by a post-fence read while the valid verify body is still stalled is never broadcast. A second tab prompts again and creates a second session (2 prompts, 2 live sessions). | Broadcast `signed-in`, 1 prompt, 1 session — R7 regression |\n| 3 | Low | `source/src/world/auth.ts:190` | A switch while the click holds a nonce and the page already shows a session sends `expectedNonce` only. The server answers 409 and the displayed session is not ended. | Same 409 — retained gap |\n| 4 | Low | `source/src/world/auth.ts:482` | Stop then restart while an owner cleanup is in flight: the new lifetime keeps showing a session the cleanup then revokes, with no re-read. | Same — retained gap |\n| 5 | Low | `source/server/ownership.ts:261` | `?fresh=1` with a refused index budget defeats the 30 s proof cache: 20 reads in 20 s gave 20 `eth_call`s and 20 budget asks; both controls gave 1. | Not compared |\n| 6 | Low | `source/src/world/auth.ts:291` | After a backward clock step of 1 h, ten owner re-checks sent 0 home reads and status stayed `owner` for a sold seat, a revoked session and a 429. | Not compared |\n| 7 | Info | `source/src/world/auth.ts:467` | A wallet lock with no click in progress now revokes a committed session held by a retained owner. The comment at `auth.ts:459-460` still says a lock leaves it alone. | 0 logouts, session kept |\n| 8 | Info | `source/src/world/market.ts:68` | A denormal `priceNative` gives `floorUsd: Infinity` on the client, while the wire value is `null`. Display only. | Not compared |\n| 9 | Info | `R7/AUTH_REMEDIATION.md:3` | Verdict matrix, not a defect. | — |\n\nThe matrix records these verdicts:\n- **Sixth originals:** Audit #1 to #5 and Report R6-I1 are fixed locally for their original counterexamples. #1's fix caused finding 1, #3's caused finding 2, and #5's failure class remains in the owner re-check (finding 6).\n- **Fifth four-Low:** LOW-1, LOW-3 and LOW-4 are fixed locally; LOW-2 is only partly closed because of findings 1 and 4.\n- **R5 matrix:** 01 and 02 fail in the finding 1 orderings (01 also in finding 3), 07 fails in the finding 2 ordering, 08 is open for the owner re-check. 03 passes apart from the finding 4 gap; 04, 05, 06 and 09 pass.\n\nWhat I ran:\n- **Checksums and public suite:** `SHA256SUMS` is 190/190 OK. The public 11-file suite is 386/386 with no failures or skips.\n- **Before/after:** the project's two R7 auth closure test files pass 87/87 on the candidate and 65/87 on the parent.\n\nWhat I did not do:\n- **Independent counterexamples for #1 to #5:** closure rests on the project's R7 tests plus my probes for the findings above, not on separate counterexamples of my own.\n- **Official originals:** I did not fetch them or verify their SHA256, so that check is unavailable in this pass.\n- **Other checks not run:** public `tsc`, the Worker compile, and the optional anonymous GETs.\n- **Finding 6 side claims:** the `visible()` and stale-house (`auth.ts:309`) parts are from reading the code, not measured.\n- **Proofs:** no Foundry proofs are attached; there is no Solidity and nothing is High or Critical.","treeHash":null,"usage":{"cachedInputTokens":957149,"inputTokens":23,"model":"claude-fable-5-1","outputTokens":21429,"runtime":"claude","turns":13,"wallClockMs":228643}}],"verification":[]}