Various AT Protocol integrations with obsidian
20
fork

Configure Feed

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

switch back to browser client

+62 -106
+3 -3
bun.lock
··· 12 12 "@atcute/identity-resolver-node": "^1.0.3", 13 13 "@atcute/leaflet": "^1.0.17", 14 14 "@atcute/microcosm": "^1.0.1", 15 - "@atcute/oauth-node-client": "^1.1.0", 15 + "@atcute/oauth-browser-client": "^3.0.0", 16 16 "@atcute/pckt": "^0.1.5", 17 17 "@atcute/standard-site": "^1.0.0", 18 18 "obsidian": "latest", ··· 71 71 72 72 "@atcute/multibase": ["@atcute/multibase@1.1.7", "", { "dependencies": { "@atcute/uint8array": "^1.1.0" } }, "sha512-YmWds7U52b7Qri0xNfGeqSOvgyNfHR8Yy/NNDQx4d5TkCX2fHJIo0pXquEhCyMNAwKt53uH5yQDswy4TNP1Zhw=="], 73 73 74 + "@atcute/oauth-browser-client": ["@atcute/oauth-browser-client@3.0.0", "", { "dependencies": { "@atcute/client": "^4.2.1", "@atcute/identity-resolver": "^1.2.2", "@atcute/lexicons": "^1.2.7", "@atcute/multibase": "^1.1.7", "@atcute/oauth-crypto": "^0.1.0", "@atcute/oauth-types": "^0.1.0", "nanoid": "^5.1.6" } }, "sha512-7AbKV8tTe7aRJNJV7gCcWHSVEADb2nr58O1p7dQsf73HSe9pvlBkj/Vk1yjjtH691uAVYkwhHSh0bC7D8XdwJw=="], 75 + 74 76 "@atcute/oauth-crypto": ["@atcute/oauth-crypto@0.1.0", "", { "dependencies": { "@atcute/multibase": "^1.1.7", "@atcute/uint8array": "^1.1.0", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.6" } }, "sha512-qZYDCNLF/4B6AndYT1rsQelN8621AC5u/sL5PHvlr/qqAbmmUwCBGjEgRSyZtHE1AqD60VNiSMlOgAuEQTSl3w=="], 75 77 76 78 "@atcute/oauth-keyset": ["@atcute/oauth-keyset@0.1.0", "", { "dependencies": { "@atcute/oauth-crypto": "^0.1.0" } }, "sha512-+wqT/+I5Lg9VzKnKY3g88+N45xbq+wsdT6bHDGqCVa2u57gRvolFF4dY+weMfc/OX641BIZO6/o+zFtKBsMQnQ=="], 77 - 78 - "@atcute/oauth-node-client": ["@atcute/oauth-node-client@1.1.0", "", { "dependencies": { "@atcute/client": "^4.2.1", "@atcute/identity": "^1.1.3", "@atcute/identity-resolver": "^1.2.2", "@atcute/lexicons": "^1.2.7", "@atcute/oauth-crypto": "^0.1.0", "@atcute/oauth-keyset": "^0.1.0", "@atcute/oauth-types": "^0.1.1", "@atcute/util-fetch": "^1.0.5", "@badrap/valita": "^0.4.6", "nanoid": "^5.1.6" } }, "sha512-xCp/VfjtvTeKscKR/oI2hdMTp1/DaF/7ll8b6yZOCgbKlVDDfhCn5mmKNVARGTNaoywxrXG3XffbWCIx3/E87w=="], 79 79 80 80 "@atcute/oauth-types": ["@atcute/oauth-types@0.1.1", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.7", "@atcute/oauth-keyset": "^0.1.0", "@badrap/valita": "^0.4.6" } }, "sha512-u+3KMjse3Uc/9hDyilu1QVN7IpcnjVXgRzhddzBB8Uh6wePHNVBDdi9wQvFTVVA3zmxtMJVptXRyLLg6Ou9bqg=="], 81 81
+3 -2
oauth-callback.html
··· 107 107 <script> 108 108 (function() { 109 109 try { 110 - // Extract OAuth parameters from URL 111 - const params = new URLSearchParams(window.location.search); 110 + // Extract OAuth parameters from URL hash (not search string) 111 + // OAuth providers redirect with params in the hash fragment 112 + const params = new URLSearchParams(window.location.hash.slice(1)); 112 113 113 114 // Build Obsidian URI with all OAuth callback parameters 114 115 const obsidianUri = `obsidian://atmosphere-oauth?${params.toString()}`;
+1 -1
package.json
··· 32 32 "@atcute/identity-resolver-node": "^1.0.3", 33 33 "@atcute/leaflet": "^1.0.17", 34 34 "@atcute/microcosm": "^1.0.1", 35 - "@atcute/oauth-node-client": "^1.1.0", 35 + "@atcute/oauth-browser-client": "^3.0.0", 36 36 "@atcute/pckt": "^0.1.5", 37 37 "@atcute/standard-site": "^1.0.0", 38 38 "obsidian": "latest",
+15 -12
src/lib/client.ts
··· 2 2 import { resolveActor } from "./identity"; 3 3 import { isActorIdentifier } from "@atcute/lexicons/syntax"; 4 4 import { ResolvedActor } from "@atcute/identity-resolver"; 5 - import type { OAuthSession } from "@atcute/oauth-node-client"; 6 - import { OAuthHandler } from "./oauth/oauth"; 7 - import { OAuthSessionStore } from "./oauth/oauthStore"; 5 + import { OAuthHandler, } from "./oauth/oauth"; 6 + import { OAuthUserAgent, Session } from "@atcute/oauth-browser-client"; 8 7 9 8 export class ATClient extends Client { 10 9 private hh: Handler; 11 10 actor?: ResolvedActor 12 11 13 - constructor(store: OAuthSessionStore) { 14 - const oauth = new OAuthHandler(store); 12 + constructor() { 13 + const oauth = new OAuthHandler(); 15 14 const hh = new Handler(oauth); 16 15 super({ handler: hh }); 17 16 this.hh = hh; 18 17 } 19 18 20 19 get loggedIn(): boolean { 21 - return (!!this.actor?.did && !!this.hh.session?.did) 20 + return (!!this.actor?.did && !!this.hh.session?.info.sub) 22 21 } 23 22 24 23 async login(identifier: string): Promise<void> { 25 24 await this.hh.login(identifier); 26 - this.actor = await this.hh.getActor(this.hh.session!.did); 25 + this.actor = await this.hh.getActor(this.hh.session!.info.sub); 27 26 } 28 27 29 28 async restoreSession(did: string): Promise<void> { ··· 45 44 } 46 45 47 46 /** 48 - * Custom handler that wraps OAuthSession and adds PDS routing logic 47 + * Custom handler that wraps OAuthUserAgent and adds PDS routing logic 49 48 */ 50 49 export class Handler implements FetchHandlerObject { 51 50 cache: Cache; 52 51 oauth: OAuthHandler; 53 - session?: OAuthSession 52 + session?: Session; 53 + agent?: OAuthUserAgent; 54 54 actor?: ResolvedActor; 55 55 56 56 constructor(oauth: OAuthHandler) { ··· 61 61 async login(identifier: string): Promise<void> { 62 62 const session = await this.oauth.authorize(identifier); 63 63 this.session = session; 64 + this.agent = new OAuthUserAgent(session); 64 65 } 65 66 async restoreSession(did: string): Promise<void> { 66 67 const session = await this.oauth.restore(did); 67 68 this.session = session; 69 + this.agent = new OAuthUserAgent(session); 68 70 } 69 71 async logout(identifier: string): Promise<void> { 70 72 await this.oauth.revoke(identifier); 71 73 this.session = undefined; 74 + this.agent = undefined; 72 75 } 73 76 74 77 handleOAuthCallback(params: URLSearchParams): void { ··· 101 104 return null; 102 105 } 103 106 104 - const own = (repo === this.session?.did) 107 + const own = (repo === this.session?.info.sub) 105 108 if (!own) { 106 109 // resolve to get user's PDS 107 110 const actor = await this.getActor(repo); ··· 126 129 // use configureable public fetch for external PDS 127 130 const sfh = simpleFetchHandler({ service: pds }); 128 131 resp = await sfh(pathname, init); 129 - } else if (this.session) { 132 + } else if (this.agent) { 130 133 // oauth handler if we are logged in 131 - resp = await this.session.handle(pathname, init); 134 + resp = await this.agent.handle(pathname, init); 132 135 } else { 133 136 // default public fetch to bsky 134 137 const sfh = simpleFetchHandler({ service: "https://bsky.social" });
+39 -41
src/lib/oauth/oauth.ts
··· 1 - import { OAuthClient, MemoryStore, type StoredState, type OAuthSession } from '@atcute/oauth-node-client'; 1 + import { 2 + configureOAuth, 3 + createAuthorizationUrl, 4 + finalizeAuthorization, 5 + getSession, 6 + deleteStoredSession, 7 + OAuthUserAgent, 8 + Session, 9 + } from '@atcute/oauth-browser-client'; 2 10 import { compositeResolver } from 'lib/identity'; 3 11 import { Notice } from 'obsidian'; 4 - import { OAuthSessionStore } from './oauthStore'; 5 - import { ActorIdentifier, isDid } from "@atcute/lexicons/syntax"; 12 + import { isDid, type ActorIdentifier } from "@atcute/lexicons/syntax"; 6 13 import metadata from '../../../client-metadata.json' with { type: 'json' }; 7 14 8 - const TEN_MINUTES_MS = 10 * 60_000; 9 15 10 16 export class OAuthHandler { 11 - private oauth: OAuthClient 12 - private sessionStore: OAuthSessionStore; 13 17 private callbackResolver: ((value: URLSearchParams) => void) | null = null; 14 18 private callbackRejecter: ((reason?: Error) => void) | null = null; 15 19 private callbackTimeout: ReturnType<typeof setTimeout> | null = null; 16 20 17 - constructor(sessionStore: OAuthSessionStore) { 18 - this.sessionStore = sessionStore; 19 - // Initialize OAuth client with hosted redirect URL 20 - this.initClient(metadata.redirect_uris[0] || ""); 21 - } 22 - 23 - initClient(redirectUri: string): void { 24 - this.oauth = new OAuthClient({ 21 + constructor() { 22 + configureOAuth({ 25 23 metadata: { 26 24 client_id: metadata.client_id, 27 - redirect_uris: [redirectUri], 28 - scope: 'atproto include:at.margin.authFull repo:site.standard.document repo:network.cosmik.card repo:network.cosmik.collection repo:network.cosmik.collectionLink', 29 - }, 30 - actorResolver: compositeResolver, 31 - stores: { 32 - sessions: this.sessionStore, 33 - states: new MemoryStore<string, StoredState>({ 34 - maxSize: 10, 35 - ttl: TEN_MINUTES_MS, 36 - ttlAutopurge: true, 37 - }), 25 + redirect_uri: metadata.redirect_uris[0]!, 38 26 }, 39 - }); 27 + identityResolver: compositeResolver, 28 + }) 40 29 } 41 30 42 31 handleCallback(params: URLSearchParams): void { ··· 51 40 } 52 41 } 53 42 54 - async authorize(identifier: string): Promise<OAuthSession> { 55 - const redirectUri = metadata.redirect_uris[0]!; 56 - 57 - // Reinitialize client with current redirect URI 58 - this.initClient(redirectUri); 59 - 60 - const { url } = await this.oauth.authorize({ 43 + async authorize(identifier: string): Promise<Session> { 44 + const authUrl = await createAuthorizationUrl({ 61 45 target: { type: 'account', identifier: identifier as ActorIdentifier }, 62 - redirectUri: redirectUri, 46 + scope: metadata.scope, 63 47 }); 48 + await sleep(200); 64 49 65 50 // Create promise for callback 66 51 const waitForCallback = new Promise<URLSearchParams>((resolve, reject) => { ··· 77 62 }, 5 * 60_000); 78 63 }); 79 64 80 - window.open(url.href, '_blank'); 81 - new Notice('Continue login in the browser') 65 + window.open(authUrl, '_blank'); 66 + new Notice('Continue login in the browser'); 82 67 83 68 const params = await waitForCallback; 84 - const { session } = await this.oauth.callback(params, { redirectUri }); 85 - 69 + const { session } = await finalizeAuthorization(params); 86 70 return session; 87 71 } 88 72 89 - async restore(did: string): Promise<OAuthSession> { 73 + async restore(did: string): Promise<Session> { 90 74 if (!isDid(did)) { 91 75 throw new Error("Invalid DID: " + did); 92 76 } 93 - return await this.oauth.restore(did) 77 + const session = await getSession(did, { allowStale: false }); 78 + if (!session) { 79 + throw new Error("No session found for DID: " + did); 80 + } 81 + return session; 94 82 } 95 83 96 84 async revoke(did: string): Promise<void> { 97 85 if (!isDid(did)) { 98 86 throw new Error("Invalid DID: " + did); 99 87 } 100 - await this.oauth.revoke(did); 88 + const session = await getSession(did, { allowStale: true }); 89 + if (session) { 90 + try { 91 + const agent = new OAuthUserAgent(session); 92 + await agent.signOut(); 93 + } catch (error) { 94 + console.error('Error during sign out:', error); 95 + } 96 + } 97 + deleteStoredSession(did); 101 98 } 99 + 102 100 }
-38
src/lib/oauth/oauthStore.ts
··· 1 - import { Store, StoredSession } from "@atcute/oauth-node-client"; 2 - import { Did } from "@atcute/lexicons/syntax"; 3 - import AtmospherePlugin from "main"; 4 - 5 - 6 - export class OAuthSessionStore implements Store<Did, StoredSession> { 7 - plugin: AtmospherePlugin; 8 - 9 - constructor(plugin: AtmospherePlugin) { 10 - this.plugin = plugin; 11 - } 12 - 13 - async get(key: Did): Promise<StoredSession | undefined> { 14 - const sessions = this.plugin.settings.oauth.sessions ?? {}; 15 - return sessions[key]; 16 - } 17 - 18 - async set(key: Did, value: StoredSession): Promise<void> { 19 - if (!this.plugin.settings.oauth.sessions) { 20 - this.plugin.settings.oauth.sessions = {}; 21 - } 22 - this.plugin.settings.oauth.sessions[key] = value; 23 - await this.plugin.saveSettings(); 24 - } 25 - 26 - async delete(key: Did): Promise<void> { 27 - if (this.plugin.settings.oauth.sessions) { 28 - delete this.plugin.settings.oauth.sessions[key]; 29 - await this.plugin.saveSettings(); 30 - return; 31 - } 32 - } 33 - 34 - async clear(): Promise<void> { 35 - this.plugin.settings.oauth.sessions = {}; 36 - await this.plugin.saveSettings(); 37 - } 38 - }
+1 -4
src/main.ts
··· 5 5 import { StandardFeedView, VIEW_ATMOSPHERE_STANDARD_FEED } from "views/standardfeed"; 6 6 import { ATClient } from "lib/client"; 7 7 import { Clipper } from "lib/clipper"; 8 - import { OAuthSessionStore } from "lib/oauth/oauthStore"; 9 8 10 9 export default class AtmospherePlugin extends Plugin { 11 10 settings: AtProtoSettings = DEFAULT_SETTINGS; ··· 14 13 15 14 async onload() { 16 15 await this.loadSettings(); 17 - this.client = new ATClient(new OAuthSessionStore(this)); 16 + this.client = new ATClient(); 18 17 this.clipper = new Clipper(this); 19 18 20 19 this.registerObsidianProtocolHandler('atmosphere-oauth', (params) => { ··· 25 24 urlParams.set(key, String(value)); 26 25 } 27 26 } 28 - 29 27 this.client.handleOAuthCallback(urlParams); 30 28 new Notice('Authentication completed! Processing...'); 31 29 } catch (error) { ··· 96 94 console.error("Failed to restore session:", e); 97 95 // Clear invalid session data 98 96 this.settings.did = undefined; 99 - this.settings.oauth.sessions = {}; 100 97 await this.saveSettings(); 101 98 new Notice("Session expired. Please login by opening settings"); 102 99 return false;
-5
src/settings.ts
··· 3 3 import { isActorIdentifier } from "@atcute/lexicons/syntax"; 4 4 import { VIEW_TYPE_ATMOSPHERE_BOOKMARKS } from "./views/bookmarks"; 5 5 import { VIEW_ATMOSPHERE_STANDARD_FEED } from "./views/standardfeed"; 6 - import { StoredSession } from "@atcute/oauth-node-client"; 7 6 8 7 export interface AtProtoSettings { 9 8 did?: string; 10 9 clipDir: string; 11 - oauth: { 12 - sessions?: Record<string, StoredSession> | null; 13 - } 14 10 publish: { 15 11 useFirstHeaderAsTitle: boolean; 16 12 }; ··· 18 14 19 15 export const DEFAULT_SETTINGS: AtProtoSettings = { 20 16 clipDir: "AtmosphereClips", 21 - oauth: {}, 22 17 publish: { 23 18 useFirstHeaderAsTitle: false, 24 19 }