Thread viewer for Bluesky

reworked authentication to call user's actual PDS

Changed files
+79 -8
+1 -1
api.js
··· 84 84 85 85 class BlueskyAPI extends Minisky { 86 86 87 - /** @param {string} host, @param {boolean} useAuthentication */ 87 + /** @param {string | undefined} host, @param {boolean} useAuthentication */ 88 88 constructor(host, useAuthentication) { 89 89 super(host, useAuthentication ? new LocalStorageConfig() : undefined); 90 90
+53 -3
minisky.js
··· 21 21 22 22 23 23 /** 24 + * Thrown when DID or DID document is invalid. 25 + */ 26 + 27 + class DIDError extends Error {} 28 + 29 + 30 + /** 24 31 * Base API client for connecting to an ATProto XRPC API. 25 32 */ 26 33 27 34 class Minisky { 28 35 36 + /** @param {string} did, @returns {Promise<string>} */ 37 + 38 + static async pdsEndpointForDid(did) { 39 + let url; 40 + 41 + if (did.startsWith('did:plc:')) { 42 + url = new URL(`https://plc.directory/${did}`); 43 + } else if (did.startsWith('did:web:')) { 44 + let host = did.replace(/^did:web:/, ''); 45 + url = new URL(`https://${host}/.well-known/did.json`); 46 + } else { 47 + throw new DIDError("Unknown DID type: " + did); 48 + } 49 + 50 + let response = await fetch(url); 51 + let text = await response.text(); 52 + let json = text.trim().length > 0 ? JSON.parse(text) : undefined; 53 + 54 + if (response.status == 200) { 55 + let service = (json.service || []).find(s => s.id == '#atproto_pds'); 56 + if (service) { 57 + return service.serviceEndpoint; 58 + } else { 59 + throw new DIDError("Missing #atproto_pds service definition"); 60 + } 61 + } else { 62 + throw new APIError(response.status, json); 63 + } 64 + } 65 + 29 66 /** 30 67 * @typedef {object} MiniskyOptions 31 68 * @prop {boolean} [sendAuthHeaders] 32 69 * @prop {boolean} [autoManageTokens] 33 70 * 34 - * @param {string} host, @param {object} config, @param {MiniskyOptions} [options] 71 + * @param {string | undefined} host, @param {object} config, @param {MiniskyOptions} [options] 35 72 */ 73 + 36 74 constructor(host, config, options) { 37 75 this.host = host; 38 76 this.config = config; 39 77 this.user = config?.user; 40 - this.baseURL = (host.includes('://') ? host : `https://${host}`) + '/xrpc'; 41 78 42 79 this.sendAuthHeaders = !!this.user; 43 80 this.autoManageTokens = !!this.user; ··· 47 84 } 48 85 } 49 86 87 + /** @returns {string} */ 88 + 89 + get baseURL() { 90 + if (this.host) { 91 + let host = (this.host.includes('://')) ? this.host : `https://${this.host}`; 92 + return host + '/xrpc'; 93 + } else { 94 + throw new AuthError('Hostname not set'); 95 + } 96 + } 97 + 50 98 /** @returns {boolean} */ 51 99 52 100 get isLoggedIn() { 53 - return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did); 101 + return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did && this.user.pdsEndpoint); 54 102 } 55 103 56 104 /** ··· 210 258 this.user.accessToken = json['accessJwt']; 211 259 this.user.refreshToken = json['refreshJwt']; 212 260 this.user.did = json['did']; 261 + this.user.pdsEndpoint = json['didDoc']['service'].find(s => s.id == '#atproto_pds')['serviceEndpoint']; 213 262 this.config.save(); 214 263 } 215 264 ··· 217 266 delete this.user.accessToken; 218 267 delete this.user.refreshToken; 219 268 delete this.user.did; 269 + delete this.user.pdsEndpoint; 220 270 this.config.save(); 221 271 } 222 272 }
+25 -4
skythread.js
··· 68 68 69 69 window.appView = new BlueskyAPI('api.bsky.app', false); 70 70 window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false); 71 - window.accountAPI = new BlueskyAPI('bsky.social', true); 71 + window.accountAPI = new BlueskyAPI(undefined, true); 72 72 73 73 if (accountAPI.isLoggedIn && !isIncognito) { 74 74 window.api = accountAPI; 75 + accountAPI.host = accountAPI.user.pdsEndpoint; 75 76 showLoggedInStatus(true, api.user.avatar); 76 77 } else if (accountAPI.isLoggedIn && isIncognito) { 77 78 window.api = appView; 79 + accountAPI.host = accountAPI.user.pdsEndpoint; 78 80 showLoggedInStatus('incognito'); 79 81 document.querySelector('#account_menu a[data-action=incognito]').innerText = '✓ Incognito mode'; 80 82 } else { ··· 213 215 214 216 if (submit.style.display == 'none') { return } 215 217 216 - let pds = new BlueskyAPI('bsky.social', true); 217 - 218 218 handle.blur(); 219 219 password.blur(); 220 220 221 221 submit.style.display = 'none'; 222 222 cloudy.style.display = 'inline-block'; 223 223 224 - pds.logIn(handle.value, password.value).then(() => { 224 + logIn(handle.value, password.value).then((pds) => { 225 225 window.api = pds; 226 226 227 227 hideLogin(); ··· 237 237 238 238 window.setTimeout(() => alert(error), 10); 239 239 }); 240 + } 241 + 242 + /** @param {string} identifier, @param {string} password, @returns {Promise<BlueskyAPI>} */ 243 + 244 + async function logIn(identifier, password) { 245 + let pdsEndpoint; 246 + 247 + if (identifier.match(/^did:/)) { 248 + pdsEndpoint = await Minisky.pdsEndpointForDid(identifier); 249 + } else if (identifier.match(/^[^@]+@[^@]+$/)) { 250 + pdsEndpoint = 'bsky.social'; 251 + } else if (identifier.match(/^[\w\-]+(\.[\w\-]+)+$/)) { 252 + let did = await appView.resolveHandle(identifier); 253 + pdsEndpoint = await Minisky.pdsEndpointForDid(did); 254 + } else { 255 + throw 'Please enter your handle or DID'; 256 + } 257 + 258 + let pds = new BlueskyAPI(pdsEndpoint, true); 259 + await pds.logIn(identifier, password); 260 + return pds; 240 261 } 241 262 242 263 function loadCurrentUserAvatar() {