Thread viewer for Bluesky

reworked authentication to call user's actual PDS

Changed files
+79 -8
+1 -1
api.js
··· 84 85 class BlueskyAPI extends Minisky { 86 87 - /** @param {string} host, @param {boolean} useAuthentication */ 88 constructor(host, useAuthentication) { 89 super(host, useAuthentication ? new LocalStorageConfig() : undefined); 90
··· 84 85 class BlueskyAPI extends Minisky { 86 87 + /** @param {string | undefined} host, @param {boolean} useAuthentication */ 88 constructor(host, useAuthentication) { 89 super(host, useAuthentication ? new LocalStorageConfig() : undefined); 90
+53 -3
minisky.js
··· 21 22 23 /** 24 * Base API client for connecting to an ATProto XRPC API. 25 */ 26 27 class Minisky { 28 29 /** 30 * @typedef {object} MiniskyOptions 31 * @prop {boolean} [sendAuthHeaders] 32 * @prop {boolean} [autoManageTokens] 33 * 34 - * @param {string} host, @param {object} config, @param {MiniskyOptions} [options] 35 */ 36 constructor(host, config, options) { 37 this.host = host; 38 this.config = config; 39 this.user = config?.user; 40 - this.baseURL = (host.includes('://') ? host : `https://${host}`) + '/xrpc'; 41 42 this.sendAuthHeaders = !!this.user; 43 this.autoManageTokens = !!this.user; ··· 47 } 48 } 49 50 /** @returns {boolean} */ 51 52 get isLoggedIn() { 53 - return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did); 54 } 55 56 /** ··· 210 this.user.accessToken = json['accessJwt']; 211 this.user.refreshToken = json['refreshJwt']; 212 this.user.did = json['did']; 213 this.config.save(); 214 } 215 ··· 217 delete this.user.accessToken; 218 delete this.user.refreshToken; 219 delete this.user.did; 220 this.config.save(); 221 } 222 }
··· 21 22 23 /** 24 + * Thrown when DID or DID document is invalid. 25 + */ 26 + 27 + class DIDError extends Error {} 28 + 29 + 30 + /** 31 * Base API client for connecting to an ATProto XRPC API. 32 */ 33 34 class Minisky { 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 + 66 /** 67 * @typedef {object} MiniskyOptions 68 * @prop {boolean} [sendAuthHeaders] 69 * @prop {boolean} [autoManageTokens] 70 * 71 + * @param {string | undefined} host, @param {object} config, @param {MiniskyOptions} [options] 72 */ 73 + 74 constructor(host, config, options) { 75 this.host = host; 76 this.config = config; 77 this.user = config?.user; 78 79 this.sendAuthHeaders = !!this.user; 80 this.autoManageTokens = !!this.user; ··· 84 } 85 } 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 + 98 /** @returns {boolean} */ 99 100 get isLoggedIn() { 101 + return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did && this.user.pdsEndpoint); 102 } 103 104 /** ··· 258 this.user.accessToken = json['accessJwt']; 259 this.user.refreshToken = json['refreshJwt']; 260 this.user.did = json['did']; 261 + this.user.pdsEndpoint = json['didDoc']['service'].find(s => s.id == '#atproto_pds')['serviceEndpoint']; 262 this.config.save(); 263 } 264 ··· 266 delete this.user.accessToken; 267 delete this.user.refreshToken; 268 delete this.user.did; 269 + delete this.user.pdsEndpoint; 270 this.config.save(); 271 } 272 }
+25 -4
skythread.js
··· 68 69 window.appView = new BlueskyAPI('api.bsky.app', false); 70 window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false); 71 - window.accountAPI = new BlueskyAPI('bsky.social', true); 72 73 if (accountAPI.isLoggedIn && !isIncognito) { 74 window.api = accountAPI; 75 showLoggedInStatus(true, api.user.avatar); 76 } else if (accountAPI.isLoggedIn && isIncognito) { 77 window.api = appView; 78 showLoggedInStatus('incognito'); 79 document.querySelector('#account_menu a[data-action=incognito]').innerText = '✓ Incognito mode'; 80 } else { ··· 213 214 if (submit.style.display == 'none') { return } 215 216 - let pds = new BlueskyAPI('bsky.social', true); 217 - 218 handle.blur(); 219 password.blur(); 220 221 submit.style.display = 'none'; 222 cloudy.style.display = 'inline-block'; 223 224 - pds.logIn(handle.value, password.value).then(() => { 225 window.api = pds; 226 227 hideLogin(); ··· 237 238 window.setTimeout(() => alert(error), 10); 239 }); 240 } 241 242 function loadCurrentUserAvatar() {
··· 68 69 window.appView = new BlueskyAPI('api.bsky.app', false); 70 window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false); 71 + window.accountAPI = new BlueskyAPI(undefined, true); 72 73 if (accountAPI.isLoggedIn && !isIncognito) { 74 window.api = accountAPI; 75 + accountAPI.host = accountAPI.user.pdsEndpoint; 76 showLoggedInStatus(true, api.user.avatar); 77 } else if (accountAPI.isLoggedIn && isIncognito) { 78 window.api = appView; 79 + accountAPI.host = accountAPI.user.pdsEndpoint; 80 showLoggedInStatus('incognito'); 81 document.querySelector('#account_menu a[data-action=incognito]').innerText = '✓ Incognito mode'; 82 } else { ··· 215 216 if (submit.style.display == 'none') { return } 217 218 handle.blur(); 219 password.blur(); 220 221 submit.style.display = 'none'; 222 cloudy.style.display = 'inline-block'; 223 224 + logIn(handle.value, password.value).then((pds) => { 225 window.api = pds; 226 227 hideLogin(); ··· 237 238 window.setTimeout(() => alert(error), 10); 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; 261 } 262 263 function loadCurrentUserAvatar() {