From 93efeb004068d1f47f5fb03752aa61a7cf829f38 Mon Sep 17 00:00:00 2001 From: Kuba Suder Date: Mon, 20 May 2024 01:54:07 +0200 Subject: [PATCH] reworked authentication to call user's actual PDS --- api.js | 2 +- minisky.js | 56 +++++++++++++++++++++++++++++++++++++++++++++++++--- skythread.js | 29 +++++++++++++++++++++++---- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/api.js b/api.js index 19722cb..8167d9b 100644 --- a/api.js +++ b/api.js @@ -84,7 +84,7 @@ class LocalStorageConfig { class BlueskyAPI extends Minisky { - /** @param {string} host, @param {boolean} useAuthentication */ + /** @param {string | undefined} host, @param {boolean} useAuthentication */ constructor(host, useAuthentication) { super(host, useAuthentication ? new LocalStorageConfig() : undefined); diff --git a/minisky.js b/minisky.js index b8c1d7f..8124f0d 100644 --- a/minisky.js +++ b/minisky.js @@ -20,24 +20,61 @@ class APIError extends Error { class AuthError extends Error {} +/** + * Thrown when DID or DID document is invalid. + */ + +class DIDError extends Error {} + + /** * Base API client for connecting to an ATProto XRPC API. */ class Minisky { + /** @param {string} did, @returns {Promise} */ + + static async pdsEndpointForDid(did) { + let url; + + if (did.startsWith('did:plc:')) { + url = new URL(`https://plc.directory/${did}`); + } else if (did.startsWith('did:web:')) { + let host = did.replace(/^did:web:/, ''); + url = new URL(`https://${host}/.well-known/did.json`); + } else { + throw new DIDError("Unknown DID type: " + did); + } + + let response = await fetch(url); + let text = await response.text(); + let json = text.trim().length > 0 ? JSON.parse(text) : undefined; + + if (response.status == 200) { + let service = (json.service || []).find(s => s.id == '#atproto_pds'); + if (service) { + return service.serviceEndpoint; + } else { + throw new DIDError("Missing #atproto_pds service definition"); + } + } else { + throw new APIError(response.status, json); + } + } + /** * @typedef {object} MiniskyOptions * @prop {boolean} [sendAuthHeaders] * @prop {boolean} [autoManageTokens] * - * @param {string} host, @param {object} config, @param {MiniskyOptions} [options] + * @param {string | undefined} host, @param {object} config, @param {MiniskyOptions} [options] */ + constructor(host, config, options) { this.host = host; this.config = config; this.user = config?.user; - this.baseURL = (host.includes('://') ? host : `https://${host}`) + '/xrpc'; this.sendAuthHeaders = !!this.user; this.autoManageTokens = !!this.user; @@ -47,10 +84,21 @@ class Minisky { } } + /** @returns {string} */ + + get baseURL() { + if (this.host) { + let host = (this.host.includes('://')) ? this.host : `https://${this.host}`; + return host + '/xrpc'; + } else { + throw new AuthError('Hostname not set'); + } + } + /** @returns {boolean} */ get isLoggedIn() { - return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did); + return !!(this.user && this.user.accessToken && this.user.refreshToken && this.user.did && this.user.pdsEndpoint); } /** @@ -210,6 +258,7 @@ class Minisky { this.user.accessToken = json['accessJwt']; this.user.refreshToken = json['refreshJwt']; this.user.did = json['did']; + this.user.pdsEndpoint = json['didDoc']['service'].find(s => s.id == '#atproto_pds')['serviceEndpoint']; this.config.save(); } @@ -217,6 +266,7 @@ class Minisky { delete this.user.accessToken; delete this.user.refreshToken; delete this.user.did; + delete this.user.pdsEndpoint; this.config.save(); } } diff --git a/skythread.js b/skythread.js index d26fafc..40d508b 100644 --- a/skythread.js +++ b/skythread.js @@ -68,13 +68,15 @@ function init() { window.appView = new BlueskyAPI('api.bsky.app', false); window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false); - window.accountAPI = new BlueskyAPI('bsky.social', true); + window.accountAPI = new BlueskyAPI(undefined, true); if (accountAPI.isLoggedIn && !isIncognito) { window.api = accountAPI; + accountAPI.host = accountAPI.user.pdsEndpoint; showLoggedInStatus(true, api.user.avatar); } else if (accountAPI.isLoggedIn && isIncognito) { window.api = appView; + accountAPI.host = accountAPI.user.pdsEndpoint; showLoggedInStatus('incognito'); document.querySelector('#account_menu a[data-action=incognito]').innerText = '✓ Incognito mode'; } else { @@ -213,15 +215,13 @@ function submitLogin() { if (submit.style.display == 'none') { return } - let pds = new BlueskyAPI('bsky.social', true); - handle.blur(); password.blur(); submit.style.display = 'none'; cloudy.style.display = 'inline-block'; - pds.logIn(handle.value, password.value).then(() => { + logIn(handle.value, password.value).then((pds) => { window.api = pds; hideLogin(); @@ -239,6 +239,27 @@ function submitLogin() { }); } +/** @param {string} identifier, @param {string} password, @returns {Promise} */ + +async function logIn(identifier, password) { + let pdsEndpoint; + + if (identifier.match(/^did:/)) { + pdsEndpoint = await Minisky.pdsEndpointForDid(identifier); + } else if (identifier.match(/^[^@]+@[^@]+$/)) { + pdsEndpoint = 'bsky.social'; + } else if (identifier.match(/^[\w\-]+(\.[\w\-]+)+$/)) { + let did = await appView.resolveHandle(identifier); + pdsEndpoint = await Minisky.pdsEndpointForDid(did); + } else { + throw 'Please enter your handle or DID'; + } + + let pds = new BlueskyAPI(pdsEndpoint, true); + await pds.logIn(identifier, password); + return pds; +} + function loadCurrentUserAvatar() { api.loadCurrentUserAvatar().then(data => { if (data) { -- 2.51.2