{"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":"e817a62e-1b9f-4469-90d7-7a761579af81","kind":"audit","nodes":[{"acceptedSubmissionHash":"aae9853b35bbeda2720212264accac1e66de000b6938e741ce2f0099f5b20a93","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":"41e109aa23fe84bd87233adbafb58ac75449a1d2f769d21ef182cd18b5313708","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":"fca579e45543c94ad3690021b52023d9bd739a3d50e1883aa318572ac4b6c97f","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":"587c2bb0fa53eba16d118c902974910ebecefe13e53b3db19b7f00e38be835f7","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":"3d054d7bc3133cd6ad06d1a167bb521c7537030c95893925247046af424375bc","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 - tenth offline Audit9 source-closure audit (World/Member M1).\nLength/format: Markdown five-row closure matrix, concise summary and separate source/release verdicts; evidence appendix with commands, errors, reproductions and immutable source/line links.\n\nQuestion: Does this exact candidate close Audit9's 3 Low + 2 Info source blockers and the two adjacent counterexamples without reopening prior Auth/ownership/artifact boundaries? Seek any-severity findings within these mechanisms; do not assume a pass.\nPeriod: latest Audit9 completed 2026-10-05 04:30:23.485 UTC; candidate source frozen 10:07:04 UTC; frozen TEAM measurements completed 14:04:44 UTC that day. Earlier Report9's bounded pass does not overrule Audit9.\n\nCandidate: https://github.com/tungweb3/imd-ember-world-review/tree/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643\nOriginal Audit9: https://github.com/Identity-md/research/blob/911652a2b1a7ab7be7d16bc37d97ab9376028fe6/jobs/d76a2a79-7394-420a-99d2-df2ba6a23f45/files/AUDIT.md\nRead Submission10/REVIEW_INPUTS.md and FinalClosure/{CLOSURE_MATRIX.md,TEST_RESULTS.json,ARTIFACT_CLOSURE.json,SOURCE_MANIFEST.json}, plus manifests/submission10-published-source.json. FinalClosure paths are under Submission10/. Historical namespaces are context.\n\nScope: unofficial TypeScript Cloudflare Worker/React SIWE World + Member M1; no Solidity. M1 persists public profiles, so World is not wholly read-only. Selected sources:143 (127 raw exact;16 historically masked files/57 lines). Exclude 3D/scene/media/avatar/selfie/UI/full frontend, private Git/backups/databases, Genesis/Mint, Ember Coin and Fren Pet. Missing full frontend inputs mean unavailable build evidence, not a pass.\n\nOffline/local synthetic tests only. Public repo/prior-report GETs and locked dependency downloads allowed. No production endpoint tests/writes, real wallets/login/signatures, asset actions, approvals, claims, bridges, payments, deployment or new jobs. Never request private credentials/databases.\nFresh exact public checkout, Node24.x; in source/ run:\nnpm ci --ignore-scripts\nnode scripts/review-tests.mjs --check\nnpm run test:review\nnode scripts/verify-artifact-closure.mjs\nUse locked real viem2.56.9, actual AuthClient/Worker and migration-backed SQLite. Run all supported files; no private-source selector, global-module substitute, crypto/Worker/SQLite shim, dropped failing test or hidden skip. Record commands/exits/errors/skips/cancel/todo/retry reasons. Real Windows file-symlink EPERM is failure/unavailable evidence, never an assertion pass. Separate environmental failures from source defects.\n\nReview these five mechanisms and reverse controls:\n1 Low1: use live post-await clocks for final eligibility and admitted/refused no-eligible lane. Test24h-1/24h/24h+1ms, D1 delays0/1/2/5000ms, both enrichment reads, refused/failed/successful refresh, strict ownership proof29999/30000/30001ms, sale, rollback/NaN/Infinity and later recovery. Preserve original proof.checkedAt/index/producer timestamps. Expired/unavailable cannot become complete-empty/not-owned authority; no index/budget/RPC amplification.\n2 Low2: same-client stop/restart A-to-B, locked[] or no-provider clears stale public identity and reconciles first observation independently of cookie A; passive first observation must not logout. Retain actual B-to-A/current address cleanup, explicit grants, binding/account-event fences and passive provider replacement. Adjacent early-click: hold restarted eth_accounts(B) or[], restore cookie A, click before reply, release it. Prior-life A must not enable a stale same-session fast path; late[] must not erase a newer explicit grant.\n3 Low3: B verify commits with response/nonce cleanup held; stop; other context installs cookie A; restart provider[]; restore A; first accountsChanged(C). B's old lifetime must not cause expectedAddress=A logout/A-row revocation. Preserve B's nonce-specific cleanup, actual current C-to-D cleanup and held-completion fencing. Separate A's live SQLite row from shared cookie after delayed clear-cookie headers; old callbacks cannot install B into new UI.\n4 Info4: actual replay CLI --minimize uses validated artifact writer. Test dangling/existing final file links, nonregular outputs, unsafe parent links/junctions, outside-root/namespace attempts, safe regular replacement and default replay without persistence. Preserve input bytes. Adjacent input-parent junction alias: differently spelled input/output paths resolving to the same input must be rejected; safe separate output through that alias preserves both input views. For deliberate invalid HARNESS-CAUSAL witnesses, child exit1 alone proves neither rejection nor an Auth vulnerability: inspect ARTIFACT_REJECTED, bytes/existence/hashes. State locally controlled-root assumption; no hostile concurrent ancestor-swap or SMB/NFS guarantee.\n5 Info5: read back persisted nested quoted/bare route-prefix filenames/suffix variants: /api/auth/session.log, /api/auth/verify.backup, /api/me/home.private.json must be masked. Preserve exact allowed routes/query/subroute tokens, network URLs, relative IDs, nonce/action/event identities and replay structure. Exact routes cannot exempt arbitrary filename prefixes. Synthetic filenames do not demonstrate production data leakage.\n\nTEAM claims to verify independently:25 supported files; review659/659; artifact28/28; verifier19/19; fresh private/public review659/659; private full1709/1709. Positive runs exit0, zero fail/cancel/skipped/todo. Same-evaluator vulnerable baseline64:40 pass/24 assertion fail; baseline verifier19:14 pass/5 assertion fail, exit1, no EPERM/setup failures. Prior EPERM/setup retries are documented. Use current TEST_RESULTS.json; historical613/613 and13/13 are not current totals. Tests are bounded, not exhaustive state-machine/global RPC/concurrency proof.\n\nFor every finding and adjacent case: severity, exact source location/event order, expected/actual, relevant synthetic SQLite rows, prompt/connect/challenge/verify/logout/hint/broadcast/index/budget/RPC counts, command/exit/hash, and CLOSED/PARTIAL/OPEN/accepted-limit/UNKNOWN with rationale. Cite exact-pin sources/lines beside claims. Separate REVIEWER reproductions, TEAM measurements, history, inference and unavailable checks.\nReturn separate SOURCE-CLOSURE and RELEASE-READINESS verdicts: PASS/BLOCKED/UNKNOWN. Release baseline remains UNKNOWN; this source was not deployed. Offline closure does not measure production Cloudflare/browser/provider/cookies/ERC1271/M1 authorization/D1/WAF/limiter/upstream/process-death/cross-isolate behavior. Bound accepted limits; UNKNOWN is not a demonstrated source defect. Completed/accepted, passing tests or Low/Info labels are not certification/endorsement/zero vulnerabilities/fund-safety proof.\n\nPublication provenance: private source a2e6aca828858730cfb6b60931abea20ba9b6ab6. Final pin directly extends official public347268a7ecae700088547c2402db9a3eb07a6fd2. All143 source bytes match tested local projection59dfc4a90a52de181c1420f0babe6170c1dc08d7; differences are docs/checksums only. Intermediate local Git history is private, not published. Privacy-gate receipt is private; publication checks are not independent source review.","parentJobId":null,"planHash":"d3cf9b08e5fb157db84cc5481c9e4311fd6feddbd8fc49620e566f9801c2acad","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"e817a62e-1b9f-4469-90d7-7a761579af81","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":"51514","feedbackHash":"5703040c2660ed95a19a2545ff70bca663700917312f9bca46030dfc9898b2a6","nodeKey":"audit_economics","submissionHash":"aae9853b35bbeda2720212264accac1e66de000b6938e741ce2f0099f5b20a93","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51229","feedbackHash":"0aca5e6f814d1918f3336a6b4006b3e6f331c6129f1eb71e6ffe7a97bab647e4","nodeKey":"audit_flow","submissionHash":"41e109aa23fe84bd87233adbafb58ac75449a1d2f769d21ef182cd18b5313708","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52140","feedbackHash":"00d07f696bc13cac09d92d7937a295dd6c9defa46d4693269762041d56fb28b8","nodeKey":"audit_judge","submissionHash":"fca579e45543c94ad3690021b52023d9bd739a3d50e1883aa318572ac4b6c97f","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51231","feedbackHash":"802c873767d076b3f38a94d523f057dbca89810557dc2b88b92db931b0c6f90a","nodeKey":"audit_math","submissionHash":"587c2bb0fa53eba16d118c902974910ebecefe13e53b3db19b7f00e38be835f7","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51043","feedbackHash":"17412a8ec4d8fc26eae9446183b25b61ac81324b6e8c66fb8c2ba576e12e5083","nodeKey":"audit_permissions","submissionHash":"3d054d7bc3133cd6ad06d1a167bb521c7537030c95893925247046af424375bc","tag1":"review:submission","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"f2142d11a6c4e24fd1b487ca4e3684e029fc6c1099a82e9d0b245f6159fbaf82","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2565f234b0a569e9","findings":[{"citation":"resolved","description":"Audit9 Low3 filtered accountChanged() to owners of the current lifetime (line 595), but providerChanged('discovery') at line 308 still derives `uncertain` from every retained owner, and cancelOwners('lock-reconcile') at line 309 iterates all of them (line 236), so a detached old-lifetime owner whose cancellationReason is 'stop' is re-labelled 'lock-reconcile' by an event in a lifetime it does not belong to. The new lifetime also bumps gen, clears its public account and records a 'lock'/'reconcile' cleanup plan carrying the old owner's flowId (observed plans:[{eventId:2,reason:'lock',kind:'reconcile',flowId:1}]). When the old lifetime finally observes B's verify response, settleCancelledOwner() plans by the rewritten reason: planCleanup('verify-settled') returns 'reconcile' instead of 'verify-owner', reconcileLockedOwner() runs a canonical read and releases the owner, and no expectedNonce logout is ever sent again. The stopped lifetime's policy ('Security cleanup still runs after teardown', 'Cancellation disposition belongs to the original operation') is silently replaced by a cookie reconcile. Reverse controls hold: cookie A is never revoked (no expectedAddress logout), B's cookie-A collision still yields 409 ACCOUNT_CONTEXT_CHANGED on the server, the same-lifetime lock-then-stop ordering still ends in 'stop'. Fix: compute `uncertain` from retainedOwners.filter(o=>o.life===this.life) and let cancelOwners only re-dispose owners of the current lifetime (or never downgrade a 'stop' disposition to 'lock-reconcile'); keep the same-lifetime lock-reconcile behaviour. Offline synthetic AuthClient/Worker/node:sqlite only; real wallet/browser scheduling unmeasured.","line":308,"path":"source/src/world/auth.ts","reproduction":"From source/ after npm ci --ignore-scripts, run with `node --input-type=module < probe` the script sha256 82f130a57310ef52f1e8d2a359a39202af523cf299e540913c152209d3afe0a3 (probe-provider-discovery-detached-owner.mjs; it imports tests/auth-r7-fixtures.mjs). Order, cookie-B-kept variant: tab with provider(B); click; /api/auth/verify commits at the Worker (session row B nonce N, cookie B set) but the client's fetch result is held; q.stop() -> cancelOwners('stop'), automaticCleanup('stop') dispatches logout expectedNonce=N which fails before the Worker (TypeError in beforeSend) -> owner RETAINED, inFlight=false, cancellationReason='stop'; provider locks ([]); q.restart() -> new life reads cookie B (session B, account null); passive discovery of a second provider(C): q.notifyProvider('discovery'). Then release the held verify body. EXPECTED (and observed in the no-discovery control): afterDiscovery.reason='stop'; after observation one more logout expectedNonce=N, status 204 with 1 Set-Cookie; sessions row B revoked_at=1790596800000; client session null/status visitor; cleanup CONSUMED; counts prompt1 challenge1 verify1 session4 home1 logout2 (nonce2, address0) hint2 broadcast0 index1 rpc0. ACTUAL with discovery: afterDiscovery.reason='lock-reconcile', plans [{eventId:2,reason:'lock',kind:'reconcile',flowId:1}], new provider asked eth_accounts once; after observation no further logout (logout1, nonce1, address0, none finished), cleanup RELEASED, retainedCount0, sessions row B revoked_at=null (live), client displays session B with account C, status 'mismatch'; counts prompt1 challenge1 verify1 session4 home1 hint2 broadcast0 index1 rpc0. Cookie-A-replaced variant: control attempts the nonce logout (409 ACCOUNT_CONTEXT_CHANGED, consumed), discovery path attempts none and releases; A.revoked_at stays null in both. Probe exit 1 at 'passive discovery in a new lifetime must not rewrite a detached owner disposition' (actual 'lock-reconcile', expected 'stop').","severity":"low","snippet":"      const account=this.observedAccount,uncertain=this.lifecycle.retainedOwners.length>0;","title":"Passive provider discovery in a new lifetime treats a stopped lifetime's retained verify owner as current uncertainty and rewrites its 'stop' disposition to 'lock-reconcile', dropping the nonce revoca"},{"citation":"resolved","description":"home() now samples a live clock after its awaits (lines 351/357), but assets() awaits this.world() and this.sightings() (line 369) and then passes req.now (request start) to status() at line 371, so a seat whose owner-specific sighting crosses the inclusive 24 h boundary during the D1 read is reported counts=true (no reason) although an immediately following request reports counts=false/offline-24h. The route /api/wallet/:address/assets (server/auth.ts:735) also passes no `clock`, so even a req.clock?.()??req.now fallback would not help there. This is the public, unverified roster view (no ownerOf, no session, no authority, Cache-Control public max-age=300), so it is Info: inaccurate public counting/size hints, not an eligibility or authorization defect, and no index/budget/RPC amplification (0/0/0 per request). Fix: sample the clock once after the sightings await (pass clock:()=>clock(deps) from the assets route and use it for status(), like home()), preserving fetchedAt and the inclusive comparator. Offline real Worker and migration-backed node:sqlite with synthetic identities; production D1 latency unmeasured.","line":371,"path":"source/server/ownership.ts","reproduction":"From source/ after npm ci --ignore-scripts run with `node --input-type=module < probe` the script sha256 c3d5b10ef22f87da46577e302cd6d1e836d75657227def8998464f9958b9694d (probe-assets-clock.mjs; imports tests/wallet-harness.mjs). Setup per delay in [0,1,2,5000] ms: fresh Worker, seat 7 agent 707 offline, swarm owners[7]=X (synthetic), seat_presence row (7, X, last_online_at=t-86399999, updated_at same) with t=1790596800000; wrap db.prepare so the first `SELECT token_id,last_online_at ...` advances the injected clock by `delay` after it returns; GET /api/wallet/<X>/assets twice. Counts per run: assets GET 2, sightings reads 2, index 0, budget 0, rpc 0, challenge/verify/logout/prompt/hint 0, sessions table 0 rows, seat_presence row unchanged. EXPECTED first.counts = [true,true,false,false] (inclusive 24 h rule at the live post-await clock: ages 86399999/86400000/86400001/86404999 ms). ACTUAL first.counts = [true,true,true,true] with lastOnlineAt=1790510400001 and fetchedAt=1790596800000, while next.counts = [true,true,false,false] with reason 'offline-24h'. Probe exit 1 at 'public assets view must evaluate counts at the live post-await clock'.","severity":"info","snippet":"      seats:ids.map(id=>({...this.status(id,world.agents.get(id),seen.get(id),req.now),image:null})),","title":"Public assets view still evaluates the 24 h counting rule at request-start time after the D1 sightings await (Audit9 Low1 fix not applied to assets())"},{"citation":"resolved","description":"The Audit9 Info5 fix stops dots and punctuation from inheriting a route exemption, but a route token followed by a space or tab is still treated as a complete route and the rest of the segment as prose. The module's own policy for bare absolute paths (lines 19-21) is that a path may contain spaces or parentheses and that the remainder of the segment is conservatively removed because its end is ambiguous; a bare path whose first segments spell an exact route is the one case where that policy is not applied, so `/api/auth/session private.log` persists unchanged and `/api/auth/session (private)/x.ts` persists its middle segment ('(private)') with only the trailing '/x.ts' masked. The quoted form `\"/api/auth/session private.log\"` and Node fs error messages (which quote paths) are masked correctly, so realistic diagnostics are covered; this is an Info policy-boundary gap on synthetic filenames, not evidence of production leakage, and exact routes, query/subroute tokens, URLs, relative IDs and replay structure are preserved. Fix options: apply the route exemption only when the route is the whole segment or followed by end/punctuation (not whitespace plus more text), or when followed by whitespace require the next token not to look like a path continuation (contains '/' or '\\\\' or a file extension); accept the trade-off that prose like 'GET /api/auth/session returned 429' would then be masked, or keep the current behaviour as a documented accepted limit.","line":17,"path":"source/tests/auth-artifacts.mjs","reproduction":"From source/ after npm ci --ignore-scripts: `node --input-type=module -e \"import {sanitizeArtifact} from './tests/auth-artifacts.mjs';for(const c of ['Error at /api/auth/session private.log','Cannot read /api/auth/session (private)/x.ts','/api/auth/session dir/file.ts','Cannot read \\\"/api/auth/session private.log\\\"','Error at /root/private directory/project.ts'])console.log(JSON.stringify(c),'=>',JSON.stringify(sanitizeArtifact(c)))\"`. EXPECTED (generalized masking guarantee H-S1/H-S3 for bare spaced paths): 'Error at [local-path]', 'Cannot read [local-path]', '[local-path]'. ACTUAL: 'Error at /api/auth/session private.log' (unchanged), 'Cannot read /api/auth/session (private)[local-path]', '/api/auth/session dir/file.ts' (unchanged); controls: the quoted variant gives 'Cannot read \"[local-path]\"' and the non-route spaced path gives 'Error at [local-path]'. The same strings persist verbatim through createArtifactStore().write (store under a synthetic root/tmp/artifacts, file core500-RESULT.json) because write() applies sanitizeArtifact before serialization. No database rows, no prompt/challenge/verify/logout/hint/index/budget/RPC.","severity":"info","snippet":"    const route=[...routeIds].find(id=>chunk.startsWith(id,match.index)&&/^(?:$|[ \\t]|[,;|)\"'<>](?=[ \\t]|$))/.test(chunk.slice(match.index+id.length)));","title":"Route-prefix exemption still lets a space-separated bare path that starts with an exact route survive persisted diagnostics"}],"hash":"3d054d7bc3133cd6ad06d1a167bb521c7537030c95893925247046af424375bc","nodeId":"c486cc8e-9d88-4d6d-b520-58491fa3df58","outcome":"completed","summary":"All evidence is in hand: 25 files executed with per-file counts summing to 659, findings file validated against the tree, and git working tree unchanged. Here is the review.\n\n## Summary and verdicts\n\n**SOURCE-CLOSURE: BLOCKED (bounded).** All five Audit9 items are closed as specified at this pin, and nothing from Audit8/R8/R7 reopened. One new Low sits inside the requested mechanism 2/3 reverse controls: a passive provider discovery in a new lifetime rewrites a stopped lifetime's retained verify owner from `stop` to `lock-reconcile`, so the committed session is never revoked by its nonce. Two Info gaps are non-blocking. If the requester accepts the Low as a policy limit, the remaining matrix is PASS.\n\n**RELEASE-READINESS: UNKNOWN.** This source was not deployed. Offline tests say nothing about Cloudflare, browsers, providers, cookies, ERC-1271, M1 authorization, D1, WAF, limiters, upstreams, process death or cross-isolate behaviour. The type-check fails only on withheld frontend modules, so build evidence is unavailable, not failing.\n\nREVIEWER reproduced every TEAM claim independently on Linux, Node v24.21.0, locked viem 2.56.9: review 659/659, verifier 19/19, exit 0, zero fail/cancel/skip/todo, no stderr. The same-evaluator vulnerable baseline rebuilt from parent 347268a gives 40/64 and 14/19 with 5 ERR_ASSERTION, matching TEAM exactly. The four runnable Audit9 original reproductions now fail at their vulnerable-behaviour assertions on this candidate. All 143 manifest hashes match the checkout.\n\nFindings written to `.imd-findings.json` (3 findings, snippets verified against the tree):\n\n- **Low, `source/src/world/auth.ts:308`.** `providerChanged('discovery')` derives `uncertain` from every retained owner and `cancelOwners` re-disposes all of them. With cookie B kept, control revokes B (204, row revoked_at set, client visitor, owner CONSUMED). With discovery, no second nonce logout is sent, owner RELEASED, B row stays live, new lifetime shows B as mismatch with account C, and a `lock`/`reconcile` plan with the old flowId is recorded. Fix: filter by `owner.life===this.life` as line 595 does.\n- **Info, `source/server/ownership.ts:371`.** `assets()` still passes `req.now` to `status()` after the sightings await; the route also injects no `clock`. Sighting age 86400001 ms yields counts=true, the next request says false. Public unverified view, no authority, no index/budget/RPC.\n- **Info, `source/tests/auth-artifacts.mjs:17`.** A route token followed by whitespace is exempt, so `/api/auth/session private.log` persists verbatim and `/api/auth/session (private)/x.ts` leaks its middle segment. Quoted and Node-error forms mask correctly. Policy-boundary gap on synthetic names.\n\nAccepted limits noted, not reported: `best()` ranking inside `proof()` uses `req.now` for the N-3 cut, which can only under-count with `recheck:'partial'`; the server's conditional nonce logout returns 409 once another context's cookie replaces B, by design.\n\n## Closure matrix\n\n| Audit9 item | Pinned source | REVIEWER evidence | Reverse controls | Status |\n|---|---|---|---|---|\n| Low1 post-await eligibility | [ownership.ts#L351](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L351), [#L357](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L357) | ownership-audit9 25/25; baseline 17/25; Audit9 repro #1 now eligible 0 at delay 2ms, checkedAt unchanged, index/budget/rpc 1/1/1 | 24h±1ms, delays 0/1/2/5000, refused/failed/successful lane, 29999/30000/30001, rollback/NaN/Infinity, recovery all hold | CLOSED for home(); adjacent Info OPEN in assets() [#L371](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L371) |\n| Low2 restart adopts first reply | [auth.ts#L264](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99","treeHash":null,"usage":{"cachedInputTokens":3558283,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":62937,"runtime":"claude","turns":59,"wallClockMs":1143870}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4199bf0c80a9d6cf","findings":[{"citation":"resolved","description":"Audit9 Low3 was closed by filtering retained owners by the current lifetime in accountChanged (src/world/auth.ts:595, 'Old lifetimes retain their own nonce cleanup, never this lifetime's account-change intent'). The parallel path providerChanged('discovery') (auth.ts:304-314) was not updated: `uncertain` counts retainedOwners of every lifetime, and cancelOwners('lock-reconcile') (auth.ts:236) calls AuthLifecycle.abandon, which unconditionally overwrites cancellationReason (authLifecycle.ts:103) from 'stop' to 'lock-reconcile'. From then on every settle/retry of that old owner is routed through the lock path: revokeAbandoned (auth.ts:632) and settleCancelledOwner via planCleanup('verify-settled') (authCleanup.ts:19) both become reconcileLockedOwner, which only reads the cookie and releases the owner (authLifecycle.ts:94-97) and 'never revokes a committed session'. The stop-abandoned verify's nonce-specific logout is therefore never retried, the committed session row stays live, and the restarted UI adopts it from the cookie. In the identical sequence without the discovery event, cancellationReason stays 'stop', the retry logout is sent with expectedNonce and the row is revoked (REVIEWER control run). No cross-account authority is gained and no foreign row (e.g. a replacement cookie A) is revoked by this path, because planCleanup('lock') with a RETAINED owner yields 'reconcile'; the defect is the lost old-lifetime nonce cleanup that Audit9 Low3's closure says is preserved. Fix: in providerChanged('discovery') compute `uncertain` from owners whose `owner.life===this.life` (as accountChanged does) and let cancelOwners/abandon skip owners that are already abandoned with a terminal reason ('stop'/'explicit-signout'), or have abandon() refuse to downgrade a non-lock reason to 'lock-reconcile'.","line":308,"path":"source/src/world/auth.ts","reproduction":"Offline fixtures only (tests/auth-r7-fixtures.mjs: real AuthClient, real Worker, migration-backed SQLite). Sequence: (1) tab q with provider B; ready; q.signIn(); hold the /api/auth/verify response at intercept after headers (cookie B is already set). (2) q.stop(): teardown runs automaticCleanup('stop') -> planCleanup -> verify-owner -> revokeAbandoned sends POST /api/auth/logout {expectedNonce:B.nonce}; make that request fail at the network (beforeSend throws). lifecycleSnapshot.cleanup.cancellationReason==='stop'. (3) q.restart(); session read restores PRESENT B. (4) Fire a passive discovery: deps.provider() returns a different provider object for the same account B; q.notifyProvider('discovery'). Observed: cancellationReason becomes 'lock-reconcile' although the owner belongs to the previous life. (5) Release the held logout (fails, ok=false; cleanupDone leaves it RETAINED, inFlight=false). (6) Release the verify response; the old signIn continuation calls settleCancelledOwner. EXPECTED (per Low3 closure): planCleanup('verify-settled') -> verify-owner -> second POST /api/auth/logout {expectedNonce:B.nonce} -> 204 -> sessions row for B revoked_at set; client session null. ACTUAL: plan 'reconcile' -> reconcileLockedOwner -> GET /api/auth/session -> owner RELEASED; nonce logouts total 1 (the failed one), no retry; sessions row for B revoked_at NULL; client state.session = B. REVIEWER measurements (same script, discovery=false control vs discovery=true): CONTROL nonceLogouts=[network-failure,204], ownerStatus=CONSUMED, B revoked=true, clientSession=null; CANDIDATE nonceLogouts=[network-failure], ownerStatus=RELEASED, B revoked=false, clientSession=B. Both: prompts 1, challenge 1, verify 1, address-asserted logouts 0, broadcasts 0, session reads 4. Script: /tmp/probe-low3-discovery.mjs run as `FIX=$PWD/tests/auth-r7-fixtures.mjs node /tmp/probe-low3-discovery.mjs` from source/ on the pinned checkout c2f21a9, Node v24.21.0, viem 2.56.9; exit 0 with the two JSON lines above.","severity":"low","snippet":"      const account=this.observedAccount,uncertain=this.lifecycle.retainedOwners.length>0;\n      if(abandoned||uncertain){this.gen++;this.lifecycle.cancel();this.cancelOwners('lock-reconcile');","title":"Passive provider discovery in a new lifetime relabels an old lifetime's retained verify owner to lock-reconcile, dropping its nonce cleanup"},{"citation":"resolved","description":"Audit9 Low1 moved home()'s final eligibility to a live post-await clock (ownership.ts:357-359). assets() still evaluates the same `counts` predicate (ownership.ts:151, 'One predicate ... so no second copy of the 24 h rule exists') with req.now after awaiting world() and the seat_presence D1 read (ownership.ts:367-371), and the Worker passes no clock to it (server/auth.ts:735 passes only `now`). A sighting that crosses the inclusive 24 h boundary while D1 waits is reported as counts:true in the public wallet view while /api/me/home for the same row and the same clock answers counts:false. This grants nothing: assets is the unverified public roster view, the answer is cached public max-age=300, and owner rights come only from home(). It is reported because the task asks for adjacent live-clock counterexamples and because the response is dated by lastOnlineAt/fetchedAt as if current. Fix: pass clock:()=>clock(deps) from the assets route and sample it after the sightings await (as home() does), or document the entry-time rule for the public view.","line":371,"path":"source/server/ownership.ts","reproduction":"Offline harness (tests/wallet-harness.mjs setup, fakeChain owners {7:A}, fakeImd seats {7:'707'}, nobody online). Sign in A; insert seat_presence(token_id 7, owner A, last_online_at = now-(86400000-1)). Wrap db.prepare so the first 'SELECT token_id,last_online_at' advances the injected clock by D ms after the read. GET /api/wallet/<A>/assets then GET /api/me/home. REVIEWER measurements (script /tmp/probe-assets-clock.mjs, pinned checkout, Node v24.21.0): D=0: assets counts=true, home counts=true (age 86399999 < 86400000, both agree). D=1: assets counts=true at reply age 86400000 (boundary, inclusive) while home counts=false at age 86400001 (the second read advanced again); D=2: assets counts=true with reply age 86400001 (> boundary) vs home false; D=5000: assets counts=true with reply age 86404999 vs home eligible=0. Expected: the public view's counts/reason evaluated at the reply clock (false, reason offline-24h for D>=2). Actual: counts:true, reason absent, Cache-Control public, max-age=300. Index/RPC/budget counts unchanged (assets makes no keyed read with no character collections configured).","severity":"info","snippet":"      seats:ids.map(id=>({...this.status(id,world.agents.get(id),seen.get(id),req.now),image:null})),","title":"Public assets view applies the inclusive 24h counting rule at request-entry time, not after the D1 sightings wait, unlike the Low1-fixed home()"},{"citation":"resolved","description":"Info4's alias control resolves the input with realpathSync and compares it lexically with resolve(out) (replay-auth-trace.mjs:16-19). Symlink and junction aliases are caught because realpath canonicalises them and the store's validate() refuses link components (tests/auth-artifacts.mjs:64). A case-variant spelling is a different alias: on a case-insensitive filesystem (macOS APFS default, or a Linux casefold directory) `tmp/Store/core500-failure-original.json` names the same file as `tmp/store/core500-failure-original.json`, path.relative on POSIX is case-sensitive so the check passes, Node's JS realpathSync preserves the caller's spelling so validate() (auth-artifacts.mjs:64 `realpathSync(cursor)!==cursor`) also passes, and write() renames the temp file over the input's directory entry (auth-artifacts.mjs:97). The trace was already read (line 27), so the replay runs and the minimized output replaces the original failure trace: input bytes are not preserved. Windows is not affected because win32 path.relative compares case-insensitively; Linux ext4 without casefold is not affected. NOT reproduced here: this checkout was run on case-sensitive Linux ext4 where the variant spelling creates a separate directory. Locally controlled root assumption is unchanged; impact is loss of the operator's own replay input, no Auth effect. Fix: compare file identity, not spelling: lstat the resolved output (when it exists) and reject if dev/ino equal the input's, or make the store's write() refuse when the final path's inode equals a caller-supplied protected inode.","line":19,"path":"source/scripts/replay-auth-trace.mjs","reproduction":"On a case-insensitive volume (macOS default APFS) from source/: create a valid trace as scripts/verify-artifact-closure.mjs cliInput does, at tmp/store/core500-failure-original.json (sha256 S0); run `node scripts/replay-auth-trace.mjs --replay tmp/store/core500-failure-original.json --minimize tmp/Store/core500-failure-original.json`. Expected: exit 1 with ARTIFACT_REJECTED 'minimized output must be separate from the replay input' and sha256(input)==S0. Predicted actual (from the code path; unverified on this Linux host): exit 1 with HARNESS-CAUSAL, no ARTIFACT_REJECTED, and tmp/store/core500-failure-original.json now holds the minimized JSON (sha256 != S0). On this Linux ext4 host the same command creates tmp/Store/ as a distinct directory and the input is preserved (REVIEWER did not run it; H-C5 in the verifier covers only the symlink alias).","severity":"info","snippet":"    if(relative(inputPath,output)==='')throw new Error('minimized output must be separate from the replay input');","title":"Replay CLI input/output identity check is lexical, so a case-variant spelling of the input path on a case-insensitive volume is accepted as a separate output and overwrites the input"}],"hash":"41e109aa23fe84bd87233adbafb58ac75449a1d2f769d21ef182cd18b5313708","nodeId":"d087ccaf-a9b9-493d-9aad-c0a1783360b5","outcome":"completed","summary":"Everything is verified and the findings file validates against the tree. Here is the closure report.\n\n## Summary and verdicts\n\n**SOURCE-CLOSURE: BLOCKED (one new Low in the Low3 sibling path).** The five Audit9 items are closed as bounded on this exact pin, and the reviewer rerun reproduces every TEAM count. But the Low3 fix was applied only to the account-change path. The passive provider-discovery path still treats old-lifetime verify owners as this lifetime's, relabels them, and loses their nonce cleanup. That is reproducible with the shipped fixtures.\n\n**RELEASE-READINESS: UNKNOWN.** This source was not deployed. Offline closure measures nothing about Cloudflare, browsers, providers, cookies, ERC-1271, D1, WAF, limiters, process death or cross-isolate behavior.\n\nFindings are in `.imd-findings.json` at the repository root (one Low, two Info). Passing tests and Low/Info labels are not certification or zero-vulnerability proof.\n\n| Audit9 item | Mechanism reviewed | REVIEWER result | Verdict |\n|---|---|---|---|\n| Low1 | Live post-await clocks for final eligibility and lane ([ownership.ts:351-358](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L351-L358)) | 25 formal cases pass. checkedAt, index read_at and producer timestamps preserved. Rollback/NaN/Infinity give 503, no amplification. Adjacent gap: public `assets()` still uses the entry clock ([ownership.ts:371](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L371)), reproduced, display-only. `best()` ranking also uses `req.now` but only matters above 256 candidates. | CLOSED (bounded), Info adjacent |\n| Low2 | New lifetime clears public account before the first fenced reply ([auth.ts:264](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L264), [auth.ts:283-298](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L283-L298)) | A-to-B, locked and no-provider restarts keep cookie A, zero logouts/prompts. Early click obtains a real grant. Late empty reply is dropped by the gen fence. B-to-A cleanup and passive replacement fences hold. No counterexample found. | CLOSED (bounded) |\n| Low3 | Current-life owner filter in accountChanged ([auth.ts:595](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L595)) | The audited sequence passes: first accountsChanged(C) does not revoke row A, C-to-D keeps authority, server refuses nonce logouts with a non-matching token ([server/auth.ts:632-640](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/auth.ts#L632-L640)). New Low: `providerChanged('discovery')` ([auth.ts:308-309](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L308-L309)) counts old-life owners and relabels them to lock-reconcile, so the stop-abandoned verify's nonce logout is never retried and the committed row stays live. | PARTIAL |\n| Info4 | CLI minimization through the validated writer ([replay-auth-trace.mjs:16-25](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/scripts/replay-auth-trace.mjs#L16-L25)) | Verifier 19/19 locally with real file symlinks. Input bytes survive the junction alias collision. Case-variant spelling on case-insensitive volumes passes the lexical identity check ([line 19](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/scripts/replay-auth-trace.mjs#L19)); not reproducible on this Linux host, reported as Info. Controlled-root assumption and no concurrent ancestor-swap guarantee accepted. | CLOSED (bounded), UNKNOWN on case-insensitive volumes |\n| Info5 ","treeHash":null,"usage":{"cachedInputTokens":1634267,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":47874,"runtime":"claude","turns":37,"wallClockMs":648564}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1507f63d3f1b973a","findings":[{"citation":"resolved","description":"providerChanged('discovery') calls cancelOwners('lock-reconcile') over every retained owner (auth.ts:236), and AuthLifecycle.abandon (authLifecycle.ts:102-104) overwrites o.cancellationReason even when the owner is already abandoned with a revoking disposition ('context-switch' from a genuine accountsChanged, or 'stop' from teardown). The comment at auth.ts:235 says the cancellation disposition belongs to the original operation. After the flip, revokeAbandoned (auth.ts:632) and settleCancelledOwner (auth.ts:617-619) route the owner to reconcileLockedOwner, which only reads the cookie and releases the owner; the pending nonce logout that cleanupDone deferred until response observation (authLifecycle.ts:113) is never dispatched again. A passive discovery event, which by design has no switch authority, thereby cancels a cleanup that an authoritative account switch had ordered. The same unfiltered cancelOwners also reaches old-lifetime owners (uncertain is not filtered by owner.life, unlike accountChanged's currentOwners at auth.ts:595); for those the outcome is the same release-without-revocation. Minimal fix: never downgrade an abandoned owner's disposition, e.g. in abandon() return false when o.abandoned is already true (or only allow lock-reconcile on owners that are not yet abandoned), and filter cancelOwners('lock-reconcile') to owners of the current life.","line":309,"path":"source/src/world/auth.ts","reproduction":"Offline synthetic fixtures (tests/auth-r7-fixtures.mjs: real AuthClient, real Worker, migration-backed node:sqlite; synthetic accounts B and C). Order: (1) ready(); click sign-in with wallet B: 1 eth_requestAccounts-free prompt (personal_sign 1), POST /api/auth/challenge 1, POST /api/auth/verify 1; the verify response headers are consumed (B's session row created, cookie B installed) while the body is held. (2) accountsChanged([C]) -> accountChanged: owner abandoned with cancellationReason 'context-switch', revokeAbandoned dispatches POST /api/auth/logout {expectedNonce}; the transport fails before dispatch (synthetic TypeError), so no server write; cleanupDone leaves the owner RETAINED/abandoned/not inFlight (snapshot: status RETAINED, reason context-switch). (3) providerChanged('discovery') with a new provider object for the same account C -> snapshot: status RETAINED, reason 'lock-reconcile'. (4) release the verify body -> settleCancelledOwner -> reconcileLockedOwner -> GET /api/auth/session -> owner RELEASED. ACTUAL: SQLite sessions row for B: revoked_at NULL (expires_at = issue+7d); browser cookie still holds B; GET /api/auth/session returns signedIn:true address B; client state account C, session B, status 'mismatch'; logout count 1 (0 finished), broadcast 0. EXPECTED (control run identical except step 3 omitted): second nonce logout dispatched when the body lands -> 204, B row revoked_at = now, cookie cleared, GET /api/auth/session signedIn:false, client status 'connected', logout count 2. Same outcome when the first life is stopped instead of switched (stop disposition flipped after restart + discovery): B row stays revoked_at NULL and the new life adopts B as signedInNoHouse, whereas the control run retries the nonce logout. Probe scripts were run from /tmp against the pinned checkout; no repository file was changed.","severity":"low","snippet":"if(abandoned||uncertain){this.gen++;this.lifecycle.cancel();this.cancelOwners('lock-reconcile');","title":"Passive provider discovery downgrades an already-abandoned verify owner's cleanup disposition to lock-reconcile, so a transport-failed nonce revocation is never retried and the switched-away account's"},{"citation":"resolved","description":"Audit9 Low1 moved the final eligibility sample and the lane sample to the live clock (home(), lines 351 and 357), but the N-3 cap ranking in best() evaluates rank(...) with req.now after awaiting this.sightings (line 260-263). The same stale-ranked cut is persisted by keepIndex (line 273). The comment at line 148-150 states that one predicate exists so no second copy of the 24 h rule exists; the clock domain differs here. Effect is limited to households with more than CANDIDATE_CAP (256) registered candidates (largest holder observed is 20), and only when sightings cross the inclusive boundary during the D1 wait. Fix: pass the live clock (current()) to rank/order in best(), or compute the cut with the same post-await sample that status() uses.","line":262,"path":"source/server/ownership.ts","reproduction":"wallet-harness setup, real Worker and node:sqlite: an address owns seats 1..257 (roster owners[] and chain ownerOf agree; every seat is a registered agent with a decimal agentId; none online). seat_presence: seats 1..256 last_online_at = now-86400000 (exact inclusive boundary); seat 257 last_online_at = now-1000. The first 'SELECT token_id,last_online_at' read advances the injected clock by 1 ms (also tried 5000 ms). GET /api/me/home. ACTUAL: 200, eligible 0, size null, recheck 'partial', 256 seats listed, seat 257 absent (never proven), counts [] ; eth_call ownerOf 2 (two Multicall chunks), index 1, budget 1. EXPECTED with live-clock ranking: seats 1..256 rank 1 (expired at the live clock), seat 257 rank 0 and kept -> eligible 1, size 's', recheck 'partial'. Control with 0 ms delay: eligible 256, size 'xl', recheck 'partial' (seat 257 cut by id among 257 rank-0 seats, as designed).","severity":"info","snippet":"const order=(id:string)=>rank(agents.get(id),sightings.get(id),req.now);","title":"CANDIDATE_CAP ranking after the seat_presence await still uses the request-start clock, so a seat that counts at the live clock can be cut in favour of seats whose sightings expired during the D1 wait"},{"citation":"resolved","description":"assets() awaits world() and sightings() and then calls status() with req.now, the same pre-await pattern Audit9 Low1 removed from home(). The route passes no clock at all (server/auth.ts:735 builds the request without clock), so even a clock-aware status() would not help without also threading the clock. The view is unverified, carries no owner rights and is served with Cache-Control public, max-age=300, so the impact is a displayed counts/reason flag that can be up to one D1-wait stale; no eligibility authority is affected. Fix: sample a live clock after the awaits (and pass clock from the route) or document the view as request-dated.","line":371,"path":"source/server/ownership.ts","reproduction":"wallet-harness setup: seat 7 owned by address X, registered agent, offline; seat_presence row (7, X, now-86400000). The 'SELECT token_id,last_online_at' read advances the injected clock by 1 ms. GET /api/wallet/<X>/assets. ACTUAL: 200, seats[0].counts true, reason absent, lastOnlineAt now-86400000, Cache-Control public, max-age=300. EXPECTED at the live clock (now+1): counts false, reason 'offline-24h' (the same input through GET /api/me/home with the same delay yields counts false). Control with 0 ms delay: counts true is correct (inclusive boundary).","severity":"info","snippet":"seats:ids.map(id=>({...this.status(id,world.agents.get(id),seen.get(id),req.now),image:null})),","title":"Public assets view evaluates the 24 h counting rule with the request-start clock after awaiting roster and seat_presence reads"},{"citation":"resolved","description":"The negative lookahead (?!file:) is only applied at the scheme start, but the scheme class [A-Za-z0-9+.-]+ admits '.' and '-', so 'module.file://' or 'x-file://' is classified as a network URL and its path is preserved verbatim in persisted artifacts. Separately, the URL tail [^\\s\"'<>]+ consumes ',/root/...' or '(/root/...)' that follows a URL with no whitespace, so that absolute path is never seen by sanitizeBare. These are not the Audit9 Info5 route-prefix issue (which is closed: '/api/auth/session.log' and its quoted/bare variants are masked) but they are residual gaps in the stated 'arbitrary roots' redaction guarantee. Fix: require the scheme to be exactly file-free (e.g. (?![A-Za-z0-9+.-]*file:) or exclude '.' and '-' before a file: match) and terminate the URL token at ',' '(' and ')' or run sanitizeBare on the URL tail after the first path segment. Synthetic inputs only; no production artifact leakage is demonstrated.","line":11,"path":"source/tests/auth-artifacts.mjs","reproduction":"node -e import sanitizeArtifact from tests/auth-artifacts.mjs; sanitizeArtifact('Loaded module.file:///root/reviewer/private/project.ts') -> ACTUAL unchanged string; EXPECTED 'Loaded module.[local-path]' or '[local-path]'. sanitizeArtifact('see x-file:///root/reviewer/private/project.ts') -> ACTUAL unchanged. sanitizeArtifact('https://example.com/a,/root/reviewer/private/project.ts') -> ACTUAL unchanged; EXPECTED 'https://example.com/a,[local-path]'. Controls that behave as intended: 'at file:///root/reviewer/private/project.ts:12:3' -> 'at [local-path]'; 'url=file:///root/private.ts' -> 'url=[local-path]'; \"open '/root/private/x.json'\" -> \"open '[local-path]'\"; '/api/auth/session.log' -> '[local-path]'; 'node:internal/modules/esm/resolve:272' preserved.","severity":"info","snippet":"const networkURLs=/\\b(?!file:)[A-Za-z][A-Za-z0-9+.-]+:\\/\\/[^\\s\"'<>]+/gi;","title":"Network-URL preservation in the artifact sanitizer keeps absolute local paths when a file: URL is preceded by a dotted/hyphenated token or when an absolute path directly follows a URL without whitespa"}],"hash":"587c2bb0fa53eba16d118c902974910ebecefe13e53b3db19b7f00e38be835f7","nodeId":"3df7fd3f-0b59-4b67-b594-c4d55bc1e63b","outcome":"completed","summary":"The review is complete. The findings file holds 4 substantiated findings (1 Low, 3 Info), the tree is clean, and all evidence is below.\n\n## Summary\n\nThe exact candidate at `c2f21a9` closes all five Audit9 blockers and both adjacent counterexamples. I reproduced the supported runner (659/659), the artifact suite (28/28) and the standalone verifier (19/19) on a fresh Linux checkout with locked viem 2.56.9 and Node 24.21.0, all exit 0 with empty stderr and no skip/todo/cancel. All 143 selected source hashes match the Submission10 manifest and `SHA256SUMS` verifies 277/277. The pin's parent is the official public `347268a`. Nothing from prior Auth/ownership/artifact boundaries reopened in my probes.\n\nBeyond the five items, I found one new Low and three Info defects inside the same mechanisms. The Low: a passive provider discovery rewrites an already-abandoned verify owner's cleanup disposition to `lock-reconcile`, so when the first nonce revocation failed at transport, the switched-away account's committed session is never revoked and the page adopts it as a mismatch session. The Infos are a residual request-start clock in the cap ranking and the public assets view, and two sanitizer regex gaps unrelated to the route-prefix issue.\n\n**SOURCE-CLOSURE: BLOCKED** (new Low in `src/world/auth.ts`; the five Audit9 items themselves are CLOSED bounded).\n**RELEASE-READINESS: UNKNOWN** (source not deployed; no production Cloudflare/browser/provider/D1 measurement exists).\n\n## Closure matrix\n\n| Audit9 item | Mechanism reviewed | REVIEWER result | Status |\n|---|---|---|---|\n| Low1 post-await eligibility | [ownership.ts#L351](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L351), [#L357](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L357) sample the live clock for lane and final view; `checkedAt`/index dates preserved. 25 formal cases pass locally: 24h−1/24h/24h+1 ms, delays 0/1/2/5000, refused/failed/successful lane, 29999/30000/30001 ms TTL, sale/rollback/NaN/Infinity, recovery. | Residual: [#L262](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L262) cap ranking still uses `req.now` after the sightings await (257-seat household, 1 ms delay: eligible 0/partial instead of 1). Public [`assets()`#L371](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L371) same pattern, cached, no authority. | CLOSED (bounded); 2 Info residuals |\n| Low2 restarted client identity | [auth.ts#L264](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L264) clears public account per lifetime; [#L283-L297](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L283) fenced first reply. 11 Auth11 cases pass: A→B/locked/no-provider, B→A cleanup once, early click (1 eth_requestAccounts, 0 prior-life fast path), late `[]` vs explicit grant, passive replacement. | No counterexample found in this mechanism. | CLOSED (bounded) |\n| Low3 old-life owner authority | [auth.ts#L595](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L595) filters owners by `life`; first accountsChanged(C) no longer revokes A; C→D keeps authority; B nonce cleanup retained; late cookie clear reconciled. 3 cases pass. | Adjacent new Low: [auth.ts#L309](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L309) + [#L236](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L236) + [authLifecycle.ts#L103](https://github.com/tungweb3/imd-ember-world-review/blob/c2f","treeHash":null,"usage":{"cachedInputTokens":2542755,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":57996,"runtime":"claude","turns":54,"wallClockMs":837842}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4ebee02fc506f27c","findings":[{"citation":"resolved","description":"Audit9 Low1 was closed in home() by sampling the live clock after the sightings/lane awaits (ownership.ts:351 laneNow, :357 now). The sibling public view assets() runs the same shared counts() predicate (ownership.ts:151) through status() but passes req.now, the request-entry time, after awaiting this.world() and this.sightings(). A seat_presence sighting that crosses the inclusive 24h boundary during the D1 wait is therefore reported counts:true (no reason) by /api/wallet/:a/assets while /api/me/home for the same owner, seat and instant reports counts:false, reason:'offline-24h'. The server route passes no clock for assets (server/auth.ts:735: {chain,db,now,waitUntil,budget}), so the fix is bounded: pass clock:()=>clock(deps) at auth.ts:735 and use req.clock?.()??req.now in assets() after the sightings read, as home() does. Impact is display-only: assets is explicitly the unverified public roster view, grants no owner rights, and is served with Cache-Control public max-age=300, so a 1 ms boundary disagreement is immaterial to authority. This is an Info-level consistency gap adjacent to Low1, not a reopening of it: home() is correct. Reproduced by REVIEWER offline with the real Worker and migration-backed SQLite: synthetic owner with seat 7 (agent '707', offline), seat_presence last_online_at = now-(86400000-1); db.prepare wrapper advances the injected clock by D ms when the first 'SELECT token_id,last_online_at' completes. D=0,1: both views counts:true (age <= 86400000). D=2: assets counts:true, home eligible 0 / counts:false (age 86400001). D=5000: assets counts:true, home counts:false (age 86404999).","line":371,"path":"source/server/ownership.ts","reproduction":"Node 24.x, viem 2.56.9, from source/ after npm ci --ignore-scripts. Scratch test (REVIEWER): import {setup,newAccount,fakeImd,fakeChain} from tests/wallet-harness.mjs and ONLINE_WINDOW_MS from server/ownership.ts. w=setup({chain:fakeChain({owners:{7:addr}}),imd:fakeImd({seats:{7:'707'},owners:[,,,,,,,addr],online:[]})}); browser.signIn(account) -> 200; at=w.clock.now(); INSERT INTO seat_presence(token_id,owner,last_online_at,updated_at) VALUES(7,addr,at-(ONLINE_WINDOW_MS-1),same); wrap w.db.prepare so the first SELECT token_id,last_online_at ... .all() advances w.clock by 2 ms; GET /api/wallet/<Addr>/assets then GET /api/me/home. Expected: assets.seats[0].counts === home.seats[0].counts === false at age 86400001 ms. Actual: assets.seats[0].counts=true, reason absent; home.eligible=0, home.seats[0].counts=false, reason 'offline-24h'. Same with a 5000 ms advance (age 86404999: assets true, home false). With 0 or 1 ms advance both views agree (true). Observed TAP: 'not ok 3 - assets counts at request-start clock after a 2ms D1 wait  expected: false actual: true'; 'not ok 4 ... 5000ms ... expected: false actual: true'.","severity":"info","snippet":"      seats:ids.map(id=>({...this.status(id,world.agents.get(id),seen.get(id),req.now),image:null})),","title":"Audit9 Low1 adjacent: public /api/wallet/:a/assets still evaluates the 24h counting rule at request-start time after its D1 await"},{"citation":"resolved","description":"The Audit9 Info4 adjacent control requires that differently spelled input/output paths resolving to the same input be rejected. The CLI canonicalizes only symlinks: inputPath=realpathSync(resolve(file)) and output=resolve(out), then compares with path.relative. Node's JS fs.realpathSync does not canonicalize letter case and path.posix.relative is case-sensitive, so on a case-insensitive filesystem (macOS APFS/HFS+ default, or an ext4 casefold directory) --replay tmp/store/core500-failure-original.json --minimize tmp/Store/core500-failure-original.json passes this check. createArtifactStore({requestedDir:'tmp/Store'}) then also passes: inside(allowed,directory) is satisfied ('Store' is under 'tmp'), each chain component lstat()s to an existing non-symlink directory and realpathSync(cursor)===cursor because realpathSync returns the caller's spelling, file(name) accepts 'core500-failure-original.json' (namespace regex), regular(path) sees the input's own regular inode, and write() finishes with renameSync(temp,path), which on a case-insensitive directory replaces the input's directory entry. Result: the replay input is overwritten by the minimized artifact, the exact outcome the collision check exists to prevent. On Windows path.win32.relative compares case-insensitively, so the check holds there; on Linux (this review environment) paths are case-sensitive, so the scenario cannot occur and was NOT reproduced here: this is a code-reading lead with the concrete failing input stated, severity Info (local developer tool, controlled root, data-loss of a tmp artifact, no Auth impact). A robust fix compares filesystem identity rather than spelling: lstat both paths after resolving and reject when dev/ino match (the store already has same(a,b)), or use realpathSync.native on both sides and additionally reject when the output's final entry stats equal the input's.","line":19,"path":"source/scripts/replay-auth-trace.mjs","reproduction":"UNVERIFIED HERE (Linux, case-sensitive; Windows protected by path.win32.relative). On macOS APFS default volume, Node 24: cd source; node -e \"import('./tests/auth-scheduler-driver.mjs').then(async m=>{const t=await m.runSeed(0,{retainTrace:true});t.actions.push({type:'worker',id:t.actions.find(a=>a.type==='worker').id});const {createArtifactStore}=await import('./tests/auth-artifacts.mjs');const s=createArtifactStore({sourceDir:'.',requestedDir:'tmp/store'});s.write('core500-failure-original.json',{trace:t});})\"; shasum -a 256 tmp/store/core500-failure-original.json (record H0 and byte size); node scripts/replay-auth-trace.mjs --replay tmp/store/core500-failure-original.json --minimize tmp/Store/core500-failure-original.json. Expected: exit 1 with ARTIFACT_REJECTED 'minimized output must be separate from the replay input' and tmp/store/core500-failure-original.json unchanged (hash H0). Predicted actual: stderr shows the HARNESS-CAUSAL failure and no ARTIFACT_REJECTED line, exit 1, and tmp/store/core500-failure-original.json now hashes differently and contains {invariant:'HARNESS-CAUSAL',trace:{...minimization...}}: the input was replaced. Control: the same two commands with --minimize tmp/store/core500-failure-original.json (same spelling) are rejected as the existing test 'Audit9 minimization CLI never overwrites its replay input' shows.","severity":"info","snippet":"    if(relative(inputPath,output)==='')throw new Error('minimized output must be separate from the replay input');","title":"Audit9 Info4 adjacent (unverified on Linux): replay input/output collision check is case-sensitive, so on a case-insensitive filesystem a differently-cased parent directory spelling bypasses it and th"}],"hash":"aae9853b35bbeda2720212264accac1e66de000b6938e741ce2f0099f5b20a93","nodeId":"efa4139e-2e25-4c0f-a978-52de8a84e9c8","outcome":"completed","summary":"Evidence gathering is complete and the tree is unmodified. Here is the closure review.\n\n## Summary and verdicts\n\n**SOURCE-CLOSURE: PASS (bounded, offline).** All five Audit9 blockers and the two adjacent counterexamples are closed in the pinned source. REVIEWER reruns reproduced TEAM's headline counts exactly, and reverting the Low1 and Low3 fixes in a scratch copy reproduced TEAM's listed baseline failure names, so the evaluators are non-vacuous. No prior Auth, ownership or artifact boundary was reopened. Two Info-level adjacent observations were found and written to `.imd-findings.json`. Neither reopens an Audit9 item.\n\n**RELEASE-READINESS: UNKNOWN.** This source was not deployed. Offline tests say nothing about Cloudflare, real browsers or providers, cookie flags, ERC-1271, M1 authorization, D1, WAF, limiters, upstreams, process death or cross-isolate behavior. The full frontend is withheld, so no build evidence exists here. Passing tests and Low/Info labels are not certification or fund-safety proof.\n\n| Audit9 item | Source (exact pin) | REVIEWER evidence | Reverse controls kept | Status |\n|---|---|---|---|---|\n| Low1 post-await eligibility | [ownership.ts#L351](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L351), [#L357-L359](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L357-L359) | 25/25 cases pass. Reverting `now`/`laneNow` to `req.now` fails exactly TEAM's 8 names. REVIEWER sale scenario: cached within epoch, 503 when the D1 wait crosses 30 s, next request proves at latest with 2 RPC total. | `checkedAt`, index `read_at` and `lastOnlineAt` unchanged; inclusive 24h at 86,400,000 ms; strict 29,999/30,000/30,001; rollback/NaN/Infinity 503; lane budget 1, RPC 1 | CLOSED |\n| Low2 restart retains prior account | [auth.ts#L259](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L259), [#L264](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L264), [#L281-L298](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L281-L298) | 8/8 cases pass. 4 REVIEWER adjacent scenarios pass (locked then A, B then A, locked then C then D, B then C then A). Approximate reversion fails 3 of 8. | Passive first reply never logs out (0 logout, 0 prompt, rows unchanged); genuine B-to-A cleans once with 204; late `[]` cannot erase a grant (gen/accountEvents/binds fences at L285) | CLOSED |\n| Low3 stale owner authorizes revocation | [auth.ts#L595-L598](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L595-L598) | 3/3 cases pass. Removing the `owner.life===this.life` filter fails exactly TEAM's 2 names. REVIEWER: first observation C leaves A's row live; C-to-D revokes A once via `expectedAddress`. | B's nonce cleanup stays RETAINED with reason `stop`, then CONSUMED; C-to-D keeps displayed-session authority; UI follows the canonical cookie read | CLOSED, one accepted limit |\n| Info4 replay CLI bypasses store | [replay-auth-trace.mjs#L16-L25](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/scripts/replay-auth-trace.mjs#L16-L25), [#L34](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/scripts/replay-auth-trace.mjs#L34), [auth-artifacts.mjs#L51](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/tests/auth-artifacts.mjs#L51), [#L82-L97](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/tests/auth-artifacts.mjs#L82-L97) | 9/9 CLI cases and verifier H-C1 to H-C5 pass with real file symlinks on Linux (no EPERM","treeHash":null,"usage":{"cachedInputTokens":5276934,"inputTokens":1058,"model":"claude-fable-5-1","outputTokens":67993,"runtime":"claude","turns":66,"wallClockMs":1121296}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"04cf70660d4946e6","findings":[{"citation":"resolved","description":"OPEN; Audit9 Low3 is only PARTIAL. providerChanged('discovery') treats every retained owner as current uncertainty and calls cancelOwners('lock-reconcile'). AuthLifecycle.abandon unconditionally changes the reason even when already abandoned with stop/context-switch. The late verify then goes through reconcileLockedOwner and releases the owner without retrying its deferred nonce logout. A stopped or genuinely switched-away B session remains live after a transport-failed first cleanup. This violates preserved original-operation nonce responsibility; no signature bypass, cross-account privilege or foreign-session revocation demonstrated. Filter discovery to current-life owners and prevent passive lock reconciliation from downgrading an existing revoking disposition; preserve legitimate same-lifetime lock-to-stop promotion and cleanup retry/idempotence. Source chain: auth.ts:236,308-311,614-643; authLifecycle.ts:102-114; authCleanup.ts:19-20. Independently reproduced against c2f21a9 on Linux Node24.21.0, real viem2.56.9/AuthClient/Worker/migration-backed node:sqlite. Immutable source: https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L309. Source SHA256 07afd30b8dcd8b1b1d74f3a8940f2f4d9a7037b06246911290ec8392f968275b.","line":309,"path":"source/src/world/auth.ts","reproduction":"REVIEWER command: node /tmp/imd-a10-probe-auth.mjs; exit 1 ERR_ASSERTION actual lock-reconcile expected stop. Script SHA256 3ca70d941a7e1b9b0805f57de3fb6a48c919978d56015394c540c092b3ff96cf. Use tests/auth-r7-fixtures.mjs setup/provider/tab and synthetic privateKeyToAccount('0x'+'22'.repeat(32)) for B, 11 for A, 33 for C. ready -> signIn B; hold verify in intercept after Worker commit and cookie headers. stop (or same-life accountsChanged C); fail the first nonce logout in beforeSend with TypeError, before Worker dispatch; wait cleanup RETAINED/not inFlight. For stop variants lock provider while stopped, optionally browser.signIn A, then restart and restore the cookie. Replace provider with C and notifyProvider('discovery'); release held verify. Actual stop changes to lock-reconcile, owner RELEASED, no second logout. B row expires_at=1791201600000, revoked_at=NULL; B challenge used_at=1790596800000, invalidated_at=NULL. With no discovery the second expectedNonce logout is 204 and B.revoked_at=1790596800000 (owner CONSUMED). In replacement-A control second logout is 409 and A stays live; discovery attempts no second logout. Same-life switch produces the same lost cleanup. All six runs prompt/connect/challenge/verify=1/0/1/1, address logouts=0. Broadcast=0 except same-life discovery emits one signed-in hint; its session/home/hint/index/budget/RPC counts=3/1/1/1/1/0 versus control 3/0/0/0/0/0. Stop-B control/discovery sessionGET=4/4, homeGET=1/1, hint=2/2, index/budget/RPC=1/1/0; nonce logouts=2/1. No member writes. Full reproducible script:\nimport assert from 'node:assert/strict';\nimport {privateKeyToAccount} from '/tmp/imd-audit10-review/source/node_modules/viem/_esm/accounts/index.js';\nimport {setup,provider,tab,ready,defer,until,flush,rows,logouts,prompts,connects,routeEvents,statusOf} from '/tmp/imd-audit10-review/source/tests/auth-r7-fixtures.mjs';\nconst A=privateKeyToAccount('0x'+'11'.repeat(32)),B=privateKeyToAccount('0x'+'22'.repeat(32)),C=privateKeyToAccount('0x'+'33'.repeat(32));\nconst addr=x=>x.address.toLowerCase(),results=[];\nfor(const mode of ['stop-B','stop-A','switch'])for(const discovery of [false,true]){\n const w=setup(),b=w.browser(),p=provider(B),gate=defer();let chosen=p,committed=false,failed=false,budget=0,hint=0;\n w.env.CHAIN_LIMITER={limit:async({key})=>{if(key==='chain:index')budget++;return {success:true};}};\n const q=tab(w,b,p,{getProvider:()=>chosen,beforeSend:async(path,init)=>{\n   if(path==='/api/auth/logout'&&JSON.parse(init.body).expectedNonce&&!failed){failed=true;throw new TypeError('synthetic transport failure before Worker');}\n },intercept:async(path,r)=>{if(path==='/api/auth/verify'){committed=true;await gate.promise;}return r;}});\n const set=q.c.deps.hint.set;q.c.deps.hint.set=v=>{hint++;set(v);};\n await ready(q);const flow=q.signIn();await until(()=>committed);\n if(mode==='switch')p.switchTo(C);else q.stop();\n await until(()=>failed&&!q.c.lifecycleSnapshot.cleanup.inFlight);\n const before=q.c.lifecycleSnapshot;\n if(mode!=='switch'){\n   p.switchTo(null);\n   if(mode==='stop-A')assert.equal((await b.signIn(A)).verify.status,200);\n   q.restart();await until(()=>q.c.state.session?.address===addr(mode==='stop-A'?A:B)&&!q.c.state.checking);await flush();\n }\n const next=provider(C);\n if(discovery){chosen=next;q.observeProvider(next);q.notifyProvider('discovery');await flush(20);}\n const afterDiscovery=q.c.lifecycleSnapshot;\n gate.resolve();await flow;await flush(40);\n const final=q.c.lifecycleSnapshot,db=rows(w),client={account:q.c.state.account,session:q.c.state.session?.address??null,status:statusOf(q.c.state,w.clock.now())};\n const counts={prompt:prompts([p,next]),connect:connects([p,next]),challenge:routeEvents(q,'/api/auth/challenge').length,verify:routeEvents(q,'/api/auth/verify').length,\n session:routeEvents(q,'/api/auth/session').length,home:q.events.filter(e=>e.kind==='route'&&e.path.startsWith('/api/me/home')).length,\n logout:logouts(q).length,nonce:logouts(q).filter(e=>e.nonceAssertion).","severity":"low","snippet":"      if(abandoned||uncertain){this.gen++;this.lifecycle.cancel();this.cancelOwners('lock-reconcile');","title":"Passive discovery overwrites abandoned verify cleanup, leaving the cancelled session live"},{"citation":"resolved","description":"OPEN adjacent temporal consistency defect. best() awaits owner-specific sightings but ranks with req.now, although home() now correctly counts at the live clock. With over 256 registered candidates, seats expiring during the wait tie with the still-recent seat and lower IDs crowd it out. Both keepIndex at line273 and the proof cut at line295 inherit the stale ranking. The result is partial/unavailable, not false complete-empty or forged ownership. Info: extreme-household availability/selection inconsistency; no additional RPC amplification or unauthorized owner rights. Sample current() once after the sightings await for the ranking, preserving original index/proof producer timestamps and all caps. Exact c2f21a9, real Worker/viem2.56.9/SQLite, synthetic offline fixtures. Immutable source: https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L262. Source SHA256 858cdfc9eec304e2c15b7f7b1ae66f846a9677c87a91c4333f56dd607abce207.","line":262,"path":"source/server/ownership.ts","reproduction":"REVIEWER command node /tmp/imd-a10-probe-ownership.mjs cap; exit1 ERR_ASSERTION cap must prioritize the still-counting seat (false !== true). At t=1790596800000, A owns registered offline seats1..257 in roster and chain; seat_presence1..256=(A,last_online_at=t-86400000,updated_at=same);257=(A,t-1000,t-1000). First actual SQLite sightings completion advances injected clock by1ms (also5000). GET /api/me/home -> HTTP200 eligible0 size:null recheck:partial, 256 seats,257 absent; index_candidates.ids also1..256, read_at=t. Expected seat257 prioritized over expired1..256, eligible1 size:s (still partial). Delay0 control correctly counts256 with id tie-break. checkedAt=t+delay, unrenewed index t. Session A expires_at1791201600000 revoked_atNULL; auth rows unchanged; setup challenge/verify1/1, home1, sightings2, index/budget/RPC1/1/2, prompt/connect/logout/hint/broadcast0. No M1 writes. Script SHA256 837138d0f4b9c3e533c4bbece3874715834c0653c1a9a23ea32b6f3e256189e8. Full script (imports may be adjusted to the exact checkout location):\nimport assert from 'node:assert/strict';\nimport {privateKeyToAccount} from '/tmp/imd-audit10-review/source/node_modules/viem/_esm/accounts/index.js';\nimport {setup,fakeImd,fakeChain} from '/tmp/imd-audit10-review/source/tests/wallet-harness.mjs';\nconst A=privateKeyToAccount('0x'+'11'.repeat(32)),a=A.address.toLowerCase(),W=86400000,results=[];\nfunction delayFirst(w,delay){const prepare=w.db.prepare.bind(w.db);let reads=0;\n w.db.prepare=sql=>{const wrap=s=>({...s,bind:(...args)=>wrap(s.bind(...args)),all:async()=>{const r=await s.all();if(sql.startsWith('SELECT token_id,last_online_at')){reads++;if(reads===1)w.clock.advance(delay);}return r;}});return wrap(prepare(sql));};return ()=>reads;\n}\nfor(const delay of (process.argv[2]==='cap'?[]:[0,1,2,5000])){\n const owners=[];owners[7]=a;\n const w=setup({chain:fakeChain({owners:{7:a}}),imd:fakeImd({seats:{7:'707'},owners,online:[]})}),b=w.browser();let budget=0;\n w.env.CHAIN_LIMITER={limit:async()=>{budget++;return {success:true};}};\n const t=w.clock.now(),seen=t-W+1;w.db.raw.prepare('INSERT INTO seat_presence(token_id,owner,last_online_at,updated_at) VALUES(7,?,?,?)').run(a,seen,seen);\n const reads=delayFirst(w,delay),r=await b.get('/api/wallet/'+a+'/assets'),first=await r.json(),next=await(await b.get('/api/wallet/'+a+'/assets')).json();\n const result={mode:'assets',delay,t,now:w.clock.now(),first,next,cache:r.headers.get('cache-control'),expected:delay<=1,counts:{assets:2,sightings:reads(),index:w.chain.state.calls.filter(c=>!c.body).length,budget,rpc:w.chain.state.calls.filter(c=>c.body).length},sessions:w.db.raw.prepare('SELECT * FROM sessions').all(),presence:w.db.raw.prepare('SELECT * FROM seat_presence').all()};\n results.push(result);console.log(JSON.stringify(result));\n}\nfor(const delay of [0,1,5000]){\n const owners=[],seats={},onchain={};for(let id=1;id<=257;id++){owners[id]=a;seats[id]=String(id+700);onchain[id]=a;}\n const w=setup({chain:fakeChain({owners:onchain}),imd:fakeImd({seats,owners,online:[]})}),b=w.browser();let budget=0;\n w.env.CHAIN_LIMITER={limit:async()=>{budget++;return {success:true};}};\n assert.equal((await b.signIn(A)).verify.status,200);\n const t=w.clock.now(),insert=w.db.raw.prepare('INSERT INTO seat_presence(token_id,owner,last_online_at,updated_at) VALUES(?,?,?,?)');\n for(let id=1;id<=257;id++){const seen=id===257?t-1000:t-W;insert.run(id,a,seen,seen);}\n const reads=delayFirst(w,delay),r=await b.get('/api/me/home'),home=await r.json();await Promise.all(w.kept);\n const result={mode:'cap',delay,t,now:w.clock.now(),status:r.status,eligible:home.eligible,size:home.size,recheck:home.recheck,checkedAt:home.checkedAt,seats:home.seats?.length,has257:home.seats?.some(s=>s.tokenId==='257'),countingIds:home.seats?.filter(s=>s.counts).map(s=>s.tokenId),indexRows:w.db.raw.prepare('SELECT * FROM index_candidates').all(),sessions:w.db.raw.prepare('SELECT address,expires_at,revoked_at FROM sessions').all(),counts:{challenge:1,verify:1,home:1,sig","severity":"info","snippet":"        const order=(id:string)=>rank(agents.get(id),sightings.get(id),req.now);","title":"Candidate-cap ranking uses the stale request clock and omits a still-eligible seat"},{"citation":"resolved","description":"OPEN adjacent display consistency gap; the original Audit9 Low1 final household authority and lane fixes remain effective. assets() awaits world/sightings then passes req.now to status; its Worker caller server/auth.ts:735 supplies no live clock. Sighting expiry during D1 yields counts:true in this public unverified roster while the next request is false. Info only: public max-age=300 display hints, not owner authority; no index/budget/RPC amplification. Thread the live clock from the route and sample after awaited enrichment (account for later character I/O if enabled), without changing fetchedAt or the inclusive comparator. Synthetic D1 delays do not establish production effects. Immutable source: https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L371. Source SHA256 858cdfc9eec304e2c15b7f7b1ae66f846a9677c87a91c4333f56dd607abce207.","line":371,"path":"source/server/ownership.ts","reproduction":"REVIEWER command node /tmp/imd-a10-probe-ownership.mjs; exit1 ERR_ASSERTION assets must count at the post-await clock (true !== false). At t=1790596800000 put seat7/agent707 offline, roster owner A=0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a; insert seat_presence(7,A,1790510400001,1790510400001). After first real SELECT token_id,last_online_at completes advance clock D in0/1/2/5000ms. Two GET /api/wallet/A/assets. First counts=true for all four; expected true,true,false,false at ages86399999/86400000/86400001/86404999. Immediate second counts=true,true,false,false, with offline-24h reason on expired cases. Original presence row unchanged; sessions empty; fetchedAt=t. Each run assetsGET2/sightings2, index/budget/RPC0/0/0, prompt/connect/challenge/verify/logout/hint/broadcast0. No M1 writes. Script SHA256 837138d0f4b9c3e533c4bbece3874715834c0653c1a9a23ea32b6f3e256189e8. Full script (imports may be adjusted to the exact checkout location):\nimport assert from 'node:assert/strict';\nimport {privateKeyToAccount} from '/tmp/imd-audit10-review/source/node_modules/viem/_esm/accounts/index.js';\nimport {setup,fakeImd,fakeChain} from '/tmp/imd-audit10-review/source/tests/wallet-harness.mjs';\nconst A=privateKeyToAccount('0x'+'11'.repeat(32)),a=A.address.toLowerCase(),W=86400000,results=[];\nfunction delayFirst(w,delay){const prepare=w.db.prepare.bind(w.db);let reads=0;\n w.db.prepare=sql=>{const wrap=s=>({...s,bind:(...args)=>wrap(s.bind(...args)),all:async()=>{const r=await s.all();if(sql.startsWith('SELECT token_id,last_online_at')){reads++;if(reads===1)w.clock.advance(delay);}return r;}});return wrap(prepare(sql));};return ()=>reads;\n}\nfor(const delay of (process.argv[2]==='cap'?[]:[0,1,2,5000])){\n const owners=[];owners[7]=a;\n const w=setup({chain:fakeChain({owners:{7:a}}),imd:fakeImd({seats:{7:'707'},owners,online:[]})}),b=w.browser();let budget=0;\n w.env.CHAIN_LIMITER={limit:async()=>{budget++;return {success:true};}};\n const t=w.clock.now(),seen=t-W+1;w.db.raw.prepare('INSERT INTO seat_presence(token_id,owner,last_online_at,updated_at) VALUES(7,?,?,?)').run(a,seen,seen);\n const reads=delayFirst(w,delay),r=await b.get('/api/wallet/'+a+'/assets'),first=await r.json(),next=await(await b.get('/api/wallet/'+a+'/assets')).json();\n const result={mode:'assets',delay,t,now:w.clock.now(),first,next,cache:r.headers.get('cache-control'),expected:delay<=1,counts:{assets:2,sightings:reads(),index:w.chain.state.calls.filter(c=>!c.body).length,budget,rpc:w.chain.state.calls.filter(c=>c.body).length},sessions:w.db.raw.prepare('SELECT * FROM sessions').all(),presence:w.db.raw.prepare('SELECT * FROM seat_presence').all()};\n results.push(result);console.log(JSON.stringify(result));\n}\nfor(const delay of [0,1,5000]){\n const owners=[],seats={},onchain={};for(let id=1;id<=257;id++){owners[id]=a;seats[id]=String(id+700);onchain[id]=a;}\n const w=setup({chain:fakeChain({owners:onchain}),imd:fakeImd({seats,owners,online:[]})}),b=w.browser();let budget=0;\n w.env.CHAIN_LIMITER={limit:async()=>{budget++;return {success:true};}};\n assert.equal((await b.signIn(A)).verify.status,200);\n const t=w.clock.now(),insert=w.db.raw.prepare('INSERT INTO seat_presence(token_id,owner,last_online_at,updated_at) VALUES(?,?,?,?)');\n for(let id=1;id<=257;id++){const seen=id===257?t-1000:t-W;insert.run(id,a,seen,seen);}\n const reads=delayFirst(w,delay),r=await b.get('/api/me/home'),home=await r.json();await Promise.all(w.kept);\n const result={mode:'cap',delay,t,now:w.clock.now(),status:r.status,eligible:home.eligible,size:home.size,recheck:home.recheck,checkedAt:home.checkedAt,seats:home.seats?.length,has257:home.seats?.some(s=>s.tokenId==='257'),countingIds:home.seats?.filter(s=>s.counts).map(s=>s.tokenId),indexRows:w.db.raw.prepare('SELECT * FROM index_candidates').all(),sessions:w.db.raw.prepare('SELECT address,expires_at,revoked_at FROM sessions').all(),counts:{challenge:1,verify:1,home:1,sightings:reads(),index:w.chain.state.calls.filter(c=>!c.body).length,budget,rpc:w.chain.state.","severity":"info","snippet":"      seats:ids.map(id=>({...this.status(id,world.agents.get(id),seen.get(id),req.now),image:null})),","title":"Public assets counts expired sightings using request-entry time after D1 await"},{"citation":"resolved","description":"PARTIAL Audit9 Info5: dot/suffix filenames are fixed, but the route-prefix exemption treats whitespace as a route terminator. A bare local filename whose initial segments spell an allowed route bypasses the conservative remainder-of-line masking rule. Quoted paths mask correctly. This is an opt-in diagnostic redaction policy gap, not demonstrated production leakage or an Auth vulnerability. Require an unambiguous complete route token before exempting it, or explicitly narrow the claimed spaced-path guarantee and accept that limit. Preserve exact route/query/subroute identifiers, URLs, relative identifiers and replay structure. Reproduced by reading the actual file written by createArtifactStore, on exact c2f21a9 with Node24.21.0. Immutable source: https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/tests/auth-artifacts.mjs#L17. Source SHA256 af7e838ffa6cbd835a118c1e62b085ed15d5d2475d4e7663ce23668503db0d50.","line":17,"path":"source/tests/auth-artifacts.mjs","reproduction":"REVIEWER command node /tmp/imd-a10-probe-redaction.mjs; exit1 ERR_ASSERTION expected Error at [local-path], actual Error at /api/auth/session private.log. Script SHA256 aca91d15f68e018510291981baa1a5bdc622dd79c2ff4624f4e718ab2d372488. Write core500-RESULT.json through createArtifactStore under a synthetic source/tmp/artifacts with nested messages [Error at /api/auth/session private.log, Cannot read /api/auth/session (private)/x.ts, /api/auth/session dir/file.ts]. Read back: first/third unchanged; second becomes Cannot read /api/auth/session (private)[local-path]. Expected full absolute filename masking in all three. Quoted same path and non-route spaced path mask; /api/auth/session.log, /api/auth/verify.backup and /api/me/home.private.json mask; exact routes/query/subroute, network URL, relative ID, n1 nonce, actions/events remain byte-equivalent. Persisted output830 bytes SHA25609be431559624b3f6920191eae4172376da683d5ece76510b9c9978cac259dbf. No DB rows or prompt/connect/challenge/verify/logout/hint/broadcast/index/budget/RPC calls. Full script:\nimport assert from 'node:assert/strict';\nimport {mkdirSync,readFileSync} from 'node:fs';\nimport {createHash} from 'node:crypto';\nimport {createArtifactStore,sanitizeArtifact} from '/tmp/imd-audit10-review/source/tests/auth-artifacts.mjs';\nconst root='/tmp/imd-a10-redaction-fixture';mkdirSync(root,{recursive:true});\nconst store=createArtifactStore({sourceDir:root,requestedDir:'tmp/artifacts'});\nconst messages=['Error at /api/auth/session private.log','Cannot read /api/auth/session (private)/x.ts','/api/auth/session dir/file.ts','Cannot read \"/api/auth/session private.log\"','Error at /root/private directory/project.ts'];\nconst controls={routes:['/api/auth/session','/api/auth/verify','/api/me/home','/api/me/home?fresh=1','/api/me/home/refresh'],url:'https://example.com/api/auth/session.log',relative:'tests/auth-artifacts.test.mjs',nonce:'n1',actions:[{type:'start',tab:'a'}],events:[{index:0,type:'headers',id:'r1'}]};\nconst filenames=['/api/auth/session.log','/api/auth/verify.backup','/api/me/home.private.json'];\nstore.write('core500-RESULT.json',{nested:{errors:messages},filenames,controls});\nconst bytes=readFileSync(store.directory+'/core500-RESULT.json'),saved=JSON.parse(bytes);\nassert.deepEqual(saved.controls,controls);assert.deepEqual(saved.filenames,filenames.map(()=>'[local-path]'));\nassert.equal(saved.nested.errors[3],'Cannot read \"[local-path]\"');assert.equal(saved.nested.errors[4],'Error at [local-path]');\nconsole.log(JSON.stringify({messages,saved,bytes:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex')}));\nfor(const text of ['Loaded module.file:///root/reviewer/private/project.ts','see x-file:///root/reviewer/private/project.ts','https://example.com/a,/root/reviewer/private/project.ts','at file:///root/reviewer/private/project.ts:12:3','node:internal/modules/esm/resolve:272'])console.log(JSON.stringify({input:text,actual:sanitizeArtifact(text)}));\nassert.equal(saved.nested.errors[0],'Error at [local-path]','persisted bare route-prefix filename with spaces must be masked');\n","severity":"info","snippet":"    const route=[...routeIds].find(id=>chunk.startsWith(id,match.index)&&/^(?:$|[ \\t]|[,;|)\"'<>](?=[ \\t]|$))/.test(chunk.slice(match.index+id.length)));","title":"Bare route-prefix filenames containing spaces survive persisted artifact redaction"}],"hash":"fca579e45543c94ad3690021b52023d9bd739a3d50e1883aa318572ac4b6c97f","nodeId":"2f79cf0f-f0b7-4bec-9c81-5357a4c90873","outcome":"completed","summary":"**SOURCE-CLOSURE: BLOCKED.** The candidate fixes the original reproductions and both named adjacent cases, but does not preserve every required boundary. I independently reproduced **1 Low and 3 Info findings**, merged duplicates, and saved them—with executable reproduction text—to [.imd-findings.json](/home/seat/.identitymd/work/e817a62e-1b9f-4469-90d7-7a761579af81/2f79cf0f-f0b7-4bec-9c81-5357a4c90873/.imd-findings.json).\n\n**RELEASE-READINESS: UNKNOWN.** This source was not deployed or tested against production.\n\nThe controlling history is [Audit9 at its immutable pin](https://github.com/Identity-md/research/blob/911652a2b1a7ab7be7d16bc37d97ab9376028fe6/jobs/d76a2a79-7394-420a-99d2-df2ba6a23f45/files/AUDIT.md), completed October 5 at 04:30 UTC. Earlier Report9 acceptance does not override it.\n\n| Audit9 mechanism | Reviewer closure | Evidence and reverse controls |\n|---|---|---|\n| **Low1 — post-await eligibility** | **CLOSED original; PARTIAL broader mechanism** | All 25 formal cases passed: inclusive 24-hour boundaries, both enrichment reads, refresh lanes, strict proof TTL, invalid clocks and recovery. Final eligibility preserves proof/index timestamps. Two adjacent **Info** defects remain: stale candidate-cap ranking and public-assets counting. [Final-clock implementation](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/server/ownership.ts#L349-L371). |\n| **Low2 — same-client restart** | **CLOSED, bounded** | Restart clears prior identity; first B/empty/no-provider observations preserve cookie A. Held initial replies plus early clicks obtain current explicit grants; late empty replies cannot erase B. Genuine B-to-A cleanup succeeds when its completion is awaited. One timing-sensitive suite assertion is detailed below. [Lifetime/binding fences](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L257-L299). |\n| **Low3 — detached verify owner** | **PARTIAL; Low finding OPEN** | The original first-C event preserves A’s SQLite row; current C-to-D cleanup and delayed-cookie reconciliation pass. However, passive discovery can overwrite an abandoned owner’s disposition and suppress its nonce-cleanup retry. [Current-life account guard](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L594-L612), [unfiltered discovery path](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/src/world/auth.ts#L304-L313). |\n| **Info4 — replay output containment** | **CLOSED, bounded** | Actual CLI controls reject unsafe final/parent links, nonregular targets, namespace/root escapes and input collisions. Separate output through an aliased input preserves both input views; default replay persists nothing new. Linux symlinks were exercised; Windows junction behavior remains TEAM evidence. [CLI validation/writer](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/scripts/replay-auth-trace.mjs#L13-L35). |\n| **Info5 — route-prefix filename masking** | **PARTIAL; Info finding OPEN** | Required `.log`, `.backup`, `.private.json` and attached suffix variants are masked, with exact protocol/replay controls preserved. Bare filenames containing spaces still inherit the route exemption and survive persistence. [Exemption and masking policy](https://github.com/tungweb3/imd-ember-world-review/blob/c2f21a9ef9e1a093ed2c5808f8a99e4751fde643/source/tests/auth-artifacts.mjs#L8-L35). |\n\n**Retained findings and reproductions**\n\n**F1 — Low: passive discovery abandons required nonce cleanup.** B’s verify commits and sets its cookie while the client response is held. Stop the client; make its first nonce logout fail before Worker dispatch; restart; replace the provider passively; release verify. Discovery changes `stop` to `lock-reconcile`; the late completion reconciles and r","treeHash":null,"usage":{"cachedInputTokens":3481472,"inputTokens":216137,"model":"gpt-6-astra","outputTokens":24749,"runtime":"codex","turns":8,"wallClockMs":888613}}],"verification":[]}