Summary#
Several components make authenticated PDS calls via authenticatedXrpc on the main thread, loading session directly from IndexedDB. If a main-thread XRPC call overlaps with a worker PDS call, both read/write the same OAuthSession in IndexedDB — same DPoP nonce field, same last-writer-wins race as the worker-internal issue.
Affected call sites#
High risk (can race with cabinet operations)#
| File | Function | What it does |
|---|---|---|
lib/preview.ts |
decryptDocumentBlob |
Authenticated blob fetch for file previews |
components/cabinet/DirectoryReadme.tsx |
fetchAndDecryptReadme |
Same pattern for README previews |
components/cabinet/ShareDialog.tsx |
handleShare |
Fetches document record + creates grant |
routes/cabinet/shared.tsx |
fetchGrants |
Lists outgoing grants via listOutgoingGrants |
routes/cabinet/shared.tsx |
handleRevoke |
Revokes grant via revokeGrant |
Low risk (user-initiated, not concurrent with cabinet)#
| File | Function | What it does |
|---|---|---|
routes/cabinet/settings.tsx |
save handler | Reads/writes account config |
lib/pairing.ts |
all functions | Device pairing protocol |
stores/auth.ts |
checkIdentity, publishIdentityKey |
Identity setup |
Root cause#
authenticatedXrpc in lib/api.ts loads session from IndexedDB, creates a DPoP proof, makes the request, and handles token refresh — all on the main thread. The worker's session gate does the same thing independently. Both compete for the same IndexedDB session record.
Proposed fix#
Migrate high-risk call sites to worker PDS methods (most already have WASM equivalents):
revokeGrant→worker.grantRevoke()(exists)listOutgoingGrants→worker.listGrantsRaw()+ client-side filteringShareDialoggrant creation →worker.documentFetchContentKey()+worker.grantCreate()(both exist)- Preview blob fetch →
worker.documentFetchContentKey()through gate + unauthenticatedgetBlob(blobs are public in atproto), or add a WASM export that returns full metadata createPendingShare→ needs new WASM export (no equivalent yet)
Low-risk call sites can remain on main thread for now.
Note on blob fetch#
com.atproto.sync.getBlob may or may not require authentication depending on the PDS implementation. The Rust opake-core attaches auth headers to getBlob calls. The sharing.ts incoming grant flow fetches blobs without auth and it works for cross-PDS access. Testing needed to confirm whether the user's own PDS requires auth for getBlob.