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 pds_auth_server in OAuth sessions for token refresh

refreshPdsSession was using pds_endpoint (the data PDS, e.g.
porcini.us-east.host.bsky.network) for the token endpoint, but
Bluesky-hosted PDS nodes don't serve /oauth/token — the auth
server (bsky.social) does. This caused token refresh to 404,
deleting the session and forcing users to re-login.

Now stores pds_auth_server from the OAuth request into the session
table and uses it for refresh, with fallback to pds_endpoint for
self-hosted PDS where they're the same.

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

+17 -7
+1 -1
packages/hatk/package.json
··· 1 1 { 2 2 "name": "@hatk/hatk", 3 - "version": "0.0.1-alpha.48", 3 + "version": "0.0.1-alpha.49", 4 4 "license": "MIT", 5 5 "bin": { 6 6 "hatk": "dist/cli.js"
+5
packages/hatk/src/database/db.ts
··· 156 156 157 157 // OAuth tables 158 158 await port.executeMultiple(OAUTH_DDL) 159 + 160 + // Migrations: add pds_auth_server to existing sessions tables 161 + try { 162 + await run(`ALTER TABLE _oauth_sessions ADD COLUMN pds_auth_server TEXT`) 163 + } catch {} 159 164 } 160 165 161 166 interface MigrationChange {
+5 -3
packages/hatk/src/oauth/db.ts
··· 15 15 CREATE TABLE IF NOT EXISTS _oauth_sessions ( 16 16 did TEXT PRIMARY KEY, 17 17 pds_endpoint TEXT NOT NULL, 18 + pds_auth_server TEXT, 18 19 access_token TEXT NOT NULL, 19 20 refresh_token TEXT, 20 21 dpop_jkt TEXT NOT NULL, ··· 162 163 did: string, 163 164 data: { 164 165 pdsEndpoint: string 166 + pdsAuthServer?: string 165 167 accessToken: string 166 168 refreshToken?: string 167 169 dpopJkt: string ··· 169 171 }, 170 172 ): Promise<void> { 171 173 await runSQL( 172 - `INSERT OR REPLACE INTO _oauth_sessions (did, pds_endpoint, access_token, refresh_token, dpop_jkt, token_expires_at, updated_at) 173 - VALUES ($1,$2,$3,$4,$5,$6,CURRENT_TIMESTAMP)`, 174 - [did, data.pdsEndpoint, data.accessToken, data.refreshToken || null, data.dpopJkt, data.tokenExpiresAt || null], 174 + `INSERT OR REPLACE INTO _oauth_sessions (did, pds_endpoint, pds_auth_server, access_token, refresh_token, dpop_jkt, token_expires_at, updated_at) 175 + VALUES ($1,$2,$3,$4,$5,$6,$7,CURRENT_TIMESTAMP)`, 176 + [did, data.pdsEndpoint, data.pdsAuthServer || null, data.accessToken, data.refreshToken || null, data.dpopJkt, data.tokenExpiresAt || null], 175 177 ) 176 178 } 177 179
+6 -3
packages/hatk/src/oauth/server.ts
··· 606 606 if (!did) throw new Error('PDS token response missing sub (DID)') 607 607 608 608 // Store PDS session server-side — pds_endpoint is the actual data PDS 609 - // (e.g. leccinum.us-west.host.bsky.network), not the auth server (bsky.social) 609 + // (e.g. leccinum.us-west.host.bsky.network), pds_auth_server is the OAuth server (bsky.social) 610 610 await storeSession(did, { 611 611 pdsEndpoint: request.pds_endpoint, 612 + pdsAuthServer: request.pds_auth_server, 612 613 accessToken: tokenData.access_token, 613 614 refreshToken: tokenData.refresh_token, 614 615 dpopJkt: serverJkt, ··· 808 809 809 810 export async function refreshPdsSession( 810 811 config: OAuthConfig, 811 - session: { did: string; pds_endpoint: string; refresh_token: string; dpop_jkt: string }, 812 + session: { did: string; pds_endpoint: string; pds_auth_server?: string; refresh_token: string; dpop_jkt: string }, 812 813 ): Promise<{ accessToken: string; refreshToken?: string; expiresAt?: number } | null> { 813 814 if (!session.refresh_token) return null 814 815 815 - const tokenEndpoint = `${session.pds_endpoint}/oauth/token` 816 + // Use auth server for token endpoint (falls back to pds_endpoint for sessions created before this fix) 817 + const tokenEndpoint = `${session.pds_auth_server || session.pds_endpoint}/oauth/token` 816 818 const clientId = pdsClientId(config.issuer, config) 817 819 const dpopProof = await createDpopProof(serverPrivateJwk, serverPublicJwk, 'POST', tokenEndpoint) 818 820 ··· 866 868 // Update stored session 867 869 await storeSession(session.did, { 868 870 pdsEndpoint: session.pds_endpoint, 871 + pdsAuthServer: session.pds_auth_server, 869 872 accessToken: tokenData.access_token, 870 873 refreshToken: tokenData.refresh_token || session.refresh_token, 871 874 dpopJkt: session.dpop_jkt,