{"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":"7716c3f5-5d6c-4953-a643-141da678d051","kind":"audit","nodes":[{"acceptedSubmissionHash":"f0856d79833829c8de1b7a82b59e0880e885d20fa728656bf4b1ff1c5a8aa510","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":"23c76103818771a6ebcb4092b2a35076b0a29b47baa27eda4bdd976c9e1ec98b","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":"63293e0212a0e1bda71739653d1573ad98998d56f78aad4114e9602db9f9cfb1","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":"167646680bd5a66824ec617a14cc731136d9654b97a4cbcd93aeabfee161b4e9","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":"1529a94d78d30fffc7c091070347fc3f80c39c0501d827f9baa0b42961dc472e","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 - eighth Swarm audit / targeted Audit7 closure\n\nQuestion: Does this pinned candidate close the prior six Low and two Info findings, and what blocks SOURCE-CLOSURE or RELEASE-READINESS? Seek scoped regressions of any severity; no promised pass or zero-findings outcome.\nPeriod: 2026-10-05 pinned snapshot cutoff; captured prior records are comparison evidence.\nLength and format: Markdown finding table, reproductions and coverage appendix; preserve code/hashes/URLs.\n\nExact public snapshot: https://github.com/tungweb3/imd-ember-world-review/tree/88c130283efc45260f9e00da8d2d3055c38483bd\nPrevious public: 7215c5d89a96bc79113a85766c04868d54393f3c\nFrozen private source: bb7549e0a2576ba4da0ea7c4147c4aba1a7f577f\nTEAM deployed Worker: cdd3ef36-ca81-439a-8798-a64e30cf01d3\nRecord: 20261004T180257Z-bb7549e\nRead Submission8/README.md and its closure matrix, test/reference, deployment/boundary, prior-original and REVIEW_INPUTS links; manifests/submission8-published-source.json; source/docs/security/AUDIT8_CLOSURE.md and AUDIT8_REVIEW_RUNNER.md. Cite actual pinned paths/lines. Older namespaces are historical.\n\nUnofficial TypeScript Cloudflare Worker/React SIWE; NO SOLIDITY. Persistent public names mean World is not wholly read-only. Scope: Auth/server authority, ownership discovery/proof/index/budget and related freshness/numbers. Exclude Genesis/Mint/Ember Coin/full 3D/scene/geometry/media/music/avatar/selfie/unrelated features. Supplied 140 source files: 124 exact, 16 preserved-redaction files/57 masked lines. No private history or full standalone frontend is supplied.\n\nOffline/local fixtures only. Public source/prior-document GETs and fresh dependency downloads are allowed. No live site/API test requests or writes, real wallets/signatures, transactions/approval/permit/delegation/bridging/mint, funding/payment/job submission, publication or deployment. Never request owner credentials/private databases. External deployment records are TEAM readbacks, not your live measurements; mark unavailable access unknown.\n\nFresh checkout, Node 24, then in source/: npm ci --ignore-scripts; npm run test:review -- --check; npm run test:review. Require real locked viem 2.56.9/all 23 files. No stubs, omitted failing files, private source selectors/global crypto replacements or leaked outputs. Default persists no scheduler artifacts; opt-in sanitized/replayable output stays inside explicit source/tmp under the supplied policy.\n\nProvenance: public 574/574, core 500/500 (428 unique digests), additional 90/90 (54 unique) are inherited executions: all 140 public raw inputs/evaluator bytes remain unchanged for this candidate. Inheritance is not a fresh rerun; distinguish your own measurements. Private 1606/1606, TypeScript and Vite steps completed successfully. Overall canonical deploy exited1 at final local record-directory rename EPERM AFTER Wrangler exited0. The original nonzero receipt is retained; unchanged pending record restored; upload/live HTTP correspondence checked separately. Do not rewrite overall exit0, call it a test failure, or claim full public frontend build. Inspect BUILD_DEPLOYMENT.json for the operational limitation and point-in-time byte evidence.\n\nEight causal fixes to test hardest, with baseline controls and actual effects:\n1. Passive provider discovery must preserve cookie-restored/accepted session and selected page-used wallet. Test first/late announcements and reannouncements. Explicit provider selection and observed account changes remain genuine context changes with cleanup/fencing.\n2. 20 overlapping ordinary AND fresh=1 home reads with advancing live clock share one index read, one chain-index budget charge and one proof per measured cohort. Re-read clock after queue/budget admission; stale request-start time cannot invalidate a newly completed index or amplify admission.\n3. Same-account unlock after retained verify owner's first reconciliation503 must re-read canonically, preserve committed session and release lock responsibility. Lock is not logout. Provider/account/generation guards and later stop must not revive or cross-revoke another lifetime.\n4. Ordinary valid canonical ABSENT/hint during an original same-click signature must not discard it solely because a read counter changed. Each click still needs its OWN preflight; PRESENT/UNKNOWN/context/expiry fence signing. Challenge lease uses finite nonnegative elapsed time from POST dispatch through completion; exact 5min/backward clock fails closed. SIWE clock tolerance and Worker nonce authority stay unchanged.\n5. Queued independent roster/discovery intent re-evaluates after predecessor success OR failure503. Identical pending contexts share one bounded failure; failure stamps no proof epoch. Test later retry, changed roster, held proof crossing expiry/latest-block renewal and 512 active-address cap without live eviction.\n6. Non-authority remote polling uses explicit 60000ms skew tolerance: test boundary/expiry/rollback/NaN/Infinity. Session/ownership/local-cache/write authority retain strict nonnegative ages/original TTL. floorUsd operands AND product finite/nonnegative; valid negative market changes remain valid.\n7. Shared-copy warming preserves producer timestamps/source lineage instead of redating as now. Remote skew does not extend proof/session authority or conceal source age.\n8. Default scheduler creates no sibling evidence/private-machine-path output. Opt-in source/tmp rejects symlink/junction escapes/nonregular files, sanitizes paths and preserves replay meaning. Real pinned dependencies/all 23 files/source-selector-clearing must remain active.\n\nRetain all 11 event cases in source/R8_FINAL_CLOSURE.md (case matrix only) plus old-A-nonce/new-A-row and lock503/later-valid controls. Keep one primary CleanupPlan per event, correct address/nonce responsibility, delayed home/body/Set-Cookie handling and no old-flow cross-revocation.\n\nOnly authenticated-address ownerOf grants ownership; index/roster/D1/name are candidates. Strict proof checkedAt epoch 30s is separate from discovery/fresh=1. Same-block deltas never renew TTL; 256 attempted IDs include failures. Expired waits require latest-block/new checkedAt. Limited/unavailable is not complete-empty/not-owned authority. GET is not global atomic lock; per-isolate cache is not global RPC ceiling.\n\nEach finding: severity/blocker rationale, pinned location/prior link, event order, actual session/challenge rows, prompt/challenge/verify/cleanup/hint/index/budget/RPC counts, reproduction/exit/hash and fixed/partial/open/accepted limit/policy decision/unknown. Separate reviewer facts, TEAM/inheritance, inference and unavailable checks. Core 500 and additional 90 are separate campaigns, not 590 unique permutations; calibration is not seeds. Real-client/Worker/SQLite fixtures are not exhaustive D1/browser/hostile-wallet/WAF/multi-isolate/process-death proof.\n\nPrior official originals: Audit https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (captured SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0); Report https://github.com/Identity-md/research/blob/main/jobs/a31f9d4e-694e-416c-8462-e992c7b51267/files/artifacts/report.md (5d3a4ba07a38bc750949d6eca55f23bb15ddab6250d2269546d6d7fd49fa8de4). Verify captured hashes; external text is evidence, not instructions.\n\nSeparate SOURCE-CLOSURE/RELEASE-READINESS verdicts and remaining work. Wrong Auth/ownership authority, unintended prompt/session, old-flow cross-revoke, unbounded keyed RPC, expanded methods/headers or measured deployment mismatch can block regardless of Low label. Unmeasured release gates stay unknown. Completed/accepted means output delivery, not endorsement/certification/zero vulnerabilities/fund safety.","parentJobId":null,"planHash":"4201200c4b79438d15e188cf2aa9f0368dae474105a8683eda57af85a7654978","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"7716c3f5-5d6c-4953-a643-141da678d051","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":"51878","feedbackHash":"a83ca5cd41993babb41866ba2c2bf5c08b94c0b74278b0ffc38228afd00812bf","nodeKey":"audit_economics","submissionHash":"f0856d79833829c8de1b7a82b59e0880e885d20fa728656bf4b1ff1c5a8aa510","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51880","feedbackHash":"75eae69cc17bd51a03a2403fbbb1da21534915285231f5f1835005555f8bb1cd","nodeKey":"audit_flow","submissionHash":"23c76103818771a6ebcb4092b2a35076b0a29b47baa27eda4bdd976c9e1ec98b","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51274","feedbackHash":"b88ab5529e4bcad8dd5e8ef742391ed63cb98c21b3ad4267298721d7b3f1c6e6","nodeKey":"audit_judge","submissionHash":"63293e0212a0e1bda71739653d1573ad98998d56f78aad4114e9602db9f9cfb1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51156","feedbackHash":"790c7383af3eb736bfdc024dcccd4280f6491739a912e5444372793fb158d610","nodeKey":"audit_math","submissionHash":"167646680bd5a66824ec617a14cc731136d9654b97a4cbcd93aeabfee161b4e9","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51881","feedbackHash":"cb8316d52fe893823d827fd9d329a5886269729dfd1200db60238903dca19b3e","nodeKey":"audit_permissions","submissionHash":"1529a94d78d30fffc7c091070347fc3f80c39c0501d827f9baa0b42961dc472e","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"0d661ffd2689cb85cb696900c2ff47f02448ff02b9c81998c2219c5c1d7bbd07","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1a7ecd03bd365366","findings":[{"citation":"resolved","description":"State: policy decision / accepted limit to confirm (OWNERSHIP_FRESHNESS.md line 9 says a delta finishing at/after its deadline is unavailable), not a SOURCE-CLOSURE blocker. Blocker: no (fails closed: no ownership granted, no authority change, RPC bounded by chain:index/home limiters). Mechanism: Ownership.proof() re-evaluates the epoch at proofNow (line 289-291); when the old epoch is still valid it issues a delta ownersOf() pinned to the old block for newly discovered ids (line 298), then re-reads the clock (line 307) and throws OwnershipUnavailable when done - checkedAt >= 30000 ms (line 308). The completed, valid pinned-block result is discarded, nothing is stored, and the request answers 503 OWNERSHIP_UNAVAILABLE. Because the failure propagates through the ProofFlight promise, every overlapping same-context caller queued behind it (line 234-237) receives the same 503. The very next request creates a fresh epoch at latest and succeeds, so the request could have re-proved at latest itself (the documented rule for discovery waits crossing the deadline, OWNERSHIP_FRESHNESS.md line 16) instead of answering 503. Window: any request carrying a new candidate (roster or fresh=1 index) that starts its delta within the last RPC-latency of a 30 s epoch. Counts (my reproduction, direct Ownership class, offline fixture, live clock advanced 30 ms per budget / 200 ms per index read / 100 ms per eth_call): ordinary variant: epoch at T0; request at T0+29.9 s with roster seat 8 added: result OWNERSHIP_UNAVAILABLE, eth_call tags ['latest','0x1406f40'], rpc 2, index 1, budget 1; retry: eligible 2, rpc 3 (tags add 'latest'). Control at T0+29.8 s: eligible 2 at the old checkedAt, rpc 2. Cohort: 20 overlapping identical requests at T0+29.9 s: 20 errors / 0 ok / rpc 2. fresh=1 variant: epoch 2 created from the 5-minute candidate cache; Check again (fresh=1) at epoch+29.7 s whose index read names newly bought seat 8: result OWNERSHIP_UNAVAILABLE with index 2, budget 2, rpc 3 (chain:index budget and the delta eth_call both spent); fresh=1 retry: eligible 2, rpc 4. Expected: the request whose delta crossed the deadline starts a new latest proof (new checkedAt) or at minimum keeps the valid old epoch's seats as 'limited' rather than 503; one RPC should not be spent and discarded. Separation: reproduced facts above are my own offline measurements; the policy sentence is a TEAM document; production frequency unmeasured (no live requests made). Minimal fix preserving the design: when valid && delta completes with done past the deadline, fall through to a fresh latest proof in the same request (checkedAt=done-side clock) rather than throwing; keep the throw for the first/expired-epoch case.","line":308,"path":"source/server/ownership.ts","reproduction":"Offline, no network. From a fresh checkout with Node 24.19.0: cd source && npm ci --ignore-scripts; create /tmp/probes/ownership-deadline.mjs (symlink source/node_modules into /tmp/probes) importing Ownership from <src>/server/ownership.ts with the advancingFixture pattern of tests/ownership-audit8.test.mjs (roster ['7'], indexIds ['7'], ownerById {7:A,8:A}; clock +30 ms per budget, +200 ms per index read, +100 ms per eth_call; request.clock=()=>state.live). Steps: (1) home(A,req(START),false) -> eligible 1, checkedAt=START+230, rpc 1. (2) state.live=checkedAt+30000-100; state.roster=['7','8']; home(A,req(state.live),false). Expected: eligible 2 (new latest epoch or served pinned delta). Actual: rejects OWNERSHIP_UNAVAILABLE; state.rpc=2 with tags ['latest','0x1406f40'] (the delta eth_call was sent and its answer discarded). (3) same call again: eligible 2, checkedAt=START+30230, rpc 3. Cohort: 20 x home(A,req(state.live),false) launched together at step (2): 20 rejections, 0 views, rpc 2. fresh=1 path: after step (1) set state.live=START+250000, home(...,false) -> new epoch checkedAt=START+250000 (rpc 2, index still 1); state.live=250000+29700; state.indexIds=['7','8']; home(A,req(state.live),true). Expected: eligible 2. Actual: OWNERSHIP_UNAVAILABLE with index 2 / budget 2 / rpc 3; retry fresh=1 -> eligible 2, rpc 4. Control: step (2) at checkedAt+29800 instead of +29900 -> eligible 2 at the original checkedAt, rpc 2 (the Audit5 'later same-roster request after the pending epoch deadline' test expects the 503 for a first/expired epoch; this case is a valid epoch whose 100 ms delta crosses the boundary).","severity":"low","snippet":"      if(!isFreshAge(done,checkedAt,OWNERSHIP_TTL_MS))throw new OwnershipUnavailable();","title":"Delta ownerOf proof that completes at/after its epoch deadline is discarded as 503 instead of re-proving at latest; one keyed RPC wasted and every same-context waiter fails"},{"citation":"resolved","description":"State: policy decision to confirm (AUDIT8_CLOSURE.md: 'Selecting another wallet explicitly keeps the intended cleanup'); blocker: no (no cross-account authority, no privilege gain; the user's own restored session is revoked and one extra signature is required). This is the residual of prior Low #1 (unintended session revocation class), now reduced to an explicit pick. Mechanism: providerChanged('selection') (line 293) only returns early when p===this.bound (line 294); when the page has used no provider yet (WalletRegistry.current() is null because needsChoice: wallet.ts line 86-88, two announcements and no remembered rdns), this.bound is null, so the first pick runs line 304-306: gen++, cancelOwners('context-switch'), sessionKnown=false, automaticCleanup('provider-switch',...,held). planCleanup (authCleanup.ts line 22) returns displayed-session for the restored session, so POST /api/auth/logout {expectedAddress:<session address>} is sent with the live cookie before the picked provider's eth_accounts is read, and the Worker revokes the row (204, Set-Cookie clears). Nothing compares the picked wallet's account with the session address; the previous wallet context that a 'switch' would replace does not exist. Event order (my reproduction, real AuthClient + real Worker + node:sqlite via tests/auth-r7-fixtures.mjs and WalletRegistry): announce wallet one (account A) and wallet two (account B) -> needsChoice true, current() null -> browser signs in as A (direct challenge/verify, 200) -> AuthClient.start(): GET /api/auth/session PRESENT(A), account null, rows created 1 / live 1 / revoked 0 -> registry.choose(options[0]) (the wallet holding A) -> cleanupPlans [{eventId:1,reason:'provider-switch',kind:'displayed-session'}] -> POST /api/auth/logout addressAssertion true, nonceAssertion false, 204 -> rows created 1 / live 0 / revoked 1; client session null, ended 'revoked', account A, notice null; GET /api/auth/session signedIn false. Counts: prompts 0, challenges 0, verifies 0, cleanups 1, hints 0, RPC 0. Expected under the closure wording 'passive provider discovery must preserve cookie-restored session ... explicit selection remains a genuine context change': debatable for a pick that replaces no bound provider; a conservative alternative is to treat a pick from bound===null like discovery (bind, read eth_accounts, and only an account other than the session's is a switch). Realistic triggers are narrow: the remembered wallet is not announcing while another is, two extensions claim the same rdns, or localStorage was cleared while the cookie survived. Separation: reproduced fact above (offline fixtures); real extension/browser behaviour unmeasured.","line":306,"path":"source/src/world/auth.ts","reproduction":"Offline. cd source && npm ci --ignore-scripts; probe (Node 24) importing WalletRegistry from <src>/src/world/wallet.ts and {setup,newAccount,provider,tab,until,flush,rows,logouts,routeEvents} from <src>/tests/auth-r7-fixtures.mjs: const w=setup(),A=newAccount(),b=w.browser(),first=provider(A),second=provider(newAccount()); const reg=new WalletRegistry({ethereum:null,addEventListener,removeEventListener,dispatchEvent:()=>true},null); reg.start(); announce first as rdns io.example.one and second as io.example.two (reg.state.needsChoice===true, reg.current()===null); await b.signIn(A) (verify 200); const q=tab(w,b,first,{getProvider:()=>reg.current()}); reg.subscribeProvider(reason=>q.notifyProvider(reason)); await until(()=>q.c.state.restored&&q.c.state.session); // PRESENT(A), account null, rows live 1. reg.choose(reg.state.options[0]); await flush(30). Expected: session for A preserved (no logout; account A becomes connected to its own session). Actual: logouts(q) = [{addressAssertion:true,nonceAssertion:false,status:204}], rows {created:1,live:0,revoked:1}, q.c.state.session null, ended 'revoked', GET /api/auth/session signedIn:false, prompts 0.","severity":"info","snippet":"    this.automaticCleanup('provider-switch',this.gen,this.life,abandoned,held);","title":"An initial wallet pick from the no-provider state (two wallets announced, none in use) is treated as a provider switch and revokes the cookie-restored session even when the picked wallet holds the ses"},{"citation":"resolved","description":"State: partial (defense-in-depth gap in the Fix 8 'sanitizes paths' claim); blocker: no. The primary mechanism of Fix 8 holds in my run: the default review command persisted zero artifacts and created no sibling evidence directory, hiddenKeys removes sourceRoot/runtimeExe/runtimeExecutable/executable/cwd, and opt-in output is confined to source/tmp with symlink/junction and non-regular-file rejection. The secondary string sanitizer, however, recognises only drive-letter paths and POSIX paths beginning /Users/, /home/ or /tmp/. A reviewer or CI checkout under any other root (common: /srv, /opt, /var/lib/<ci>, /root, /workspace, /Volumes on macOS, /dev/shm, /mnt/<x> for WSL where only the trailing /Users/... part is masked) leaves a machine path in any artifact string that happens to carry one (an error message such as Node's ERR_MODULE_NOT_FOUND 'Cannot find module <abs path>' or an ENOENT text written into failure.message/detail by the replay CLI's sanitizeArtifact({status:'FAIL',message:error.message,...}) in scripts/replay-auth-trace.mjs line 11). The unit test auth-artifacts.test.mjs line 14 only exercises C:\\ and /home/ inputs, and its !first.includes(root) assertion is satisfied by hiddenKeys regardless of the regex. Concrete inputs and actual outputs from sanitizeArtifact({message:'Cannot find module '+p}): '/home/ci/imd/source/src/world/auth.ts' -> 'Cannot find module [local-path]' (masked); 'C:\\\\ci\\\\imd\\\\source\\\\tests\\\\x.mjs' -> masked; '/srv/ci/imd/source/src/world/auth.ts' -> unchanged; '/opt/build/imd/source/tests/auth-r8.test.mjs' -> unchanged; '/var/lib/jenkins/workspace/imd/source' -> unchanged; '/root/imd/source' -> unchanged; '/workspace/imd/source/tests' -> unchanged; '/Volumes/Work/imd/source' -> unchanged; '/dev/shm/imd/source' -> unchanged; '/mnt/c/Users/dev/imd/source' -> '/mnt/c[local-path]' (prefix leaks). Minimal fix: mask by the artifact store's own realpath(sourceDir) (and os.tmpdir()/homedir()) rather than a fixed root list, or mask any absolute POSIX path segment that resolves inside the checkout.","line":10,"path":"source/tests/auth-artifacts.mjs","reproduction":"Offline. cd source && npm ci --ignore-scripts; node --input-type=module -e \"const {sanitizeArtifact}=await import('./tests/auth-artifacts.mjs');for(const p of ['/home/ci/imd/source/src/world/auth.ts','/srv/ci/imd/source/src/world/auth.ts','/opt/build/imd/source/tests/auth-r8.test.mjs','/var/lib/jenkins/workspace/imd/source','/root/imd/source','/workspace/imd/source/tests','/Volumes/Work/imd/source','/dev/shm/imd/source','/mnt/c/Users/dev/imd/source'])console.log(JSON.stringify(sanitizeArtifact({message:'Cannot find module '+p})))\". Expected: every message reads 'Cannot find module [local-path]'. Actual (Node 24.19.0): only the /home/... input is fully masked; /srv, /opt, /var, /root, /workspace, /Volumes and /dev/shm inputs are returned unchanged, and /mnt/c/Users/... becomes '/mnt/c[local-path]'.","severity":"info","snippet":"  if(typeof value==='string')return value.replace(/(?:[A-Za-z]:[\\\\/]|\\/(?:Users|home|tmp)\\/)[^\\s\"'<>|]+/g,'[local-path]');","title":"Artifact path sanitizer only masks Windows drive paths and /home, /Users, /tmp roots; checkout paths under /srv, /opt, /var, /root, /workspace, /Volumes or /dev/shm pass through unmasked"}],"hash":"1529a94d78d30fffc7c091070347fc3f80c39c0501d827f9baa0b42961dc472e","nodeId":"cfa09edd-11aa-4857-99d8-c44ef499dd0c","outcome":"completed","summary":"All three snippets match their cited lines, the findings file parses, and the repository tree is unchanged apart from the ignored findings file and the ignored dependency install. Nothing further is needed; here is the review.\n\n## Verdicts and findings\n\n**SOURCE-CLOSURE:** none of my findings blocks closure. All eight Audit7 rows are closed at the pinned candidate, three of them with a documented policy residual that the requester should confirm rather than a defect that reopens the row. **RELEASE-READINESS:** unknown. Every production fact in `Submission8/BUILD_DEPLOYMENT.json` is a TEAM readback that I did not and may not re-measure (no live requests were made), the canonical deploy invocation exited 1 on a Windows EPERM after Wrangler exited 0, and the public snapshot cannot build the full frontend. That is a release gate, not a source defect.\n\n| # | Severity | Location | Finding | State | Blocker |\n|---|---|---|---|---|---|\n| 1 | Low | `source/server/ownership.ts:308` | A delta `ownerOf` proof on a still-valid epoch that completes at or after the 30 s deadline is thrown away as 503 `OWNERSHIP_UNAVAILABLE`. The keyed RPC is spent and discarded, and all same-context waiters inherit the 503. The next request re-proves at latest and succeeds, so the request could have done that itself. | policy decision (matches `OWNERSHIP_FRESHNESS.md` line 9) | no |\n| 2 | Info | `source/src/world/auth.ts:306` | An initial wallet pick while no provider is in use (two wallets announced, `needsChoice`) is handled as a provider switch and revokes the cookie-restored session via `POST /api/auth/logout {expectedAddress}` before the picked wallet's account is read, even when it holds the session's own account. Residual of prior Low #1, now requiring an explicit pick. | policy decision | no |\n| 3 | Info | `source/tests/auth-artifacts.mjs:10` | The artifact string sanitizer masks only drive-letter paths and `/home/`, `/Users/`, `/tmp/` roots. Checkouts under `/srv`, `/opt`, `/var`, `/root`, `/workspace`, `/Volumes`, `/dev/shm` pass through unmasked; `/mnt/c/Users/...` leaks its prefix. The primary mechanism (hidden keys, zero default artifacts, `source/tmp` confinement) holds. | partial | no |\n\nClosure matrix dispositions, in my judgement: rows 1, 2, 3, 4, 5, 6, 7, 8 are **fixed**. Row 1 carries finding 2 as an explicit-pick residual. Row 3 carries a documented residual: a lock followed by a 503 reconciliation and then a page teardown revokes the committed session through the owner's nonce, per `AUTH_STATE_MACHINE.md` row 32 (I measured it: rows live 1 → 0, one `logout` with `nonceAssertion:true`, 204). Row 5 carries finding 1. Row 6 and 7 accept up to 60 s of future stamp by the stated policy, so a Worker clock more than 60 s ahead still produces the failure ladder, and a warmed copy dated in the future gets its TTL from the producer stamp. Row 8 carries finding 3.\n\n## Reproductions\n\n**Reviewer facts (my own measurements, fresh checkout of `88c130283efc45260f9e00da8d2d3055c38483bd`, offline fixtures only).** Node v24.19.0 downloaded to scratch, then in `source/`: `npm ci --ignore-scripts` exit 0, real `viem` 2.56.9 in the lockfile tree; `npm run test:review -- --check` exit 0 (`{\"node\":\"v24.19.0\",\"viem\":\"2.56.9\",\"testFiles\":23}`); `npm run test:review` exit 0.\n\n| Measurement | Mine | TEAM / inherited |\n|---|---|---|\n| Review tests | 574 pass / 0 fail / 0 skipped / 23 files, 22.3 s | 574/574 (inherited from `f94d2aa6`) |\n| TAP stdout | sha256 `d2b88a4944e43f42baeec23ac07640f5e248422dfa1c84f0e028ab121bff382a`, 1,713,027 bytes | `ae1b1bf2219a398b3b60f665cee32dc9d5e283dd56206259850b89956659cb85`, 1,714,249 bytes (timing lines differ) |\n| Core oracle | 500/500, 428 unique digests, workerCalls 3572, dbComparisons 5477, cookieComparisons 3649, projectionComparisons 3402, preHeaderFailures 38 | identical numbers in `REFERENCE_SCHEDULER.json` |\n| Audit8 oracle | 90/90, 54 unique digests, 30 per kernel, workerCalls 646 | identical |\n| Artifacts | `\"artifacts\"","treeHash":null,"usage":{"cachedInputTokens":4867225,"inputTokens":642,"model":"claude-fable-5-1","outputTokens":79519,"runtime":"claude","turns":78,"wallClockMs":1238882}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1ccda5cdb3f24f3e","findings":[{"citation":"resolved","description":"OPEN; SOURCE-CLOSURE blocker: stale ownership authority violates the fixed checkedAt epoch, despite Low impact (household/owner UI; no asset transfer). On public 88c130283efc45260f9e00da8d2d3055c38483bd, proof() validates completion freshness at lines 307-308, but home() then awaits D1 sightings at line 337 (and can await lane work at 347-349) without rechecking at the final response. With a valid session A and an ownerOf(A,7) proof at T, a second home request at T+29999 reuses that proof; only 1 ms of sighting-query latency, with seat 7 transferring to B during the wait, takes completion to T+30000, lines 350-352 still return HTTP 200 eligible=1 and checkedAt=T. AuthClient accepts this response at source/src/world/auth.ts:357-361; statusOf returns owner. This is a boundary x invariant gap after the repaired proof-completion guard, not the accepted stale cache interval before expiry. Minimal fix: check the live Worker clock against proof.checkedAt after all awaited home enrichment/lane work, and either obtain a new latest-block proof with new checkedAt or return OWNERSHIP_UNAVAILABLE; never return the expired fallback on lane failure. Recheck ranking/status time as appropriate. Related prior comparison: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (captured SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0), findings 2/5; this final-sightings path is an additional issue, not evidence that their exact repair probes still fail. Reviewer offline measurement; no live D1 claim. Public previous-source comparison shows the final home/sightings path was already present at 7215c5d89a96bc79113a85766c04868d54393f3c; this is an additional pre-existing scoped defect, not a claim that Submission8 introduced it.","line":337,"path":"source/server/ownership.ts","reproduction":"Fresh pinned checkout; Node v24.21.0 and locked real viem 2.56.9 installed with npm ci --ignore-scripts. Run the following from source/ with node --input-type=module via stdin. It uses the actual Worker, migrated SQLite and supplied local upstream fixtures, changes no production files, and asserts the observed defect; exit 0. Event order: one fixture sign-in commits A; initial home proves seat 7 at T; at T+29999 another home call reuses this proof; its post-proof seat_presence query advances injected time by 0/1/2 ms and transfers the fixture seat to B; home completes. Actual HTTP 200, eligible=1, owner state and original checkedAt=T at ages 29999/30000/30001. Age 29999 is the valid baseline. Expected at >=30000: unavailable or latest reproof yielding zero. Subsequent request at >=30000 yields eligible=0 with total proof RPC=2, proving the stale response is not a fixture's permanent ownership claim. A separate cold-proof control held sightings for 29999/30000/30001 ms and reproduced the same expiry boundary. Per run before recovery: session rows created/live/revoked=1/1/0; challenges total/used/pending/invalidated=1/1/0/0; challenge/verify=1/1 setup, UI prompts/cleanup/hints=0/0/0, NFT index/budget/owner-proof RPC=1/1/1 (setup also performs one eth_getCode). No rows are cross-revoked. Reproduction source SHA256 (including terminal newline): 6e51c65963827486f0a4e3786539877f60316e30ffe3634f697bb3346beb1ae5.\n\nimport assert from 'node:assert/strict';\nimport {setup,newAccount,fakeChain,fakeImd} from './tests/wallet-harness.mjs';\nimport {MULTICALL3,ALCHEMY_NFTS_URL} from './server/ownership.ts';\nimport {INITIAL,statusOf} from './src/world/auth.ts';\nfor(const delay of [0,1,2]){\n const account=newAccount(),a=account.address.toLowerCase(),owners=[];owners[7]=a;\n const chain=fakeChain({owners:{7:a}}),w=setup({chain,imd:fakeImd({seats:{7:'707'},owners,online:[7]})}),b=w.browser();\n let budget=0;w.env.CHAIN_LIMITER={limit:async({key})=>{if(key==='chain:index')budget++;return {success:true};}};\n assert.equal((await b.signIn(account)).verify.status,200);\n const initial=await (await b.get('/api/me/home')).json();assert.equal(initial.eligible,1);\n w.clock.advance(29999);\n const prepare=w.db.prepare.bind(w.db);let held=true;\n w.db.prepare=sql=>{\n  const wrap=s=>({...s,bind:(...args)=>wrap(s.bind(...args)),all:async()=>{\n   if(held&&sql.startsWith('SELECT token_id,last_online_at')){held=false;chain.state.owners[7]='0x'+'2'.repeat(40);w.clock.advance(delay);}\n   return s.all();\n  }});return wrap(prepare(sql));\n };\n const r=await b.get('/api/me/home'),home=await r.json();\n const status=statusOf({...INITIAL,session:{address:a,expiresAt:w.clock.now()+600000},sessionKnown:true,home},w.clock.now());\n const rpc=()=>chain.state.calls.filter(c=>c.body&&JSON.parse(c.body).method==='eth_call'&&JSON.parse(c.body).params[0].to===MULTICALL3).length;\n console.log(JSON.stringify({delay,age:w.clock.now()-home.checkedAt,http:r.status,eligible:home.eligible,status,proofRpc:rpc(),index:chain.state.calls.filter(c=>c.url.startsWith(ALCHEMY_NFTS_URL+'?')).length,budget,\n  sessions:w.db.raw.prepare('SELECT count(*) created,sum(revoked_at IS NULL) live,sum(revoked_at IS NOT NULL) revoked FROM sessions').get(),\n  challenges:w.db.raw.prepare('SELECT count(*) total,sum(used_at IS NOT NULL) used,sum(used_at IS NULL AND invalidated_at IS NULL) pending,sum(invalidated_at IS NOT NULL) invalidated FROM login_challenges').get()}));\n assert.equal(home.checkedAt,initial.checkedAt);assert.equal(home.eligible,1);assert.equal(status,'owner');\n if(delay){const next=await (await b.get('/api/me/home')).json();assert.equal(next.eligible,0);assert.equal(rpc(),2);console.log(JSON.stringify({delay,control:'next request',eligible:next.eligible,proofRpc:rpc()}));}\n}\n","severity":"low","snippet":"    let proof=await this.proof(a,world.owners,world.agents,req,fresh),seen=await this.sightings(proof.ids,a,req.db);","title":"Home response can grant ownership after its 30-second proof expires during D1 sightings"},{"citation":"resolved","description":"PARTIAL closure of prior Info #8 (https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md, captured SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0). The default-disabled behavior and existing-link controls pass, but the explicit opt-in requirement to reject symlink/nonregular output is not satisfied. existsSync follows a symbolic link and returns false when its target is missing, so file() skips lstat on that existing directory entry. writeFileSync at line 32 follows the link and creates the missing target, which can be outside the validated artifact directory or source/tmp if writable. A preexisting dangling core500-RESULT.json link therefore defeats containment without a race. Severity Info: local opt-in artifact hygiene, not wallet/Worker authority; it prevents declaring all eight findings fully fixed. Minimal fix: lstat the output path independently of target existence (treat only ENOENT for the directory entry as absent), reject symbolic links/nonregular entries, and open with an appropriate no-follow/exclusive/atomic strategy. Inspect existing directory components without following dangling links as well. Reviewer reproduction used only source/tmp and removed its fixture.","line":28,"path":"source/tests/auth-artifacts.mjs","reproduction":"From source/ on Node v24.21.0, run the following with node --input-type=module via stdin; exit 0. Create a synthetic source root S inside the real checkout's explicit source/tmp/reviewer-artifact-probe and request tmp/store under S. Precreate S/tmp/store/core500-RESULT.json as a relative symlink to ../../escaped.json with that target absent. Expected: reject the symlink and create no target. Actual: write returns true, creates S/escaped.json outside the allowed S/tmp, and leaves the output as a symlink; serialized trace actions retain their original meaning. Control: a second write through the identical link is rejected once its target exists. No session/challenge rows; prompt/challenge/verify/cleanup/hint/index/budget/RPC counts all zero. This safe fixture keeps all physical writes inside the real source/tmp and removes them in finally. Reproduction source SHA256 (including terminal newline): 42d1ac35dbb54e49717f934fba1782cc20bda7f6e535ae945191bfe089af48a8.\n\nimport assert from 'node:assert/strict';\nimport {mkdirSync,symlinkSync,existsSync,readFileSync,rmSync,lstatSync} from 'node:fs';\nimport {join,resolve} from 'node:path';\nimport {createArtifactStore} from './tests/auth-artifacts.mjs';\n// Synthetic source root contained in the real checkout's explicit source/tmp.\nconst root=resolve('tmp/reviewer-artifact-probe');\nassert.equal(existsSync(root),false);\ntry {\n const dir=join(root,'tmp/store'),escaped=join(root,'escaped.json');\n mkdirSync(dir,{recursive:true});\n const output=join(dir,'core500-RESULT.json');\n symlinkSync('../../escaped.json',output);\n assert.equal(existsSync(output),false);\n assert.equal(lstatSync(output).isSymbolicLink(),true);\n const store=createArtifactStore({sourceDir:root,requestedDir:'tmp/store'});\n const wrote=store.write('core500-RESULT.json',{status:'PASS',trace:{actions:[{type:'start',tab:'a'}]}});\n console.log(JSON.stringify({wrote,escapedCreated:existsSync(escaped),outsideAllowedTmp:!escaped.startsWith(join(root,'tmp')+'/'),\n  outputStillSymlink:lstatSync(output).isSymbolicLink(),escaped:JSON.parse(readFileSync(escaped,'utf8'))}));\n assert.equal(wrote,true);assert.equal(existsSync(escaped),true);\n assert.throws(()=>store.write('core500-RESULT.json',{status:'FAIL'}),/not a regular file/);\n console.log('existing-symlink control: rejected');\n} finally {rmSync(root,{recursive:true,force:true});}\n","severity":"info","snippet":"    validate();const path=join(directory,name);if(existsSync(path)&&(!lstatSync(path).isFile()||lstatSync(path).isSymbolicLink()))throw new Error('artifact output is not a regular file');return path;","title":"Dangling output symlink bypasses opt-in scheduler artifact containment"}],"hash":"167646680bd5a66824ec617a14cc731136d9654b97a4cbcd93aeabfee161b4e9","nodeId":"9cbf3e88-209b-49cf-8e34-3098389a9d7c","outcome":"completed","summary":"**SOURCE-CLOSURE: blocked. RELEASE-READINESS: not established.**\n\nTwo substantiated findings, including executable offline reproductions, are saved in [.imd-findings.json](/home/imd/.identitymd/work/7716c3f5-5d6c-4953-a643-141da678d051/9cbf3e88-209b-49cf-8e34-3098389a9d7c/.imd-findings.json). Production files remain unchanged.\n\nReviewed public commit: `88c130283efc45260f9e00da8d2d3055c38483bd`, compared with `7215c5d89a96bc79113a85766c04868d54393f3c`.\n\n| Finding | Severity / disposition | Concrete failure | Closure consequence |\n|---|---|---|---|\n| F1 — Ownership proof expires during final D1 read | **Low · open** — [ownership.ts:337–352](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/server/ownership.ts#L337-L352) | A home request reuses a proof aged 29,999 ms. A 1 ms sighting-query delay crosses expiry; the response still grants eligibility for a transferred seat. | **SOURCE-CLOSURE blocker:** expired ownership evidence grants owner status. This additional defect existed in the previous source too. |\n| F2 — Dangling symlink bypasses artifact containment | **Info · partial closure of prior #8** — [auth-artifacts.mjs:28–32](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/tests/auth-artifacts.mjs#L28-L32) | An accepted output filename is a symlink whose target does not yet exist. `existsSync` returns false, bypassing `lstat`; writing follows the link outside the allowed `tmp` directory. | Prevents declaring all eight findings closed. Local opt-in artifact issue; no demonstrated wallet or Worker compromise. |\n\n**Reproductions**\n\nF1 uses the actual Worker, migrated SQLite and supplied offline chain fixtures:\n\n1. Authenticate A and prove seat 7 at time T.\n2. At T+29,999, request home again.\n3. During the final `seat_presence` query, transfer the fixture seat to B and advance the clock by 1 ms.\n4. Observe HTTP 200, `eligible=1`, original `checkedAt=T`, and client status `owner`.\n\n| Query delay | Proof age at response | Actual eligibility | Expected |\n|---:|---:|---:|---|\n| 0 ms | 29,999 ms | 1 | Valid cached result |\n| 1 ms | 30,000 ms | 1 | Unavailable or fresh proof yielding 0 |\n| 2 ms | 30,001 ms | 1 | Unavailable or fresh proof yielding 0 |\n\nThe next request reproves at latest and returns eligibility 0. Before that recovery, each run has session rows **created/live/revoked = 1/1/0**, challenge rows **total/used/pending/invalidated = 1/1/0/0**, and **index/budget/proof-RPC = 1/1/1**. Setup makes one challenge, one verify and one `eth_getCode`; UI prompts, cleanup and hints are zero.\n\nThe completion guard in `proof()` executes before the later D1 await. Recheck the live clock after all home enrichment and lane work; expired evidence must be reproved or rejected.\n\nF2 safely creates a synthetic source root S inside the checkout’s explicit `source/tmp`, then:\n\n```text\nS/tmp/store/core500-RESULT.json -> ../../escaped.json\n```\n\nWith `S/escaped.json` absent, `write()` returns true and creates it outside `S/tmp`. The identical link is rejected on a second write, after its target exists. All Auth/index/budget/RPC counts are zero. The fixture was removed afterward.\n\nBoth reproduction commands exited **0**, asserting the observed defects. Exact executable code is embedded in the JSON. Reproduction SHA-256 values, including terminal newline:\n\n```text\nF1  6e51c65963827486f0a4e3786539877f60316e30ffe3634f697bb3346beb1ae5\nF2  42d1ac35dbb54e49717f934fba1782cc20bda7f6e535ae945191bfe089af48a8\n```\n\n**Prior eight-finding disposition**\n\nThese dispositions concern the original failing cases, separately from F1’s additional ownership defect.\n\n| Prior item | Disposition | Fresh reviewer evidence |\n|---|---|---|\n| 1. Passive provider discovery | Fixed in exercised cases | First/late/second-provider announcements and reannouncements preserve the session and selected provider. Explicit selection/account changes retain cleanup. |\n| 2. Overlapping","treeHash":null,"usage":{"cachedInputTokens":4602240,"inputTokens":225680,"model":"gpt-6-astra","outputTokens":21269,"runtime":"codex","turns":7,"wallClockMs":780450}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"09078b7cdfb673fe","findings":[{"citation":"resolved","description":"Audit7 finding 1 was repaired for the EIP-6963 discovery path: bind(p,discovery=true,previousAccount) only treats an eth_accounts answer as an identity change when a previous wallet account was actually observed (auth.ts:282, 'only a proven account change owns cleanup'), so a restored session A plus a first-seen wallet account B shows 'mismatch' and sends nothing. The accountsChanged path has no such guard. accountChanged(a) computes `other` purely from the displayed session and the incoming account; with s.account===null (wallet locked at page load, eth_accounts answered []) the first account the wallet ever reports is treated as an A->B switch: gen++, session cleared, automaticCleanup('account-switch',...,held,true) sends POST /api/auth/logout {expectedAddress:A} (204), the server revokes A's live row, and 'signed-out' is broadcast to sibling tabs. Nothing changed: no account was ever established for this page. MetaMask-class wallets answer eth_accounts [] while locked and emit accountsChanged([selected]) on unlock, so a user who returns with a valid 7-day cookie and unlocks a wallet whose selected account differs from the session loses the session server-side, the same class of unintended revocation as Audit7 #1, reached through a different transport. Fix: in accountChanged, treat an incoming account with no previously observed account (this.s.account===null and no click/owner context) the way the discovery path does: set the account, show mismatch, and leave cleanup to a proven change or an explicit action.","line":583,"path":"source/src/world/auth.ts","reproduction":"Fresh checkout, Node 24.19.0, source/: npm ci --ignore-scripts. Script using tests/auth-r7-fixtures.mjs (run with node): w=setup(); A,B=newAccount(); b=w.browser(); p=provider(null) /* eth_accounts -> [] */; b.signIn(A).verify.status===200; q=tab(w,b,p); await until(restored&&session); assert q.c.state.account===null; rows before {created:1,live:1,revoked:0}; p.switchTo(B) /* accountsChanged([B]) */; await flush(40). MEASURED: logouts=[{address:true,nonce:false,status:204}], rows after {created:1,live:0,revoked:1}, state.session=null, state.account=B, cleanupPlans=[{reason:'account-switch',kind:'displayed-session'}], broadcast=['signed-out'], prompts 0, challenge 0, verify 0. CONTROL (same state, discovery path): getProvider:()=>chosen with chosen=null, then chosen=provider(B); q.notifyProvider('discovery'); await account===B. MEASURED: logouts=[], rows {created:1,live:1,revoked:0}, state.session=A (mismatch shown), plans=[], broadcast=[]. Expected: both first observations behave like the control (session kept, no logout, no broadcast); actual: the unlock path revokes A's row.","severity":"low","snippet":"    const other=!!a&&!!this.s.session&&this.s.session.address!==a;","title":"Wallet locked at page load that unlocks to another account revokes the cookie-restored session, unlike the equivalent passive-discovery observation (Audit7 #1 partial)"},{"citation":"resolved","description":"Fix 2 makes same-context waiters chain onto the pending ProofFlight and re-evaluate after it settles (ownership.ts:234-237). Each re-evaluation re-asks the candidates Cache with ttl=OWNERSHIP_TTL_MS (30 s) for fresh=1, measured against the entry's `at`, which Cache.get sets when the load STARTS (ownership.ts:202), and the discovery keep predicate compares d.indexed.at (the index-read start, line 268) against the live clock with the same 30 s bound (line 284). When budget admission + index read + proof take >= 30 s of live clock (the pinned CHAIN_TIMEOUT_MS is 10 s per page and NFT_PAGE_CAP is 5, so a 3-page read at the timeout already crosses it), the first waiter re-evaluates after the predecessor completes, finds the just-completed answer 'too old' for fresh, charges chain:index again and reads the index again, and the next waiter does the same after it. Every waiter also starts a new proof epoch at latest because the previous epoch is already past OWNERSHIP_TTL_MS. The cohort that fix 2 says shares 'one index read, one chain-index budget charge and one proof' instead costs N of each, serially, and the last waiter answers after N x (index time) (10 minutes measured for N=20). The amplification is bounded only by the per-location chain:index limiter (20/min), which such a cohort then exhausts for every other address at that location (their reads become 'limited'). Ordinary (300 s) cohorts are unaffected. Fix: let a waiter that joined while the predecessor's index read was in flight accept that read's completion as its cohort answer (date the fresh window from completion, or carry a 'satisfied by flight X' marker), instead of re-admitting against the start timestamp.","line":266,"path":"source/server/ownership.ts","reproduction":"Same fixture shape as tests/ownership-audit8.test.mjs advancingFixture (Ownership with offline gateway/chain fixtures, request.clock=()=>state.live, budget advances live by 30 ms, ownerOf advances 100 ms) but the index read advances the live clock by 30_000 ms (or 29_900 ms). 20 overlapping ownership.home(A, request(START+i), fresh=true) with the first index read gated until all 20 have entered. MEASURED (index 30 s): budget=20, index=20, rpc=20, 20 distinct checkedAt epochs, all 20 views eligible=1/recheck none, live clock advanced 602,600 ms. MEASURED (index 29.9 s): budget=20, index=20, rpc=20. CONTROLS in the same script: 20 ordinary readers with the 30 s index read -> budget=1,index=1,rpc=1, 1 epoch, 30,130 ms; mixed 10 ordinary + 10 fresh with a 200 ms index read -> budget=1,index=1,rpc=1, 1 epoch. Expected per the fix statement: 1/1/1 for the fresh cohort as well; actual: 20/20/20.","severity":"low","snippet":"          const indexed=await this.candidates.get(address,current(),fresh?OWNERSHIP_TTL_MS:CANDIDATES_TTL_MS,async()=>{","title":"Overlapping fresh=1 home cohort behind a slow NFT index read serialises into one budget charge, index read and proof epoch per waiter (Audit7 #2 partial)"},{"citation":"resolved","description":"file(name) only inspects an existing output path when existsSync(path) is true. existsSync follows symlinks, so a symlink whose target does not exist yet reports false, the lstat/isSymbolicLink check is skipped, and writeFileSync(path,...) then follows the link and CREATES the target outside source/tmp. The directory-level validate() walks only the components of the artifact directory, not the output file itself, so it does not catch this either. AUDIT8_REVIEW_RUNNER.md states that 'a symlink/junction escape, or a non-regular output file is rejected'; that holds for symlinks to existing files/directories but not for dangling ones, which is the natural state of a pre-planted link (the attacker's target file does not exist until the write). Impact is local-only (a reviewer machine with a hostile pre-planted link in an opt-in directory), hence low, but it directly contradicts the fix-8 guarantee. Fix: use lstatSync(path,{throwIfNoEntry:false}) (or fs.lstat in a try) and reject any existing lstat entry that is not a regular file, and open with O_NOFOLLOW/'wx' semantics (e.g. fs.openSync(path,'wx') after unlinking only a verified regular file).","line":28,"path":"source/tests/auth-artifacts.mjs","reproduction":"Node 24.19.0, fixture outside the tree: mkdir -p /tmp/fx/src/tmp/auth-reference-scheduler /tmp/fx/outside; symlinkSync('/tmp/fx/outside/escaped.json','/tmp/fx/src/tmp/auth-reference-scheduler/core500-RESULT.json') (target absent); store=createArtifactStore({sourceDir:'/tmp/fx/src',requestedDir:'tmp/auth-reference-scheduler'}); store.write('core500-RESULT.json',{status:'PASS',note:'escape'}). MEASURED: write threw: null; target exists before write: false; after write: true; /tmp/fx/outside/escaped.json contains {\"status\":\"PASS\",\"note\":\"escape\"}; the namespace entry is still a symlink. Expected: throw 'artifact output is not a regular file' and write nothing outside source/tmp. CONTROL: a symlink to an EXISTING file or directory is rejected as documented (tests/auth-artifacts.test.mjs covers only the existing-directory case).","severity":"low","snippet":"    validate();const path=join(directory,name);if(existsSync(path)&&(!lstatSync(path).isFile()||lstatSync(path).isSymbolicLink()))throw new Error('artifact output is not a regular file');return path;","title":"Opt-in artifact store writes through a dangling symlink inside the scheduler namespace, escaping source/tmp (Audit7 #8 partial)"},{"citation":"resolved","description":"The string sanitizer is the last line of defence for the fix-8 claim that persisted artifacts expose no private machine path. It is a prefix allow-list, not a path detector: checkouts under /root (Docker/CI default), /var/lib/<ci>/workspace, /opt, /srv, /workspace, /data or /mnt/<drive>/<non-Users> are not masked, and WSL paths such as /mnt/c/Users/... are only partially masked ('/mnt/c[local-path]'). Today the only strings that reach the artifact files are oracle messages and failure.message/detail from the driver, which currently carry no absolute paths (my opt-in run: 22 files, 0 matches for /home/, /tmp/, drive letters, sourceRoot, runtimeExe or cwd), so this is informational: a future assertion message or a Node error text containing a file URL would leak under those roots. Fix: mask any absolute POSIX path segment that resolves inside the checkout (replace the realpath of sourceDir and its parents) rather than enumerating home-directory prefixes.","line":10,"path":"source/tests/auth-artifacts.mjs","reproduction":"node --input-type=module -e \"import {sanitizeArtifact} from './source/tests/auth-artifacts.mjs'; for(const s of ['/home/u/src/x.mjs','/root/work/source/tests/x.mjs','/var/lib/ci/src/x.mjs','/opt/build/x.mjs','/srv/jobs/x.mjs','/mnt/c/Users/u/x.mjs'])console.log(s,'->',sanitizeArtifact(s))\". MEASURED: '/home/u/src/x.mjs' -> '[local-path]'; '/root/work/source/tests/x.mjs' -> unchanged; '/var/lib/ci/src/x.mjs' -> unchanged; '/opt/build/x.mjs' -> unchanged; '/srv/jobs/x.mjs' -> unchanged; '/mnt/c/Users/u/x.mjs' -> '/mnt/c[local-path]'. Expected: every absolute machine path masked.","severity":"info","snippet":"  if(typeof value==='string')return value.replace(/(?:[A-Za-z]:[\\\\/]|\\/(?:Users|home|tmp)\\/)[^\\s\"'<>|]+/g,'[local-path]');","title":"sanitizeArtifact masks only /home, /Users, /tmp and drive-letter paths; other absolute machine paths pass through verbatim"},{"citation":"resolved","description":"AUDIT8_REVIEW_RUNNER.md and TEST_RESULTS.json ('viemRealPackage': true) present `npm run test:review -- --check` as verification of 'the actual pinned viem package'. checkReviewPrerequisites only compares three version strings. A directory containing nothing but {\"name\":\"viem\",\"version\":\"2.56.9\"} passes --check with the same REVIEW_PREREQUISITES line a genuine install prints. The product tests would then fail at import time, so a stub cannot fake a green run, but the --check receipt itself is not evidence of a real locked package, and the published TEAM claim rests on it. Informational: the independent reviewer's own `npm ci --ignore-scripts` (which verifies the lockfile's sha512 integrity) is what establishes the real package, as it did here. Fix: compare the installed package's `_integrity`/package.json `dist.integrity` or hash the installed tree against lock.packages['node_modules/viem'].integrity before printing the receipt.","line":25,"path":"source/scripts/review-tests.mjs","reproduction":"Copy package.json, package-lock.json and scripts/review-tests.mjs into an empty directory, create the 23 listed test paths as empty files, write node_modules/viem/package.json = {\"name\":\"viem\",\"version\":\"2.56.9\"} and nothing else, then run `node scripts/review-tests.mjs --check`. MEASURED: prints REVIEW_PREREQUISITES {\"node\":\"v24.19.0\",\"viem\":\"2.56.9\",\"testFiles\":23} and exits 0, identical to the genuine checkout's receipt. Expected: a mismatch/missing-integrity error.","severity":"info","snippet":"  if(version!==pkg.dependencies.viem||version!==lock.packages['node_modules/viem']?.version)throw new Error('Local viem differs from package.json/package-lock.json; run npm ci --ignore-scripts');","title":"Review runner prerequisite accepts any node_modules/viem whose package.json version string matches; it does not verify the locked package bytes"},{"citation":"resolved","description":"Fix 3 keeps a lock from revoking a committed verify (planCleanup('lock') -> 'reconcile'; AUDIT8_CLOSURE.md 'Lock is not logout'). But cancelOwners('stop') in the teardown overwrites the owner's cancellationReason from 'lock-reconcile' to 'stop' (authLifecycle.ts abandon() re-sets the reason on an already-abandoned RETAINED owner), and planCleanup('stop') then selects 'verify-owner', so revokeAbandoned sends POST /api/auth/logout {expectedNonce} and the server revokes the session this very flow committed. The revocation is correctly scoped (same lifetime, nonce-conditional, no cross-revocation of another row was observed) and fail-safe, so this is not a security defect; it is a product/policy outcome the closure documents do not state: a user whose wallet auto-locks during verify while /api/auth/session is 503 and who then navigates away must sign again next visit even though their verify succeeded. Listed so the requester can record it as accepted policy or decide that an unreconciled lock owner should be left for the next life's canonical read (LOW-4 restart path) rather than revoked.","line":23,"path":"source/src/world/authCleanup.ts","reproduction":"tests/auth-r7-fixtures.mjs: provider(A); hold POST /api/auth/verify in beforeSend; after it is dispatched, p.switchTo(null) (lock), make GET /api/auth/session answer 503, release the verify (200, cookie set). Wait until lifecycleSnapshot.cleanup.status==='RETAINED' && cancellationReason==='lock-reconcile'. MEASURED before stop: rows {created:1,live:1,revoked:0}, logouts 0. Then q.stop() (page teardown) and flush. MEASURED after stop: logouts=[{nonce:true,address:false,status:204}], rows {created:1,live:0,revoked:1}, owner status CONSUMED with cancellationReason 'stop', cleanupPlans [lock:reconcile, verify-settled:reconcile, stop:verify-owner]; a subsequent GET /api/auth/session with the same cookie jar answers {signedIn:false}. Compare the documented row 'Lock, no active click: committed row survives lock and stop'.","severity":"info","snippet":"  if(owner)return {kind:'verify-owner',expectedNonce:owner.nonce};","title":"Policy decision to confirm: an unreconciled lock-reconcile verify owner is revoked by page teardown ('stop'), so a committed same-account session is lost when a lock coincides with a failed session re"}],"hash":"23c76103818771a6ebcb4092b2a35076b0a29b47baa27eda4bdd976c9e1ec98b","nodeId":"6c214d2c-6612-4c06-83ad-6b10e68f5a8d","outcome":"completed","summary":"All six findings validate against the tree and the checkout itself is untouched. Here is the review.\n\n## Verdicts\n\n**SOURCE-CLOSURE: partial.** Of the eight Audit7 rows, five are fixed in the pinned source (3, 4, 5, 6, 7). Three are partial: row 1 (a locked wallet that unlocks to another account still revokes a restored session), row 2 (a fresh=1 cohort behind a slow index read still amplifies budget, index and proof work) and row 8 (a dangling symlink escapes source/tmp). None of the three is a wrong Auth or ownership authority, an unintended prompt, an old-flow cross-revocation, an unbounded keyed RPC or an expanded method or header. Nothing found blocks closure as a security matter, but rows 1, 2 and 8 cannot be marked fixed as stated.\n\n**RELEASE-READINESS: unknown.** The public suite, the 500-seed and 90-seed schedulers and the artifact policy all reproduce on my machine. Live deployment correspondence, bindings, migrations and WAF behaviour are TEAM readbacks in `Submission8/BUILD_DEPLOYMENT.json` that I could not measure under the no-live-request rule. The canonical deploy's overall exit 1 is a record-rename EPERM after Wrangler exit 0 and is documented honestly. The full private frontend build is not publicly reproducible.\n\n## Findings\n\n| # | Sev | Location (pinned 88c1302) | Audit7 row | Disposition | Blocker |\n|---|---|---|---|---|---|\n| F1 | Low | `source/src/world/auth.ts:583` | 1 | partial | no |\n| F2 | Low | `source/server/ownership.ts:266` | 2 | partial | no |\n| F3 | Low | `source/tests/auth-artifacts.mjs:28` | 8 | partial | no |\n| F4 | Info | `source/tests/auth-artifacts.mjs:10` | 8 | accepted limit | no |\n| F5 | Info | `source/scripts/review-tests.mjs:25` | runner | accepted limit | no |\n| F6 | Info | `source/src/world/authCleanup.ts:23` | 3 | policy decision | no |\n\nRows 3, 4, 5, 6 and 7: fixed. Each reproduced through its own test file in my run and through my extra probes listed in the appendix.\n\n**F1. Unlock to a different account revokes a restored session.** Event order: cookie session A restored, wallet locked at load (eth_accounts answers empty), then accountsChanged([B]). Measured: one POST /api/auth/logout with expectedAddress, status 204, sessions row A revoked, cleanup plan `account-switch/displayed-session`, broadcast `signed-out`, 0 prompts, 0 challenges, 0 verifies. Control with the same state through passive discovery: 0 logouts, A kept, mismatch shown. The guard added for discovery at line 282 has no counterpart on the accountsChanged path, so a first-ever observation is treated as an A to B switch.\n\n**F2. Fresh cohort behind a 30 s index read.** 20 overlapping fresh=1 reads with the index read costing 30 s of live clock: budget 20, index 20, RPC 20, 20 proof epochs, last reader answered after 602.6 s. Same at 29.9 s. Controls: 20 ordinary readers with the same slow read give 1/1/1, and a mixed 10 ordinary plus 10 fresh cohort with a 200 ms read gives 1/1/1. The fresh window is measured from the load start, so each chained waiter re-admits. Bounded only by the per-location chain:index limiter, which the cohort then exhausts for other addresses.\n\n**F3. Dangling symlink write-through.** A symlink named `core500-RESULT.json` whose target does not exist passes the existsSync gate, and the write creates the target outside source/tmp. Measured in a /tmp fixture: no throw, target created with the artifact body. Symlinks to existing files or directories are rejected as documented.\n\n**F4, F5, F6** are informational: the path sanitizer only masks /home, /Users, /tmp and drive letters (my opt-in run leaked nothing); the `--check` receipt passes for a version-only viem stub (my own `npm ci` with lockfile integrity is what established the real package); teardown revokes an unreconciled lock owner's own session, correctly scoped to its nonce, which the docs should state as policy.\n\n## Reproductions and coverage appendix\n\nReviewer measurements, fresh checkout at `88c130283efc45260f9e00da8d2d3055c38483bd`, Nod","treeHash":null,"usage":{"cachedInputTokens":3127123,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":70293,"runtime":"claude","turns":60,"wallClockMs":960328}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"34d50a3af6e87087","findings":[{"citation":"resolved","description":"OPEN; SOURCE-CLOSURE blocker despite Low severity: household/owner UI accepts expired ownership evidence (no asset transfer demonstrated). On pinned public 88c130283efc45260f9e00da8d2d3055c38483bd, proof() checks freshness at 307-308, then home() awaits sightings (337), and potentially lane work (347-349), without validating proof.checkedAt again before returning at 350-352. AuthClient/statusOf accepts the resulting home as owner. A session-A home at checkedAt+29999 ms reuses seat-7 proof; 1 ms of sightings latency crossing the strict deadline suffices to return eligible=1 at age 30000 after the fixture owner transfers to B. Related prior Audit7 #2/#5: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (captured SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0, independently matched). This is an additional scoped path, not a failure of the exact supplied repair test. Recheck the live clock after all awaited enrichment/lane work, and reprove at latest or fail unavailable on expiry/rollback; do not return an expired fallback. This is an offline reviewer measurement, not live D1 evidence. The complete home() body was also byte-compared with the previous public 7215c5d89a96bc79113a85766c04868d54393f3c via its raw public source and is unchanged: this is pre-existing scoped behavior, not an asserted Submission8-introduced regression.","line":337,"path":"source/server/ownership.ts","reproduction":"Fresh pinned checkout, Node v24.21.0, npm ci --ignore-scripts with real locked viem 2.56.9. Run the following from source/ with node --input-type=module via stdin. It asserts actual defective behavior and exits 0. At added sightings latency 0/1/2 ms: HTTP 200, proof age 29999/30000/30001, eligible=1, status=owner, index/budget/proof RPC=1/1/1, session rows created/live/revoked=1/1/0, challenge rows total/used/pending/invalidated=1/1/0/0. Setup challenge/verify=1/1; measured RPC methods contain only the ownership eth_call, no eth_getCode; UI prompts/cleanup/hints=0. For the expired cases the next request returns eligible=0 and total proof RPC=2. Expected at age >=30000: latest reproof returning zero or OWNERSHIP_UNAVAILABLE, never expired owner authority. Reproduction JavaScript SHA256 (UTF-8, including final newline): 0f0ee4f1cad69bac80881b67f9e575d2f969b24f14ff4d6676361d5fffe7e4e1.\n\nimport assert from 'node:assert/strict';\nimport {setup,newAccount,fakeChain,fakeImd} from './tests/wallet-harness.mjs';\nimport {MULTICALL3,ALCHEMY_NFTS_URL} from './server/ownership.ts';\nimport {INITIAL,statusOf} from './src/world/auth.ts';\nfor(const delay of [0,1,2]){\n const account=newAccount(),a=account.address.toLowerCase(),owners=[];owners[7]=a;\n const chain=fakeChain({owners:{7:a}}),w=setup({chain,imd:fakeImd({seats:{7:'707'},owners,online:[7]})}),b=w.browser();\n let budget=0;w.env.CHAIN_LIMITER={limit:async({key})=>{if(key==='chain:index')budget++;return {success:true};}};\n assert.equal((await b.signIn(account)).verify.status,200);\n const initial=await (await b.get('/api/me/home')).json();assert.equal(initial.eligible,1);\n w.clock.advance(29999);\n const prepare=w.db.prepare.bind(w.db);let held=true;\n w.db.prepare=sql=>{const wrap=s=>({...s,bind:(...args)=>wrap(s.bind(...args)),all:async()=>{\n   if(held&&sql.startsWith('SELECT token_id,last_online_at')){held=false;chain.state.owners[7]='0x'+'2'.repeat(40);w.clock.advance(delay);}\n   return s.all();}});return wrap(prepare(sql));};\n const r=await b.get('/api/me/home'),home=await r.json();\n const status=statusOf({...INITIAL,session:{address:a,expiresAt:w.clock.now()+600000},sessionKnown:true,home},w.clock.now());\n const rpc=()=>chain.state.calls.filter(c=>c.body&&JSON.parse(c.body).method==='eth_call'&&JSON.parse(c.body).params[0].to===MULTICALL3).length;\n console.log(JSON.stringify({delay,age:w.clock.now()-home.checkedAt,http:r.status,eligible:home.eligible,status,proofRpc:rpc(),rpcMethods:chain.state.calls.filter(c=>c.body).map(c=>JSON.parse(c.body).method),\n index:chain.state.calls.filter(c=>c.url.startsWith(ALCHEMY_NFTS_URL+'?')).length,budget,\n sessions:w.db.raw.prepare('SELECT count(*) created,sum(revoked_at IS NULL) live,sum(revoked_at IS NOT NULL) revoked FROM sessions').get(),\n challenges:w.db.raw.prepare('SELECT count(*) total,sum(used_at IS NOT NULL) used,sum(used_at IS NULL AND invalidated_at IS NULL) pending,sum(invalidated_at IS NOT NULL) invalidated FROM login_challenges').get()}));\n assert.equal(home.checkedAt,initial.checkedAt);assert.equal(home.eligible,1);assert.equal(status,'owner');\n if(delay){const next=await (await b.get('/api/me/home')).json();assert.equal(next.eligible,0);assert.equal(rpc(),2);\n console.log(JSON.stringify({delay,control:'next request',eligible:next.eligible,proofRpc:rpc()}));}\n}\n","severity":"low","snippet":"    let proof=await this.proof(a,world.owners,world.agents,req,fresh),seen=await this.sightings(proof.ids,a,req.db);","title":"Home returns owner authority after the proof expires during post-proof D1 work"},{"citation":"resolved","description":"PARTIAL Audit7 #1; SOURCE-CLOSURE blocker for unintended session revocation. Passive bind/discovery at lines 280-285 correctly requires a previously observed wallet account before cleanup. accountChanged instead compares the first observed account only against the displayed cookie session. With a restored session A, initially locked provider eth_accounts=[], no click and no retained owner, accountsChanged([B]) is treated as an A-to-B switch even though the page never observed account A. Lines 593-597 select displayed-session cleanup: POST /api/auth/logout with expectedAddress A revokes the live row and broadcasts signed-out. The same first account B observed by passive discovery preserves A and shows mismatch. Require an established prior account or active flow context before classifying first unlock as an account switch; preserve cleanup for a proven A-to-B change. Pinned public 88c130283efc45260f9e00da8d2d3055c38483bd. Prior: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (#1; SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0 independently verified). Real AuthClient/Worker/SQLite offline reproduction; no real extension claim.","line":583,"path":"source/src/world/auth.ts","reproduction":"Node v24.21.0 and genuine locked viem 2.56.9 installed via npm ci --ignore-scripts. Run below from source/ with node --input-type=module on stdin (exit 0 asserting defective behavior). unlock-first: session created/live/revoked goes 1/1/0 -> 1/0/1, challenge total/used/pending/invalidated remains 1/1/0/0, one address-conditional logout 204, zero nonce cleanup, plan account-switch:displayed-session, one signed-out broadcast. No post-setup prompts/challenge/verify; setup challenge/verify=1/1. Discovery-first control preserves 1/1/0, no cleanup or broadcast. Observed-switch control first binds A then emits B and correctly revokes once. Default empty-world fixture needs no ownerOf proof (no seat candidates). Expected first-unlock behavior matches discovery-first, while genuine observed switch remains fenced. Additional measured upstream counts: one index fetch before the event, no RPC methods, unchanged after the event. Thus the event adds zero index/budget/proof RPC work; emitted hint/broadcast count is one for unlock-first and observed-switch, zero for discovery-first. Reproduction JavaScript SHA256 (UTF-8, including final newline): fb0c6cf88a50c39000fb01a037499bd866b9226c083dc079e9d19d4b3635342a.\n\nimport assert from 'node:assert/strict';\nimport {setup,newAccount,provider,tab,until,flush,rows,logouts,routeEvents,prompts} from './tests/auth-r7-fixtures.mjs';\nfor(const mode of ['unlock-first','discovery-first','observed-switch']){\n const w=setup(),A=newAccount(),B=newAccount(),b=w.browser(),p=provider(mode==='observed-switch'?A:null);\n assert.equal((await b.signIn(A)).verify.status,200);\n let chosen=mode==='discovery-first'?null:p;\n const q=tab(w,b,p,{getProvider:()=>chosen});\n await until(()=>q.c.state.restored&&q.c.state.session&&!q.c.state.checking);\n if(mode!=='observed-switch')assert.equal(q.c.state.account,null);\n const before=rows(w).counts;\n const beforeCalls=w.chain.state.calls.length;\n if(mode==='discovery-first'){chosen=provider(B);q.notifyProvider('discovery');}else p.switchTo(B);\n await flush(40);\n const result={mode,before,after:rows(w).counts,sessionRetained:!!q.c.state.session,\n   plans:q.c.lifecycleSnapshot.cleanupPlans,logouts:logouts(q).map(e=>({address:e.addressAssertion,nonce:e.nonceAssertion,status:e.status})),\n   broadcast:q.channels.flatMap(c=>c.messages),prompts:prompts([p]),challenge:routeEvents(q,'/api/auth/challenge').length,\n   verify:routeEvents(q,'/api/auth/verify').length,\n upstreamCallsBeforeEvent:beforeCalls,upstreamCallsAfterEvent:w.chain.state.calls.length,\n rpcMethods:w.chain.state.calls.filter(c=>c.body).map(c=>JSON.parse(c.body).method),indexFetches:w.chain.state.calls.filter(c=>c.url.includes('getNFTsForOwner?')).length};\n console.log(JSON.stringify(result));\n assert.equal(rows(w).counts.live,mode==='discovery-first'?1:0);\n assert.equal(logouts(q).length,mode==='discovery-first'?0:1);q.stop();\n}\n","severity":"low","snippet":"    const other=!!a&&!!this.s.session&&this.s.session.address!==a;","title":"First accountsChanged observation from a locked wallet revokes the cookie-restored session"},{"citation":"resolved","description":"PARTIAL Audit7 #2; blocks an all-eight-fixed SOURCE-CLOSURE verdict under the specified overlapping fresh=1 cohort requirement. Successful waiters recurse at 234-237. Candidate cache entry at 202 and index at 268 are dated before completion; the fresh keep predicate at 284 rejects the just-completed index when discovery plus proof consumes >=30000 ms. Twenty already-overlapping same-address fresh requests then each admit a new index cycle and start a new proof epoch. This is achievable within configured timeouts: four 7500-ms pages each below CHAIN_TIMEOUT_MS=10000, within NFT_PAGE_CAP=5, total 30000 ms; no single 30-second network fetch is needed. Measured 20 index cycles, 80 page fetches, 20 chain:index charges, 20 owner-proof RPCs and 20 checkedAt epochs, 602600 ms injected elapsed. It is bounded by existing request/admission controls; this does NOT demonstrate an unbounded/global RPC bypass or exhaustion of the 20-per-minute limiter (the measured slow cohort spans minutes). Preserve index producer age and strict proof TTL, while recognizing the completed discovery flight as satisfying waiters that joined that cohort, instead of recursively re-admitting them solely because its start timestamp aged. Prior #2: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0). Pinned public 88c130283efc45260f9e00da8d2d3055c38483bd. Offline injected-clock measurement, not production latency.","line":266,"path":"source/server/ownership.ts","reproduction":"Run below in source/ on Node v24.21.0 with locked genuine viem 2.56.9, via node --input-type=module stdin. Exit 0 asserting observed behavior. Gate first page until all 20 Ownership.home(A, req(START+i), fresh) calls have entered. Budget advances live clock by 30 ms, each page by pageMs, each proof by 100 ms. Four-page fresh controls at 7475 and 7500 ms/page give budget/indexCycles/pageFetches/RPC/epochs = 20/20/80/20/20, elapsed 600600/602600 ms, all eligible=1/complete. Same four-page ordinary cohort gives 1/1/4/1/1 and 30130 ms. A 200-ms single-page fresh cohort gives 1/1/1/1/1 and 330 ms. Expected: one shared discovery admission/cycle and proof for the already-overlapping fresh cohort too (four network pages for one cycle). Direct Ownership fixture: no session/challenge rows, prompts/challenge/verify/cleanup/hints all zero; address A and ownerOf are fixture inputs, not an auth bypass. Reproduction JavaScript SHA256 (UTF-8, including final newline): b27ee42b60115e8e6f062b264d062c14a31e634b504966456ec791dc141876dc.\n\nimport assert from 'node:assert/strict';\nimport {decodeFunctionData,encodeFunctionResult,encodeAbiParameters,multicall3Abi} from 'viem';\nimport {Ownership,ALCHEMY_RPC_URL,ALCHEMY_NFTS_URL,MULTICALL3} from './server/ownership.ts';\nimport {SEAT_COLLECTION} from './src/world/market.ts';\nconst A='0x'+'1'.repeat(40),START=Date.UTC(2026,9,4,12),BLOCK=21000000n;\nconst abi=[{type:'function',name:'ownerOf',stateMutability:'view',inputs:[{name:'tokenId',type:'uint256'}],outputs:[{name:'',type:'address'}]}];\nconst defer=()=>{let resolve;return {promise:new Promise(r=>resolve=r),resolve};},tick=()=>new Promise(r=>setImmediate(r));\nfor(const [fresh,pageMs,pages] of [[false,7500,4],[true,200,1],[true,7475,4],[true,7500,4]]){\n const s={live:START,budget:0,index:0,pageFetches:0,rpc:0},gate=defer(),entered=defer();\n const gateway={async source(name){const owners=[];owners[7]=A;return {state:'fresh',fetchedAt:s.live,url:'fixture://'+name,\n data:name==='swarm'?{at:1,seats:{7:{tokenId:7,agentId:'707'}},owners}:{count:1,workers:[{seat:{tokenId:'7',agentId:'707'},working:0,runtimes:[],lastHeartbeatAt:'2026-10-04T11:59:00Z'}]}};}};\n const fetcher=async(input,init={})=>{\n  if(String(input).startsWith(ALCHEMY_NFTS_URL+'?')){\n   const page=Number(new URL(input).searchParams.get('pageKey')??0);if(page===0)s.index++;s.pageFetches++;s.live+=pageMs;\n   if(s.pageFetches===1){entered.resolve();await gate.promise;}\n   return Response.json({ownedNfts:[{contract:{address:SEAT_COLLECTION},tokenId:'7'}],pageKey:page+1<pages?String(page+1):null});\n  }\n  assert.equal(String(input),ALCHEMY_RPC_URL);s.rpc++;s.live+=100;\n  const calls=decodeFunctionData({abi:multicall3Abi,data:JSON.parse(init.body).params[0].data}).args[0];\n  return Response.json({jsonrpc:'2.0',id:1,result:encodeFunctionResult({abi:multicall3Abi,functionName:'aggregate3',result:calls.map(c=>\n   c.target===MULTICALL3?{success:true,returnData:encodeAbiParameters([{type:'uint256'}],[BLOCK])}:{success:true,returnData:encodeFunctionResult({abi,functionName:'ownerOf',result:A})})})});\n };\n const o=new Ownership(gateway,[]),req=i=>({chain:{key:'offline-fixture',fetch:fetcher},now:START+i,clock:()=>s.live,budget:async()=>{s.budget++;s.live+=30;await tick();return true;}});\n const promises=Array.from({length:20},(_,i)=>o.home(A,req(i),fresh));\n await entered.promise;await tick();gate.resolve();\n const views=await Promise.all(promises),epochs=new Set(views.map(v=>v.checkedAt)).size;\n const expected=fresh&&pageMs*pages>=29900?20:1;\n console.log(JSON.stringify({fresh,pageMs,pages,budget:s.budget,indexCycles:s.index,pageFetches:s.pageFetches,rpc:s.rpc,epochs,elapsed:s.live-START,\n allEligible:views.every(v=>v.eligible===1),allComplete:views.every(v=>!v.recheck)}));\n assert.deepEqual([s.budget,s.index,s.rpc,epochs],[expected,expected,expected,expected]);\n}\n","severity":"low","snippet":"          const indexed=await this.candidates.get(address,current(),fresh?OWNERSHIP_TTL_MS:CANDIDATES_TTL_MS,async()=>{","title":"Slow paginated fresh home cohort serializes into repeated index admissions and proof epochs"},{"citation":"resolved","description":"PARTIAL Audit7 #8. existsSync follows the link and returns false when its target is absent, so file() does not lstat the existing symlink entry. writeFileSync at 32 then follows it and creates the target outside the permitted source/tmp. Directory validation does not inspect this final component. A deterministic preplanted dangling core500-RESULT.json symlink defeats the documented output containment without any race. Default-disabled output still works; no remote wallet/Worker authority is affected. Info severity reflects local opt-in artifact hygiene and a preexisting hostile filesystem entry. It prevents declaring the artifact-containment requirement fully closed. Use lstat independently of target existence and a no-follow/exclusive safe open strategy, checking directory components too. Pinned public 88c130283efc45260f9e00da8d2d3055c38483bd; policy source/docs/security/AUDIT8_REVIEW_RUNNER.md. Prior #8: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0). Duplicate flow/math reports merged.","line":28,"path":"source/tests/auth-artifacts.mjs","reproduction":"Node v24.21.0, pinned clean checkout. Run the script below from source/ with node --input-type=module stdin (exit 0). It creates a synthetic source S under the real source/tmp/reviewer-artifact-probe, links S/tmp/store/core500-RESULT.json to ../../escaped.json while the target is absent, and invokes createArtifactStore({sourceDir:S,requestedDir:'tmp/store'}).write(...). Expected: reject nonregular output and create nothing outside S/tmp. Actual: wrote=true, S/escaped.json created outside allowed S/tmp, output remains symlink, JSON trace unchanged. Control: second write through the same link is rejected once its target exists. All physical writes remain in the real source/tmp and are removed in finally. No session/challenge rows or prompt/challenge/verify/cleanup/hint/index/budget/RPC activity. Reproduction JavaScript SHA256 (UTF-8, including final newline): b688b0d47f181b6366fd38ca9ef4ab9596a3c15a13fe10480ed1d3a44d28b1a6.\n\nimport assert from 'node:assert/strict';\nimport {mkdirSync,symlinkSync,existsSync,readFileSync,rmSync,lstatSync} from 'node:fs';\nimport {join,resolve} from 'node:path';\nimport {createArtifactStore,sanitizeArtifact} from './tests/auth-artifacts.mjs';\nconst root=resolve('tmp/reviewer-artifact-probe');assert.equal(existsSync(root),false);\ntry{\n const dir=join(root,'tmp/store'),escaped=join(root,'escaped.json');mkdirSync(dir,{recursive:true});\n const output=join(dir,'core500-RESULT.json');symlinkSync('../../escaped.json',output);\n assert.equal(existsSync(output),false);assert.equal(lstatSync(output).isSymbolicLink(),true);\n const store=createArtifactStore({sourceDir:root,requestedDir:'tmp/store'});\n const wrote=store.write('core500-RESULT.json',{status:'PASS',trace:{actions:[{type:'start',tab:'a'}]}});\n console.log(JSON.stringify({wrote,escapedCreated:existsSync(escaped),outsideAllowedTmp:!escaped.startsWith(join(root,'tmp')+'/'),\n outputStillSymlink:lstatSync(output).isSymbolicLink(),escaped:JSON.parse(readFileSync(escaped,'utf8'))}));\n assert.equal(wrote,true);assert.equal(existsSync(escaped),true);\n assert.throws(()=>store.write('core500-RESULT.json',{status:'FAIL'}),/not a regular file/);\n console.log('existing-symlink control rejected');\n for(const p of ['/home/ci/source/x.mjs','/root/work/source/x.mjs','/var/lib/ci/source/x.mjs','/opt/build/x.mjs','/srv/jobs/x.mjs','/workspace/source/x.mjs','/Volumes/Work/source/x.mjs','/dev/shm/x.mjs','/mnt/c/Users/u/x.mjs']){\n   console.log(JSON.stringify({input:p,output:sanitizeArtifact({message:'Cannot find module '+p})}));\n }\n const next=createArtifactStore({sourceDir:root,requestedDir:'tmp/clean'});\n next.write('core500-RESULT.json',{message:'Cannot find module /root/private-project/source/x.mjs',sourceRoot:'/root/private-project/source',trace:{actions:[{type:'start',tab:'a'}]}});\n const v=JSON.parse(readFileSync(join(root,'tmp/clean/core500-RESULT.json'),'utf8'));\n assert.equal(v.message,'Cannot find module /root/private-project/source/x.mjs');assert.equal(v.sourceRoot,undefined);\n assert.deepEqual(v.trace.actions,[{type:'start',tab:'a'}]);console.log(JSON.stringify({persisted:v}));\n}finally{rmSync(root,{recursive:true,force:true});}\n","severity":"info","snippet":"    validate();const path=join(directory,name);if(existsSync(path)&&(!lstatSync(path).isFile()||lstatSync(path).isSymbolicLink()))throw new Error('artifact output is not a regular file');return path;","title":"Dangling scheduler output symlink bypasses source/tmp containment"},{"citation":"resolved","description":"PARTIAL Audit7 #8, separate mechanism from symlink containment. The string sanitizer enumerates only Windows drive prefixes and /Users/, /home/, /tmp/. Error/detail strings containing /root, /var/lib, /srv, /opt, /workspace, /Volumes or /dev/shm paths persist verbatim through createArtifactStore.write. WSL /mnt/c/Users/... is only partly removed. Hidden-key deletion works but does not cover arbitrary failure.message/detail strings. Info: a concrete sanitizer/output-contract failure and defense-in-depth gap; no current production secret leak or naturally emitted private-path failure is claimed. Default scheduler output remains disabled. Mask the actual checkout/machine roots or recognize absolute filesystem path tokens without altering replay action values. Pinned public 88c130283efc45260f9e00da8d2d3055c38483bd. Prior #8: https://github.com/Identity-md/research/blob/main/jobs/4e150a3c-3ee4-4856-972e-db5db4f4d3fc/files/AUDIT.md (SHA256 93ddeba22bd0dbcbff5a83f65bc48e9a373c7c2b8c3f6a848920a5fc7fa939a0). Duplicate flow/permissions reports merged. Non-authority issue; prevents the absolute no-private-path guarantee but not independently a remote release exploit.","line":10,"path":"source/tests/auth-artifacts.mjs","reproduction":"Run the shared artifact probe below from source/ on Node v24.21.0, node --input-type=module stdin (exit 0). sanitizeArtifact({message:'Cannot find module /root/work/source/x.mjs'}) returns the same path; /home/ci/source/x.mjs becomes [local-path]. /var/lib, /opt, /srv, /workspace, /Volumes and /dev/shm controls remain unmasked; /mnt/c/Users/u/x.mjs becomes /mnt/c[local-path]. createArtifactStore.write persists {message:'Cannot find module /root/private-project/source/x.mjs'} unchanged, while removing sourceRoot and preserving trace.actions exactly. Expected: no absolute machine path in persisted message. Strings are synthetic, not actual disclosed private paths. Temporary fixture is removed. No session/challenge rows; all auth/index/budget/RPC counts zero. Reproduction JavaScript SHA256 (UTF-8, including final newline): b688b0d47f181b6366fd38ca9ef4ab9596a3c15a13fe10480ed1d3a44d28b1a6.\n\nimport assert from 'node:assert/strict';\nimport {mkdirSync,symlinkSync,existsSync,readFileSync,rmSync,lstatSync} from 'node:fs';\nimport {join,resolve} from 'node:path';\nimport {createArtifactStore,sanitizeArtifact} from './tests/auth-artifacts.mjs';\nconst root=resolve('tmp/reviewer-artifact-probe');assert.equal(existsSync(root),false);\ntry{\n const dir=join(root,'tmp/store'),escaped=join(root,'escaped.json');mkdirSync(dir,{recursive:true});\n const output=join(dir,'core500-RESULT.json');symlinkSync('../../escaped.json',output);\n assert.equal(existsSync(output),false);assert.equal(lstatSync(output).isSymbolicLink(),true);\n const store=createArtifactStore({sourceDir:root,requestedDir:'tmp/store'});\n const wrote=store.write('core500-RESULT.json',{status:'PASS',trace:{actions:[{type:'start',tab:'a'}]}});\n console.log(JSON.stringify({wrote,escapedCreated:existsSync(escaped),outsideAllowedTmp:!escaped.startsWith(join(root,'tmp')+'/'),\n outputStillSymlink:lstatSync(output).isSymbolicLink(),escaped:JSON.parse(readFileSync(escaped,'utf8'))}));\n assert.equal(wrote,true);assert.equal(existsSync(escaped),true);\n assert.throws(()=>store.write('core500-RESULT.json',{status:'FAIL'}),/not a regular file/);\n console.log('existing-symlink control rejected');\n for(const p of ['/home/ci/source/x.mjs','/root/work/source/x.mjs','/var/lib/ci/source/x.mjs','/opt/build/x.mjs','/srv/jobs/x.mjs','/workspace/source/x.mjs','/Volumes/Work/source/x.mjs','/dev/shm/x.mjs','/mnt/c/Users/u/x.mjs']){\n   console.log(JSON.stringify({input:p,output:sanitizeArtifact({message:'Cannot find module '+p})}));\n }\n const next=createArtifactStore({sourceDir:root,requestedDir:'tmp/clean'});\n next.write('core500-RESULT.json',{message:'Cannot find module /root/private-project/source/x.mjs',sourceRoot:'/root/private-project/source',trace:{actions:[{type:'start',tab:'a'}]}});\n const v=JSON.parse(readFileSync(join(root,'tmp/clean/core500-RESULT.json'),'utf8'));\n assert.equal(v.message,'Cannot find module /root/private-project/source/x.mjs');assert.equal(v.sourceRoot,undefined);\n assert.deepEqual(v.trace.actions,[{type:'start',tab:'a'}]);console.log(JSON.stringify({persisted:v}));\n}finally{rmSync(root,{recursive:true,force:true});}\n","severity":"info","snippet":"  if(typeof value==='string')return value.replace(/(?:[A-Za-z]:[\\\\/]|\\/(?:Users|home|tmp)\\/)[^\\s\"'<>|]+/g,'[local-path]');","title":"Artifact sanitizer leaves absolute machine paths under common POSIX roots unmasked"}],"hash":"63293e0212a0e1bda71739653d1573ad98998d56f78aad4114e9602db9f9cfb1","nodeId":"407bcaa9-2ae5-4837-9405-d472691a5536","outcome":"completed","summary":"Saved **5 reproduced, deduplicated findings** to [.imd-findings.json](/root/.identitymd-2/work/7716c3f5-5d6c-4953-a643-141da678d051/407bcaa9-2ae5-4837-9405-d472691a5536/.imd-findings.json), including executable reproductions, exact source snippets and reproduction hashes. Candidate source files remain unchanged.\n\n**SOURCE-CLOSURE: BLOCKED.** The candidate partially closes Audit7 findings 1, 2 and 8; the other five targeted findings pass the reviewed controls. An additional expired-ownership response also reproduces.\n\n**RELEASE-READINESS: BLOCKED.** Source blockers remain, and independent production gates remain unknown. No live site/API requests or deployment operations were performed.\n\nReviewed public pin: `88c130283efc45260f9e00da8d2d3055c38483bd`; previous public: `7215c5d89a96bc79113a85766c04868d54393f3c`; declared frozen private source: `bb7549e0a2576ba4da0ea7c4147c4aba1a7f577f`.\n\n| ID | Severity | Pinned location | Finding and disposition |\n|---|---|---|---|\n| F1 | Low | [ownership.ts:337](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/server/ownership.ts#L337) | **Open; source blocker.** Proof freshness is checked before awaiting sightings, but not afterward. Home can return expired ownership and produce `owner` status. The `home()` body is unchanged from the previous public source: this is a pre-existing scoped defect. |\n| F2 | Low | [auth.ts:583](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/src/world/auth.ts#L583) | **Audit7 #1 partial; source blocker.** A locked provider’s first `accountsChanged([B])` revokes cookie-restored session A despite no previously observed wallet account. Passive discovery of the same B preserves A. |\n| F3 | Low | [ownership.ts:266](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/server/ownership.ts#L266) | **Audit7 #2 partial.** A slow, overlapping `fresh=1` cohort repeatedly admits index reads and proofs. Twenty callers produced twenty discovery cycles and twenty proof epochs. This blocks the required cohort-sharing closure claim; it does not establish an unbounded RPC bypass. |\n| F4 | Info | [auth-artifacts.mjs:28](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/tests/auth-artifacts.mjs#L28) | **Audit7 #8 partial.** `existsSync` misses a dangling output symlink; `writeFileSync` follows it outside the permitted directory. Default-disabled output works. Local opt-in containment defect. |\n| F5 | Info | [auth-artifacts.mjs:10](https://github.com/tungweb3/imd-ember-world-review/blob/88c130283efc45260f9e00da8d2d3055c38483bd/source/tests/auth-artifacts.mjs#L10) | **Audit7 #8 partial.** Absolute paths under `/root`, `/var/lib`, `/srv`, `/opt` and other roots persist unmasked in artifact strings. Concrete sanitizer failure; no naturally occurring private-path disclosure was observed in the scheduler run. |\n\nF1 needs a final freshness check after awaited enrichment and lane work. F2 needs to distinguish first account observation from an established account switch. F3 needs completed-flight reuse for existing waiters while preserving original timestamps and proof deadlines. F4 needs link-aware inspection and safe file opening; F5 needs broader path sanitization that preserves replay semantics.\n\n**Reproductions**\n\nAll retained reproductions ran against the pinned code on Node `v24.21.0`, using genuine locked `viem@2.56.9`. Each exited **0 while asserting the observed defective behavior**. Full JavaScript is preserved in the JSON.\n\n| ID | Trigger and measured result | Baseline/control |\n|---|---|---|\n| F1 | Sign in A; prove seat 7 at T; request home at T+29,999 ms; during sightings, transfer fixture ownership to B and advance 1 or 2 ms. Returned HTTP 200, `eligible=1`, `status=owner`, original `checkedAt`, at ages 30,000/30,001 ms. Sessions created/live/revoked: **1/1/0**; chal","treeHash":null,"usage":{"cachedInputTokens":3395712,"inputTokens":213949,"model":"gpt-6-astra","outputTokens":24432,"runtime":"codex","turns":7,"wallClockMs":638399}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2a6239e0c1b8c774","findings":[{"citation":"resolved","description":"Fix 6 replaced the zero-tolerance comparison with isFreshPublicRemote, whose lower bound is PUBLIC_REMOTE_SKEW_MS = 60000 (source/src/shared/freshness.ts:16). That bound was taken from the gateway's inter-Worker shared-copy policy, but here the clock being compared is the browser's device clock against a Worker-produced epoch. The project's own device-clock assumption is SIWE_CLOCK_SKEW_MS = 10 min (source/src/world/siwe.ts:9): a device 2 minutes slow can sign in, yet worldReadResult classifies each server-labelled 'fresh' snapshot as false, so startPoll runs the failure ladder 30 s / 60 s / 120 s forever (30 reads per hour instead of 4) and poll.failures climbs without bound. marketView (source/src/world/market.ts:117) applies the same bound, so the same device sees market state 'stale', weather 'unknown' and the floor card hidden although the Worker answered fresh data. This is the same failure mode as prior finding #6 with a narrower trigger (lag > 60.000 s instead of > 0 s); it is not an authority issue and does not block source closure. Suggested minimal fix: for public display/polling classify by the server's state and treat a negative age as 0 (or use a bound at least as large as SIWE_CLOCK_SKEW_MS); keep the strict helper for sessions/ownership/local caches unchanged.","line":62,"path":"source/src/world/cadence.ts","reproduction":"Node 24, in source/: node --input-type=module -e \"import {worldReadResult,startPoll} from './src/world/cadence.ts';import {marketView} from './src/world/market.ts';const s=at=>({state:'fresh',data:{priceUsd:8,change24h:12},url:'x',fetchedAt:at});let clock=2_000_000,p;const env={set:(fn,ms)=>{p={fn,ms};return p;},clear:()=>{},hidden:()=>false,now:()=>clock,onVisible:()=>()=>{}};for(const behind of [60_000,60_001,120_000]){const poll=startPoll(async()=>worldReadResult({swarm:s(clock+behind),workers:s(clock+behind)},clock),env);await new Promise(r=>setTimeout(r,0));const sched=[];for(let i=0;i<5;i++){sched.push(p.ms);clock+=p.ms;await p.fn();await new Promise(r=>setTimeout(r,0));}poll.stop();console.log(behind,sched,poll.failures,marketView(s(clock+behind),clock).state,marketView(s(clock+behind),clock).weather);}\" . Measured: 60000 -> [900000 x5], failures 0, fresh/brilliant (expected). 60001 -> [30000,60000,120000,120000,120000], failures 6, stale/unknown. 120000 -> same ladder, stale/unknown. Expected for a device merely slow by 1-10 minutes (within the project's SIWE device-clock allowance): 900000 ms cadence, failures 0, market fresh.","severity":"low","snippet":"    if(core.some(s=>s.fetchedAt===null||!isFreshPublicRemote(now,s.fetchedAt,Number.MAX_VALUE)))return false;","title":"Audit7 #6 partial: a browser clock more than 60 s behind the Worker stamp still turns every good snapshot into a failed read (permanent 120 s retry ladder) and hides fresh market/floor data"},{"citation":"resolved","description":"providerChanged('selection') treats every explicit pick as a context switch and plans 'displayed-session' cleanup whenever a session is displayed, including when this.bound was null (no provider had been used by the page). With two EIP-6963 wallets installed and no remembered choice, WalletRegistry.current() returns null, the page restores the session from the cookie and shows the chooser; the user's first pick of the wallet that holds the session's own address then sends POST /api/auth/logout {expectedAddress} and the live session row is revoked (server /api/auth/session answers signedIn:false). There was no account or provider to switch away from, no prompt, and the picked account equals the session address, so a user who merely answered 'which wallet' loses a valid session and must sign again. This is documented ('an explicit wallet pick remains a context switch, even for the same address') and pre-dates Submission8, so it is not a regression of the eighth candidate and not a closure blocker; it is reported so the owner can decide whether a first selection with no prior page-used provider should be exempt (e.g. treat selection with bound===null as discovery/bind only).","line":306,"path":"source/src/world/auth.ts","reproduction":"Node 24, in source/, script importing tests/auth-r7-fixtures.mjs and src/world/wallet.ts: create a WalletRegistry with win.ethereum=null, announce two providers (rdns io.example.one holding account A, io.example.two holding B) before Auth start; browser b signs in as A via the real Worker (verify 200, sessions live=1); create the AuthClient tab with provider:()=>registry.current() and onProviderChange:registry.subscribeProvider; await restored session. Observed: current()===null, needsChoice=true, state.session=A, state.account=null. Then registry.choose(options[0]) (the wallet holding A). Observed after 40 ticks: one POST /api/auth/logout with expectedAddress (204), cleanupPlans=[provider-switch:displayed-session], sessions created 1 / live 0 / revoked 1, client session null, account A, prompts 0, GET /api/auth/session -> signedIn:false. Expected (if the first pick is not a switch): zero logouts, live 1, session A retained, account A.","severity":"info","snippet":"    this.automaticCleanup('provider-switch',this.gen,this.life,abandoned,held);","title":"Policy decision to confirm: the first explicit wallet pick, when no wallet was yet in use, revokes a cookie-restored session even when the picked wallet holds that same address"}],"hash":"f0856d79833829c8de1b7a82b59e0880e885d20fa728656bf4b1ff1c5a8aa510","nodeId":"f5662003-fd7f-45d3-bad9-7131b335fff7","outcome":"completed","summary":"# IMD Ember World, eighth Swarm audit: Audit7 closure review of public `88c130283efc45260f9e00da8d2d3055c38483bd`\n\n**Verdicts.** SOURCE-CLOSURE: seven of the eight Audit7 rows are closed in the pinned source; row 6 (clock skew) is **partial**. Nothing I measured blocks closure: no wrong Auth or ownership authority, no unintended prompt or session, no old-flow cross-revocation, no unbounded keyed RPC, no new wallet method, route or header. RELEASE-READINESS: **unknown**. Every production fact (deployed Worker bytes, bindings, migrations, WAF, D1 behaviour) is a TEAM readback I could not measure, the full frontend cannot be built from the public tree, and the regression files for `wallet.ts`, `ownership.ts` and `worker/app.ts` do not load publicly.\n\nFindings are written to `.imd-findings.json` (one Low, one Info). Everything below separates **reviewer facts** (my runs), **TEAM/inherited** records, **inference** and **unavailable** checks.\n\n## Finding table\n\n| # | Severity | Finding | Pinned location | Disposition |\n|---|---|---|---|---|\n| F1 | Low | Prior #6 only partly closed: a browser clock more than 60.000 s behind the Worker stamp still classifies every server-fresh snapshot as a failed read. Measured schedule 30 s, 60 s, then 120 s forever (30 reads/hour instead of 4), `failures` climbs, market view goes `stale`/`unknown`, floor hidden. The project's own device-clock allowance is `SIWE_CLOCK_SKEW_MS` = 10 min. | `source/src/world/cadence.ts:62`, `source/src/world/market.ts:117`, bound at `source/src/shared/freshness.ts:16` | **partial / open**, not a blocker |\n| F2 | Info | Policy to confirm: first explicit wallet pick when no provider was in use (two wallets announced, no remembered choice, cookie-restored session for A) sends `POST /api/auth/logout {expectedAddress:A}` and revokes the live session, even though the picked wallet holds A. Documented as intended, pre-dates this candidate. | `source/src/world/auth.ts:306` | **policy decision**, not a regression |\n| P1 | note | Lock with retained uncertain verify owner, reconcile GET 503, then page stop: stop re-labels the owner `stop` and sends `logout {expectedNonce}`; server answers 204 and the committed session is revoked. Matches the documented row \"stop/unmount, RETAINED, original expectedNonce\" in `AUTH_STATE_MACHINE.md`. Same lifetime, same nonce, so not a cross-revoke. | `source/src/world/auth.ts:266-268`, `authCleanup.ts:23` | **policy decision** (accepted) |\n| P2 | note | Design change in fix 1: a second provider claiming the active wallet's rdns no longer forces the chooser; the page-used provider stays pinned and the duplicate is only flagged. Its test lives in `tests/wallet-client.test.mjs`, which does not load publicly (withheld `households.ts`). | `source/src/world/wallet.ts:69-75,86-87` | **accepted limit / inherited coverage** |\n| P3 | note | Artifact sanitizer masks only `/home/`, `/tmp/`, `/Users/` and drive-letter paths. `/opt/…`, `/var/…`, `/root/…` pass through unmasked. No artifact field I inspected carries an absolute path in practice, so this is a scope note only. | `source/tests/auth-artifacts.mjs:10` | **accepted limit** |\n\n## Closure matrix dispositions (reviewer measurements)\n\n| Row | Claim | What I ran | Counts observed | Disposition |\n|---|---|---|---|---|\n| 1 passive discovery | late or repeated announcements and same-address objects never revoke the restored session; explicit pick still switches | real `WalletRegistry` plus real Worker: `window.ethereum` in use, late announce of another object, self-announce, renamed re-announce, second late announce; then pick of the active object; then pick of another wallet | 0 logouts, rows created 1 / live 1 / revoked 0, 0 prompts, `current()` unchanged through four announcements; pick of active object: 0 logouts; pick of other wallet: 1 logout (`expectedAddress`, 204), live 0 / revoked 1, 0 prompts | **fixed** |\n| 2 overlapping home reads | one index, one budget, one proof per cohort with advancing live","treeHash":null,"usage":{"cachedInputTokens":4950367,"inputTokens":610,"model":"claude-fable-5-1","outputTokens":87353,"runtime":"claude","turns":81,"wallClockMs":1298648}}],"verification":[]}