AppView in a box as a Vite plugin thing hatk.dev
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: store actual PDS endpoint from DID discovery for OAuth sessions

The session's pdsEndpoint was derived from pds_auth_server, but for
bsky.social users the auth server (bsky.social) differs from the data
PDS (e.g. leccinum.us-west.host.bsky.network). XRPC proxy calls were
going to the wrong host, causing immediate InvalidToken errors.

Also handle AT Proto PascalCase error codes (InvalidToken, ExpiredToken)
alongside OAuth snake_case (invalid_token) in the DPoP retry logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+19 -6
+1 -1
packages/hatk/package.json
··· 1 1 { 2 2 "name": "@hatk/hatk", 3 - "version": "0.0.1-alpha.42", 3 + "version": "0.0.1-alpha.43", 4 4 "license": "MIT", 5 5 "bin": { 6 6 "hatk": "dist/cli.js"
+5 -2
packages/hatk/src/oauth/db.ts
··· 34 34 dpop_jkt TEXT NOT NULL, 35 35 pds_request_uri TEXT, 36 36 pds_auth_server TEXT, 37 + pds_endpoint TEXT, 37 38 pds_code_verifier TEXT, 38 39 pds_state TEXT, 39 40 did TEXT, ··· 94 95 dpopJkt: string 95 96 pdsRequestUri?: string 96 97 pdsAuthServer?: string 98 + pdsEndpoint?: string 97 99 pdsCodeVerifier?: string 98 100 pdsState?: string 99 101 did?: string ··· 102 104 }, 103 105 ): Promise<void> { 104 106 await runSQL( 105 - `INSERT INTO _oauth_requests (request_uri, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, dpop_jkt, pds_request_uri, pds_auth_server, pds_code_verifier, pds_state, did, login_hint, expires_at) 106 - VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)`, 107 + `INSERT INTO _oauth_requests (request_uri, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, dpop_jkt, pds_request_uri, pds_auth_server, pds_endpoint, pds_code_verifier, pds_state, did, login_hint, expires_at) 108 + VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`, 107 109 [ 108 110 requestUri, 109 111 data.clientId, ··· 115 117 data.dpopJkt, 116 118 data.pdsRequestUri || null, 117 119 data.pdsAuthServer || null, 120 + data.pdsEndpoint || null, 118 121 data.pdsCodeVerifier || null, 119 122 data.pdsState || null, 120 123 data.did || null,
+9 -2
packages/hatk/src/oauth/server.ts
··· 207 207 let pdsCodeVerifier: string | undefined 208 208 let pdsState: string | undefined 209 209 210 + let pdsEndpoint: string | undefined 211 + 210 212 if (did) { 211 213 const discovery = await discoverAuthServer(did, _plcUrl) 212 214 pdsAuthServer = discovery.authServerEndpoint 215 + pdsEndpoint = discovery.pdsEndpoint 213 216 214 217 // Create PKCE for our PAR to the PDS 215 218 pdsCodeVerifier = randomToken() ··· 301 304 dpopJkt: dpop.jkt, 302 305 pdsRequestUri, 303 306 pdsAuthServer, 307 + pdsEndpoint, 304 308 pdsCodeVerifier, 305 309 pdsState, 306 310 did, ··· 336 340 // Discover PDS auth server 337 341 const discovery = await discoverAuthServer(did, _plcUrl) 338 342 const pdsAuthServer = discovery.authServerEndpoint 343 + const pdsEndpoint = discovery.pdsEndpoint 339 344 340 345 // Create PKCE for PAR to PDS 341 346 const pdsCodeVerifier = randomToken() ··· 413 418 dpopJkt: serverJkt, 414 419 pdsRequestUri, 415 420 pdsAuthServer, 421 + pdsEndpoint, 416 422 pdsCodeVerifier, 417 423 pdsState, 418 424 did, ··· 529 535 const did = tokenData.sub 530 536 if (!did) throw new Error('PDS token response missing sub (DID)') 531 537 532 - // Store PDS session server-side 538 + // Store PDS session server-side — pds_endpoint is the actual data PDS 539 + // (e.g. leccinum.us-west.host.bsky.network), not the auth server (bsky.social) 533 540 await storeSession(did, { 534 - pdsEndpoint: request.pds_auth_server.replace('/oauth', ''), 541 + pdsEndpoint: request.pds_endpoint, 535 542 accessToken: tokenData.access_token, 536 543 refreshToken: tokenData.refresh_token, 537 544 dpopJkt: serverJkt,
+4 -1
packages/hatk/src/pds-proxy.ts
··· 65 65 } 66 66 67 67 // Step 3: handle expired PDS token — refresh and retry 68 - if (result.body.error === 'invalid_token') { 68 + // The PDS returns 'InvalidToken' or 'ExpiredToken' (AT Proto PascalCase convention) 69 + // while the OAuth spec uses 'invalid_token' (RFC 6750 snake_case) 70 + const err = result.body.error 71 + if (err === 'invalid_token' || err === 'InvalidToken' || err === 'ExpiredToken') { 69 72 const refreshed = await refreshPdsSession(oauthConfig, session) 70 73 if (refreshed) { 71 74 accessToken = refreshed.accessToken